Demo 26: How to use Arduino ESP32 I2S (external DAC and built-in DAC) to play wav music file from sdcard

1. Introduction
- ESP32 has two I2S peripherals. They can be configured to input and output sample data. They also supports DMA to stream sample data without needing CPU operations. I2S output can also be routed directly to the Digital to Analog Converter output (GPIO25 and GPIO26) without needing external I2S codec.
- In this demo I will show you how to use Arduino ESP32 I2S to play wav music file from sdcard. I chose wav file because it is not compressed like mp3 file. So we need not to de-compress it.
- There are 2 demos for this post:
    1. I used external I2S codec, 2 speakers and 1 module micro sdcard.
    2. I Used internal DAC, 2 speakers and 1 module micro sdcard.
Figure: I used external I2S codec for this demo, 2 speakers and 1 module micro sdcard
2. Hardware
Connect hardware like below:
[ESP32 IO32 – CS MICROSD]
[ESP32 IO14 – MOSI MICROSD]
[ESP32 IO13 – MISO MICROSD]
[ESP32 IO27 – SCK MICROSD]
[ESP32 IO26 – I2S codec BCK]
[ESP32 IO22 – I2S codec DATA]
[ESP32 IO25 – I2S codec LRCK]
[ESP32 GND – I2S codec GND]
[ESP32 GND – GND MICROSD]
[5V – VCC MICROSD]
[5V – I2S codec]
3. Software
- We will re-use Demo 7 for sdcard reading and I2S driver here. You can download the document about the wav file format here.
Note: You can down full project including wav file sample : https://github.com/nhatuan84/esp32-i2s-sdcard-wav-player
3.1 The code with external DAC 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <mySD.h>
#include "driver/i2s.h"
#include "freertos/queue.h"

#define CCCC(c1, c2, c3, c4)    ((c4 << 24) | (c3 << 16) | (c2 << 8) | c1)

/* these are data structures to process wav file */
typedef enum headerState_e {
    HEADER_RIFF, HEADER_FMT, HEADER_DATA, DATA
} headerState_t;

typedef struct wavRiff_s {
    uint32_t chunkID;
    uint32_t chunkSize;
    uint32_t format;
} wavRiff_t;

typedef struct wavProperties_s {
    uint32_t chunkID;
    uint32_t chunkSize;
    uint16_t audioFormat;
    uint16_t numChannels;
    uint32_t sampleRate;
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;
} wavProperties_t;
/* variables hold file, state of process wav file and wav file properties */    
File root;
headerState_t state = HEADER_RIFF;
wavProperties_t wavProps;

//i2s configuration 
int i2s_num = 0; // i2s port number
i2s_config_t i2s_config = {
     .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
     .sample_rate = 36000,
     .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,
     .dma_buf_len = 64   //Interrupt level 1
    };
    
i2s_pin_config_t pin_config = {
    .bck_io_num = 26, //this is BCK pin
    .ws_io_num = 25, // this is LRCK pin
    .data_out_num = 22, // this is DATA output pin
    .data_in_num = -1   //Not used
};
//
void debug(uint8_t *buf, int len){
  for(int i=0;i<len;i++){
    Serial.print(buf[i], HEX);
    Serial.print("\t");
  }
  Serial.println();
}
/* write sample data to I2S */
int i2s_write_sample_nb(uint32_t sample){
  return i2s_write_bytes((i2s_port_t)i2s_num, (const char *)&sample, sizeof(uint32_t), 100);
}
/* read 4 bytes of data from wav file */
int read4bytes(File file, uint32_t *chunkId){
  int n = file.read((uint8_t *)chunkId, sizeof(uint32_t));
  return n;
}

/* these are function to process wav file */
int readRiff(File file, wavRiff_t *wavRiff){
  int n = file.read((uint8_t *)wavRiff, sizeof(wavRiff_t));
  return n;
}
int readProps(File file, wavProperties_t *wavProps){
  int n = file.read((uint8_t *)wavProps, sizeof(wavProperties_t));
  return n;
}

void setup()
{
  Serial.begin(115200);
  Serial.print("Initializing SD card...");
  if (!SD.begin(32, 14, 13, 27)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
  delay(1000);
  /* open wav file and process it */
  root = SD.open("T.WAV");
  if (root) {    
    int c = 0;
    int n;
    while (root.available()) {
      switch(state){
        case HEADER_RIFF:
        wavRiff_t wavRiff;
        n = readRiff(root, &wavRiff);
        if(n == sizeof(wavRiff_t)){
          if(wavRiff.chunkID == CCCC('R', 'I', 'F', 'F') && wavRiff.format == CCCC('W', 'A', 'V', 'E')){
            state = HEADER_FMT;
            Serial.println("HEADER_RIFF");
          }
        }
        break;
        case HEADER_FMT:
        n = readProps(root, &wavProps);
        if(n == sizeof(wavProperties_t)){
          state = HEADER_DATA;
        }
        break;
        case HEADER_DATA:
        uint32_t chunkId, chunkSize;
        n = read4bytes(root, &chunkId);
        if(n == 4){
          if(chunkId == CCCC('d', 'a', 't', 'a')){
            Serial.println("HEADER_DATA");
          }
        }
        n = read4bytes(root, &chunkSize);
        if(n == 4){
          Serial.println("prepare data");
          state = DATA;
        }
        //initialize i2s with configurations above
        i2s_driver_install((i2s_port_t)i2s_num, &i2s_config, 0, NULL);
        i2s_set_pin((i2s_port_t)i2s_num, &pin_config);
        //set sample rates of i2s to sample rate of wav file
        i2s_set_sample_rates((i2s_port_t)i2s_num, wavProps.sampleRate); 
        break; 
        /* after processing wav file, it is time to process music data */
        case DATA:
        uint32_t data; 
        n = read4bytes(root, &data);
        i2s_write_sample_nb(data); 
        break;
      }
    }
    root.close();
  } else {
    Serial.println("error opening test.txt");
  }
  i2s_driver_uninstall((i2s_port_t)i2s_num); //stop & destroy i2s driver 
  Serial.println("done!");
}

void loop()
{
}
3.2 The code with built-in DAC
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <mySD.h>
#include "driver/i2s.h"
#include "freertos/queue.h"

#define CCCC(c1, c2, c3, c4)    ((c4 << 24) | (c3 << 16) | (c2 << 8) | c1)

/* these are data structures to process wav file */
typedef enum headerState_e {
    HEADER_RIFF, HEADER_FMT, HEADER_DATA, DATA
} headerState_t;

typedef struct wavRiff_s {
    uint32_t chunkID;
    uint32_t chunkSize;
    uint32_t format;
} wavRiff_t;

typedef struct wavProperties_s {
    uint32_t chunkID;
    uint32_t chunkSize;
    uint16_t audioFormat;
    uint16_t numChannels;
    uint32_t sampleRate;
    uint32_t byteRate;
    uint16_t blockAlign;
    uint16_t bitsPerSample;
} wavProperties_t;
/* variables hold file, state of process wav file and wav file properties */    
File root;
headerState_t state = HEADER_RIFF;
wavProperties_t wavProps;

//i2s configuration 
int i2s_num = 0; // i2s port number
i2s_config_t i2s_config = {
     .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
     .sample_rate = 44100,
     .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, /* the DAC module will only take the 8bits from MSB */
     .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
     .communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S_MSB,
     .intr_alloc_flags = 0, // default interrupt priority
     .dma_buf_count = 8,
     .dma_buf_len = 64,
     .use_apll = 0
    };
//
void debug(uint8_t *buf, int len){
  for(int i=0;i<len;i++){
    Serial.print(buf[i], HEX);
    Serial.print("\t");
  }
  Serial.println();
}
/* write sample data to I2S */
int i2s_write_sample_nb(uint8_t sample){
  return i2s_write_bytes((i2s_port_t)i2s_num, (const char *)&sample, sizeof(uint8_t), 100);
}
/* read 4 bytes of data from wav file */
int read4bytes(File file, uint32_t *chunkId){
  int n = file.read((uint8_t *)chunkId, sizeof(uint32_t));
  return n;
}

int readbyte(File file, uint8_t *chunkId){
  int n = file.read((uint8_t *)chunkId, sizeof(uint8_t));
  return n;
}

/* these are function to process wav file */
int readRiff(File file, wavRiff_t *wavRiff){
  int n = file.read((uint8_t *)wavRiff, sizeof(wavRiff_t));
  return n;
}
int readProps(File file, wavProperties_t *wavProps){
  int n = file.read((uint8_t *)wavProps, sizeof(wavProperties_t));
  return n;
}

void setup()
{
  Serial.begin(115200);
  Serial.print("Initializing SD card...");
  if (!SD.begin(32, 14, 13, 27)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
  delay(1000);
  /* open wav file and process it */
  root = SD.open("T.WAV");
  if (root) {    
    int c = 0;
    int n;
    while (root.available()) {
      switch(state){
        case HEADER_RIFF:
        wavRiff_t wavRiff;
        n = readRiff(root, &wavRiff);
        if(n == sizeof(wavRiff_t)){
          if(wavRiff.chunkID == CCCC('R', 'I', 'F', 'F') && wavRiff.format == CCCC('W', 'A', 'V', 'E')){
            state = HEADER_FMT;
            Serial.println("HEADER_RIFF");
          }
        }
        break;
        case HEADER_FMT:
        n = readProps(root, &wavProps);
        if(n == sizeof(wavProperties_t)){
          state = HEADER_DATA;
        }
        break;
        case HEADER_DATA:
        uint32_t chunkId, chunkSize;
        n = read4bytes(root, &chunkId);
        if(n == 4){
          if(chunkId == CCCC('d', 'a', 't', 'a')){
            Serial.println("HEADER_DATA");
          }
        }
        n = read4bytes(root, &chunkSize);
        if(n == 4){
          Serial.println("prepare data");
          state = DATA;
        }
        //initialize i2s with configurations above
        i2s_driver_install((i2s_port_t)i2s_num, &i2s_config, 0, NULL);
        i2s_set_pin((i2s_port_t)i2s_num, NULL);
        //set sample rates of i2s to sample rate of wav file
        i2s_set_sample_rates((i2s_port_t)i2s_num, wavProps.sampleRate); 
        break; 
        /* after processing wav file, it is time to process music data */
        case DATA:
        uint8_t data; 
        n = readbyte(root, &data);
        i2s_write_sample_nb(data); 
        break;
      }
    }
    root.close();
  } else {
    Serial.println("error opening test.txt");
  }
  i2s_driver_uninstall((i2s_port_t)i2s_num); //stop & destroy i2s driver 
  Serial.println("done!");
}

void loop()
{
}
4. Result

Post a Comment

25 Comments

Anonymous said…
Thank you very much Tuan. You guides absolutely fantastic.
Regards
Venkatesh (Bangalore/India)
Anonymous said…
Thank you for this guide, you are doing quite a good job! I'm trying to do it a bit different, but I can't get it working. I've got some PAM8403 and some cheap speakers, so I wanted to use the internal DAC and connect it to the PAM8403 connect this to the speakers. When I'm using code as for piezo speakers, everything is running fine. But with your code (and the code I'm trying to use from http://esp-idf.readthedocs.io/en/latest/api-reference/peripherals/i2s.html) I'm just having a buzzing with one frequency. Can you maybe show how to change your code to use it with the internal DAC and connect speakers to them?

Many thanks in advance!
Best Regards
Thomas (Germany)
Hi Thomas,

I think you can not connect directly internal DAC to speakers. Pls try this :
http://users.ece.utexas.edu/~valvano/Volume1/E-Book/C13_DACSound_files/c13-image010.png

Regards,
Anonymous said…
Hi, thank you for your fast answer.

So I think this is for power reasons?

Therefore I'm using the PAM8403 (https://www.diodes.com/assets/Datasheets/PAM8403.pdf). I've got a board like this https://de.aliexpress.com/item/5PCS-PAM8403-Super-mini-digital-amplifier-board-2-3W-Class-D-digital-amplifier-board-efficient-2/1442947325.html?spm=a2g0x.10010108.1000016.1.6b3ac68duAxVq1&isOrigTitle=true

So I tried to connect GIO25 & GIO26 to this part as left and Right. But I don't get any sound, just noise. So I assume, that maybe I did something wrong with my code.

Can you show me how to modify your code correctly, to use the internal DACs?

Many thanks in advance!!
Hi

You just use the configuration in the esp-idf i2s.
Do you have oscilloscope? Please measure the signal on the GPIO 25&26.


Regards.
Hi Thomas,

I made a demo that using internal DAC for you. :)
Unknown said…
Hi,

the demo with external DAC works fine, but the one with internal DAC: I get no output here, neither on PIN 22 nor on 25/26. Any idea? Does it work for you?
Best Dirk
Hi

It worked for me. Please try to attach speaker directly to pin 25 or 26 without amplifier.

Regards,
Practical_IOT said…
I'm trying to use your demo code but when my SD card module is connected to IO12, the ESP32 fails to boot. As soon as i disconnect IO12, it boots normally (but obviously, the SD card interface is disconnceted, so the init fails. The ESP32 is "falling back to built in command interpreter" Any thoughts?

which board are you using?
If it uses IO12 then you can try another pin.

Regards,
Unknown said…
Since using the internal DAC only worked with non-standard sample rates for me (I believe 8-bit 44khz stereo or 16bit 21khz stereo or something), I got those pcm modules and built the external dac version.
One thing is: You have to put a ' state = HEADER_RIFF; ' in the end (after the i2s_driver_uninstall) in order to get the code working a second time in a sketch. It starts with 'case state' and that is still DATA after playing the last file.

What I'm actually working on is background noise in very quiet sound files. It sounds like I get a rectangle signal until the higher 8 of 16 bit in the wav file have been "in use" (>0), but I haven't yet found what to do about that.
There's no noise if I put a few 'louder' bytes at the beginning of a wav file, but that gives a knock sound every time my project starts playing such a file.
Maybe you can help with that?
Hi,

Sure, we have to put state = HEADER_RIFF; at the end to continue the next song.

about you case, why dont you ignore the '0' bytes until you meet '>0' bytes (If I understand properly)

Regards,
Yves BAZIN said…
Hello
thank you for your guide
I have tried your code using the internal DAC but I only get noise
Have try to put a resistor between the exit 25 or 26 and the ground
would you have any idea why ?
onlineshop said…
What's microsd module do you use ? SPI version ?
Thanks
maxxir said…
Hi!
Code for internal DAC wrong.
Try this as example:

dacWrite(25, 127); //1.65V to DAC1/GPIO25
dacWrite(26, 255); //3.3V to DAC2/GPIO26

Much simpler then I2S sound!

Best regards
Nicolas said…
This is great stuff! In 10 minutes I had sound coming out.
I am using this board : https://www.aliexpress.com/item/Raspberry-Pi-pHAT-Sound-Card-I2S-interface-PCM5102-DAC-Module-24-bit-Audio-Board-With-Stereo/32742005765.html?spm=2114.search0104.3.87.4e663092ny6WYV&ws_ab_test=searchweb0_0,searchweb201602_5_5722916_10152_10151_10065_10344_5722816_10068_10342_10343_10340_10341_10696_5722616_10084_10083_10618_10304_10307_10301_5722716_5711216_10059_308_100031_10103_10624_5722516_10623_10622_10621_10620_5711316,searchweb201603_32,ppcSwitch_5&algo_expid=0f018a45-a2dd-40ae-9e92-66a8288e08b6-13&algo_pvid=0f018a45-a2dd-40ae-9e92-66a8288e08b6&priceBeautifyAB=0

Normally made for the Pi, but also working as long as set FLT, DMP, SCL, FMT & XMT to ground. Of course you can set some pins HIGH if you want to change the filter of the data format, it's at page 3 : http://www.ti.com/lit/ds/slas764b/slas764b.pdf

I still have problem though, the audio is playback too fast, I checked the sample rate that is read in the wavProps datastruct, and it is 44100 and seems correct. I am not sure what could cause the problem, any idea ?

Thanks a lot,
Nicolas.


Nicolas said…
Ok, I am not sure but I think the problem is coming from the file that is Mono instead of Stereo.

Now another issue I have is that some .wave files I have here, or that I am exporting from an Audio software, don't always follow exactly the Wav standard.

For instead the SubChunk1ID is not starting at the normal position but few bytes after. I am trying to scan through the file to auto-detect "FMT" and "DATA". It works out, but when I try to send the data out, it's all noisy so I think I am shifting something around.

Will investigate further.
Fabiano (fabiuz4@hotmail.it) said…
I'm trying to use to following component: https://www.adafruit.com/product/3006

It works fine but at the beginning it starts to play with a "white noise" overlapped to the original sound. This weird effect lasts few moments (few hundreds of millisecond, the length varies a lot between various attempts). Then it continues to perfectly playing.

I was wondering if someone got the same problem with the PCM5102, the one used by the author...
Nicolas said…
I added this to your code to be able to playback non-standard wave files (those were exported from Logic Pro X).

To detect the "FMT" header:

case HEADER_FMT:
//n = readProps(root, &wavProps);

for (int i = 0; i < 100; i++) {
reading = root.read();
if (reading == 0x66) {
reading_2 = root.read();
reading_3 = root.read();
reading_4 = root.read();

//search for Subchunk1ID, Contains the letters, "fmt " (0x666d7420 big-endian form).
if ((reading_2 == 0x6d) && (reading_3 == 0x74) && (reading_4 == 0x20)) {
Serial.println("FMT reached");
break;
}
}
}

n = readProps(root, &wavProps);
if (n == sizeof(wavProperties_t)) {
state = HEADER_DATA;
}

Serial.println(wavProps.chunkSize, DEC);
Serial.println(wavProps.audioFormat , DEC);
Serial.print("number of channels : ");
Serial.println(wavProps.numChannels , DEC);
Serial.print("sample_rate : ");
Serial.println(wavProps.sampleRate, DEC);
Serial.print("bitdepth : ");
Serial.println(wavProps.bitsPerSample, DEC);

state = HEADER_DATA;
break;
Jean-Paul said…
Hello,
Is it possible to use the esp32 file system to store the file instead of inside SD card?
Thank you.
Hi

Yes you can. Just replace the read file function above :)

regards,
Jean-Paul said…
Hello,
Thanks for this answer, can you explain the lines to delete and the new lines to create.
I do not know how to find that by myself. (Excuse me)
Thank you.
Sergei said…
Hi

thank you for your guide

It is possible to add the program code to play all wav files one by one which are located in the folder of the SD card?

Thanks a lot,
Sergio
Jean Paul said…
Hello,
I got this compilation error
'i2s_write_bytes' was not declared in this scope
Can you help me please.
JeremyJoe said…
@JeanPaul,
replace the call to `i2s_write_bytes` with this:

```
size_t i2s_bytes_write = 0;
i2s_write((i2s_port_t)i2s_num, (const void*) &sample, sizeof(uint8_t), &i2s_bytes_write, 0);
return i2s_bytes_write;
```