项目介绍
本次最终决定使用MAX78000FTHR开发板制作一个MP3播放器。
最新代码请移步我的代码仓:https://github.com/Vandoul/max78000_mp3.git
项目设计思路
基于SD卡demo,移植libmad解码库实现一个mp3音频的解码,然后驱动MAX9876进行音乐播放。
搜集素材的思路
硬件相关
主要用到的硬件有SD卡、MAX9867音频编解码芯片、以及MAX78000的I2C和I2S外设。
MAX9867是一款超低功耗立体声音频编解码芯片,可以用于手机、音乐播放器等产品上。该芯片有立体声差分麦克风输入功能,可以连接模拟或数字麦克分进行音频数据的采集。
该芯片还有立体声耳机放大器,支持差分、单端以及无滤波电容的输出配置。供电方面MAX9867采用1.8V单电源供电,支持1.65至3.6V逻辑电平,通过I2C进行配置。MAX9867的数字音频接口数据示例如下:
MAX78000的I2S接口的数据示例如下:
通过的硬件的了解可以知道软件需要怎么配置,只要根据文档里响应的说明进行一直的配置,就可以让MAX78000和MA9867进行正确的数据交互了。
软件相关
通过网上搜索资料了解了MP3格式。参考的资料有:
https://en.wikipedia.org/wiki/ID3
https://blog.csdn.net/e28sean/article/details/8588434
https://blog.csdn.net/bbdxf/article/details/7436185
https://blog.csdn.net/bbdxf/article/details/7438006
https://blog.csdn.net/wlsfling/article/details/5875959
http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html
MP3文件由帧构成,帧是最小组成单位。帧有3种,TAG_V2标签帧、数据帧和TAG_V1标签帧。
MP3文件的标签帧有ID3V1和ID3V2两个版本。ID3V2又分1,2,3,4四个版本。而常用的格式是ID3V2.3,下面主要介绍一下ID3V2.3的格式。
首先说一下ID3V1和ID3V2的区别。
ID3V1保存在MP3文件尾部,固定是128字节,以"TAG"这三个字符开头,格式如下:
{
char Header[3]; /* 标签头,固定是"TAG" */
char Title[30]; /* 标题 */
char Artist[30]; /* 作者 */
char Album[30]; /* 专集 */
char Year[4]; /* 4位数发布年代 */
char Comment[30]; /* 备注,28或30字节长度 */
char Genre; /* 类型,具体类型需要查表预定义的类型表 */
}
扩展TAG如下,位于TAG前面,固定长度是227字节:
{
char Header[4]; /* 标签头,固定是"TAG+" */
char Title[60]; /* 标题 */
char Artist[60]; /* 作者 */
char Album[60]; /* 专集 */
char Speed; /* 0=未设置,1=慢速,2=中速,3=快速,4=hardcore */
char Genre[30]; /* 类型,字符串 */
char StartTime[6]; /* 音乐开始时间,格式mmm:ss */
char EndTime[6]; /* 音乐结束时间,格式mmm:ss */
}
ID3V2保存在文件前面,格式如下:
标签头:
{
char Header[3]; /* 标签头,固定是"ID3" */
char Ver; /* 版本号,3=ID3V2.3,4=ID3V2.4 */
char Revision; /* 副版本号 */
char Flag; /* 存放标志的字节,这个版本只定义了三位即abc00000
,a=是否不同步,b=是否有扩展头部,c=是否为测试标签 */
char Size[4]; /* 标签大小,每字节最高位始终是0,即有效位数是28位。
包括标签帧和标签头,减去扩展标签头的10个字节 */
}
大小计算:
size = (Size[0] << 21) + (Size[1] << 14) + (Size[2] << 7) + Size[3]
标签帧:
{
char FrameID[4]; /* 用四个字节标识一个帧,说明器内容,参考对照表 */
char Size[4]; /* 帧内容的大小,不包括帧头,不小于1 */
char Flags[2]; /* 存放标志,只定义了6位,稍后详细解说 */
}
对照表:
TIT2=标题,表示内容为歌曲标题
TPE1=作者
TALB=专集
TRCK=音轨
TYER=年代
TCON=类型
COMM=备注
等等,更多参考协议。
大小计算:
size = (Size[0] << 24) + (Size[1] << 16) + (Size[2] << 8) + Size[3]
标识:
abc00000 ijk00000
a=标签保护标志,设置时认为此帧作废
b=文件保护标志,设置时认为此帧作废
c=只读标志,设置认为此帧不能修改
i=压缩标志,设置时一个字节存放两个BCD码表示数字
j=加密标志,(基本没用)
k=组标志,设置说明此帧和其他某帧是一组
数据帧:
{
uint32_t fsync:11; //同步信息,全是1
uint32_t mpegVer:2; //版本
uint32_t layer:2; //层
uint32_t crc:1; //CRC校验
uint32_t bitrateIndex:4; //位率
uint32_t samplingRateFreq:2; //采样频率
uint32_t paddingBit:1; //帧长调节
uint32_t privateBit:1; //保留字
uint32_t channelMode:2; //声道模式
uint32_t modeExtension:2; //扩展模式
uint32_t copyright:1; //版权
uint32_t original:1; //原版标志
uint32_t emphasis:2; //强调模式
}
版本:0=MPEG2.5,1=保留,2=MPEG2,3=MPEG1
层:0=保留,1=Layer3,2=Layer2,3=Layer1
CRC:0=CRC保护,1=无CRC
位率:参考位率表
采样频率:参考采样频率表
帧长调节:1=
声道模式:0=Stereo立体声,1=Joint stereo,2=双声道,3=单声道
位率表:
采样频率表:
例子:
如上图标出了TAG_V2和标签帧,截图最后是附带的图片,数据有50多K数据。再后面通过同步标志FFFB判断数据帧的开启位置。
前四字节是帧头,接着32字节是信道信息,后面是帧内容。
FF FB E0 00 = 0b1111_1111_1111_1011_1110_0000_0000_0000
版本:3=MPEG1;层:1=Layer3;CRC:1=不校验;
位率:14=320Kbps;采样频率:0=44.1KHz;帧长调整:0=无调整
声道:0=立体声Stereo;扩充模式:0=无;版权:0=不合法
原版标志:0=非原版;强调方式:0
MPEG1,Layer1:
帧长度=(48000*Bitrate)/Sampling_freq + Padding
MPEG1,Layer2/3:
帧长度=(144000*Bitrate)/Sampling_freq + Padding
MPEG2/2.5,Layer1:
帧长度=(24000*Bitrate)/Sampling_freq + Padding
MPEG2/2.5,Layer2/3:
帧长度=(72000*Bitrate)/Sampling_freq + Padding
准备过程
可选的解码库有Helix和libmad,我选用的是libmad。
libmad工程里面有个minimad.c,这个就是使用的例子,下面对于关键代码简单说明一下。
该例程是通过对音频数据文件进行map,直接进行了内存映射,所以可以一次性读取出mp3的数据,然后在解码中直接进行解码,完了之后进行输出就可以了。一首MP3歌曲通常是几MB或是几十MB的大小,对于单片机来说这个过程是无法实现的,因为单片机本身没有这么大的内存,所以在具体的实现中需要分批次进行数据的读取。
首先是mad_decode_init初始化以及其所需的相关回调函数:
static
int decode(unsigned char const *start, unsigned long length)
{
struct buffer buffer;
struct mad_decoder decoder;
int result;
/* initialize our private message structure */
//这是私有类型,在input里面使用
buffer.start = start;
buffer.length = length;
/* configure input, output, and error functions */
// 初始化mad_decoder结构体
mad_decoder_init(&decoder, &buffer,
input, 0 /* header */, 0 /* filter */, output,
error, 0 /* message */);
/* start decoding */
// 开始解码,内部会调用初始化传入的input、output、error等接口来传递相关的数据或信息。
result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
/* release the decoder */
//解码完成,释放资源
mad_decoder_finish(&decoder);
return result;
}
static
enum mad_flow input(void *data,
struct mad_stream *stream)
{
struct buffer *buffer = data;
//如果没有数据了,则返回结束状态
if (!buffer->length)
return MAD_FLOW_STOP;
//将输入装载进stream中,用于解码使用
mad_stream_buffer(stream, buffer->start, buffer->length);
buffer->length = 0;
//流没有结束则返回继续状态
return MAD_FLOW_CONTINUE;
}
static
enum mad_flow output(void *data,
struct mad_header const *header,
struct mad_pcm *pcm)
{
unsigned int nchannels, nsamples;
mad_fixed_t const *left_ch, *right_ch;
/* pcm->samplerate contains the sampling frequency */
//pcm中包含了通道数、数据长度以及通道数据
nchannels = pcm->channels;
nsamples = pcm->length;
left_ch = pcm->samples[0];
right_ch = pcm->samples[1];
while (nsamples--) {
signed int sample;
/* output sample(s) in 16-bit signed little-endian PCM */
//通过scale进行数据处理和精度转换,putchar模拟了播放
sample = scale(*left_ch++);
putchar((sample >> 0) & 0xff);
putchar((sample >> 8) & 0xff);
if (nchannels == 2) {
sample = scale(*right_ch++);
putchar((sample >> 0) & 0xff);
putchar((sample >> 8) & 0xff);
}
}
return MAD_FLOW_CONTINUE;
}
static
enum mad_flow error(void *data,
struct mad_stream *stream,
struct mad_frame *frame)
{
struct buffer *buffer = data;
//输出解码过程中的错误信息
fprintf(stderr, "decoding error 0x%04x (%s) at byte offset %u\n",
stream->error, mad_stream_errorstr(stream),
stream->this_frame - buffer->start);
/* return MAD_FLOW_BREAK here to stop decoding (and propagate an error) */
return MAD_FLOW_CONTINUE;
}
然后是核心的run函数,下面使用run_sync进行分析,其主要执行过程就是通过input回调函数进行stream数据对象的设置,然后在mad_frame_decode中进行解码,在mad_synth_frame中进行PCM数据合成,最后通过output回调函数进行PCM数据的输出。具体代码如下,主要的代码有注释进行说明:
static
int run_sync(struct mad_decoder *decoder)
{
enum mad_flow (*error_func)(void *, struct mad_stream *, struct mad_frame *);
void *error_data;
int bad_last_frame = 0;
struct mad_stream *stream;
struct mad_frame *frame;
struct mad_synth *synth;
int result = 0;
//input必须要有
if (decoder->input_func == 0)
return 0;
//error可选,没有的话就用默认的
if (decoder->error_func) {
error_func = decoder->error_func;
error_data = decoder->cb_data;
}
else {
error_func = error_default;
error_data = &bad_last_frame;
}
stream = &decoder->sync->stream;
frame = &decoder->sync->frame;
synth = &decoder->sync->synth;
//初始化状态
mad_stream_init(stream);
mad_frame_init(frame);
mad_synth_init(synth);
mad_stream_options(stream, decoder->options);
do {
//调用input读取待解码数据
switch (decoder->input_func(decoder->cb_data, stream)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
continue;
case MAD_FLOW_CONTINUE:
break;
}
while (1) {
//解码帧头,minimad未提供此接口
if (decoder->header_func) {
if (mad_header_decode(&frame->header, stream) == -1) {
if (!MAD_RECOVERABLE(stream->error))
break;
switch (error_func(error_data, stream, frame)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
case MAD_FLOW_CONTINUE:
default:
continue;
}
}
switch (decoder->header_func(decoder->cb_data, &frame->header)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
continue;
case MAD_FLOW_CONTINUE:
break;
}
}
//解码帧数据
if (mad_frame_decode(frame, stream) == -1) {
if (!MAD_RECOVERABLE(stream->error))
break;
switch (error_func(error_data, stream, frame)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
break;
case MAD_FLOW_CONTINUE:
default:
continue;
}
}
else
bad_last_frame = 0;
//过滤操作,minimad未提供此接口
if (decoder->filter_func) {
switch (decoder->filter_func(decoder->cb_data, stream, frame)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
continue;
case MAD_FLOW_CONTINUE:
break;
}
}
//合成PCM数据
mad_synth_frame(synth, frame);
//输出
if (decoder->output_func) {
switch (decoder->output_func(decoder->cb_data,
&frame->header, &synth->pcm)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
case MAD_FLOW_CONTINUE:
break;
}
}
}
}
while (stream->error == MAD_ERROR_BUFLEN);
fail:
result = -1;
done:
mad_synth_finish(synth);
mad_frame_finish(frame);
mad_stream_finish(stream);
return result;
}
实现过程
主逻辑
解码逻辑参考的run_sync函数,主要有以下步骤:
1.通过readMP3_frame读取一帧MP3数据,同时得到了采样率;
2.通过input函数设置stream数据对象的数据帧地址等;
3.通过mad_frame_decode函数进行解码;
4.通过mad_synth_frame进行PCM数据的合成;
5.通过output函数进行I2S数据的合成、硬件的初始化、启动DMA传输、MP3数据帧的读取等
整个过程是对步骤2-5的重复。
下面是主逻辑的代码,主要步骤有注释。
int decode(const char *path)
{
// ID3V2帧格式定义
struct ID3V2_info info;
// libmad 3个数据对象定义
struct mad_stream stream;
struct mad_frame frame;
struct mad_synth synth;
int result = 0;
FRESULT err; //FFat Result (Struct)
FIL file;
if ((err = f_open(&file, (const TCHAR*)path, FA_READ)) != FR_OK) {
myprintf("Error opening file: %s\n", FF_ERRORS[err]);
return -1;
}
myprintf("File opened!%d\n", f_tell(&file));
do {
// 读取MP3文件格式
if(loadMP3_info(&file, &info)) {
myprintf("load info failed!\n");
break;
} else {
myprintf("ver:%d, flag:%02x, size:%d(0x%x)\n", info.ver, info.flag, info.size, info.size);
}
/* initialize our private message structure */
/* init frame buffer. */
int bitrate, simplingrate;
readBuff.f = &file;
readBuff.size = readMP3_frame(&file, readBuff.buff, &bitrate, &simplingrate); // 读取第一帧MP3
readBuff.is_first_frame = 1;
// 初始化libmad数据对象
mad_stream_init(&stream);
mad_frame_init(&frame);
mad_synth_init(&synth);
// 初始化flag
mad_stream_options(&stream, 0);
initBuff();
while(1) {
// input,设置stream数据对象,用于后面解码
switch(input(&readBuff, &stream)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_CONTINUE:
break;
case MAD_FLOW_IGNORE:
default:
continue;
}
// decode,解码数据
if (mad_frame_decode(&frame, &stream) == -1) {
if (!MAD_RECOVERABLE(stream.error))
break;
switch (error(&readBuff, &stream, &frame)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
break;
case MAD_FLOW_CONTINUE:
default:
continue;
}
}
// 合成PCM数据
mad_synth_frame(&synth, &frame);
// 播放PCM数据
switch(output(&readBuff, &frame.header, &synth.pcm)) {
case MAD_FLOW_STOP:
goto done;
case MAD_FLOW_BREAK:
goto fail;
case MAD_FLOW_IGNORE:
break;
case MAD_FLOW_CONTINUE:
default:
continue;
}
}
fail:
result = -1;
done:
// 清除数据对象占用的空间
mad_synth_finish(&synth);
mad_frame_finish(&frame);
mad_stream_finish(&stream);
} while(0);
myprintf("decode end\n");
f_close(&file);
return result;
}
播放函数
播放函数相对简单,结合注释基本上可以理解的。具体代码如下:
static enum mad_flow output(void *data,
struct mad_header const *header,
struct mad_pcm *pcm)
{
struct read_buffer *buff = (struct read_buffer *)data;
unsigned int channels, nsamples;
mad_fixed_t const *left_ch, *right_ch;
struct frame_buffer *fbuff = NULL;
do {
fbuff = getEmptyBuff(); //获取一个空的缓存数据
} while(fbuff == NULL);
/* pcm->samplerate contains the sampling frequency */
(void)channels;
channels = pcm->channels;
nsamples = pcm->length;
left_ch = pcm->samples[0];
right_ch = pcm->samples[1];
for(int i=0; i< nsamples; i++) {
fbuff->buff[i] = scale_combine(*left_ch++, *right_ch++); // 将PCM数据写入缓存中
}
nsamples = sizeof(uint16_t)*nsamples;
// myprintf("<%d,%d>\n", fbuff->index, (int)nsamples);
fbuff->bytes = nsamples;
setBuffReady(fbuff);
if(buff->is_first_frame) {
buff->is_first_frame = 0;
max9867_hwif_init(header->samplerate, BITS_PER_CHANNEL); // 第一次调用时初始化硬件
}
if(!dma_is_run) {
channels = MXC_I2S_TXDMAConfig(fbuff->buff, nsamples); // 启动DMA传输
setBuffChannel(fbuff, channels);
dma_is_run = 1;
// puts("#");
myprintf("+<%d,%d,%d>\n", fbuff->index, (int)channels, (int)nsamples);
printBuffFlags();
}
// load new data.
buff->size = readMP3_frame(buff->f, &buff->buff[0], NULL, NULL); // 从SD卡读取一帧MP3数据帧
if(buff->size < 0) {
buff->size = readMP3_frame(buff->f, &buff->buff[0], NULL, NULL);
if(buff->size < 0) {
buff->size = 0;
}
}
return MAD_FLOW_CONTINUE;
}
跳过标签
由于标签的存在会影响正常的播放,所以加入了标签跳过函数。
#define MP3_TAG_CHECK(tag) (((tag)[0] == 'T') && \
((((tag)[1]>='0') && ((tag)[1]<='9')) || (((tag)[1]>='A') && ((tag)[1]<='Z'))) && \
((((tag)[2]>='0') && ((tag)[2]<='9')) || (((tag)[2]>='A') && ((tag)[2]<='Z'))) && \
((((tag)[3]>='0') && ((tag)[3]<='9')) || (((tag)[3]>='A') && ((tag)[3]<='Z'))))
void jumpMP3_tag(FIL *f)
{
struct ID3V2_tag tag;
UINT size;
int err = FR_OK;
while(1) {
f_read(f, &tag, sizeof(struct ID3V2_tag), &size);
if(FR_OK != err) {
myprintf("Error reading file: %s\n", FF_ERRORS[err]);
return ;
}
if(size != sizeof(struct ID3V2_tag)) {
myprintf("Read tag failed!");
f_lseek(f, f_tell(f) - size);
return ;
}
if(MP3_TAG_CHECK(tag.tag)) { //如果是一个TAG,就跳过它
size = tag.size[0]<<24|tag.size[1]<<16|tag.size[2]<<8|tag.size[3];
f_lseek(f, f_tell(f) + size);
continue;
}
// 如果不是TAG,就调整文件指针后退出
f_lseek(f, f_tell(f) - size);
break;
}
}
小结
在制作的过程中,主要是解决一下几个问题的过程:
首先是对MP3数据帧的了解,通过MP3数据帧格式的了解,就可以从MP3文件中读取MP3数据帧了,从而得到采样率等相关参数,可以用来初始化硬件;
然后是对libmad的PCM数据的了解,合成的PCM原数据是需要通过scale函数进行转换,原来不知道这个函数的作用,自己手动对PCM进行组装放进DMA缓存中进行播放,发现根本播放不了,所以需要先进行scale再进行左右声道数据的组装。
最后是对MAX78000的I2S数据传输的了解以及对MAX9867芯片数据传输的了解,从而确保数据的一致性,保证了音乐播放的正确性。
未来计划
经过本次活动,了解了音频解码相关的一些知识,接下去计划做一些语音处理、语音识别相关的尝试,未来根据语音处理相关的经验尝试更多的模拟信号处理,比如震动特征识别、零点跟踪等方向。