DSP

CSR8675项目实战:BlueEarphone 左右声道各10个Speaker EQ

2019-07-13 15:17发布

为了让CSR867x的开发更容易,现与思度科技联合推出CSR867x学习板【淘宝链接:思度科技CSR开发板】。 技术交流QQ群号:743434463
开发板会员QQ群号:725398389(凭订单号入群,赠PPT、项目源码、视频教程)
——————————正文分割线———————————–

1. 引言

熟悉CSR867X芯片方案的开发人员一定对UFE工具印象深刻。UFE(universal front end)工具是ADK自带的声学调音工具,能够满足音乐播放、免提通话等场景的声学调音的一般需求。其音乐播放调音界面如下:
在这里插入图片描述
从上图可以看出,音频数据从右侧天线输入,其音频编码格式为APTX_HD,采样率44.1k,采样精度24位。在经过Decoder解码器解码后转成双声道的PCM格式音频数据,经过Compander(压扩器)、User EQ(用户均衡器)、Bass Enhancement(低音增强)、Speaker EQ(扬声器均衡器)、Stereo Enhancement(立体声增强)、Crossover(分频器)、Main Volume(主音量控制)等模块的处理后通过模拟通道输出。其中除了Main Volume和Crossover模块以外,所有模块对音频信号的处理都是不区分左右通道的,例如Speaker EQ模块:
在这里插入图片描述
可以看到Speaker EQ模块有10个点的滤波器,每个滤波器的参数有Gain(增益)、Freq(频率)、Q值(滤波器品质因数)和带宽(一个计算值,不能设置)。这10个滤波器的参数会同时作用在左右声道。 本文给出了一种支持左右声道各10个Speaker EQ的方案,并给出一个基于matlab GUI的调音工具设计。

2. 主要功能

  • 左右通道各10个Speaker EQ(支持任何蓝牙音频解码格式、任何音源)
  • 可在VM层读写每个Speaker EQ的每个参数(滤波器类型、增益、频率、品质因数)
  • 可通过matlab GUI实时调节每个Speaker EQ的每个参数
修改后的系统框图如下:
在这里插入图片描述

3. 项目难点

3.1. 改造音频链路

基本思路是,在现有音频链路的基础上,将整个Speaker EQ模块改为左通道独享,然后拷贝一个完整的Speaker EQ模块单独给右通道使用。首先创建修改原有的speaker eq的数组定义,使其只处理左通道数据: // music_example_config.asm .VAR SpkrEqDefnTable[$user_eq.DEFINITION_TABLE_SIZE] = MAX_NUM_SPKR_EQ_BANKS, MAX_NUM_SPKR_EQ_STAGES, &spkr_eq_left_dm2, &spkr_eq_left_dm2, &SpkrEqCoefsA, &SpkrEqCoefsB, &$audio_proc.hq_peq.initialize, &$audio_proc.hq_peq.zero_delay_data, &$audio_proc.hq_peq.process; 接着创建一个新的R speaker eq数组定义,其只处理右通道的数据: // music_example_config.asm .VAR SpkrEqDefnTable2[$user_eq.DEFINITION_TABLE_SIZE] = MAX_NUM_SPKR_EQ_BANKS, MAX_NUM_SPKR_EQ_STAGES, &spkr_eq_right_dm2, &spkr_eq_right_dm2, &SpkrEqCoefsC, &SpkrEqCoefsD, &$audio_proc.hq_peq.initialize, &$audio_proc.hq_peq.zero_delay_data, &$audio_proc.hq_peq.process; 然后为R speaker eq创建滤波器系数存储空间,并将此系数关联到右通道的音频数据流: // music_example_config.asm .VAR/DM2 spkr_eq_right_dm2[MAX_SPKR_EQ_OBJECT_SIZE] = &stream_map_right_in, // PTR_INPUT_DATA_BUFF_FIELD &stream_map_right_in, // PTR_OUTPUT_DATA_BUFF_FIELD MAX_NUM_SPKR_EQ_STAGES, // MAX_STAGES_FIELD &SpkrEqCoefsC, // PARAM_PTR_FIELD 0 ...; .VAR SpkrEqCoefsC[3+6*MAX_NUM_SPKR_EQ_STAGES] = $spkrEq.Fs44.NumBands, $spkrEq.Fs44.GainExp, $spkrEq.Fs44.GainMant, $spkrEq.Fs44.Stage1.b2, $spkrEq.Fs44.Stage1.b1, $spkrEq.Fs44.Stage1.b0, $spkrEq.Fs44.Stage1.a2, $spkrEq.Fs44.Stage1.a1, $spkrEq.Fs44.Stage2.b2, $spkrEq.Fs44.Stage2.b1, $spkrEq.Fs44.Stage2.b0, $spkrEq.Fs44.Stage2.a2, $spkrEq.Fs44.Stage2.a1, $spkrEq.Fs44.Stage3.b2, $spkrEq.Fs44.Stage3.b1, $spkrEq.Fs44.Stage3.b0, $spkrEq.Fs44.Stage3.a2, $spkrEq.Fs44.Stage3.a1, $spkrEq.Fs44.Stage4.b2, $spkrEq.Fs44.Stage4.b1, $spkrEq.Fs44.Stage4.b0, $spkrEq.Fs44.Stage4.a2, $spkrEq.Fs44.Stage4.a1, $spkrEq.Fs44.Stage5.b2, $spkrEq.Fs44.Stage5.b1, $spkrEq.Fs44.Stage5.b0, $spkrEq.Fs44.Stage5.a2, $spkrEq.Fs44.Stage5.a1, $spkrEq.Fs44.Stage6.b2, $spkrEq.Fs44.Stage6.b1, $spkrEq.Fs44.Stage6.b0, $spkrEq.Fs44.Stage6.a2, $spkrEq.Fs44.Stage6.a1, $spkrEq.Fs44.Stage7.b2, $spkrEq.Fs44.Stage7.b1, $spkrEq.Fs44.Stage7.b0, $spkrEq.Fs44.Stage7.a2, $spkrEq.Fs44.Stage7.a1, $spkrEq.Fs44.Stage8.b2, $spkrEq.Fs44.Stage8.b1, $spkrEq.Fs44.Stage8.b0, $spkrEq.Fs44.Stage8.a2, $spkrEq.Fs44.Stage8.a1, $spkrEq.Fs44.Stage9.b2, $spkrEq.Fs44.Stage9.b1, $spkrEq.Fs44.Stage9.b0, $spkrEq.Fs44.Stage9.a2, $spkrEq.Fs44.Stage9.a1, $spkrEq.Fs44.Stage10.b2, $spkrEq.Fs44.Stage10.b1, $spkrEq.Fs44.Stage10.b0, $spkrEq.Fs44.Stage10.a2, $spkrEq.Fs44.Stage10.a1, $spkrEq.Fs44.Stage1.scale, $spkrEq.Fs44.Stage2.scale, $spkrEq.Fs44.Stage3.scale, $spkrEq.Fs44.Stage4.scale, $spkrEq.Fs44.Stage5.scale, $spkrEq.Fs44.Stage6.scale, $spkrEq.Fs44.Stage7.scale, $spkrEq.Fs44.Stage8.scale, $spkrEq.Fs44.Stage9.scale, $spkrEq.Fs44.Stage10.scale; // 48 kHz coefficients if not using coefficient calculation routines .VAR SpkrEqCoefsD[3+6*MAX_NUM_SPKR_EQ_STAGES] = $spkrEq.Fs48.NumBands, $spkrEq.Fs48.GainExp, $spkrEq.Fs48.GainMant, $spkrEq.Fs48.Stage1.b2, $spkrEq.Fs48.Stage1.b1, $spkrEq.Fs48.Stage1.b0, $spkrEq.Fs48.Stage1.a2, $spkrEq.Fs48.Stage1.a1, $spkrEq.Fs48.Stage2.b2, $spkrEq.Fs48.Stage2.b1, $spkrEq.Fs48.Stage2.b0, $spkrEq.Fs48.Stage2.a2, $spkrEq.Fs48.Stage2.a1, $spkrEq.Fs48.Stage3.b2, $spkrEq.Fs48.Stage3.b1, $spkrEq.Fs48.Stage3.b0, $spkrEq.Fs48.Stage3.a2, $spkrEq.Fs48.Stage3.a1, $spkrEq.Fs48.Stage4.b2, $spkrEq.Fs48.Stage4.b1, $spkrEq.Fs48.Stage4.b0, $spkrEq.Fs48.Stage4.a2, $spkrEq.Fs48.Stage4.a1, $spkrEq.Fs48.Stage5.b2, $spkrEq.Fs48.Stage5.b1, $spkrEq.Fs48.Stage5.b0, $spkrEq.Fs48.Stage5.a2, $spkrEq.Fs48.Stage5.a1, $spkrEq.Fs48.Stage6.b2, $spkrEq.Fs48.Stage6.b1, $spkrEq.Fs48.Stage6.b0, $spkrEq.Fs48.Stage6.a2, $spkrEq.Fs48.Stage6.a1, $spkrEq.Fs48.Stage7.b2, $spkrEq.Fs48.Stage7.b1, $spkrEq.Fs48.Stage7.b0, $spkrEq.Fs48.Stage7.a2, $spkrEq.Fs48.Stage7.a1, $spkrEq.Fs48.Stage8.b2, $spkrEq.Fs48.Stage8.b1, $spkrEq.Fs48.Stage8.b0, $spkrEq.Fs48.Stage8.a2, $spkrEq.Fs48.Stage8.a1, $spkrEq.Fs48.Stage9.b2, $spkrEq.Fs48.Stage9.b1, $spkrEq.Fs48.Stage9.b0, $spkrEq.Fs48.Stage9.a2, $spkrEq.Fs48.Stage9.a1, $spkrEq.Fs48.Stage10.b2, $spkrEq.Fs48.Stage10.b1, $spkrEq.Fs48.Stage10.b0, $spkrEq.Fs48.Stage10.a2, $spkrEq.Fs48.Stage10.a1, $spkrEq.Fs48.Stage1.scale, $spkrEq.Fs48.Stage2.scale, $spkrEq.Fs48.Stage3.scale, $spkrEq.Fs48.Stage4.scale, $spkrEq.Fs48.Stage5.scale, $spkrEq.Fs48.Stage6.scale, $spkrEq.Fs48.Stage7.scale, $spkrEq.Fs48.Stage8.scale, $spkrEq.Fs48.Stage9.scale, $spkrEq.Fs48.Stage10.scale; 接着为R speaker EQ创建参数存储空间,用于存储VM发送过来的EQ参数(Type/Gain/Freq/Q),也是R speaker EQ的初始参数: // music_example_config.asm .VAR SpkrRParams[2*ROUND(0.5*$M.MUSIC_MANAGER.PARAMETERS.SPKR_R_STRUCT_SIZE)] = 1, // config 1, // bank 10, // band 0, // master gain 13, 96, 0, 5792, // SPRK R EQ : PEQ, 32Hz, 0dB, 1.414 13, 192, 0, 5792, // SPRK R EQ : PEQ, 64Hz, 0dB, 1.414 13, 375, 0, 5792, // SPRK R EQ : PEQ, 125Hz, 0dB, 1.414 13, 750, 0, 5792, // SPRK R EQ : PEQ, 250Hz, 0dB, 1.414 13, 1500, 0, 5792, // SPRK R EQ : PEQ, 500Hz, 0dB, 1.414 13, 3000, 0, 5792, // SPRK R EQ : PEQ, 1000Hz, 0dB, 1.414 13, 6000, 0, 5792, // SPRK R EQ : PEQ, 2000Hz, 0dB, 1.414 13, 12000, 0, 5792, // SPRK R EQ : PEQ, 4000Hz, 0dB, 1.414 13, 24000, 0, 5792, // SPRK R EQ : PEQ, 8000Hz, 0dB, 1.414 13, 48000, 0, 5792; // SPRK R EQ : PEQ, 16000Hz, 0dB, 1.414 再然后需要创建L / R speaker EQ的音频处理模块并插入到整个音频处理流程中: // music_example_config.asm #if uses_SPKR_EQ $user_eq.eqInitialize, &SpkrEqDefnTable, &SpkrEqParams, $user_eq.eqInitialize, &SpkrEqDefnTable2, &SpkrEqParams2, #endif #if uses_SPKR_EQ $music_example.peq.zerodelay, &SpkrEqDefnTable, 0, $music_example.peq.zerodelay, &SpkrEqDefnTable2, 0, #endif #if uses_SPKR_EQ $music_example.peq.process_left, &SpkrEqDefnTable, $M.MUSIC_MANAGER.CONFIG.SPKR_EQ_BYPASS, $music_example.peq.process_right, &SpkrEqDefnTable2, $M.MUSIC_MANAGER.CONFIG.SPKR_EQ_BYPASS, #endif // uses_SPKR_EQ //------------------------------------------------------------------------------ process_left: //------------------------------------------------------------------------------ // peq processing wrapper // - return without processing if bypassed // - if running user_eq (BYPASS_BIT_MASK_FIELD == USER_EQ_BYPASS) // then check whether user eq bank is 0 //------------------------------------------------------------------------------ // on entry r7 = pointer to filter definition table // r8 = bypass mask //------------------------------------------------------------------------------ r0 = M[&$M.system_config.data.CurParams + $M.MUSIC_MANAGER.PARAMETERS.OFFSET_CONFIG]; // check if EQ is bypassed null = r0 and r8; if NZ rts; r3 = m[r7 + $user_eq.EQ_PROC_FUNC]; $push_rLink_macro; pushm ; r7 = m[r7 + $user_eq.DEFINITION_TABLE_LEFT_DM_PTR]; // r7 points to left channel EQ memory structure // if Parameters is Null then no Peq Null = M[r7 + $audio_proc.peq.PARAM_PTR_FIELD]; if Z jump $pop_rLink_and_rts; call r3; popm ; jump $pop_rLink_and_rts; //------------------------------------------------------------------------------ process_right: //------------------------------------------------------------------------------ // peq processing wrapper // - return without processing if bypassed // - if running user_eq (BYPASS_BIT_MASK_FIELD == USER_EQ_BYPASS) // then check whether user eq bank is 0 //------------------------------------------------------------------------------ // on entry r7 = pointer to filter definition table // r8 = bypass mask //------------------------------------------------------------------------------ r0 = M[&$M.system_config.data.CurParams + $M.MUSIC_MANAGER.PARAMETERS.OFFSET_CONFIG]; // check if EQ is bypassed null = r0 and r8; if NZ rts; r3 = m[r7 + $user_eq.EQ_PROC_FUNC]; $push_rLink_macro; pushm ; r7 = m[r7 + $user_eq.DEFINITION_TABLE_RIGHT_DM_PTR]; // r7 points to right channel EQ memory structure if Z jump $pop_rLink_and_rts; // if Z there isn't a right channel // if Parameters is Null then no Peq Null = M[r7 + $audio_proc.peq.PARAM_PTR_FIELD]; if Z jump $pop_rLink_and_rts; // if Z there isn't a right channel call r3; popm ; jump $pop_rLink_and_rts; // music_example_system.asm #if uses_SPKR_EQ $M.system_config.data.SpkrEqDefnTable, $M.system_config.data.SpkrEqDefnTable2, #else 0, #endif 至此音频链路的改造工作已经完成了。

3.2. 打通VM和DSP之间的消息链路

首先我们来看一下设置EQ参数的消息链路: VMAudio PluginDSPMessageSendConditionallyOnTaskKalimbaSendMessage(msg_id, param_id, param_value,...)根据参数计算滤波器系数并立即生效VMAudio PluginDSP VM层对应的代码: void set_sprk_l_eq_param(uint16 band, eq_param_type_t param_type, uint32 value) { if(sinkAudioGetRoutedAudioTask()) { audio_plugin_spkr_eq_param_t param; param.id.bank = 1; param.id.band = band; param.id.param_type = param_type; param.value = value; AudioSetSpkrLEqParameter(sinkAudioGetRoutedAudioTask(), ¶m); } } Audio Plugin对应的代码: case AUDIO_PLUGIN_SET_SPKR_L_EQ_PARAMETER_MSG: { AUDIO_PLUGIN_SET_SPKR_L_EQ_PARAMETER_MSG_T* eq_message = (AUDIO_PLUGIN_SET_SPKR_L_EQ_PARAMETER_MSG_T*)message ; CsrA2dpDecoderSetSpkrLEqParameter(&eq_message->param); } break; void CsrA2dpDecoderSetSpkrLEqParameter(const audio_plugin_spkr_eq_param_t* param) { uint16 param_id; uint16 param_value; bool recalcalculate_coefficients = TRUE; param_id = csrA2dpDecoderMakeParamId(param->id.bank, param->id.band, param->id.param_type); param_value = (uint16)param->value; KALIMBA_SEND_MESSAGE(KALIMBA_SET_SPKR_L_EQ_PARAM, param_id, param_value, (uint16)recalcalculate_coefficients, 0); } DSP工程中对应的代码: //消息结构体 .VAR set_spkr_l_eq_param_message_struct[$message.STRUC_SIZE]; //消息处理函数 //------------------------------------------------------------------------------ .module $M.music_example_message.SetSpkrLEqParamMsg; //------------------------------------------------------------------------------ // receives user eq parameter update message. // If "update" field is non-zero, the coefficient calculation routine is run //------------------------------------------------------------------------------ // Parameter is sent as a short message // <> //------------------------------------------------------------------------------ // On entry // r0 = message ID (ignored) // r1 = parameter ID // r2 = value // r3 = update // r4 = unused //------------------------------------------------------------------------------ .datasegment dm ; .codesegment VM_MESSAGE_PM ; func: $push_rLink_macro ; r0 = r1; // r0 = paramID r7 = &$M.system_config.data.SpkrEqDefnTable; call $user_eq.calcParamAddrOffset; // on exit, r0 = ParamAddrOffset r0 = r0 + ($M.system_config.data.CurParams + $M.MUSIC_MANAGER.PARAMETERS.OFFSET_CONFIG); r2 = r2 and 0x00ffff; m[r0] = r2; // if update flag is zero then exit now and don't recalculate coefficients null = r3 - 0; if eq jump $pop_rLink_and_rts ; r0 = r1; r1 = &$M.system_config.data.SpkrEqCoefsA; r2 = &$M.system_config.data.SpkrEqCoefsB; r3 = ($M.system_config.data.CurParams + $M.MUSIC_MANAGER.PARAMETERS.OFFSET_CONFIG); call $user_eq.calcBandCoefs; jump $pop_rLink_and_rts ; .endmodule ; //注册消息 &$M.music_example_message.set_spkr_l_eq_param_message_struct, $music_example.GAIAMSG.SET_SPKR_L_PARAM, 接着是读取EQ参数的消息链路: VMAudio PluginDSPMessageSendConditionallyOnTaskKalimbaSendMessage读取指定滤波器的系数,准备消息内容message.send_shortMessageSendVMAudio PluginDSP VM层的代码: void get_spkr_l_eq_param(uint16 band, eq_param_type_t param_type) { audio_plugin_spkr_eq_param_id_t param; if (sinkAudioGetRoutedAudioTask()) { param.bank = 1; param.band = band; param.param_type = param_type; AudioGetSpkrLEqParameter(sinkAudioGetRoutedAudioTask(), ¶m, &theSink.task); } } case AUDIO_GET_USER_EQ_PARAMETER_CFM: { AUDIO_GET_USER_EQ_PARAMETER_CFM_T* get_user_eq_resp_msg = (AUDIO_GET_USER_EQ_PARAMETER_CFM_T *)message; GaiaHandleGetUserEqParamResponse(get_user_eq_resp_msg); } break; Audio Plugin的代码: void CsrA2dpDecoderGetSpkrLEqParameter(audio_plugin_spkr_eq_param_id_t* param_id) { uint16 param = csrA2dpDecoderMakeParamId(param_id->bank, param_id->band, param_id->param_type); KalimbaSendMessage(KALIMBA_GET_SPKR_L_EQ_PARAM, param, 0, 0, 0); } case KALIMBA_GET_USER_EQ_PARAM_RESP: { MAKE_AUDIO_MESSAGE(AUDIO_GET_USER_EQ_PARAMETER_CFM, user_eq_message); PRINT(("DECODER: User EQ Param from DSP: [%x][%x] ", m->a, m->b)); user_eq_message->data_valid = TRUE; user_eq_message->param[0].id.bank = (uint16)((m->a >> 8) & 0x000f); user_eq_message->param[0].id.band = (uint16)((m->a >> 4) & 0x000f); user_eq_message->param[0].id.param_type = (uint16)(m->a & 0x000f); user_eq_message->param[0].value = m->b; MessageSend(decoder->app_task, AUDIO_GET_USER_EQ_PARAMETER_CFM, user_eq_message); } break; DSP工程的代码: // 创建消息结构体 .VAR get_spkr_l_eq_param_message_struct[$message.STRUC_SIZE]; // 消息处理函数 //------------------------------------------------------------------------------ .module $M.music_example_message.GetSpkrLEqParamMsg; //------------------------------------------------------------------------------ // request user eq parameter message. // return message contains requested parameter //------------------------------------------------------------------------------ // Parameter is sent as a short message // <> <> <> // Reply is sent as a short message // <> <> //------------------------------------------------------------------------------ // On entry // r0 = message ID (ignored) // r1 = parameter ID // r2 = unused // r3 = unused // r4 = unused //------------------------------------------------------------------------------ .datasegment dm ; .codesegment VM_MESSAGE_PM ; func: $push_rLink_macro ; r3 = r1; // word 1 of return message (parameter ID) r0 = r1; // r0 = paramID r7 = &$M.system_config.data.SpkrEqDefnTable; call $user_eq.calcParamAddrOffset; // on exit, r0 = ParamAddrOffset r0 = r0 + ($M.system_config.data.CurParams + $M.MUSIC_MANAGER.PARAMETERS.OFFSET_CONFIG); r4 = m[r0]; // word 2 (value) r5 = 0; // word 3 r6 = 0; // word 4 r2 = $music_example.GAIAMSG.GET_SPKR_L_PARAM_RESP; call $message.send_short; jump $pop_rLink_and_rts ; .endmodule ; // 注册消息 &$M.music_example_message.get_spkr_l_eq_param_message_struct, $music_example.GAIAMSG.GET_SPKR_L_PARAM, $M.music_example_message.GetSpkrLEqParamMsg.func, $message.register_handler, 至此消息链路已准备就绪,调用VM层的Set或Get函数即可读写DSP的EQ参数设置,并能在播放过程中立即生效。

3.3. 开发matlab GUI调音工具

由于UFE工具无法显示我们新增加的R Speaker EQ,因此需要定制一个调音工具,界面如下:
在这里插入图片描述
调音界面主要分四部分:
  • 连接区域:有连接DSP,刷新参数,设置参数三个按键
  • log显示区域:用来记录并显示一些操作信息
  • 左声道EQ:用来显示和修改当前L Speaker EQ(之前的Speaker EQ)的所有参数
  • 右声道EQ:用来显示和修改当前R Speaker EQ的所有参数
工具的后台调用了ADK提供的matlab接口库,代码路径是ADK oolsmatlabwin64,有兴趣的读者可以自学一下。

4. 总结

在熟练掌握CSR867X的硬软件架构后,可以快速开发出通用ADK所没有的特 {MOD}功能,有助于提升产品的竞争力。