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.
[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
3.2 The code with built-in DAC
4. Result
- 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: external I2S codec
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() { } |
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() { } |
26 Comments
Regards
Venkatesh (Bangalore/India)
Many thanks in advance!
Best Regards
Thomas (Germany)
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,
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!!
You just use the configuration in the esp-idf i2s.
Do you have oscilloscope? Please measure the signal on the GPIO 25&26.
Regards.
I made a demo that using internal DAC for you. :)
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
It worked for me. Please try to attach speaker directly to pin 25 or 26 without amplifier.
Regards,
If it uses IO12 then you can try another pin.
Regards,
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?
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,
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 ?
Thanks
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
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.
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.
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...
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;
Is it possible to use the esp32 file system to store the file instead of inside SD card?
Thank you.
Yes you can. Just replace the read file function above :)
regards,
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.
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
I got this compilation error
'i2s_write_bytes' was not declared in this scope
Can you help me please.
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;
```
When I verified the code for the external DAC, I got a few errors:
1. "File" wasn't recognised
2. "i2s_write_bytes" wasn't recognised
I assume that changing "File" to "ext::File" would solve that problem, but replacing "i2s_write_bytes" with "i2s_write" didn't work.
Any ideas?