原始音频数据处理
NERTC SDK 的音频模块会严格控制声音设备的采集和播放逻辑,同时支持对采集到的音视频原始数据进行自定义的前处理和后处理,获取想要的播放效果。适用于非标设备接入、自定义音频效果、语音处理、语音识别等场景。
- 前处理:在音频数据发送到编码器前获取原始的音频数据进行修改,主要针对本地麦克风采集到的音频数据或自定义外部音频流。
- 后处理:即在音频数据发送给解码器后获取原始的音频数据进行修改,主要针对接收到的远端用户音频数据。
NERTC SDK 通过提供 NERtcAudioFrameObserver 类,实现采集、修改原始音频数据功能。
Android
注意事项
- 采集回调 onRecordFrame、播放回调 onPlaybackFrame 中的原始音频数据可进行处理,例如美声变声。
- 混音回调 onMixedAudioFrame 和某一用户的播放回调 onPlaybackAudioFrameBeforeMixingWithUserID 中的原始音频数据不能进行处理。
实现方法
在使用原始数据处理功能前,请确保您已在项目中实现基本的实时音视频功能。
参考以下步骤,在您的项目中实现原始音频数据处理功能:
- 加入频道前调用 setAudioFrameObserver 方法注册语音观测器,并在该方法中实现一个 NERtcEngineAudioFrameObserver 类。
- 设置回调的音频采样率。
- 通过 setRecordingAudioFrameParameters 修改回调的采集音频采样率、将回调的音频数据设置为只读模式或读写模式。
- 通过 setPlaybackAudioFrameParameters 修改回调的播放音频采样率、将回调的音频数据设置为只读模式或读写模式。
- 通过 setMixedAudioFrameParameters,设置 onMixedAudioFrame 回调中的采样率。
- SDK 返回回调。
- SDK 收到输入的采集数据和播放的音频数据时,发送 onRecordFrame 和 onPlaybackFrame 回调。
- 收到音频采集与播放混合后数据帧时,SDK 发送 onMixedAudioFrame 回调;收到某一远端用户的原始音频帧时,发送 onPlaybackAudioFrameBeforeMixingWithUserID 回调。
- 用户拿到音频数据后,根据场景需要自行进行处理。
- 完成音频数据处理后,您可以直接进行自播放,或根据场景需求再通过 onRecordFrame、onPlaybackFrame 回调发送给 SDK。
示例代码
///设置音频回调参数
NERtcAudioFrameRequestFormat formatMix = new NERtcAudioFrameRequestFormat();
formatMix.setChannels(channel);
formatMix.setSampleRate(sampleRate);
formatMix.setOpMode(readOnly.isChecked() ? NERtcAudioFrameOpMode.kNERtcAudioFrameOpModeReadOnly : NERtcAudioFrameOpMode.kNERtcAudioFrameOpModeReadWrite);
Log.i(TAG, "AudioCallback ,channel: "+formatMix.getChannels()+ " Mixed Sample:" + formatMix.getSampleRate() + " ReadWrite:" + formatMix.getOpMode());
NERtcEx.getInstance().setMixedAudioFrameParameters(formatMix);
NERtcEx.getInstance().setPlaybackAudioFrameParameters(formatMix);
NERtcEx.getInstance().setRecordingAudioFrameParameters(formatMix);
NERtcEx.getInstance().setAudioFrameObserver(observer);
///音频数据回调处理
observer = new NERtcAudioFrameObserver() {
@Override
public void onRecordFrame(NERtcAudioFrame audioFrame) {
try {
if(!isAudioCallbackDump){
return;
}
if (pcmCallbackRecordDump == null) {
pcmCallbackRecordDump = createPCMDump("Record_" +audioFrame.getFormat().getChannels()
+"_"+ audioFrame.getFormat().getSampleRate());
if(pcmCallbackMixDump == null) {
Log.e(TAG, "create dump file failed!");
return;
}
}
byte[] remaining = new byte[audioFrame.getData().remaining()];
audioFrame.getData().get(remaining);
pcmCallbackRecordDump.write(remaining);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onPlaybackFrame(NERtcAudioFrame audioFrame) {
if(!isAudioCallbackDump){
return;
}
try {
if (pcmCallbackPlaybackDump == null) {
pcmCallbackPlaybackDump = createPCMDump("PlayBack_" +audioFrame.getFormat().getChannels()
+"_"+ audioFrame.getFormat().getSampleRate());
if(pcmCallbackMixDump == null) {
Log.e(TAG, "create dump file failed!");
return;
}
}
byte[] remaining = new byte[audioFrame.getData().remaining()];
audioFrame.getData().get(remaining);
pcmCallbackPlaybackDump.write(remaining);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onPlaybackAudioFrameBeforeMixingWithUserID(long userID, NERtcAudioFrame audioFrame) {
if(!isAudioCallbackDump){
return;
}
try {
if(mRemoteUserMap.get(userID) != null){
if(mRemoteUserMap.get(userID).audioPCMDump == null){
mRemoteUserMap.get(userID).audioPCMDump = createPCMDump("PlayBackUid_"+ userID +"_"+ audioFrame.getFormat().getChannels()
+"_"+ audioFrame.getFormat().getSampleRate());
if(mRemoteUserMap.get(userID).audioPCMDump == null){
Log.e(TAG, "create dump file failed!");
return;
}
}
byte[] remaining = new byte[audioFrame.getData().remaining()];
audioFrame.getData().get(remaining);
mRemoteUserMap.get(userID).audioPCMDump.write(remaining);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onMixedAudioFrame(NERtcAudioFrame audioFrame) {
if(!isAudioCallbackDump){
return;
}
try {
if (pcmCallbackMixDump == null) {
pcmCallbackMixDump = createPCMDump("Mix_" +audioFrame.getFormat().getChannels()
+"_"+ audioFrame.getFormat().getSampleRate());
if(pcmCallbackMixDump == null) {
Log.e(TAG, "create dump file failed!");
return;
}
}
byte[] remaining = new byte[audioFrame.getData().remaining()];
audioFrame.getData().get(remaining);
pcmCallbackMixDump.write(remaining);
} catch (Exception e) {
e.printStackTrace();
}
}
};
iOS
注意事项
- 采集回调 onNERtcEngineAudioFrameDidRecord、播放回调 onNERtcEngineAudioFrameWillPlayback 中的原始音频数据可进行处理,例如美声变声。
- 混音回调 onNERtcEngineMixedAudioFrame 和某一用户的播放回调 onNERtcEnginePlaybackAudioFrameBeforeMixingWithUserID 中的原始音频数据不能进行处理。
实现方法
在使用原始数据功能前,请确保你已在项目中完成基本的实时音视频功能。
参考如下步骤,在你的项目中实现原始音频数据功能:
- 加入频道前调用 setAudioFrameObserver 方法注册语音观测器,并在该方法中实现一个 NERtcEngineAudioFrameObserver 类。
- 设置回调的音频采样率。
- 通过 setRecordingAudioFrameParameters 修改回调的采集音频采样率、将回调的音频数据设置为只读模式或读写模式。
- 通过 setPlaybackAudioFrameParameters 修改回调的播放音频采样率、将回调的音频数据设置为只读模式或读写模式。
- 通过 setMixedAudioFrameParameters,设置 onNERtcEngineMixedAudioFrame 回调中的采样率。
- SDK 返回回调。
- 收到输入的采集数据和播放的音频数据时,发送 onNERtcEngineAudioFrameDidRecord 和 onNERtcEngineAudioFrameWillPlayback 回调。
- 收到音频采集与播放混合后数据帧时,发送 onNERtcEngineMixedAudioFrame 回调;收到某一远端用户的原始音频帧时,发送 onNERtcEnginePlaybackAudioFrameBeforeMixingWithUserID 回调。
- 用户拿到音频数据后,根据场景需要自行进行处理。
- 完成音频数据处理后,可以直接进行自播放,或根据场景需求再通过 onNERtcEngineAudioFrameDidRecord 和 onNERtcEngineAudioFrameWillPlayback 回调发送给 SDK。
示例代码
//1. 注册语音观测器
[[NERtcEngine sharedEngine] setAudioFrameObserver:self];
//2.设置回调的音频格式参数
NERtcAudioFrameRequestFormat *format = [[NERtcAudioFrameRequestFormat alloc] init];
format.channels = 1;
format.sampleRate = 48000;
format.mode = kNERtcAudioFrameOpModeReadWrite; //读写模式
[[NERtcEngine sharedEngine] setRecordingAudioFrameParameters:format];
//3.在回调中处理数据
- (void)onNERtcEngineAudioFrameDidRecord:(NERtcAudioFrame *)frame {
if (!frame) {
return;
}
if (!_enableRecordAudioObserver) {
return;
}
if (!_recordSoundTouch) {
_recordSoundTouch = [[NTESSoundTouch alloc] initWithSampleRate:frame.format.sample_rate];
}
//若读写模式,SDK将会对处理后的音频数据发送,反之不会。
[_recordSoundTouch processSound:frame.data number:frame.format.samples_per_channel];
}
- (void)onNERtcEngineAudioFrameWillPlayback:(NERtcAudioFrame *)frame {
if (!frame) {
return;
}
if (!_enablePlaybackAudioObserver) {
return;
}
if (!_playbackSoundTouch) {
_playbackSoundTouch = [[NTESSoundTouch alloc] initWithSampleRate:frame.format.sample_rate];
}
//若读写模式,SDK将会对处理后的音频数据发送,反之不会。
[_playbackSoundTouch processSound:frame.data number:frame.format.samples_per_channel];
}
Windows/macOS
注意事项
- 采集回调 onAudioFrameDidRecord、播放回调 onAudioFrameWillPlayback 中的原始音频数据可进行处理,例如美声变声。
- 混音回调 onMixedAudioFrame 和某一用户的播放回调 onPlaybackAudioFrameBeforeMixing 中的原始音频数据不能进行处理。
实现方法
在使用原始数据功能前,请确保你已在项目中完成基本的实时音视频功能。
参考如下步骤,在你的项目中实现原始音频数据回调功能:
- 加入频道前基于 INERtcAudioFrameObserver 接口类实现一个 NERtcAudioFrameObserver 类,并调用 setAudioFrameObserver 方法注册语音观测器。
- 设置回调的音频采样率。
- 通过 setRecordingAudioFrameParameters 修改回调的采集音频采样率、将回调的音频数据设置为只读模式或读写模式。
- 通过 setPlaybackAudioFrameParameters 修改回调的播放音频采样率、将回调的音频数据设置为只读模式或读写模式。
- 通过 setMixedAudioFrameParameters,设置 onMixedAudioFrame 回调中的采样率。
- SDK 返回回调。
- 收到输入的采集数据和播放的音频数据时,发送 onAudioFrameDidRecord、onAudioFrameWillPlayback 回调。
- 收到音频采集与播放混合后数据帧时,发送 onMixedAudioFrame 回调;收到某一远端用户的原始音频帧时,发送 onPlaybackAudioFrameBeforeMixing 回调。
- 用户拿到音频数据后,根据场景需要自行进行处理。
- 完成音频数据处理后,可以直接进行自播放,或根据场景需求再通过回调发送给 SDK。
示例代码
你可以参考下面的示例代码片段,在项目中实现音频原始数据功能:
class AudioFrameObserver : public INERtcAudioFrameObserver
{
public:
void onAudioFrameDidRecord(NERtcAudioFrame *frame) {
FILE *fd = getFileDescr("Record", frame->format);
if (NULL != fd) {
fwrite(frame->data, frame->format.bytes_per_sample * frame->format.channels,
frame->format.samples_per_channel, fd);
}
}
void onAudioFrameWillPlayback(NERtcAudioFrame *frame) {
FILE *fd = getFileDescr("Playback", frame->format);
if (NULL != fd) {
fwrite(frame->data, frame->format.bytes_per_sample * frame->format.channels,
frame->format.samples_per_channel, fd);
}
}
void onMixedAudioFrame(nertc::NERtcAudioFrame *frame) {
FILE *fd = getFileDescr("Mixed", frame->format);
if (NULL != fd) {
fwrite(frame->data, frame->format.bytes_per_sample * frame->format.channels,
frame->format.samples_per_channel, fd);
}
}
void onPlaybackAudioFrameBeforeMixing(nertc::uid_t uid, nertc::NERtcAudioFrame *frame) {
FILE *fd = getFileDescr(std::to_string(uid), frame->format);
if (NULL != fd) {
fwrite(frame->data, frame->format.bytes_per_sample * frame->format.channels,
frame->format.samples_per_channel, fd);
}
}
AudioFrameObserver(const std::string &record_file_dir) : m_record_file_dir(record_file_dir) {}
virtual ~AudioFrameObserver()
{
for (auto &fd : m_fds)
{
fclose(fd.second.fd);
fd.second.fd = NULL;
}
m_fds.clear();
}
private:
struct file_descr {
FILE *fd;
nertc::NERtcAudioFormat fmt;
};
FILE *getFileDescr(const std::string &id, const nertc::NERtcAudioFormat &fmt) {
if (m_fds.end() == m_fds.find(id)) {
m_fds[id] = file_descr();
m_fds[id].fd = NULL;
} else if ((m_fds[id].fmt.sample_rate != fmt.sample_rate) ||
(m_fds[id].fmt.channels != fmt.channels) ||
(m_fds[id].fmt.type != fmt.type)) {
if (NULL != m_fds[id].fd) {
fclose(m_fds[id].fd);
m_fds[id].fd = NULL;
}
}
if (NULL == m_fds[id].fd) {
time_t now = time(0);
tm *ltm = localtime(&now);
std::ostringstream oss;
oss << m_record_file_dir << "/audio-frame-" << id << "-" << fmt.sample_rate << "-ch" << fmt.channels;
oss << "-" << ltm->tm_year + 1900;
oss << std::setfill('0') << std::setw(2) << ltm->tm_mon + 1;
oss << std::setfill('0') << std::setw(2) << ltm->tm_mday;
oss << std::setfill('0') << std::setw(2) << ltm->tm_hour;
oss << std::setfill('0') << std::setw(2) << ltm->tm_min;
oss << std::setfill('0') << std::setw(2) << ltm->tm_sec;
oss << ".pcm";
m_fds[id].fd = fopen(oss.str().c_str(), "wb");
m_fds[id].fmt = fmt;
}
return m_fds[id].fd;
}
std::map<std::string /*id*/, file_descr> m_fds;
std::string m_record_file_dir;
};
AudioFrameObserver *audioFrameObserver = new AudioFrameObserver("/dir/for/recor_file");
rtc_engine_->setAudioFrameObserver(audioFrameObserver);
本篇文档内容是否对您有帮助?


此文档对你是否有帮助
×


×