阅读需 5 分钟

FMOD调音框架之内存级控制

最近产品涉及音频实时处理功能,基于FMOD组件,将获取到的音频数据按照用户自定义的配置进行变换。

网上大家分享的使用都比较浅,只是简单的基于文件转换后生成另一个文件;或者是给定一个链接直接转换播放。对于我们想要实现内存级音频数据,转换后得到新的音频数据的控制流程则没有。因此在这里分享一下,不播放音频流,仅将FMOD组件作为一个转换工具库来使用。

01. 初始化

在初始化时,设置FMOD_OUTPUTTYPE_NOSOUND的输出模式很重要。该模式下,FMOD不会打开任何实际的音频设备。该模式设计目的是使音频系统初始化但不进行实际音频输出。

FMOD::System* system;
int ret = FMOD::System_Create(&system);
if (FMOD_OK != ret) {
LOGE("fmod create fail: %d", ret)
return NULL;
}

system->setOutput(FMOD_OUTPUTTYPE_NOSOUND);// ---------------------这里设置很关键
ret = system->init(32, FMOD_INIT_NORMAL, nullptr);
if (FMOD_OK != ret) {
LOGE("fmod init fail: %d", ret)
return NULL;
}

ret = setFormat(2, 44100, FMOD_SOUND_FORMAT_PCM16);
if (FMOD_OK != ret) {
LOGE("fmod fmt fail: %d", ret)
return NULL;
}

02. 创建声道

createSound时,需要选择FMOD_OPENUSER|FMOD_CREATESTREAM|FMOD_LOOP_OFF模式,即用户控制/数据流/不循环播放模式。

int setFormat(int numchannels, int freq, FMOD_SOUND_FORMAT fmt) {
FMOD_CREATESOUNDEXINFO exinfo;
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
exinfo.length = freq * numchannels * sizeof(short) * 3600 * 2;// -------- 这里设置音频数据总长度
exinfo.numchannels = numchannels;
exinfo.defaultfrequency = freq;
exinfo.format = fmt;
exinfo.userdata = inputArguments;// -------------------------- 这里设置回调参数
exinfo.decodebuffersize = BUFFER_BLOCK_SIZE;// --------------- 这里设置每次回调获取数据大小
exinfo.pcmreadcallback = handlePCMReadCallback;// ------------ 这里设置回调函数
system->setDSPBufferSize(BUFFER_BLOCK_SIZE, 4);// ----------- 这里与 decodebuffersize 一致
return system->createSound(0, FMOD_OPENUSER|FMOD_CREATESTREAM|FMOD_LOOP_OFF, &exinfo, &mSound);
}

回调函数实现:

static FMOD_RESULT handlePCMReadCallback(FMOD_SOUND *sound, void *data, unsigned int datalen) {
TInputArguments* inputArgs = nullptr;// 自定义参数类型
FMOD_Sound_GetUserData(sound, (void**)&inputArgs);
__ASSERT(inputArgs)
if (inputArgs) {
// TODO 在这里添加要处理的数据
}
return FMOD_OK;
}

03. 创建数据接收器

创建一个变换DSP,该通道仅作为最后变换的数据接收。

FMOD_DSP_DESCRIPTION dspDesc = {};
dspDesc.version = 0x00010000;
dspDesc.numinputbuffers = 1;
dspDesc.numoutputbuffers = 1;
strcpy(dspDesc.name, "Custom-DSP");
dspDesc.read = handleDSPCallback;// ------------ 这里设置回调函数
dspDesc.userdata = outputArguments;// ---------- 这里设置回调参数
system->createDSP(&dspDesc, &dsp);

数据输出回调实现:

static FMOD_RESULT handleDSPCallback(FMOD_DSP_STATE* dsp_state, float* inbuffer, float* outbuffer, unsigned int length, int inchannels, int* outchannels) {
unsigned int size = length * inchannels * sizeof(int16_t);// 16bit

int16_t* buffer16 = (int16_t *)malloc(size);
for (int i=0; i<length; ++i) {
for (int j=0, k=i*inchannels; j<inchannels; ++j, ++k) {
outbuffer[k] = inbuffer[k];// 保障音频数据继续传递

// Assuming 16-bit PCM, convert float (-1.0 to 1.0) to 16-bit samples
buffer16[k] = (int16_t)(inbuffer[k] * 32767.0f); // Convert float to PCM16
}
}

TOutputArguments* outputArgs = nullptr;
FMOD::DSP* dsp = reinterpret_cast<FMOD::DSP *>(dsp_state->instance);
dsp->getUserData(reinterpret_cast<void **>(&outputArgs));
if (outputArgs) {
// TODO 这里处理已经转换好的数据
}
free(buffer16);

return FMOD_OK;
}

04. 主流程

// 1. 创建播放流,实际不会访问音频设备
ret = system->playSound(mSound, nullptr, false, &mChannel);
if (FMOD_OK != ret) {
LOGE("fmod playSound fail: %d", ret)
return ret;
}

// 2. 获取主变声组,其他变声配置类型均添加到这里
FMOD::ChannelGroup* group = nullptr;
ret = system->getMasterChannelGroup(&group);
if (FMOD_OK != ret) {
LOGE("fmod get group fail: %d", ret)
return ret;
}

// 3. 添加变声配置,可以添加多个,但是确保接收输出的 dsp 第一个添加,以保障得到最后转换的数据。
group->addDSP(0, dsp1);
group->addDSP(0, dsp2);
// add more dsp here ...

group->addDSP(0, dsp);// ---------- 最后位置

// 4. 更新配置,开始异步转换
system->update();

至此,对于内存级音频数据控制的主要实现已完成,这对于需要音频处理而无需播放声音的情况非常有用,比如在服务器端和应用自身内部业务的音频处理、分析、音效的预加载等。