12

再做个播放器

 3 years ago
source link: https://zhuanlan.zhihu.com/p/298500587
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前年做过一个八音盒, 实际上是个MP3播放器, 用STM32F103搭配VS1003硬解压, 存储用的是16M的W25Q128, 链接:

张浩:摇摇八音盒 zhuanlan.zhihu.com ya63u2Q.jpg!mobile

后来想想, STM32的性能足够实现MP3软解压了, VS1003完全可以不用; 16M容量也太小了, 放不下几首歌, 可以改用SDIO接口的NAND, 比如MKDV1GCL, 有128M的容量. DAC倒是可以用STM32的内置DAC, 不过既然有I2S还是利用上吧, 毕竟CS4344这么便宜. 再加个功放, 八脚的FM8002, LM4890S之类都行. 最后, 加一片加速度传感器LIS3DHTR用于实现手势控制, 齐了.

原理图如下, MCU用的是STM32F401RC, 主频84M, Cortex-M4核, 比F103的性能强一些. PS. I2S的MCLK这里又踩坑了, F4系列引入了I2SEXT, 于是MCLK被从PC14移到了旁边的PC6. 没办法, 只好飞线了.

qqyQJnm.jpg!mobile

好了, 软件才是大头. 首先是USB, SDIO, FATFS, 都是以前搞过的, 复制粘贴再微调即可. F10X的USB库实在烂到惨不忍睹, F0和F4的就顺眼多了. 至于F4的SDIO库, 基本和F10X的一样, 凑和用吧.

完成了插USB识别为U盘, 从串口命令行列出SD NAND上的文件并读取文件内容, 之后就可以进行下一步了. 用VS1003做播放器时不用考虑太多, 只要监视DREQ脚的状态, 报告没数据了给喂数据即可. 软解压则不同, 得先把整个流程先规划好.

网上找了几个例子, 都是一口气顺序执行, 打开要播放的文件, 设置DAC/I2S, 循环读取文件, 解码, 放进输出缓冲区, 然后播放等待, 播放等待; 直到缓冲区放完, 然后继续读文件, 如此重复直到文件读完. 这样播放起来简单是简单, 然而MCU卡死在这里, 串口之类完全没法响应了; 以及, 硬件操作和业务逻辑完全混在一起, 这样的代码既没法维护也没法移植.

比较好的做法是什么呢? 把硬件和逻辑分开, 让解码和播放异步运行. 我们先实现一个和具体文件类型和硬件都无关的播放逻辑. 和硬件也不能算完全无关, 当年在8位AVR上播放WAV需要实现一个FIFO, 一边往里面放数据, 一边在中断里读出数据送到PWM或R2R DAC; 现在在STM32上则用DMA写DAC或I2S就行了. STM32的DMA提供半完成和全完成两个中断标志, 所以双缓冲实现起来也简单了, 把缓冲区的前后两半当作两个缓冲区就行. 打开, 关闭文件, 读取数据到输出缓冲区, 以及启动和停止DMA的操作都作为回调函数由上层提供, 主循环里执行zplay_poll(). 具体代码如下.

#include "zplay.h"

static struct {
    zplay_cfg_t cfg;
    int irq_flag_ht, irq_flag_tc;
} g;

void zplay_init(zplay_cfg_t* cfg)
{
    g.cfg = *cfg;
}

void zplay_stop(void)
{
    g.cfg.dma_write_stop_f();
    g.cfg.close_f();
}

void zplay_poll(void)
{
    if(g.irq_flag_ht) {
        g.cfg.read_f(g.cfg.buf);
        g.irq_flag_ht = 0;
    }
    if(g.irq_flag_tc) {
        g.cfg.read_f(g.cfg.buf + g.cfg.buf_size / 2);
        g.irq_flag_tc = 0;
    }
}

void zplay_isr_ht(void)
{
    g.irq_flag_ht = 1;
}

void zplay_isr_tc(void)
{
    g.irq_flag_tc = 1;
}

void zplay_start(void)
{
    if(g.cfg.open_f() < 0)
        return;
    g.irq_flag_ht = 1;
    g.irq_flag_tc = 1;
    zplay_poll();
    g.cfg.dma_write_start_f(g.cfg.buf, g.cfg.buf_size);
}

现在先实现WAV的播放, WAV文件的基本信息都在文件头部, 所以要在open_f回调函数里读取文件头部, 根据读到的采样率来初始化I2S, 并跳过其他无关部分, 直到读到数据区起始位置为止. 在read_f回调函数里要做的就简单了, 只是简单读取数据写到输出缓冲区即可.

MP3就稍微麻烦一些了, 幸好有现成的libmad和helix两个库可以用. 先试试libmad, 按网上的说法把源码里几处malloc都改掉, 用静态数据代替. 结果这东西占内存实在是大了点, 64K RAM一下子用掉了50多K. 之后实现几个回调函数. MP3和WAV不一样, 每帧里都保存了采样率/通道数/位深度等信息, 这叫啥, 流媒体? 因此在open_f里只需要打开文件和初始化解码器, 在read_f里解码第一帧时再设置I2S. 好了, 拷几个MP3进来, 串口给命令, 果然出声了(省略调试过程若干).

不过感觉效果一般般, 能明显感觉到丢帧. 估计是哪里没优化好, 折腾半天, RAM实在是不够用了. 没办法, 只能换成据说占用资源比较少的helix库了. 再试播放, 这次效果好多了. 代码如下:

#include "platform.h"

#include "coder.h"
#include "mp3dec.h"

typedef unsigned long u32_t;
typedef unsigned short u16_t;

static struct {
    const char* fname;
    FIL f;
    FILINFO inf;
    HMP3Decoder decoder;
    MP3FrameInfo frame_info;
    int first_frame;
    int offset;
} g;

static void read_f(void* out_buf);
static void i2s_adjust(int sample_rate, int buf_size);

static int open_f(void)
{
    if(f_open(&g.f, g.fname, FA_READ) != FR_OK) {
        printf("Open file %s failed.\n", g.fname);
        return -1;
    }
    g.decoder = MP3InitDecoder();
    if(g.decoder == NULL) {
        printf("Failed to allocate memory.\n");
        return -2;
    }
    g.first_frame = 1;
    return 0;
}

static void close_f(void)
{
    // close ops
    f_close(&g.f);
    MP3FreeDecoder(g.decoder);
}

static void i2s_adjust(int sample_rate, int buf_size)
{
    DMA_Cmd(DMA1_Stream4, DISABLE);
    SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, DISABLE);
    I2S_InitTypeDef i2s_is;
    I2S_Cmd(SPI2, DISABLE);
    i2s_is.I2S_Standard = I2S_Standard_Phillips;
    i2s_is.I2S_DataFormat = I2S_DataFormat_16bextended;
    i2s_is.I2S_MCLKOutput = I2S_MCLKOutput_Enable;
    i2s_is.I2S_AudioFreq = sample_rate;
    i2s_is.I2S_CPOL = I2S_CPOL_High;
    i2s_is.I2S_Mode = I2S_Mode_MasterTx;
    I2S_Init(SPI2, &i2s_is);
    I2S_Cmd(SPI2, ENABLE);
    SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE);
    DMA_Cmd(DMA1_Stream4, ENABLE);

    zplay_cfg_t cfg;
    DataConfig_t* pdc = DC_Get();
    cfg.buf = pdc->data.out_buf;
    cfg.buf_size = buf_size;    //g.synth.pcm.length * 8;    //g.synth.pcm.length * 2 * 2;    //1152 * 2 * 2;
    memset(cfg.buf, 0, DC_OUT_BUF_SIZE);

    cfg.dma_write_start_f = I2S_Write_DMA;
    cfg.dma_write_stop_f = I2S_Write_DMA_Stop;
    cfg.open_f = open_f;
    cfg.close_f = close_f;
    cfg.read_f = read_f;
    zplay_init(&cfg);
}

static void read_f(void* out_buf)
{
    static size_t size;
    static int bytesLeft;
    static unsigned char* in_ptr;
//    static int pos = 0;

    DataConfig_t* pdc = DC_Get();

    while(1) {
        if(g.first_frame) {
            size = DC_IN_BUF_SIZE;
            in_ptr = pdc->data.in_buf;
            bytesLeft = 0;
            f_lseek(&g.f, 0);
        }
        else {
            memmove(pdc->data.in_buf, in_ptr, bytesLeft);
            in_ptr = pdc->data.in_buf + bytesLeft;
            size = DC_IN_BUF_SIZE - bytesLeft;
        }

//        int ret =
        f_read(&g.f, in_ptr, size, &size);
//        printf("%d\n", ret);

        if(f_eof(&g.f)) {
            zplay_stop();
            printf("Playback ends. #1\n");
            return;
        }

        if(size <= 0) {
            zplay_stop();
            printf("Playback ends. #2\n");
            return;
        }

        g.offset = MP3FindSyncWord(pdc->data.in_buf, DC_IN_BUF_SIZE);
        if(g.offset < 0) {
            continue;    // frame start not found, read another frame.
        }
        else {
            int ret = MP3GetNextFrameInfo(g.decoder, &g.frame_info,
                pdc->data.in_buf + g.offset);
            if(ret == ERR_MP3_INVALID_FRAMEHEADER) {
                continue;    // sync word found, but not the start of a frame
            }
            else {
                printf("$$$ %d\n", ret);
                if(g.first_frame) {

                    printf("%d %d %d\n", g.frame_info.samprate,
                        g.frame_info.outputSamps, g.frame_info.nChans);
                    int samprate = g.frame_info.samprate;
                    int outputSamps = g.frame_info.outputSamps;
                    int nchans = g.frame_info.nChans;

                    if(nchans == 1)
                        samprate /= 2;
                    i2s_adjust(samprate, outputSamps * 4);
                    g.first_frame = 0;
                }
                break;
            }
        }
    }

    // Decode a frame, assuming that sync word was already found.
    in_ptr = pdc->data.in_buf + g.offset;
    bytesLeft = DC_IN_BUF_SIZE - g.offset;
    int err = MP3Decode(g.decoder, ∈_ptr, &bytesLeft, out_buf, 0);
    if(err < 0) {
        printf("### %d %d %p \n", err, bytesLeft, in_ptr);
    }
    switch(err) {
        case ERR_MP3_INDATA_UNDERFLOW:
            zplay_stop();
            return;
        case ERR_MP3_MAINDATA_UNDERFLOW:
            //                ReadMoreMp3Data();
            break;
        case ERR_MP3_FREE_BITRATE_SYNC:
            zplay_stop();
            return;
        default:
//            zplay_stop();
            return;
    }
}

void play_mp3_init(const char* fname)
{
    DataConfig_t* pdc = DC_Get();

    g.fname = fname;
    zplay_cfg_t cfg;
    cfg.buf = pdc->data.out_buf;
    cfg.buf_size = 0;    //g.synth.pcm.length * 2 * 2;    //1152 * 2 * 2;
    cfg.dma_write_start_f = I2S_Write_DMA;
    cfg.dma_write_stop_f = I2S_Write_DMA_Stop;
    cfg.open_f = open_f;
    cfg.close_f = close_f;
    cfg.read_f = read_f;
    zplay_init(&cfg);
}

接下来考虑FLAC, 查了一下FLAC的RAM要求...至少得80多K, 算了, 还是放弃吧, 以后用192K的F407再做一次吧.

原理图和程序见github链接:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK