图41.3.1.1 音频播放实验程序流程图
41.3.2 I2S函数解析
ESP-IDF提供了一套API来配置I2S。要使用此功能需要导入必要的头文件
#include "driver/i2s.h"
#include "driver/i2s_std.h"
#include "driver/i2s_pdm.h"
接下来作者将介绍一些常用的ESP32-S3中的I2S函数这些函数的描述及其作用如下
1设置I2S引脚
该函数用给定的配置来配置I2S总线该函数原型如下所示
esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin);
该函数的形参描述如下表所示
参数 | 描述 |
| I2S端口号有I2S_NUM_0、I2S_NUM_1两个端口可供配置 |
| 指向I2S配置的指针 |
表41.3.2.1 i2s_set_pin()函数形参描述
该函数的返回值描述如下表所示
表41.3.2.2 函数i2s_set_pin ()返回值描述
该函数使用i2s_pin_config_t类型的结构体变量传入该结构体的定义如下所示
表41.3.2.3 i2s_pin_config_t结构体参数值描述
完成上述结构体参数配置之后可以将结构传递给 i2s_set_pin () 函数用以实例化IIC并返回IIC句柄。
2安装I2S驱动
该函数安装I2S驱动该函数原型如下所示
esp_err_t i2s_driver_install(i2s_port_t i2s_num,
const i2s_config_t *i2s_config,
int queue_size,
void *i2s_queue);
该函数的形参描述如下表所示
参数 | 描述 |
| I2S端口号有I2S_NUM_0、I2S_NUM_1两个端口可供配置 |
| I2S配置 |
| 事件队列大小/深度。 |
| 事件队列句柄 |
表41.3.2.4 i2s_driver_install()函数形参描述
该函数的返回值描述如下表所示
| |
ESP_OK | 成功 |
ESP_ERR_INVALID_ARG | 参数错误 |
ESP_ERR_NO_MEM | 内存不足 |
ESP_ERR_INVALID_STATE | 当前I2S端口被占用 |
表41.3.2.5 函数i2s_driver_install ()返回值描述
3处理缓冲区
该函数将TX DMA缓冲区的内容归零该函数原型如下所示
esp_err_t i2s_zero_dma_buffer(i2s_port_t i2s_num);
该函数的形参描述如下表所示
参数 | 描述 |
| I2S端口号有I2S_NUM_0、I2S_NUM_1两个端口可供配置 |
表41.3.2.6 i2s_zero_dma_buffer ()函数形参描述
该函数的返回值描述如下表所示
| |
ESP_OK | 成功 |
ESP_ERR_INVALID_ARG | 参数错误 |
表41.3.2.7 函数i2s_zero_dma_buffer ()返回值描述
41.3.3 音频播放驱动解析
在IDF版的30_music例程中作者在30_music \components\BSP路径下新增了一个I2S文件夹和一个ES8388文件夹分别用于存放i2s.c、i2s.h和es8388.c以及es8388.h这四个文件。其中i2s.h和es8388.h文件负责声明I2S以及ES8388相关的函数和变量而i2s.c和es8388.c文件则实现了I2S以及ES8388的驱动代码。下面我们将详细解析这四个文件的实现内容。
1i2s驱动
音乐文件我们要通过SD卡来传给单片机那我们自然要用到文件系统。LCD、按键交互这些我们也需要实现。 由于播放功能涉及到多个外设的配合使用用文件系统读音频文件做播放控制等所以我们把ES8388的硬件驱动放到components\BSP目录下播放功能作为APP放到main目录下。
这里我们只讲解核心代码详细的源码请大家参考光盘本实验对应源码I²S的驱动主要包括两个文件i2s.c和i2s.h。
除去I²S的管脚我们需要初始其它IO的模式我们在头文件sai.h中定义SAI的引脚方便如果IO变更之后作修改
#define I2S_NUM (I2S_NUM_0) /* I2S端口 */
#define I2S_BCK_IO (GPIO_NUM_46) /* 设置串行时钟引脚ES8388_SCLK */
#define I2S_WS_IO (GPIO_NUM_9) /* 设置左右声道的时钟引脚ES8388_LRCK */
#define I2S_DO_IO (GPIO_NUM_10) /* ES8388_SDOUT */
#define I2S_DI_IO (GPIO_NUM_14) /* ES8388_SDIN */
#define IS2_MCLK_IO (GPIO_NUM_3) /* ES8388_MCLK */
#define SAMPLE_RATE (44100) /* 采样率 */
接下来开始介绍i2s.c主要是I²S的初始化代码如下
/* I2S默认配置 */
#define I2S_CONFIG_DEFAULT() { \
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX), \
.sample_rate = SAMPLE_RATE, \
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, \
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, \
.communication_format = I2S_COMM_FORMAT_STAND_I2S, \
.intr_alloc_flags = 0, \
.dma_buf_count = 8, \
.dma_buf_len = 256, \
.use_apll = false \
}
/**
* @retval ESP_OK:初始化成功;其他:失败
*/
esp_err_t i2s_init(void)
{
esp_err_t ret_val = ESP_OK;
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCK_IO,
.ws_io_num = I2S_WS_IO,
.data_out_num = I2S_DO_IO,
.data_in_num = I2S_DI_IO,
.mck_io_num = IS2_MCLK_IO,
};
i2s_config_t i2s_config = I2S_CONFIG_DEFAULT();
i2s_config.sample_rate = SAMPLE_RATE;
i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
i2s_config.use_apll = true;
ret_val |= i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
ret_val |= i2s_set_pin(I2S_NUM, &pin_config);
ret_val |= i2s_zero_dma_buffer(I2S_NUM);
return ret_val;
}
/**
* @brief I2S TRX启动
* @param 无
* @retval 无
*/
void i2s_trx_start(void)
{
i2s_start(I2S_NUM);
}
/**
* @brief I2S TRX停止
* @param 无
* @retval 无
*/
void i2s_trx_stop(void)
{
i2s_stop(I2S_NUM);
}
/**
* @brief I2S卸载
* @param 无
* @retval 无
*/
void i2s_deinit(void)
{
i2s_driver_uninstall(I2S_NUM);
}
/**
* @brief 设置采样率
* @param sampleRate : 采样率
* @param bits_sample :位宽
* @retval 无
*/
void i2s_set_samplerate_bits_sample(int samplerate,int bits_sample)
{
i2s_set_clk(I2S_NUM,samplerate,bits_sample,I2S_CHANNEL_STEREO);
}
/**
* @brief I2S传输数据
* @param buffer: 数据存储区的首地址
* @param frame_size: 数据大小
* @retval 无
*/
size_t i2s_tx_write(uint8_t *buffer, uint32_t frame_size)
{
size_t bytes_written;
i2s_write(I2S_NUM, buffer, frame_size, &bytes_written, 100);
return bytes_written;
}
/**
* @brief I2S读取数据
* @param buffer: 读取数据存储区的首地址
* @param frame_size: 读取数据大小
* @retval 无
*/
size_t i2s_rx_read(uint8_t *buffer, uint32_t frame_size)
{
size_t bytes_written;
i2s_read(I2S_NUM, buffer, frame_size, &bytes_written, 1000);
return bytes_written;
}
函数i2s_init()完成初始化I²S该初始化不需要像I²C以及IO扩展芯片那样设置传参通过配置相关的结构体并安装I²S的驱动和配置I²S引脚以及将TX DMA缓冲区的内容归零。函数sai1_samplerate_set则是用前面介绍的查表法根据采样率来设置SAI的时钟。函数i2s_trx_start()用于启动I²S驱动在调用了i2s_driver_install()之后不需要调用这个函数(它是自动启动的)但是在调用了i2s_stop()之后调用该函数是必要的。而函数i2s_trx_stop()用于停止I²S驱动在调用i2s_driver_uninstall()之前不需要调用i2s_stop()。i2s_set_samplerate_bits_sample()用于设置I2S RX和TX的时钟和位宽度。函数i2s_tx_write()用于将数据写入I2S DMA传输缓冲区。函数i2s_rx_read()从I2S DMA接收缓冲区读取数据。以上是对I²S驱动文件下部分函数的功能概述具体内容请参照该驱动文件。
2ES8388驱动
ES8388主要用来将音频信号转换为数字信号或将数字信号转换为音频信号接下来我们开始介绍ES8388的几个函数代码如下
/**
* @brief ES8388初始化
* @param 无
* @retval 0,初始化正常
* 其他,错误代码
*/
uint8_t es8388_init(i2c_obj_t self)
{
esp_err_t ret_val = ESP_OK;
if (self.init_flag == ESP_FAIL)
{
/* 初始化IIC */
iic_init(I2C_NUM_0);
}
es8388_i2c_master = self;
/* 软复位ES8388 */
ret_val |= es8388_write_reg(0, 0x80);
ret_val |= es8388_write_reg(0, 0x00);
/* 等待复位 */
vTaskDelay(100);
ret_val |= es8388_write_reg(0x01, 0x58);
ret_val |= es8388_write_reg(0x01, 0x50);
ret_val |= es8388_write_reg(0x02, 0xF3);
ret_val |= es8388_write_reg(0x02, 0xF0);
/* 麦克风偏置电源关闭 */
ret_val |= es8388_write_reg(0x03, 0x09);
/* 使能参考 500K驱动使能 */
ret_val |= es8388_write_reg(0x00, 0x06);
/* DAC电源管理不打开任何通道 */
ret_val |= es8388_write_reg(0x04, 0x00);
/* MCLK不分频 */
ret_val |= es8388_write_reg(0x08, 0x00);
/* DAC控制 DACLRC与ADCLRC相同 */
ret_val |= es8388_write_reg(0x2B, 0x80);
/* ADC L/R PGA增益配置为+24dB */
ret_val |= es8388_write_reg(0x09, 0x88);
/* ADC数据选择为left data = left ADC, right data=left ADC音频数据为16bit */
ret_val |= es8388_write_reg(0x0C, 0x4C);
/* ADC配置 MCLK/采样率=256 */
ret_val |= es8388_write_reg(0x0D, 0x02);
/* ADC数字音量控制将信号衰减 L 设置为最小 */
ret_val |= es8388_write_reg(0x10, 0x00);
/* ADC数字音量控制将信号衰减 R 设置为最小 */
ret_val |= es8388_write_reg(0x11, 0x00);
/* DAC 音频数据为16bit */
ret_val |= es8388_write_reg(0x17, 0x18);
/* DAC 配置 MCLK/采样率=256 */
ret_val |= es8388_write_reg(0x18, 0x02);
/* DAC数字音量控制将信号衰减 L 设置为最小 */
ret_val |= es8388_write_reg(0x1A, 0x00);
/* DAC数字音量控制将信号衰减 R 设置为最小 */
ret_val |= es8388_write_reg(0x1B, 0x00);
/* L混频器 */
ret_val |= es8388_write_reg(0x27, 0xB8);
/* R混频器 */
ret_val |= es8388_write_reg(0x2A, 0xB8);
vTaskDelay(100);
if (ret_val != ESP_OK)
{
while(1)
{
printf("ES8388初始化失败\r\n");
vTaskDelay(500);
}
}
else
{
printf("ES8388初始化成功\r\n");
}
return 0;
}
/**
* @brief IIC写入函数
* @param slave_addr:ES8388地址
* @param reg_add:寄存器地址
* @param data:写入的数据
* @retval 无
*/
esp_err_t es8388_write_reg(uint8_t reg_addr, uint8_t data)
{
i2c_buf_t buf[2] = {
{.len = 1, .buf = ®_addr},
{.len = 1, .buf = &data},
};
i2c_transfer(&es8388_i2c_master, ES8388_ADDR >> 1, 2, buf, I2C_FLAG_STOP);
return ESP_OK;
}
/**
* @brief 读取数据
* @param reg_add:寄存器地址
* @param p_data:读取的数据
* @retval 无
*/
esp_err_t es8388_read_reg(uint8_t reg_addr, uint8_t *pdata)
{
i2c_buf_t buf[2] = {
{.len = 1, .buf = ®_addr},
{.len = 1, .buf = pdata},
};
i2c_transfer(&es8388_i2c_master, ES8388_ADDR >> 1, 2, buf, I2C_FLAG_WRITE|
I2C_FLAG_READ |
I2C_FLAG_STOP);
return ESP_OK;
}
/**
* @brief 设置ES8388工作模式
* @param fmt : 工作模式
* @arg 1, MSB(左对齐);
* @arg 2, LSB(右对齐);
* @arg 3, PCM/DSP
* @param len : 数据长度
* @arg 0, 24bit
* @arg 1, 20bit
* @arg 2, 18bit
* @arg 3, 16bit
* @arg 4, 32bit
* @retval 无
*/
void es8388_sai_cfg(uint8_t fmt, uint8_t len)
{
fmt &= 0x03;
len &= 0x07; /* 限定范围 */
es8388_write_reg(23, (fmt << 1) | (len << 3)); /* R23,ES8388工作模式设置 */
}
/**
* @brief 设置耳机音量
* @param volume : 音量大小(0 ~ 33)
* @retval 无
*/
void es8388_hpvol_set(uint8_t volume)
{
if (volume > 33)
{
volume = 33;
}
es8388_write_reg(0x2E, volume);
es8388_write_reg(0x2F, volume);
}
/**
* @brief 设置喇叭音量
* @param volume : 音量大小(0 ~ 33)
* @retval 无
*/
void es8388_spkvol_set(uint8_t volume)
{
if (volume > 33)
{
volume = 33;
}
es8388_write_reg(0x30, volume);
es8388_write_reg(0x31, volume);
}
/**
* @brief 设置3D环绕声
* @param depth : 0 ~ 7(3D强度,0关闭,7最强)
* @retval 无
*/
void es8388_3d_set(uint8_t depth)
{
depth &= 0x7; /* 限定范围 */
es8388_write_reg(0x1D, depth << 2); /* R7,3D环绕设置 */
}
/**
* @brief ES8388 DAC/ADC配置
* @param dacen : dac使能(1)/关闭(0)
* @param adcen : adc使能(1)/关闭(0)
* @retval 无
*/
void es8388_adda_cfg(uint8_t dacen, uint8_t adcen)
{
uint8_t tempreg = 0;
tempreg |= ((!dacen) << 0);
tempreg |= ((!adcen) << 1);
tempreg |= ((!dacen) << 2);
tempreg |= ((!adcen) << 3);
es8388_write_reg(0x02, tempreg);
}
/**
* @brief ES8388 DAC输出通道配置
* @param o1en : 通道1使能(1)/禁止(0)
* @param o2en : 通道2使能(1)/禁止(0)
* @retval 无
*/
void es8388_output_cfg(uint8_t o1en, uint8_t o2en)
{
uint8_t tempreg = 0;
tempreg |= o1en * (3 << 4);
tempreg |= o2en * (3 << 2);
es8388_write_reg(0x04, tempreg);
}
/**
* @brief ES8388 MIC增益设置(MIC PGA增益)
* @param gain : 0~8, 对应0~24dB 3dB/Step
* @retval 无
*/
void es8388_mic_gain(uint8_t gain)
{
gain &= 0x0F;
gain |= gain << 4;
es8388_write_reg(0x09, gain); /* R9,左右通道PGA增益设置 */
}
/**
* @brief ES8388 ALC设置
* @param sel
* @arg 0,关闭ALC
* @arg 1,右通道ALC
* @arg 2,左通道ALC
* @arg 3,立体声ALC
* @param maxgain : 0~7,对应-6.5~+35.5dB
* @param minigain: 0~7,对应-12~+30dB 6dB/STEP
* @retval 无
*/
void es8388_alc_ctrl(uint8_t sel, uint8_t maxgain, uint8_t mingain)
{
uint8_t tempreg = 0;
tempreg = sel << 6;
tempreg |= (maxgain & 0x07) << 3;
tempreg |= mingain & 0x07;
es8388_write_reg(0x12, tempreg); /* R18,ALC设置 */
}
/**
* @brief ES8388 ADC输出通道配置
* @param in : 输入通道
* @arg 0, 通道1输入
* @arg 1, 通道2输入
* @retval 无
*/
void es8388_input_cfg(uint8_t in)
{
es8388_write_reg(0x0A, (5 * in) << 4); /* ADC1 输入通道选择L/R INPUT1 */
}
以上代码中es8388_init函数用于初始化es8388这里只是通用配置ADC&DAC初始化完成后并不能正常播放音乐还需要通过es8388_adda_cfg函数使能DAC然后通过设置es8388_output_cfg选择DAC输出通过es8388_sai_cfg配置ES8388工作模式最后设置音量才可以接收I²S音频数据实现音乐播放。
3wavplay驱动
wavpaly主要用于wav格式的音频文件解码接下来看看wavplay.c里面的几个函数代码如下
/**
* @brief WAV解析初始化
* @param fname : 文件路径+文件名
* @param wavx : 信息存放结构体指针
* @retval 0,打开文件成功
* 1,打开文件失败
* 2,非WAV文件
* 3,DATA区域未找到
*/
uint8_t wav_decode_init(uint8_t *fname, __wavctrl *wavx)
{
FIL *ftemp;
uint8_t *buf;
uint32_t br = 0;
uint8_t res = 0;
ChunkRIFF *riff;
ChunkFMT *fmt;
ChunkFACT *fact;
ChunkDATA *data;
ftemp = (FIL*)mymalloc(sizeof(FIL));
buf = mymalloc(512);
if (ftemp && buf) /* 内存申请成功 */
{
res = f_open(ftemp, (TCHAR*)fname, FA_READ); /* 打开文件 */
if (res == FR_OK)
{
f_read(ftemp, buf, 512, (UINT *)&br); /* 读取512字节在数据 */
riff = (ChunkRIFF *)buf; /* 获取RIFF块 */
if (riff->Format == 0x45564157) /* 是WAV文件 */
{
fmt = (ChunkFMT *)(buf + 12); /* 获取FMT块 */
/*读取FACT块*/
fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);
if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
{
/* 具有fact/LIST块的时候(未测试)*/
wavx->datastart = 12 + 8 + fmt->ChunkSize+8+fact->ChunkSize;
}
else
{
wavx->datastart = 12 + 8 + fmt->ChunkSize;
}
data = (ChunkDATA *)(buf + wavx->datastart); /* 读取DATA块 */
if (data->ChunkID == 0x61746164) /* 解析成功! */
{
wavx->audioformat = fmt->AudioFormat; /* 音频格式 */
wavx->nchannels = fmt->NumOfChannels; /* 通道数 */
wavx->samplerate = fmt->SampleRate; /* 采样率 */
wavx->bitrate = fmt->ByteRate * 8; /* 得到位速 */
wavx->blockalign = fmt->BlockAlign; /* 块对齐 */
wavx->bps = fmt->BitsPerSample;/* 位数,16/24/32位 */
wavx->datasize = data->ChunkSize; /* 数据块大小 */
wavx->datastart = wavx->datastart + 8;/* 数据流开始的地方. */
printf("wavx->audioformat:%d\r\n", wavx->audioformat);
printf("wavx->nchannels:%d\r\n", wavx->nchannels);
printf("wavx->samplerate:%d\r\n", wavx->samplerate);
printf("wavx->bitrate:%d\r\n", wavx->bitrate);
printf("wavx->blockalign:%d\r\n", wavx->blockalign);
printf("wavx->bps:%d\r\n", wavx->bps);
printf("wavx->datasize:%d\r\n", wavx->datasize);
printf("wavx->datastart:%d\r\n", wavx->datastart);
}
else
{
res = 3; /* data区域未找到. */
}
}
else
{
res = 2; /* 非wav文件 */
}
}
else
{
res = 1; /* 打开文件错误 */
}
}
f_close(ftemp);
free(ftemp); /* 释放内存 */
free(buf);
return 0;
}
/**
* @brief 获取当前播放时间
* @param fname : 文件指针
* @param wavx : wavx播放控制器
* @retval 无
*/
void wav_get_curtime(FIL *fx, __wavctrl *wavx)
{
long long fpos;
wavx->totsec = wavx->datasize / (wavx->bitrate / 8); /* 歌曲总长度(单位:秒) */
fpos = fx->fptr-wavx->datastart; /* 得到当前文件播放到的地方 */
wavx->cursec = fpos*wavx->totsec / wavx->datasize; /* 当前播放到第多少秒了? */
}
/**
* @brief 播放某个wav文件
* @param fname : 文件路径+文件名
* @retval KEY0_PRES,错误
* KEY1_PRES,打开文件失败
* 其他,非WAV文件
*/
uint8_t wav_play_song(uint8_t *fname)
{
uint8_t key = 0;
uint8_t t = 0;
uint8_t res;
i2s_play_end = ESP_FAIL;
i2s_play_next_prev = ESP_FAIL;
g_audiodev.file = (FIL*)malloc(sizeof(FIL));
g_audiodev.tbuf = malloc(WAV_TX_BUFSIZE);
if (g_audiodev.file || g_audiodev.tbuf)
{
/* 得到文件的信息 */
res = wav_decode_init(fname, &wavctrl);
/* 解析文件成功 */
if (res == 0)
{
if (wavctrl.bps == 16)
{
/* 飞利浦标准,16位数据长度 */
es8388_sai_cfg(0, 3);
i2s_set_samplerate_bits_sample(wavctrl.samplerate,
I2S_BITS_PER_SAMPLE_16BIT);
}
else if (wavctrl.bps == 24)
{
/* 飞利浦标准,24位数据长度 */
es8388_sai_cfg(0, 0);
i2s_set_samplerate_bits_sample(wavctrl.samplerate,
I2S_BITS_PER_SAMPLE_24BIT);
}
audio_stop();
if (MUSICTask_Handler == NULL)
{
taskENTER_CRITICAL(&my_spinlock);
/* 创建任务1,任务函数 */
xTaskCreatePinnedToCore((TaskFunction_t )music,
/* 任务名称 */
(const char* )"music",
/* 任务堆栈大小 */
(uint16_t )MUSIC_STK_SIZE,
/* 传入给任务函数的参数 */
(void* )NULL,
/* 任务优先级 */
(UBaseType_t )MUSIC_PRIO,
/* 任务句柄 */
(TaskHandle_t* )&MUSICTask_Handler,
/* 该任务哪个内核运行 */
(BaseType_t ) 0);
taskEXIT_CRITICAL(&my_spinlock);
}
/* 打开文件 */
res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);
if (res == 0)
{
/* 开始音频播放 */
audio_start();
vTaskDelay(100);
audio_start();
vTaskDelay(100);
while (res == 0)
{
while (1)
{
if (i2s_play_end == ESP_OK)
{
res = KEY0_PRES;
break;
}
key = xl9555_key_scan(0);
/* 暂停 */
if (key == KEY3_PRES)
{
if ((g_audiodev.status & 0x0F) == 0x03)
{
audio_stop();
vTaskDelay(100);
}
else if ((g_audiodev.status & 0x0F) == 0x00)
{
audio_start();
vTaskDelay(100);
}
}
/* 下一曲/上一曲 */
if (key == KEY2_PRES || key == KEY0_PRES)
{
i2s_play_next_prev = ESP_OK;
vTaskDelay(100);
res = KEY0_PRES;
break;
}
/* 暂停不刷新时间 */
if ((g_audiodev.status & 0x0F) == 0x03)
{
/* 得到总时间和当前播放的时间 */
wav_get_curtime(g_audiodev.file, &wavctrl);
audio_msg_show(wavctrl.totsec,
wavctrl.cursec,
wavctrl.bitrate);
}
t++;
if (t == 20)
{
t = 0 ;
LED_TOGGLE();
}
if ((g_audiodev.status & 0x01) == 0)
{
vTaskDelay(10);
}
else
{
break;
}
}
/* 退出切换歌曲 */
if (key == KEY2_PRES || key == KEY0_PRES)
{
res = key;
break;
}
}
audio_stop();
}
else
{
res = 0xFF;
}
}
else
{
res = 0xFF;
}
}
else
{
res = 0xFF;
}
/* 释放内存 */
free(g_audiodev.tbuf);
/* 释放内存 */
free(g_audiodev.file);
return res;
}
以上代码中wav_decode_init函数用来对wav音频文件进行解析得到wav的详细信息音频采样率位数数据流起始位置等wav_play_song函数是播放WAV最终执行的函数该函数解析完WAV文件后设置ES8388和I²S的参数采样率位数等然后不断填充数据实现WAV播放该函数中还进行了按键检测实现上下曲切换和暂停/播放等操作。
4audioplay驱动
这部分我们需要根据ES8388推荐的初始化顺序时行配置。我们需要借助SD卡和文件系统把我们需要播放的歌曲传给ES8388播放。我们在User目录下新建一个《APP》文件夹同时在该目录下新建audioplay.c和audioplay.h并加入到工程。
首先判断音乐文件类型符合条件的再把相应的文件数据发送给ES8388我们在FATFS的扩展文件中已经实现了判断文件类型这个功能在图片显示实验也演示了这部分代码的使用我们把这个功能封装成了audio_get_tnum()函数这部分参考我们光盘源码即可。接下来我们来分析一下audio play()和audio_play_song ()函数实现播放歌曲的功能代码如下
/**
* @brief 播放音乐
* @param 无
* @retval 无
*/
void audio_play(void)
{
uint8_t res;
/* 目录 */
FF_DIR wavdir;
/* 文件信息 */
FILINFO *wavfileinfo;
/* 带路径的文件名 */
uint8_t *pname;
/* 音乐文件总数 */
uint16_t totwavnum;
/* 当前索引 */
uint16_t curindex;
/* 键值 */
uint8_t key;
uint32_t temp;
/* 音乐offset索引表 */
uint32_t *wavoffsettbl;
/* 开启DAC关闭ADC */
es8388_adda_cfg(1, 0);
/* DAC选择通道1输出 */
es8388_output_cfg(1, 1);
/* 打开音乐文件夹 */
while (f_opendir(&wavdir, "0:/MUSIC"))
{
text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
vTaskDelay(200);
/* 清除显示 */
lcd_fill(30, 190, 240, 206, WHITE);
vTaskDelay(200);
}
/* 得到总有效文件数 */
totwavnum = audio_get_tnum((uint8_t *)"0:/MUSIC");
/* 音乐文件总数为0 */
while (totwavnum == NULL)
{
text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
vTaskDelay(200);
/* 清除显示 */
lcd_fill(30, 190, 240, 146, WHITE);
vTaskDelay(200);
}
/* 申请内存 */
wavfileinfo = (FILINFO*)malloc(sizeof(FILINFO));
/* 为带路径的文件名分配内存 */
pname = malloc(255 * 2 + 1);
/* 申请4*totwavnum个字节的内存,用于存放音乐文件off block索引 */
wavoffsettbl = malloc(4 * totwavnum);
/* 内存分配出错 */
while (!wavfileinfo || !pname || !wavoffsettbl)
{
text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
vTaskDelay(200);
/* 清除显示 */
lcd_fill(30, 190, 240, 146, WHITE);
vTaskDelay(200);
}
/* 记录索引,打开目录 */
res = f_opendir(&wavdir, "0:/MUSIC");
if (res == FR_OK)
{
/* 当前索引为0 */
curindex = 0;
/* 全部查询一遍 */
while (1)
{
/* 记录当前index */
temp = wavdir.dptr;
/* 读取目录下的一个文件 */
res = f_readdir(&wavdir, wavfileinfo);
if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
{
break; /* 错误了/到末尾了,退出 */
}
res = exfuns_file_type(wavfileinfo->fname);
/* 取高四位,看看是不是音乐文件 */
if ((res & 0xF0) == 0x40)
{
/* 记录索引 */
wavoffsettbl[curindex] = temp;
curindex++;
}
}
}
/* 从0开始显示 */
curindex = 0;
/* 打开目录 */
res = f_opendir(&wavdir, (const TCHAR*)"0:/MUSIC");
/* 打开成功 */
while (res == FR_OK)
{
/* 改变当前目录索引 */
dir_sdi(&wavdir, wavoffsettbl[curindex]);
/* 读取目录下的一个文件 */
res = f_readdir(&wavdir, wavfileinfo);
if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
{
/* 错误了/到末尾了,退出 */
break;
}
/* 复制路径(目录) */
strcpy((char *)pname, "0:/MUSIC/");
/* 将文件名接在后面 */
strcat((char *)pname, (const char *)wavfileinfo->fname);
/* 清除之前的显示 */
lcd_fill(30, 190, lcd_self.width - 1, 190 + 16, WHITE);
audio_index_show(curindex + 1, totwavnum);
/* 显示歌曲名字 */
text_show_string(30, 190, lcd_self.width - 60, 16,
(char *)wavfileinfo->fname, 16, 0, BLUE);
/* 播放这个音频文件 */
key = audio_play_song(pname);
/* 上一曲 */
if (key == KEY2_PRES)
{
if (curindex)
{
curindex--;
}
else
{
curindex = totwavnum - 1;
}
}
/* 下一曲 */
else if (key == KEY0_PRES)
{
curindex++;
if (curindex >= totwavnum)
{
/* 到末尾的时候,自动从头开始 */
curindex = 0;
}
}
else
{
break; /* 产生了错误 */
}
}
/* 释放内存 */
free(wavfileinfo);
/* 释放内存 */
free(pname);
/* 释放内存 */
free(wavoffsettbl);
}
/**
* @brief 播放某个音频文件
* @param fname : 文件名
* @retval 按键值
* @arg KEY0_PRES , 下一曲.
* @arg KEY2_PRES , 上一曲.
* @arg 其他 , 错误
*/
uint8_t audio_play_song(uint8_t *fname)
{
uint8_t res;
res = exfuns_file_type((char *)fname);
switch (res)
{
case T_WAV:
res = wav_play_song(fname);
break;
case T_MP3:
/* 自行实现 */
break;
default: /* 其他文件,自动跳转到下一曲 */
printf("can't play:%s\r\n", fname);
res = KEY0_PRES;
break;
}
return res;
}
这里audio_play函数在main函数里面被调用该函数首先设置ES8388相关配置然后查找SD卡里面的MUSIC文件夹并统计该文件夹里面总共有多少音频文件统计包括WAV/MP3/APE/FLAC等然后该函数调用audio_play_song函数按顺序播放这些音频文件。
在audio_play_song函数里面通过判断文件类型调用不同的解码函数本章支持WAV文件通过wav_play_song函数实现WAV解码。
41.3.4 CMakeLists.txt文件
打开本实验BSP下的CMakeLists.txt文件其内容如下所示
set(src_dirs
IIC
LCD
LED
SDIO
SPI
XL9555
ES8388
I2S)
set(include_dirs
IIC
LCD
LED
SDIO
SPI
XL9555
ES8388
I2S)
set(requires
driver
fatfs)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
上述的红色I2C、ES8388驱动需要由开发者自行添加以确保音频播放驱动能够顺利集成到构建系统中。这一步骤是必不可少的它确保了音频播放驱动的正确性和可用性为后续的开发工作提供了坚实的基础。
打开本实验main文件下的CMakeLists.txt文件其内容如下所示
idf_component_register(
SRC_DIRS
"."
"APP"
INCLUDE_DIRS
"."
"APP")
上述的红色APP驱动需要由开发者自行添加在此便不做赘述了。
41.3.5 实验应用代码
打开main/main.c文件该文件定义了工程入口函数名为app_main。该函数代码如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
esp_err_t ret;
uint8_t key = 0;
/* 初始化NVS */
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
/* 初始化LED */
led_init();
/* 初始化IIC0 */
i2c0_master = iic_init(I2C_NUM_0);
/* 初始化SPI */
spi2_init();
/* 初始化IO扩展芯片 */
xl9555_init(i2c0_master);
/* 初始化LCD */
lcd_init();
/* ES8388初始化 */
es8388_init(i2c0_master);
/* 开启DAC关闭ADC */
es8388_adda_cfg(1, 0);
es8388_input_cfg(0);
/* DAC选择通道输出 */
es8388_output_cfg(1, 1);
/* 设置耳机音量 */
es8388_hpvol_set(20);
/* 设置喇叭音量 */
es8388_spkvol_set(20);
/* 打开喇叭 */
xl9555_pin_write(SPK_EN_IO,0);
/* I2S初始化 */
i2s_init();
/* 检测不到SD卡 */
while (sd_spi_init())
{
lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
vTaskDelay(500);
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
vTaskDelay(500);
}
/* 检查字库 */
while (fonts_init())
{
/* 清屏 */
lcd_clear(WHITE);
lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
/* 更新字库 */
key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);
/* 更新失败 */
while (key)
{
lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
vTaskDelay(200);
lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
vTaskDelay(200);
}
lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
vTaskDelay(1500);
/* 清屏 */
lcd_clear(WHITE);
}
/* 为fatfs相关变量申请内存 */
ret = exfuns_init();
/* 实验信息显示延时 */
vTaskDelay(500);
text_show_string(30, 50, 200, 16, "正点原子ESP32开发板",16,0, RED);
text_show_string(30, 70, 200, 16, "音乐播放器 实验", 16, 0, RED);
text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
text_show_string(30, 110, 200, 16, "KEY0:NEXT KEY2:PREV", 16, 0, RED);
text_show_string(30, 130, 200, 16, "KEY3:PAUSE/PLAY", 16, 0, RED);
while (1)
{
/* 播放音乐 */
audio_play();
}
}
到这里本实验的代码基本就编写完成了我们准备好音乐文件放到SD卡根目录下的《MUSIC》夹下测试本实验的代码。
41.4 下载验证
在代码编译成功之后我们下载代码到开发板上程序先执行字库检测然后当检测到SD卡根目录的MUSIC文件夹有音频文件WAV格式音频的时候就开始自动播放歌曲了如图41.4.1所示
图41.4.1音乐播放中
从上图可以看出总共1首歌曲当前正在播放第1首歌曲歌曲名、播放时间、总时长、码率等也都有显示。此时LED会随着音乐的播放而闪烁。
此时我们便可以听到开发板板载喇叭播放出来的音乐了也可以在开发板的PHONE端子插入耳机来听歌。同时我们可以通过按KEY0和KEY2来切换下一曲和上一曲通过KEY_UP暂停和继续播放。