ESP32音视频传输①用I2S通过内部DAC或MAX98357A播放音乐/录音数据及接受网络广播

Posted loveliveoil

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP32音视频传输①用I2S通过内部DAC或MAX98357A播放音乐/录音数据及接受网络广播相关的知识,希望对你有一定的参考价值。

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


I2S介绍

我这里只是简单介绍下,具体介绍及使用教程可看:https://diyi0t.com/i2s-sound-tutorial-for-esp32/

I2S用于收发音频信号,有三根线组成:
串行时钟 (SCK)也称为位时钟线 (BCLK):用于在同一周期内获取所有组件。串行时钟的频率定义为:频率 = 采样率 * 每通道位数 * 通道数,例如对一个wav录音文件:
采样率:44.1 kHz
每通道位数:16
通道数:2
则串行时钟的频率为 44.1 kHz * 16 * 2 = 1.411 MHz。
字选择 (WS)或帧选择 (FS) 线
如果 WS = 0 → 使用通道 1(左通道)
如果 WS = 1 → 使用通道 2(右通道)
串行数据 (SD)线:用于传输数据

ESP32有两个I2S接口,并且ESP32内部有两个8位的DAC分别对应GPIO25GPIO26
(ESP8266没内部DAC,如果要播放录音需要外加MAX98357A)
注:而由于内部DAC方式声音太小,所以本实验优先考虑使用MAX98357A,对于内部DAC输出直接调用第三方库,而不直接对i2s就行配置了(主要原因是我直接配置内部DAC的i2s输出的声音很杂,肯定是哪里没配好,只能用第三方库了,等我搞懂了再更新吧)


一、使用外部DAC即MAX98357A播放录音

1.播放内存的录音数据

提示:主要是参考了https://www.xtronical.com/i2s-ep2/
完整示例可下载:https://www.xtronical.com/wp-content/uploads/2020/08/PlayWav.zip
我这里只是修改了一下i2s_num,以及对一些重要的注释翻译成中文

// Includes
    #include "driver/i2s.h"            // 适用于ESP32的I2S库
    #include "WavData.h"               // 把WAV格式的录音数据的bytes保存到WavData中

//  Global Variables/objects    
    static const i2s_port_t i2s_num = I2S_NUM_1;  // i2s port number,注意,如果是用内部DAC必须用I2S_NUM_0
    unsigned const char* TheData;
    uint32_t DataIdx=0;                           // index offset into "TheData" for current  data t send to I2S

    struct WavHeader_Struct
    
      //   RIFF Section    
      char RIFFSectionID[4];      // Letters "RIFF"
      uint32_t Size;              // Size of entire file less 8
      char RiffFormat[4];         // Letters "WAVE"
      
      //   Format Section    
      char FormatSectionID[4];    // letters "fmt"
      uint32_t FormatSize;        // Size of format section less 8
      uint16_t FormatID;          // 1=uncompressed PCM
      uint16_t NumChannels;       // 1=mono,2=stereo
      uint32_t SampleRate;        // 44100, 16000, 8000 etc.
      uint32_t ByteRate;          // =SampleRate * Channels * (BitsPerSample/8)
      uint16_t BlockAlign;        // =Channels * (BitsPerSample/8)
      uint16_t BitsPerSample;     // 8,16,24 or 32
    
      // Data Section
      char DataSectionID[4];      // The letters "data"
      uint32_t DataSize;          // Size of the data that follows
    WavHeader;
    
//------------------------------------------------------------------------------------------------------------------------

//------------------------------------------------------------------------------------------------------------------------
// I2S configuration structures

static const i2s_config_t i2s_config = 
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = 44100,    //设置采样率,但由于预先不知道WavData的录音数据的采样率,后面解码后会通过i2s_set_sample_rates修改
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,       // high interrupt priority
    .dma_buf_count = 8,                             // 8 buffers
    .dma_buf_len = 1024,                            // 1K per buffer, so 8K of buffer space
    .use_apll=0,
    .tx_desc_auto_clear= true, 
    .fixed_mclk=-1    
;

// These are the physical wiring connections to our I2S decoder board/chip from the esp32, there are other connections
// required for the chips mentioned at the top (but not to the ESP32), please visit the page mentioned at the top for
// further information regarding these other connections.

static const i2s_pin_config_t pin_config = 
    .bck_io_num = 27,                     // 时钟口,对应于MAX38357A的BCLK
    .ws_io_num = 26,                      // 用于声道选择,对应于MAX38357A的LRC
    .data_out_num = 25,                   // ESP32的音频输出口, 对应于MAX38357A的DIN
    .data_in_num = I2S_PIN_NO_CHANGE      // ESP32的音频输入接口,本例未用到
;

//------------------------------------------------------------------------------------------------------------------------


void setup() 
    Serial.begin(115200);
    memcpy(&WavHeader,&WavData,44);                     // Copy the header part of the wav data into our structure
    DumpWAVHeader(&WavHeader);                          // Dump the header data to serial, optional!
    if(ValidWavData(&WavHeader))
    
      i2s_driver_install(i2s_num, &i2s_config, 0, NULL);        // ESP32 will allocated resources to run I2S
      i2s_set_pin(i2s_num, &pin_config);                        // Tell it the pins you will be using
      i2s_set_sample_rates(i2s_num, WavHeader.SampleRate);      //set sample rate 
      TheData=WavData;                                          // set to start of data  
      TheData+=44;                       
                     
    else      // end code here
      while(true);


void loop()
    
                                                 
  size_t BytesWritten;                            // Returned by the I2S write routine, we are not interested in it

  // 这里可选择每次发32bit的数据,也就是4 bytes
  i2s_write(i2s_num,TheData+DataIdx,4,&BytesWritten,portMAX_DELAY); 
  DataIdx+=4;                                   // increase the data index to next two 16 bit values (4 bytes)
  if(DataIdx>=WavHeader.DataSize)               // If we gone past end of data reset back to beginning
    DataIdx=0;                                 


bool ValidWavData(WavHeader_Struct* Wav)

  
  if(memcmp(Wav->RIFFSectionID,"RIFF",4)!=0) 
      
    Serial.print("Invlaid data - Not RIFF format");
    return false;        
  
  if(memcmp(Wav->RiffFormat,"WAVE",4)!=0)
  
    Serial.print("Invlaid data - Not Wave file");
    return false;           
  
  if(memcmp(Wav->FormatSectionID,"fmt",3)!=0) 
  
    Serial.print("Invlaid data - No format section found");
    return false;       
  
  if(memcmp(Wav->DataSectionID,"data",4)!=0) 
  
    Serial.print("Invlaid data - data section not found");
    return false;      
  
  if(Wav->FormatID!=1) 
  
    Serial.print("Invlaid data - format Id must be 1");
    return false;                          
  
  if(Wav->FormatSize!=16) 
  
    Serial.print("Invlaid data - format section size must be 16.");
    return false;                          
  
  if((Wav->NumChannels!=1)&(Wav->NumChannels!=2))
  
    Serial.print("Invlaid data - only mono or stereo permitted.");
    return false;   
  
  if(Wav->SampleRate>48000) 
  
    Serial.print("Invlaid data - Sample rate cannot be greater than 48000");
    return false;                       
  
  if((Wav->BitsPerSample!=8)& (Wav->BitsPerSample!=16)) 
  
    Serial.print("Invlaid data - Only 8 or 16 bits per sample permitted.");
    return false;                        
  
  return true;



void DumpWAVHeader(WavHeader_Struct* Wav)

  if(memcmp(Wav->RIFFSectionID,"RIFF",4)!=0)
  
    Serial.print("Not a RIFF format file - ");    
    PrintData(Wav->RIFFSectionID,4);
    return;
   
  if(memcmp(Wav->RiffFormat,"WAVE",4)!=0)
  
    Serial.print("Not a WAVE file - ");  
    PrintData(Wav->RiffFormat,4);  
    return;
    
  if(memcmp(Wav->FormatSectionID,"fmt",3)!=0)
  
    Serial.print("fmt ID not present - ");
    PrintData(Wav->FormatSectionID,3);      
    return;
   
  if(memcmp(Wav->DataSectionID,"data",4)!=0)
  
    Serial.print("data ID not present - "); 
    PrintData(Wav->DataSectionID,4);
    return;
    
  // All looks good, dump the data
  Serial.print("Total size :");Serial.println(Wav->Size);
  Serial.print("Format section size :");Serial.println(Wav->FormatSize);
  Serial.print("Wave format :");Serial.println(Wav->FormatID);
  Serial.print("Channels :");Serial.println(Wav->NumChannels);
  Serial.print("Sample Rate :");Serial.println(Wav->SampleRate);
  Serial.print("Byte Rate :");Serial.println(Wav->ByteRate);
  Serial.print("Block Align :");Serial.println(Wav->BlockAlign);
  Serial.print("Bits Per Sample :");Serial.println(Wav->BitsPerSample);
  Serial.print("Data Size :");Serial.println(Wav->DataSize);


void PrintData(const char* Data,uint8_t NumBytes)

    for(uint8_t i=0;i<NumBytes;i++)
      Serial.print(Data[i]); 
      Serial.println();  

对应的接线方式如图(可在代码的pin_config 中更改):

对于如何把后缀名为.wav的录音文件转化为WavData.h,可利用wsl的命令行:xxd -i xxx.wav xxx.h

2.使用第三方库ESP8266Audio


下载地址:https://github.com/earlephilhower/ESP8266Audio
本例基于:https://diyi0t.com/i2s-sound-tutorial-for-esp32/
先下载ESP8266Audio的库zip文件,并在Arduino中安装此第三方库

#include "AudioGeneratorAAC.h"
#include "AudioOutputI2S.h"
#include "AudioFileSourcePROGMEM.h"
#include "sampleaac.h"

AudioFileSourcePROGMEM *in;
AudioGeneratorAAC *aac;
AudioOutputI2S *out;

void setup()
  Serial.begin(115200);

  in = new AudioFileSourcePROGMEM(sampleaac, sizeof(sampleaac));
  aac = new AudioGeneratorAAC();
  out = new AudioOutputI2S();
  out -> SetGain(0.5);            //设置音量0~1
  out -> SetPinout(27,33,32);     //设置接到MAX98357A的引脚, GPIO27(串行时钟SCK)-->SCLK, GPIO33(字选择WS)-->LRC, GPIO32(串行数据SD)-->DIN
  aac->begin(in, out);


void loop()
  if (aac->isRunning()) 
    aac->loop();
   else 
    aac -> stop();
    Serial.printf("Sound Generator\\n");
    delay(1000);
  

3.用第三方库ESP8266Audio接收网络广播

本例基于ESP8266Audio库的StreamMP3FromHTTPToSPDIF
然后修改一下代码,绑定MAX98357A,添加把广播源换成国内的

#include <Arduino.h>

#ifdef ESP32
    #include <WiFi.h>
#else
    #include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorMP3.h"
//#include "AudioOutputI2SNoDAC.h"
#include "AudioOutputI2S.h"

//
// Stream MP3 from HTTP to SPDIF
//

// To run, set your ESP8266 build to 160MHz, update the SSID info, and upload.

// Note:
// If using ESP8266 NodeMCU connect LED to RX pin and GND pin

// Enter your WiFi setup here:
#ifndef STASSID
#define STASSID "你的WiFi"
#define STAPSK  "WiFi密码"
#endif

const char* ssid = STASSID;
const char* password = STAPSK;

// Examples URLs
//const char *URL="http://kvbstreams.dyndns.org:8000/wkvi-am";

// 换成了国内的广播源
const char *URL="http://lhttp.qingting.fm/live/4915/64k.mp3";

// Stream URL of Logitech Media Server, aka LMS, Version: 8.2.0 (August 2021)
// const char *URL="http://192.168.1.121:9000/stream.mp3";

AudioGeneratorMP3 *mp3;
AudioFileSourceICYStream *file;
AudioFileSourceBuffer *buff;

// Output device is SPDIF
AudioOutputI2S *out;


// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)

  const char *ptr = reinterpret_cast<const char *>(cbData);
  (void) isUnicode; // Punt this ball for now
  // Note that the type and string may be in PROGMEM, so copy them to RAM for printf
  char s1[32], s2[64];
  strncpy_P(s1, type, sizeof(s1));
  s1[sizeof(s1)-1]=0;
  strncpy_P(s2, string, sizeof(s2));
  s2[sizeof(s2)-1]=0;
  Serial.printf("METADATA(%s) '%s' = '%s'\\n", ptr, s1, s2);
  Serial.flush();


// Called when there's a warning or error (like a buffer underflow or decode hiccup)
void StatusCallback(void *cbData, int code, const char *string)

  const char *ptr = reinterpret_cast<const char *>(cbData);
  // Note that the string may be in PROGMEM, so copy it to RAM for printf
  char s1[64];
  strncpy_P(s1, string, sizeof(s1));
  s1[sizeof(s1)-1]=0;
  Serial.printf("STATUS(%s) '%d' = '%s'\\n", ptr, code, s1);
  Serial.flush();



void setup()

  Serial.begin(115200);
  delay(1000);
  Serial.println("Connecting to WiFi");

  WiFi.disconnect();
  WiFi.softAPdisconnect(true);
  WiFi.mode(WIFI_STA);
  
  WiFi.begin(ssid, password);

  // Try forever
  while (WiFi.status() != WL_CONNECTED) 
    Serial.println("...Connecting to WiFi");
    delay(1000);
  
  Serial.println("Connected");

  audioLogger = &Serial;
  file = new AudioFileSourceICYStream(URL);

  // Commented out for performance issues with high rate MP3 stream
  //file->RegisterMetadataCB(MDCallback, (void*)"ICY");

  buff = new AudioFileSourceBuffer(file, 4096);	// Doubled form default 2048

  // Commented out for performance issues with high rate MP3 stream
  //buff->RegisterStatusCB(StatusCallback, (void*)"buffer");

  // Set SPDIF output
  out = new AudioOutputI2S();
  out -> SetGain(0.2);            //设置音量
  out -> SetPinout(27,33,32);     //设置接到MAX98357A的引脚, GPIO27(串行时钟SCK)-->SCLK, GPIO33(字选择WS)-->LRC, GPIO32(串行数据SD)-->DIN
  mp3 = new AudioGeneratorMP3();

  // Commented out for performance issues with high rate MP3 stream
  //mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");

  mp3->begin(buff, out);



void loop()

  // Commented out
  //static int lastms = 0;

  if (mp3->isRunning()) 
    /* Commented out
    if (millis()-lastms > 1000) 
    

ESP32 DAC

ESP32有两个DAC通道,通道1链接GPIO25, 通道2链接GPIO26;

当DAC设置为 “built-in DAC mode”的时候,I2S可以通过DAC发送数据;

使用示例:

dac_output_enable(DAC_CHANNEL_1);
dac_output_voltage(DAC_CHANNEL_1, 200);

dac一共有8位,将3.3V电压按照255均分之后,就是dac的精度的最小单位;

 

API函数如下:

esp_err_tdac_pad_get_io_num(dac_channel_tchannelgpio_num_t *gpio_num)

获取指定的DAC通道的GPIO口;

 

esp_err_tdac_output_voltage(dac_channel_tchannel, uint8_t dac_value)

设置DAC的输出电压;

 

esp_err_tdac_output_enable(dac_channel_tchannel)

 

DAC的输出使能;

 

esp_err_tdac_output_disable(dac_channel_tchannel)

 

DAC的输出失能;

 

esp_err_tdac_i2s_enable()

 

DAC 的I2S使能;

 

esp_err_tdac_i2s_disable()

 

DAC的I2S失能;

以上是关于ESP32音视频传输①用I2S通过内部DAC或MAX98357A播放音乐/录音数据及接受网络广播的主要内容,如果未能解决你的问题,请参考以下文章

ESP32使用I2S控制ADC和DAC

esp32cam micropython使用I2S驱动DAC模块播放音频

ESP32 DAC

esp32怎么实现14位dac输出

esp32的wifi模块叫什么

nanoFramework 中的 ESP32 I2S