├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── data └── sample.wav ├── include └── README ├── lib └── README ├── platformio.ini ├── src ├── audio_output │ ├── AudioOutput.h │ ├── DACOutput.cpp │ ├── DACOutput.h │ ├── I2SBase.cpp │ ├── I2SBase.h │ ├── PCMOutput.cpp │ ├── PCMOutput.h │ ├── PDMOutput.cpp │ ├── PDMOutput.h │ ├── PDMTimerOuput.cpp │ └── PDMTimerOutput.h ├── main.cpp └── wavfile │ ├── WAVFile.h │ ├── WAVFileReader.cpp │ └── WAVFileReader.h └── test └── README /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | .DS_Store -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomic14/esp32-pdm-audio/be02adf2dcb36b891f21580c65150bf26719a19e/README.md -------------------------------------------------------------------------------- /data/sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomic14/esp32-pdm-audio/be02adf2dcb36b891f21580c65150bf26719a19e/data/sample.wav -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:TinyWatch] 12 | platform = espressif32 13 | framework = arduino 14 | board = um_tinys3 15 | build_flags = 16 | ; audio settings 17 | -DPDM_GPIO_NUM=GPIO_NUM_18 18 | ; decode exceptions 19 | monitor_filters = esp32_exception_decoder 20 | monitor_speed = 115200 21 | -------------------------------------------------------------------------------- /src/audio_output/AudioOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /** 7 | * Base Class for both the DAC and I2S output 8 | **/ 9 | class AudioOutput 10 | { 11 | public: 12 | AudioOutput() {}; 13 | virtual void start(uint32_t sample_rate) = 0; 14 | virtual void stop() = 0; 15 | // override this in derived classes to turn the sample into 16 | // something the output device expects - for the default case 17 | // this is simply a pass through 18 | virtual int16_t process_sample(int16_t sample) { return sample; } 19 | // write samples to the output 20 | virtual void write(int16_t *samples, int count) = 0; 21 | }; 22 | -------------------------------------------------------------------------------- /src/audio_output/DACOutput.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "DACOutput.h" 3 | #include 4 | 5 | void DACOutput::start(uint32_t sample_rate) 6 | { 7 | // only include this if we're using DAC - the ESP32-S3 will fail compilation if we include this 8 | #ifdef USE_DAC_AUDIO 9 | // i2s config for writing both channels of I2S 10 | i2s_config_t i2s_config = { 11 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN), 12 | .sample_rate = sample_rate, 13 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, 14 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, 15 | .communication_format = I2S_COMM_FORMAT_STAND_MSB, 16 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 17 | .dma_buf_count = 4, 18 | .dma_buf_len = 1024, 19 | .use_apll = true, 20 | .tx_desc_auto_clear = true, 21 | .fixed_mclk = 0}; 22 | //install and start i2s driver 23 | esp_err_t res = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); 24 | if (res != ESP_OK) { 25 | Serial.printf("i2s_driver_install failed: %d\n", res); 26 | } 27 | // enable the DAC channels 28 | res = i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN); 29 | if (res != ESP_OK) { 30 | Serial.printf("i2s_set_dac_mode failed: %d\n", res); 31 | } 32 | // clear the DMA buffers 33 | res = i2s_zero_dma_buffer(I2S_NUM_0); 34 | if (res != ESP_OK) { 35 | Serial.printf("i2s_zero_dma_buffer failed: %d\n", res); 36 | } 37 | 38 | res = i2s_start(I2S_NUM_0); 39 | if (res != ESP_OK) { 40 | Serial.printf("i2s_start failed: %d\n", res); 41 | } 42 | #endif 43 | } 44 | -------------------------------------------------------------------------------- /src/audio_output/DACOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "I2SBase.h" 6 | 7 | /** 8 | * Use I2S to output to the DAC 9 | **/ 10 | class DACOutput : public I2SBase 11 | { 12 | public: 13 | DACOutput() : I2SBase(I2S_NUM_0) {} 14 | void start(uint32_t sample_rate); 15 | virtual int16_t process_sample(int16_t sample) 16 | { 17 | // DAC needs unsigned 16 bit samples 18 | return sample + 32768; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/audio_output/I2SBase.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include "I2SBase.h" 4 | #include 5 | #include 6 | 7 | static const char *TAG = "AUDIO"; 8 | 9 | // number of frames to try and send at once (a frame is a left and right sample) 10 | const size_t NUM_FRAMES_TO_SEND=1024; 11 | 12 | I2SBase::I2SBase(i2s_port_t i2s_port) : m_i2s_port(i2s_port) 13 | { 14 | m_tmp_frames = (int16_t *)malloc(2 * sizeof(int16_t) * NUM_FRAMES_TO_SEND); 15 | } 16 | 17 | void I2SBase::stop() 18 | { 19 | // stop the i2S driver 20 | i2s_stop(m_i2s_port); 21 | i2s_driver_uninstall(m_i2s_port); 22 | } 23 | 24 | void I2SBase::write(int16_t *samples, int count) 25 | { 26 | int sample_index = 0; 27 | while (sample_index < count) 28 | { 29 | int samples_to_send = 0; 30 | for (int i = 0; i < NUM_FRAMES_TO_SEND && sample_index < count; i++) 31 | { 32 | // shift up to 16 bit samples 33 | int sample = process_sample(samples[sample_index]); 34 | // write the sample to both channels 35 | m_tmp_frames[i * 2] = sample; 36 | m_tmp_frames[i * 2 + 1] = sample; 37 | samples_to_send++; 38 | sample_index++; 39 | } 40 | // write data to the i2s peripheral 41 | size_t bytes_written = 0; 42 | esp_err_t res = i2s_write(m_i2s_port, m_tmp_frames, samples_to_send * sizeof(int16_t) * 2, &bytes_written, 1000 / portTICK_PERIOD_MS); 43 | if (res != ESP_OK) 44 | { 45 | ESP_LOGE(TAG, "Error sending audio data: %d", res); 46 | } 47 | if (bytes_written != samples_to_send * sizeof(int16_t) * 2) 48 | { 49 | ESP_LOGE(TAG, "Did not write all bytes"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/audio_output/I2SBase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "AudioOutput.h" 7 | 8 | /** 9 | * Base Class for both the DAC, PCM and PDM output 10 | **/ 11 | class I2SBase : public AudioOutput 12 | { 13 | protected: 14 | i2s_port_t m_i2s_port = I2S_NUM_0; 15 | int16_t *m_tmp_frames = NULL; 16 | public: 17 | I2SBase(i2s_port_t i2s_port); 18 | void stop(); 19 | void write(int16_t *samples, int count); 20 | // override this in derived classes to turn the sample into 21 | // something the output device expects - for the default case 22 | // this is simply a pass through 23 | virtual int16_t process_sample(int16_t sample) { return sample; } 24 | }; 25 | -------------------------------------------------------------------------------- /src/audio_output/PCMOutput.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "PCMOutput.h" 3 | 4 | PCMOutput::PCMOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins) : I2SBase(i2s_port), m_i2s_pins(i2s_pins) 5 | { 6 | } 7 | 8 | void PCMOutput::start(uint32_t sample_rate) 9 | { 10 | // i2s config for writing both channels of I2S 11 | i2s_config_t i2s_config = { 12 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), 13 | .sample_rate = sample_rate, 14 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, 15 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, 16 | .communication_format = I2S_COMM_FORMAT_STAND_I2S, 17 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 18 | .dma_buf_count = 4, 19 | .dma_buf_len = 1024, 20 | .use_apll = false, 21 | .tx_desc_auto_clear = true, 22 | .fixed_mclk = 0}; 23 | //install and start i2s driver 24 | i2s_driver_install(m_i2s_port, &i2s_config, 0, NULL); 25 | // set up the i2s pins 26 | i2s_set_pin(m_i2s_port, &m_i2s_pins); 27 | // clear the DMA buffers 28 | i2s_zero_dma_buffer(m_i2s_port); 29 | 30 | i2s_start(m_i2s_port); 31 | } 32 | -------------------------------------------------------------------------------- /src/audio_output/PCMOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "I2SBase.h" 4 | 5 | /** 6 | * Use I2S PCM output 7 | **/ 8 | class PCMOutput : public I2SBase 9 | { 10 | private: 11 | i2s_pin_config_t m_i2s_pins; 12 | public: 13 | PCMOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins); 14 | void start(uint32_t sample_rate); 15 | }; 16 | -------------------------------------------------------------------------------- /src/audio_output/PDMOutput.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "PDMOutput.h" 3 | #include 4 | 5 | void PDMOutput::start(uint32_t sample_rate) 6 | { 7 | // i2s config for writing both channels of I2S 8 | i2s_config_t i2s_config = { 9 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_PDM), 10 | .sample_rate = sample_rate, 11 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, 12 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, 13 | .communication_format = I2S_COMM_FORMAT_STAND_I2S, 14 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 15 | .dma_buf_count = 4, 16 | .dma_buf_len = 1024, 17 | .use_apll = false, 18 | .tx_desc_auto_clear = true, 19 | .fixed_mclk = 0}; 20 | //install and start i2s driver 21 | i2s_driver_install(m_i2s_port, &i2s_config, 0, NULL); 22 | // set up the i2s pins 23 | i2s_set_pin(m_i2s_port, &m_i2s_pins); 24 | // clear the DMA buffers 25 | i2s_zero_dma_buffer(m_i2s_port); 26 | 27 | i2s_start(m_i2s_port); 28 | } 29 | -------------------------------------------------------------------------------- /src/audio_output/PDMOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "I2SBase.h" 6 | 7 | /** 8 | * Use I2S to output PDM samples 9 | **/ 10 | class PDMOutput : public I2SBase 11 | { 12 | private: 13 | i2s_pin_config_t m_i2s_pins; 14 | public: 15 | PDMOutput(i2s_pin_config_t &i2s_pins): I2SBase(I2S_NUM_0), m_i2s_pins(i2s_pins) {}; 16 | void start(uint32_t sample_rate); 17 | int16_t process_sample(int16_t sample) { 18 | float normalised = (float)sample / 32768.0f; 19 | // give it some welly 20 | normalised *= 10.0f; 21 | if (normalised > 1.0f) normalised = 1.0f; 22 | if (normalised < -1.0f) normalised = -1.0f; 23 | return normalised * 32767.0f; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/audio_output/PDMTimerOuput.cpp: -------------------------------------------------------------------------------- 1 | #include "PDMTimerOutput.h" 2 | #include "freertos/semphr.h" 3 | #include "driver/sigmadelta.h" 4 | #include 5 | #include 6 | 7 | 8 | IRAM_ATTR void onTimerCallback(void *param) 9 | { 10 | PDMTimerOutput *output = (PDMTimerOutput *)param; 11 | timer_spinlock_take(TIMER_GROUP_0); 12 | timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0); 13 | timer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0); 14 | timer_spinlock_give(TIMER_GROUP_0); 15 | output->onTimer(); 16 | } 17 | 18 | void PDMTimerOutput::start(uint32_t sample_rate) 19 | { 20 | mSampleRate = sample_rate; 21 | 22 | sigmadelta_config_t config; 23 | config.channel = SIGMADELTA_CHANNEL_0; 24 | config.sigmadelta_duty = 0; 25 | config.sigmadelta_prescale = 255; 26 | config.sigmadelta_gpio = mPDMPin; 27 | 28 | // configure the PDM 29 | esp_err_t result = sigmadelta_config(&config); 30 | if (result != ESP_OK) 31 | { 32 | Serial.printf("Error initializing PDM: %d\n", result); 33 | } 34 | // result = sigmdelta_begin(SIGMADELTA_CHANNEL_0); 35 | // if (result != ESP_OK) 36 | // { 37 | // Serial.printf("Error beginning PDM: %d\n", result); 38 | // } 39 | // pinMode(18, OUTPUT); 40 | 41 | // create a timer that will fire at the sample rate 42 | timer_config_t timer_config = { 43 | .alarm_en = TIMER_ALARM_EN, 44 | .counter_en = TIMER_PAUSE, 45 | .intr_type = TIMER_INTR_LEVEL, 46 | .counter_dir = TIMER_COUNT_UP, 47 | .auto_reload = TIMER_AUTORELOAD_EN, 48 | .divider = 80}; 49 | result = timer_init(TIMER_GROUP_0, TIMER_0, &timer_config); 50 | if (result != ESP_OK) 51 | { 52 | Serial.printf("Error initializing timer: %d\n", result); 53 | } 54 | result = timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL); 55 | if (result != ESP_OK) 56 | { 57 | Serial.printf("Error setting timer counter value: %d\n", result); 58 | } 59 | result = timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 1000000 / mSampleRate); 60 | if (result != ESP_OK) 61 | { 62 | Serial.printf("Error setting timer alarm value: %d\n", result); 63 | } 64 | // timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 1000); 65 | result = timer_enable_intr(TIMER_GROUP_0, TIMER_0); 66 | if (result != ESP_OK) 67 | { 68 | Serial.printf("Error enabling timer interrupt: %d\n", result); 69 | } 70 | result = timer_isr_register(TIMER_GROUP_0, TIMER_0, &onTimerCallback, this, ESP_INTR_FLAG_IRAM, NULL); 71 | if (result != ESP_OK) 72 | { 73 | Serial.printf("Error registering timer interrupt: %d\n", result); 74 | // print the string error 75 | const char *err = esp_err_to_name(result); 76 | Serial.printf("Error: %s\n", err); 77 | } 78 | result = timer_start(TIMER_GROUP_0, TIMER_0); 79 | if (result != ESP_OK) 80 | { 81 | Serial.printf("Error starting timer: %d\n", result); 82 | } 83 | Serial.println("PDM Started"); 84 | } 85 | 86 | void PDMTimerOutput::write(int8_t *samples, int count) 87 | { 88 | // Serial.printf("Count %d\n", mCount); 89 | while (true) 90 | { 91 | if(xSemaphoreTake(mBufferSemaphore, portMAX_DELAY)) { 92 | // is the second buffer empty? 93 | if (mSecondBufferLength == 0) 94 | { 95 | //Serial.println("Filling second buffer"); 96 | // make sure there's enough room for the samples 97 | mSecondBuffer = (int8_t *)realloc(mSecondBuffer, count); 98 | // copy them into the second buffer 99 | memcpy(mSecondBuffer, samples, count); 100 | // second buffer is now full of samples 101 | mSecondBufferLength = count; 102 | // unlock the mutext and return 103 | xSemaphoreGive(mBufferSemaphore); 104 | return; 105 | } 106 | // no room in the second buffer so wait for the first buffer to be emptied 107 | xSemaphoreGive(mBufferSemaphore); 108 | } 109 | vTaskDelay(1 / portTICK_PERIOD_MS); 110 | } 111 | } 112 | 113 | void PDMTimerOutput::onTimer() 114 | { 115 | // output a sample from the buffer if we have one 116 | if (mCurrentIndex < mBufferLength) 117 | { 118 | mCount++; 119 | // get the first sample from the buffer 120 | int16_t sample = mBuffer[mCurrentIndex]; 121 | mCurrentIndex++; 122 | sample *= 20; 123 | if (sample > 90) { 124 | sample = 90; 125 | } 126 | if (sample < -90) { 127 | sample = -90; 128 | } 129 | // write the sample to the PDM 130 | sigmadelta_set_duty(SIGMADELTA_CHANNEL_0, sample); 131 | } 132 | if(mCurrentIndex >= mBufferLength) 133 | { 134 | // do we have any data in teh second buffer? 135 | BaseType_t xHigherPriorityTaskWoken; 136 | if (xSemaphoreTakeFromISR(mBufferSemaphore, &xHigherPriorityTaskWoken) == pdTRUE) 137 | { 138 | if (mSecondBufferLength > 0) { 139 | // swap the buffers 140 | int8_t *tmp = mBuffer; 141 | mBuffer = mSecondBuffer; 142 | mBufferLength = mSecondBufferLength; 143 | mSecondBuffer = tmp; 144 | mSecondBufferLength = 0; 145 | mCurrentIndex = 0; 146 | } 147 | xSemaphoreGiveFromISR(mBufferSemaphore, &xHigherPriorityTaskWoken); 148 | } 149 | if(xHigherPriorityTaskWoken) { 150 | portYIELD_FROM_ISR(); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /src/audio_output/PDMTimerOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "AudioOutput.h" 4 | #include "freertos/FreeRTOS.h" 5 | #include "freertos/task.h" 6 | #include "freertos/semphr.h" 7 | #include "esp_log.h" 8 | #include "driver/sigmadelta.h" 9 | #include "driver/timer.h" 10 | 11 | /** 12 | * Output PDM samples using a timer 13 | **/ 14 | class PDMTimerOutput : public AudioOutput 15 | { 16 | private: 17 | gpio_num_t mPDMPin; 18 | uint32_t mSampleRate; 19 | SemaphoreHandle_t mBufferSemaphore; 20 | int8_t *mBuffer=NULL;; 21 | int mCurrentIndex=0; 22 | int mBufferLength=0; 23 | int8_t *mSecondBuffer=NULL;; 24 | int mSecondBufferLength=0; 25 | int mCount = 0; 26 | void onTimer(); 27 | public: 28 | PDMTimerOutput(gpio_num_t pdm_pin) : AudioOutput() 29 | { 30 | mPDMPin = pdm_pin; 31 | mBufferSemaphore = xSemaphoreCreateBinary(); 32 | xSemaphoreGive(mBufferSemaphore); 33 | } 34 | void write(int8_t *samples, int count); 35 | void start(uint32_t sample_rate); 36 | void stop() {} 37 | 38 | friend void onTimerCallback(void *arg); 39 | }; 40 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "SPIFFS.h" 3 | 4 | #include "audio_output/PCMOutput.h" 5 | #include "audio_output/DACOutput.h" 6 | #include "audio_output/PDMTimerOutput.h" 7 | #include "audio_output/PDMOutput.h" 8 | 9 | #include "wavfile/WAVFileReader.h" 10 | 11 | AudioOutput *audioOutput = NULL; 12 | WAVFileReader *wavFileReader = NULL; 13 | 14 | // assume the test wav file is 16KHz... 15 | #define SAMPLE_RATE 16000 16 | 17 | void setup() 18 | { 19 | Serial.begin(115200); 20 | // for(int i = 0; i<10; i++) { 21 | // Serial.printf("."); 22 | // delay(1000); 23 | // } 24 | #ifdef USE_DAC_AUDIO 25 | audioOutput = new DACOutput(I2S_NUM_0); 26 | audioOutput->start(SAMPLE_RATE); 27 | #endif 28 | #ifdef PDM_GPIO_NUM 29 | i2s_pin_config_t i2s_pdm_pins = { 30 | // no bck for PDM 31 | .bck_io_num = I2S_PIN_NO_CHANGE, 32 | // use a dummy pin for the LR clock - 45 or 46 is a good options 33 | // as these are normally not connected 34 | .ws_io_num = GPIO_NUM_45, 35 | // where should we send the PDM data 36 | .data_out_num = PDM_GPIO_NUM, 37 | // no data to read 38 | .data_in_num = I2S_PIN_NO_CHANGE}; 39 | audioOutput = new PDMOutput(i2s_pdm_pins); 40 | audioOutput->start(SAMPLE_RATE); 41 | #endif 42 | #ifdef I2S_SPEAKER_SERIAL_CLOCK 43 | // i2s speaker pins 44 | i2s_pin_config_t i2s_speaker_pins = { 45 | .bck_io_num = I2S_SPEAKER_SERIAL_CLOCK, 46 | .ws_io_num = I2S_SPEAKER_LEFT_RIGHT_CLOCK, 47 | .data_out_num = I2S_SPEAKER_SERIAL_DATA, 48 | .data_in_num = I2S_PIN_NO_CHANGE}; 49 | 50 | audioOutput = new PCMOutput(I2S_NUM_0, i2s_speaker_pins); 51 | audioOutput->start(SAMPLE_RATE); 52 | #endif 53 | 54 | // open up the WAV file from spiffs 55 | if (!SPIFFS.begin()) { 56 | Serial.println("Failed to mount file system"); 57 | } 58 | FILE *fp = fopen("/spiffs/sample.wav", "r"); 59 | if (fp == NULL) 60 | { 61 | Serial.println("Failed to open file for reading"); 62 | } 63 | wavFileReader = new WAVFileReader(fp); 64 | } 65 | 66 | void loop() 67 | { 68 | while(true) { 69 | int16_t samples[1000]; 70 | int count = wavFileReader->read(samples, 1000); 71 | if (count > 0) 72 | { 73 | audioOutput->write(samples, count); 74 | } 75 | else 76 | { 77 | wavFileReader->rewind(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/wavfile/WAVFile.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #pragma pack(push, 1) 4 | typedef struct _wav_header 5 | { 6 | // RIFF Header 7 | char riff_header[4]; // Contains "RIFF" 8 | int wav_size = 0; // Size of the wav portion of the file, which follows the first 8 bytes. File size - 8 9 | char wave_header[4]; // Contains "WAVE" 10 | 11 | // Format Header 12 | char fmt_header[4]; // Contains "fmt " (includes trailing space) 13 | int fmt_chunk_size = 16; // Should be 16 for PCM 14 | short audio_format = 1; // Should be 1 for PCM. 3 for IEEE Float 15 | short num_channels = 1; 16 | int sample_rate = 16000; 17 | int byte_rate = 32000; // Number of bytes per second. sample_rate * num_channels * Bytes Per Sample 18 | short sample_alignment = 2; // num_channels * Bytes Per Sample 19 | short bit_depth = 16; // Number of bits per sample 20 | 21 | // Data 22 | char data_header[4]; // Contains "data" 23 | int data_bytes = 0; // Number of bytes in data. Number of samples * num_channels * sample byte size 24 | // uint8_t bytes[]; // Remainder of wave file is bytes 25 | _wav_header() 26 | { 27 | riff_header[0] = 'R'; 28 | riff_header[1] = 'I'; 29 | riff_header[2] = 'F'; 30 | riff_header[3] = 'F'; 31 | wave_header[0] = 'W'; 32 | wave_header[1] = 'A'; 33 | wave_header[2] = 'V'; 34 | wave_header[3] = 'E'; 35 | fmt_header[0] = 'f'; 36 | fmt_header[1] = 'm'; 37 | fmt_header[2] = 't'; 38 | fmt_header[3] = ' '; 39 | data_header[0] = 'd'; 40 | data_header[1] = 'a'; 41 | data_header[2] = 't'; 42 | data_header[3] = 'a'; 43 | } 44 | } wav_header_t; 45 | #pragma pack(pop) 46 | -------------------------------------------------------------------------------- /src/wavfile/WAVFileReader.cpp: -------------------------------------------------------------------------------- 1 | #include "esp_log.h" 2 | #include "WAVFileReader.h" 3 | 4 | static const char *TAG = "WAV"; 5 | 6 | WAVFileReader::WAVFileReader(FILE *fp) 7 | { 8 | m_fp = fp; 9 | // read the WAV header 10 | fread((void *)&m_wav_header, sizeof(wav_header_t), 1, m_fp); 11 | // sanity check the bit depth 12 | if (m_wav_header.bit_depth != 16) 13 | { 14 | ESP_LOGE(TAG, "ERROR: bit depth %d is not supported\n", m_wav_header.bit_depth); 15 | } 16 | if (m_wav_header.num_channels != 1) 17 | { 18 | ESP_LOGE(TAG, "ERROR: channels %d is not supported\n", m_wav_header.num_channels); 19 | } 20 | ESP_LOGI(TAG, "fmt_chunk_size=%d, audio_format=%d, num_channels=%d, sample_rate=%d, sample_alignment=%d, bit_depth=%d, data_bytes=%d\n", 21 | m_wav_header.fmt_chunk_size, m_wav_header.audio_format, m_wav_header.num_channels, m_wav_header.sample_rate, m_wav_header.sample_alignment, m_wav_header.bit_depth, m_wav_header.data_bytes); 22 | } 23 | 24 | int WAVFileReader::read(int16_t *samples, int count) 25 | { 26 | size_t read = fread(samples, sizeof(int16_t), count, m_fp); 27 | return read; 28 | } 29 | -------------------------------------------------------------------------------- /src/wavfile/WAVFileReader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "WAVFile.h" 4 | #include 5 | 6 | class WAVFileReader 7 | { 8 | private: 9 | wav_header_t m_wav_header; 10 | 11 | FILE *m_fp; 12 | 13 | public: 14 | WAVFileReader(FILE *fp); 15 | int sample_rate() { return m_wav_header.sample_rate; } 16 | int read(int16_t *samples, int count); 17 | int rewind() { return fseek(m_fp, sizeof(wav_header_t), SEEK_SET); } 18 | }; 19 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Test Runner and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html 12 | --------------------------------------------------------------------------------