基于MAX78000实现MP3播放器
使用MAX78000FTHR开发板制作一个MP3音乐播放器。
标签
嵌入式系统
MAX78000
LIBMAD
MP3播放器
wakojosin
更新2023-01-31
800

项目介绍

    本次最终决定使用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的数字音频接口数据示例如下:

FhZEutUtN1UVScNBYEFKay8wy9zg

MAX78000的I2S接口的数据示例如下:

Fi19FkBonpN5a-vV4zsk2laSe2S1

通过的硬件的了解可以知道软件需要怎么配置,只要根据文档里响应的说明进行一直的配置,就可以让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=单声道

 

位率表:

FiliyQ_zuP4EDRfkMK00lNx5B6N7

采样频率表:

FgYxWVDbgNYLPyOBzeEZMU2y-MQm

例子:

Fvww8cFrgUZwdpY1jKl8myMJbNZY

如上图标出了TAG_V2和标签帧,截图最后是附带的图片,数据有50多K数据。再后面通过同步标志FFFB判断数据帧的开启位置。

FuaXuv1f7tOkQQIwZdbVA7B2snky

前四字节是帧头,接着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芯片数据传输的了解,从而确保数据的一致性,保证了音乐播放的正确性。

未来计划

经过本次活动,了解了音频解码相关的一些知识,接下去计划做一些语音处理、语音识别相关的尝试,未来根据语音处理相关的经验尝试更多的模拟信号处理,比如震动特征识别、零点跟踪等方向。

附件下载
MAX78000FTHR_MP3.zip
源码
团队介绍
个人
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号