ESP2-I2S音频播放笔记
ESP32的I2S设计的比较奇怪,或者也可以说比较强大。I2S在我们印象中是用来传输音频数字信号的通信接口,但是参考ESP32的数据手册会发现远远不止如此。初次看他这部分的手册总是会把人看的云里雾里。
ESP32的硬件I2S可以实现功能主要有以下几个场景:
- 驱动LCD液晶屏
- 可以连接CAMERA
- 可以连接内部DA实现音频播放
- 可以连接内部AD实现录音
下面笔记记录的都是和音频播放相关,其他模式暂时没做讨论。
ESP32一共有两个I2S:I2S0、I2S1。
但是只有I2S0支持连接内部ADC和DAC,也就是要实现录音和播放只能使用I2S0。
当 I2S—ADC 要把I2S0配置为主机接收模式
当 I2S—DAC 要把I2S0配置为主机发送模式
I2S的时钟源有两个:
- PLL_D2_CLK(ADC/DAC模式下要使用该时钟)
- APLL_CLK
使用ESP32-IDF配置音频播放的时候,不需要手动指定哪底层已经封装好了
I2S FIFO读写:
ESP32的FIFO读写长度都是按照32位来的。而对FIFO的操作方式有两种:一种是通过CPU直接进行操作;另一种是通过DMA进行读写操作。音频播放呢就是把写入FIFO中的数据通过DMA搬到DA然后输出一个模拟量。录音就是通过DMA把AD采集到的值搬倒FIFO里面去,然后读走保存。
关于FIFO的配置还涉及到单声道双声道的问题,可以去研读esp32参考手册。
音频播放位数问题:
理论上我们采集的音频使用的AD位数越高,失真率就越小,播放出来音质就会越高。但是ESP32有个限制就是DA输出是8位的,也就是不管你用16位音源还是12位或者8位音源最终都会被转成8位的送到DAC去播放。所以这个也限制了ESP32使用内部DA播放做不到太好的音质。
播放的音频文件也没必要用16位采样的音源,浪费存储空间,数据还要进行转换才能送入DA播放。
音频播放初始化代码:
1 |
static void csound_audioInit()<br> {<br> i2s_config_t i2s_config = {<br> .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN,<br> .sample_rate = 16000,<br> .bits_per_sample = 16,<br> .communication_format = I2S_COMM_FORMAT_I2S_MSB,<br> .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, //影响数据传输的格式,根据音频文件进行选择<br> .intr_alloc_flags = 0,<br> .dma_buf_count = 4,<br> .dma_buf_len = 256,<br> }; <br> i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); //install and start i2s driver <br> i2s_set_dac_mode(I2S_DAC_CHANNEL_RIGHT_EN); //可以控制有几个声道出声音<br> } <br> static void csound_audioDeinit()<br> {<br> i2s_set_dac_mode(I2S_DAC_CHANNEL_DISABLE);<br> i2s_driver_uninstall(I2S_NUM_0);<br> } |
把音频数据进行转换的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Scale data to 16bit/32bit for I2S DMA output. // DAC can only output 8bit data value. // I2S DMA will still send 16 bit or 32bit data, the highest 8bit contains DAC data. // 并且根据音量计算出播放DA值的大小 static int csound_audioDataScale(uint8_t* d_buff, uint8_t* s_buff, uint32_t len) { float scale=csoundVolume*0.01; float value=0; uint32_t j = 0; for (int i = 0; i < len; i++) { d_buff[j++] = 0; value=(float)s_buff[i]*scale; //根据音量大小成比例的调整DA输出值的幅度 d_buff[j++] = (uint8_t)value; } return (len * 2); } |
我们的音频PCM数据采样位数是8位,但是在DMA传输过程中,最少是一次传输16位(前面提到FIFO按照32位来读写和这里不矛盾,低16位会自动被填充0)。那么我们就要把8位的数据转换成16位的,再送去FIFO。
这里我自己加了个音量转换,最终送出去的数据,会根据音量大小进行成比例的放大缩小。
最后就是播放音频的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//播放音频 static void csound_audioPlay(audioTableStruct *ats) { size_t bytes_written; uint8_t* i2s_write_buff = (uint8_t*) calloc(4096, sizeof(char)); int offset = 0; int tot_size=ats->length; while (offset < tot_size) { int play_len = ((tot_size - offset) > (4 * 256)) ? (4 * 256) : (tot_size - offset); int i2s_wr_len = csound_audioDataScale(i2s_write_buff, (uint8_t*)(ats->ptable + offset), play_len); i2s_write(I2S_NUM_0, i2s_write_buff, i2s_wr_len, &bytes_written, portMAX_DELAY); offset += play_len; } free(i2s_write_buff); } |
播放的思路,就是分段从我的音频table里面读数据,读回来以后进行数据转换,然后再写入i2s缓冲区中,等待DMA把他都搬入DA,重复执行直到播放完毕。
请问 i2s_write 这个函数哪里来的,是不是这决定输出的是什么?
这是SDK提供的接口函数
请问我乘以一个小于1的音量,声音为什么会失真呢
这个时间很久远了,我都忘记细节了