为了让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参数的消息链路:
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参数的消息链路:
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}功能,有助于提升产品的竞争力。