├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── include └── README ├── lib ├── README ├── audio_input │ ├── library.json │ └── src │ │ ├── ADCSampler.cpp │ │ ├── ADCSampler.h │ │ ├── I2SMEMSSampler.cpp │ │ ├── I2SMEMSSampler.h │ │ ├── I2SSampler.cpp │ │ └── I2SSampler.h ├── audio_output │ ├── library.json │ └── src │ │ ├── DACOutput.cpp │ │ ├── DACOutput.h │ │ ├── I2SOutput.cpp │ │ ├── I2SOutput.h │ │ ├── Output.cpp │ │ ├── Output.h │ │ └── OutputBuffer.h ├── indicator_led │ └── src │ │ ├── GenericDevBoardIndicatorLed.cpp │ │ ├── GenericDevBoardIndicatorLed.h │ │ ├── IndicatorLed.cpp │ │ └── IndicatorLed.h ├── indicator_led_pico │ └── src │ │ ├── TinyPICOIndicatorLed.cpp │ │ └── TinyPICOIndicatorLed.h └── transport │ └── src │ ├── EspNowTransport.cpp │ ├── EspNowTransport.h │ ├── Transport.cpp │ ├── Transport.h │ ├── UdpTransport.cpp │ └── UdpTransport.h ├── platformio.ini ├── platformio_override.example.ini ├── src ├── Application.cpp ├── Application.h ├── config.cpp ├── config.h └── main.cpp └── test └── README /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [atomic14] 4 | ko_fi: atomic14 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build Environments 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Cache pip 12 | uses: actions/cache@v2 13 | with: 14 | path: ~/.cache/pip 15 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 16 | restore-keys: | 17 | ${{ runner.os }}-pip- 18 | - name: Cache PlatformIO 19 | uses: actions/cache@v2 20 | with: 21 | path: ~/.platformio 22 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 23 | - name: Set up Python 24 | uses: actions/setup-python@v2 25 | - name: Install PlatformIO 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install --upgrade platformio 29 | - name: Build firmware 30 | run: pio run 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | package-lock.json 7 | 8 | platformio_override.ini 9 | -------------------------------------------------------------------------------- /.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 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.rb": "ruby", 4 | "SP_ENC.C": "cpp", 5 | "SP_DEC.C": "cpp", 6 | "DTX.C": "cpp", 7 | "HOST.C": "cpp", 8 | "VAD.C": "cpp" 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 atomic14 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | This project is a work-in-progress (currently non-functional test/beta), multi-room intercom system based on the original esp32 Walkie Talkie code by Atomic14. 3 | 4 | It uses i2S microphones and amplifiers, and transmits the audio over UDP broadcast 5 | 6 | Platform.IO is not an environment I am not used to coding in. There may be cringeworthy code in my changes. If you spot something, please point it out. 7 | 8 | The theory of operation is to have 5 buttons on the console for 4 rooms and a 'broadcast' button. Each unit has it's own room number and will only play messages destined for it, or the broadcast address. 9 | 10 | A single byte header is applied to every packet sent, ranging from 0x00 (broadcast) to 0x04 depending on button pressed. There is no reason you could not add more, the only restriction being io pin availability for the tx button. Each speaker is hard coded to an ID and only plays messages with the header of that ID or 0x00. 11 | 12 | Audio data is transmitted over UDP broadcast 13 | 14 | 15 | # Working 16 | Confirmed Transmit to the 4 channels + broadcast via wireshark 17 | 18 | # Not Tested but appears to work 19 | Receive of only the corresponding channel 20 | 21 | As of 23/3/2023 I am still waiting on some i2s microphones to arrive from china. Once i get them I should be able to fully test 22 | 23 | # Setup 24 | 25 | Everything is configured from the `src/config.h` file. 26 | 27 | Make sure you update the WiFi SSID and Password: 28 | 29 | ``` 30 | // WiFi credentials 31 | #define WIFI_SSID << YOUR_SSID >> 32 | #define WIFI_PSWD << YOUR_PASSWORD >> 33 | ``` 34 | 35 | The pins for the microphone and the amplifier board are all setup in the same `config.h` file. 36 | 37 | # Building and Running 38 | 39 | Open up the project in Platform.IO and connect your ESP32. You should be able to just hit build and run. 40 | 41 | Obviously, you'll need two ESP32 boards and components to do anything :) 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/audio_input/library.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "flags": "-Ofast" 4 | } 5 | } -------------------------------------------------------------------------------- /lib/audio_input/src/ADCSampler.cpp: -------------------------------------------------------------------------------- 1 | #include "ADCSampler.h" 2 | 3 | #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 4 | 5 | ADCSampler::ADCSampler(adc_unit_t adcUnit, adc1_channel_t adcChannel, const i2s_config_t &i2s_config) : I2SSampler(I2S_NUM_0, i2s_config) 6 | { 7 | m_adcUnit = adcUnit; 8 | m_adcChannel = adcChannel; 9 | } 10 | 11 | void ADCSampler::configureI2S() 12 | { 13 | //init ADC pad 14 | i2s_set_adc_mode(m_adcUnit, m_adcChannel); 15 | // enable the adc 16 | i2s_adc_enable(m_i2sPort); 17 | } 18 | 19 | int ADCSampler::read(int16_t *samples, int count) 20 | { 21 | // read from i2s 22 | size_t bytes_read = 0; 23 | i2s_read(m_i2sPort, samples, sizeof(int16_t) * count, &bytes_read, portMAX_DELAY); 24 | int samples_read = bytes_read / sizeof(int16_t); 25 | for (int i = 0; i < samples_read; i++) 26 | { 27 | samples[i] = (2048 - (uint16_t(samples[i]) & 0xfff)) * 15; 28 | } 29 | return samples_read; 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /lib/audio_input/src/ADCSampler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "I2SSampler.h" 5 | 6 | class ADCSampler : public I2SSampler 7 | { 8 | private: 9 | adc_unit_t m_adcUnit; 10 | adc1_channel_t m_adcChannel; 11 | 12 | protected: 13 | void configureI2S(); 14 | 15 | public: 16 | ADCSampler(adc_unit_t adc_unit, adc1_channel_t adc_channel, const i2s_config_t &i2s_config); 17 | virtual int read(int16_t *samples, int count); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/audio_input/src/I2SMEMSSampler.cpp: -------------------------------------------------------------------------------- 1 | #include "I2SMEMSSampler.h" 2 | #include "soc/i2s_reg.h" 3 | 4 | I2SMEMSSampler::I2SMEMSSampler( 5 | i2s_port_t i2s_port, 6 | i2s_pin_config_t &i2s_pins, 7 | i2s_config_t i2s_config, 8 | int raw_samples_size, 9 | bool fixSPH0645) : I2SSampler(i2s_port, i2s_config) 10 | { 11 | m_i2sPins = i2s_pins; 12 | m_fixSPH0645 = fixSPH0645; 13 | m_raw_samples_size = raw_samples_size; 14 | m_raw_samples = (int32_t *)malloc(sizeof(int32_t) * raw_samples_size); 15 | } 16 | 17 | I2SMEMSSampler::~I2SMEMSSampler() 18 | { 19 | free(m_raw_samples); 20 | } 21 | 22 | void I2SMEMSSampler::configureI2S() 23 | { 24 | if (m_fixSPH0645) 25 | { 26 | // FIXES for SPH0645 27 | #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 28 | REG_SET_BIT(I2S_TIMING_REG(m_i2sPort), BIT(9)); 29 | REG_SET_BIT(I2S_CONF_REG(m_i2sPort), I2S_RX_MSB_SHIFT); 30 | #endif 31 | } 32 | 33 | i2s_set_pin(m_i2sPort, &m_i2sPins); 34 | } 35 | 36 | int I2SMEMSSampler::read(int16_t *samples, int count) 37 | { 38 | // read from i2s 39 | size_t bytes_read = 0; 40 | if (count>m_raw_samples_size) 41 | { 42 | count = m_raw_samples_size; // Buffer is too small 43 | } 44 | i2s_read(m_i2sPort, m_raw_samples, sizeof(int32_t) * count, &bytes_read, portMAX_DELAY); 45 | int samples_read = bytes_read / sizeof(int32_t); 46 | for (int i = 0; i < samples_read; i++) 47 | { 48 | int32_t temp = m_raw_samples[i] >> 11; 49 | samples[i] = (temp > INT16_MAX) ? INT16_MAX : (temp < -INT16_MAX) ? -INT16_MAX : (int16_t)temp; 50 | } 51 | return samples_read; 52 | } 53 | -------------------------------------------------------------------------------- /lib/audio_input/src/I2SMEMSSampler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "I2SSampler.h" 4 | 5 | class I2SMEMSSampler : public I2SSampler 6 | { 7 | private: 8 | i2s_pin_config_t m_i2sPins; 9 | bool m_fixSPH0645; 10 | int32_t *m_raw_samples; 11 | int m_raw_samples_size; 12 | 13 | protected: 14 | void configureI2S(); 15 | 16 | public: 17 | I2SMEMSSampler( 18 | i2s_port_t i2s_port, 19 | i2s_pin_config_t &i2s_pins, 20 | i2s_config_t i2s_config, 21 | int raw_samples_size, 22 | bool fixSPH0645 = false); 23 | ~I2SMEMSSampler(); 24 | virtual int read(int16_t *samples, int count); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/audio_input/src/I2SSampler.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "I2SSampler.h" 3 | #include "driver/i2s.h" 4 | 5 | I2SSampler::I2SSampler(i2s_port_t i2sPort, const i2s_config_t &i2s_config) : m_i2sPort(i2sPort), m_i2s_config(i2s_config) 6 | { 7 | } 8 | 9 | void I2SSampler::start() 10 | { 11 | //install and start i2s driver 12 | i2s_driver_install(m_i2sPort, &m_i2s_config, 0, NULL); 13 | // set up the I2S configuration from the subclass 14 | configureI2S(); 15 | } 16 | 17 | void I2SSampler::stop() 18 | { 19 | // stop the i2S driver 20 | i2s_driver_uninstall(m_i2sPort); 21 | } 22 | -------------------------------------------------------------------------------- /lib/audio_input/src/I2SSampler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /** 7 | * Base Class for both the ADC and I2S sampler 8 | **/ 9 | class I2SSampler 10 | { 11 | protected: 12 | i2s_port_t m_i2sPort = I2S_NUM_0; 13 | i2s_config_t m_i2s_config; 14 | virtual void configureI2S() = 0; 15 | virtual void processI2SData(void *samples, size_t count){ 16 | // nothing to do for the default case 17 | }; 18 | 19 | public: 20 | I2SSampler(i2s_port_t i2sPort, const i2s_config_t &i2sConfig); 21 | void start(); 22 | virtual int read(int16_t *samples, int count) = 0; 23 | void stop(); 24 | int sample_rate() 25 | { 26 | return m_i2s_config.sample_rate; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/audio_output/library.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "flags": "-Ofast" 4 | } 5 | } -------------------------------------------------------------------------------- /lib/audio_output/src/DACOutput.cpp: -------------------------------------------------------------------------------- 1 | #include "DACOutput.h" 2 | 3 | #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 4 | 5 | void DACOutput::start(int 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_DAC_BUILT_IN), 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_t)(I2S_COMM_FORMAT_I2S_MSB), 14 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 15 | .dma_buf_count = 2, 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(I2S_NUM_0, &i2s_config, 0, NULL); 22 | // enable the DAC channels 23 | i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN); 24 | // clear the DMA buffers 25 | i2s_zero_dma_buffer(I2S_NUM_0); 26 | 27 | i2s_start(I2S_NUM_0); 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /lib/audio_output/src/DACOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "Output.h" 6 | 7 | /** 8 | * Base Class for both the ADC and I2S sampler 9 | **/ 10 | class DACOutput : public Output 11 | { 12 | public: 13 | DACOutput(i2s_port_t i2s_port) : Output(i2s_port) {} 14 | void start(int 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 | -------------------------------------------------------------------------------- /lib/audio_output/src/I2SOutput.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "I2SOutput.h" 3 | 4 | I2SOutput::I2SOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins) : Output(i2s_port), m_i2s_pins(i2s_pins) 5 | { 6 | } 7 | 8 | void I2SOutput::start(int 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 | #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) 17 | .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), 18 | #else 19 | .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S), 20 | #endif 21 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 22 | .dma_buf_count = 2, 23 | .dma_buf_len = 1024, 24 | .use_apll = false, 25 | .tx_desc_auto_clear = true, 26 | .fixed_mclk = 0}; 27 | //install and start i2s driver 28 | i2s_driver_install(m_i2s_port, &i2s_config, 0, NULL); 29 | // set up the i2s pins 30 | i2s_set_pin(m_i2s_port, &m_i2s_pins); 31 | // clear the DMA buffers 32 | i2s_zero_dma_buffer(m_i2s_port); 33 | 34 | i2s_start(m_i2s_port); 35 | } 36 | -------------------------------------------------------------------------------- /lib/audio_output/src/I2SOutput.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Output.h" 4 | 5 | /** 6 | * Base Class for both the ADC and I2S sampler 7 | **/ 8 | class I2SOutput : public Output 9 | { 10 | private: 11 | i2s_pin_config_t m_i2s_pins; 12 | 13 | public: 14 | I2SOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins); 15 | void start(int sample_rate); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/audio_output/src/Output.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "Output.h" 3 | #include 4 | #include 5 | 6 | static const char *TAG = "OUT"; 7 | 8 | // number of frames to try and send at once (a frame is a left and right sample) 9 | const int NUM_FRAMES_TO_SEND = 256; 10 | 11 | Output::Output(i2s_port_t i2s_port) : m_i2s_port(i2s_port) 12 | { 13 | // this will contain the prepared samples for sending to the I2S device 14 | m_frames = (int16_t *)malloc(2 * sizeof(int16_t) * NUM_FRAMES_TO_SEND); 15 | } 16 | 17 | Output::~Output() 18 | { 19 | free(m_frames); 20 | } 21 | 22 | void Output::stop() 23 | { 24 | // stop the i2S driver 25 | i2s_stop(m_i2s_port); 26 | i2s_driver_uninstall(m_i2s_port); 27 | } 28 | 29 | void Output::write(int16_t *samples, int count) 30 | { 31 | int sample_index = 0; 32 | while (sample_index < count) 33 | { 34 | int samples_to_send = 0; 35 | for (int i = 0; i < NUM_FRAMES_TO_SEND && sample_index < count; i++) 36 | { 37 | int sample = process_sample(samples[sample_index]); 38 | m_frames[i * 2] = sample; // left channel 39 | m_frames[i * 2 + 1] = sample; // right channel 40 | samples_to_send++; 41 | sample_index++; 42 | } 43 | // write data to the i2s peripheral 44 | size_t bytes_written = 0; 45 | i2s_write(m_i2s_port, m_frames, samples_to_send * sizeof(int16_t) * 2, &bytes_written, portMAX_DELAY); 46 | if (bytes_written != samples_to_send * sizeof(int16_t) * 2) 47 | { 48 | ESP_LOGE(TAG, "Did not write all bytes"); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/audio_output/src/Output.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /** 7 | * Base Class for both the DAC and I2S output 8 | **/ 9 | class Output 10 | { 11 | private: 12 | int16_t *m_frames; 13 | 14 | protected: 15 | i2s_port_t m_i2s_port = I2S_NUM_0; 16 | 17 | public: 18 | Output(i2s_port_t i2s_port); 19 | ~Output(); 20 | virtual void start(int sample_rate) = 0; 21 | void stop(); 22 | // override this in derived classes to turn the sample into 23 | // something the output device expects - for the default case 24 | // this is simply a pass through 25 | virtual int16_t process_sample(int16_t sample) { return sample; } 26 | void write(int16_t *samples, int count); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/audio_output/src/OutputBuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /** 7 | * @brief Circular buffer for 8 bit unsigned PCM samples 8 | * 9 | */ 10 | class OutputBuffer 11 | { 12 | private: 13 | // how many samples should we buffer before outputting data? 14 | int m_number_samples_to_buffer; 15 | // where are we reading from 16 | int m_read_head; 17 | // where are we writing to 18 | int m_write_head; 19 | // keep track of how many samples we have 20 | int m_available_samples; 21 | // the total size of the buffer 22 | int m_buffer_size; 23 | // are we currently buffering samples? 24 | bool m_buffering; 25 | // the sample buffer 26 | uint8_t *m_buffer; 27 | // thread safety 28 | SemaphoreHandle_t m_semaphore; 29 | 30 | public: 31 | OutputBuffer(int number_samples_to_buffer) : m_number_samples_to_buffer(number_samples_to_buffer) 32 | { 33 | // create a semaphore and make it available for locking 34 | m_semaphore = xSemaphoreCreateBinary(); 35 | xSemaphoreGive(m_semaphore); 36 | // set reading and writing to the beginning of the buffer 37 | m_read_head = 0; 38 | m_write_head = 0; 39 | m_available_samples = 0; 40 | // we'll start off buffering data as we have no samples yet 41 | m_buffering = true; 42 | // make sufficient space for the bufferring and incoming data 43 | m_buffer_size = 3 * number_samples_to_buffer; 44 | m_buffer = (uint8_t *)malloc(m_buffer_size); 45 | memset(m_buffer, 0, m_buffer_size); 46 | if (!m_buffer) 47 | { 48 | Serial.println("Failed to allocate buffer"); 49 | } 50 | } 51 | 52 | // we're adding samples that are 8 bit as they are coming from the transport 53 | void add_samples(const uint8_t *samples, int count) 54 | { 55 | xSemaphoreTake(m_semaphore, portMAX_DELAY); 56 | // copy the samples into the buffer wrapping around as needed 57 | for (int i = 0; i < count; i++) 58 | { 59 | m_buffer[m_write_head] = samples[i]; 60 | m_write_head = (m_write_head + 1) % m_buffer_size; 61 | } 62 | m_available_samples += count; 63 | xSemaphoreGive(m_semaphore); 64 | } 65 | 66 | // convert the samples to 16 bit as they are going to the output 67 | void remove_samples(int16_t *samples, int count) 68 | { 69 | xSemaphoreTake(m_semaphore, portMAX_DELAY); 70 | for (int i = 0; i < count; i++) 71 | { 72 | samples[i] = 0; 73 | // if we have no samples and we aren't already buffering then we need to start buffering 74 | if (m_available_samples == 0 && !m_buffering) 75 | { 76 | Serial.println("Buffering"); 77 | m_buffering = true; 78 | samples[i] = 0; 79 | } 80 | // are we buffering? 81 | if (m_buffering && m_available_samples < m_number_samples_to_buffer) 82 | { 83 | // just return 0 as we don't have enough samples yet 84 | samples[i] = 0; 85 | } 86 | else 87 | { 88 | // we've buffered enough samples so no need to buffer anymore 89 | m_buffering = false; 90 | // just send back the samples we've got and move the read head forward 91 | int16_t sample = m_buffer[m_read_head]; 92 | samples[i] = (sample - 128) << 5; 93 | m_read_head = (m_read_head + 1) % m_buffer_size; 94 | m_available_samples--; 95 | } 96 | } 97 | xSemaphoreGive(m_semaphore); 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /lib/indicator_led/src/GenericDevBoardIndicatorLed.cpp: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | #include "GenericDevBoardIndicatorLed.h" 3 | 4 | #ifdef LED_BUILTIN 5 | const uint8_t BUILT_IN_LED = LED_BUILTIN; 6 | #else 7 | const uint8_t BUILT_IN_LED = GPIO_NUM_2; 8 | #endif 9 | 10 | GenericDevBoardIndicatorLed::GenericDevBoardIndicatorLed() 11 | { 12 | pinMode(BUILT_IN_LED, OUTPUT); 13 | } 14 | 15 | // we don't really have any colors so just use the built in LED 16 | void GenericDevBoardIndicatorLed::set_led_rgb(uint32_t color) 17 | { 18 | if (color == 0) 19 | { 20 | digitalWrite(BUILT_IN_LED, LOW); 21 | } 22 | else 23 | { 24 | digitalWrite(BUILT_IN_LED, HIGH); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/indicator_led/src/GenericDevBoardIndicatorLed.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IndicatorLed.h" 4 | 5 | class GenericDevBoardIndicatorLed : public IndicatorLed 6 | { 7 | protected: 8 | void set_led_rgb(uint32_t color); 9 | 10 | public: 11 | GenericDevBoardIndicatorLed(); 12 | }; -------------------------------------------------------------------------------- /lib/indicator_led/src/IndicatorLed.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "IndicatorLed.h" 4 | 5 | void update_indicator_task(void *param) 6 | { 7 | IndicatorLed *indicator = reinterpret_cast(param); 8 | while (true) 9 | { 10 | if (indicator->m_is_flashing) 11 | { 12 | indicator->set_led_rgb(indicator->m_flash_color); 13 | vTaskDelay(100); 14 | } 15 | indicator->set_led_rgb(indicator->m_default_color); 16 | vTaskDelay(100); 17 | } 18 | } 19 | 20 | void IndicatorLed::begin() 21 | { 22 | TaskHandle_t task_handle; 23 | xTaskCreate(update_indicator_task, "Indicator LED Task", 4096, this, 0, &task_handle); 24 | } 25 | 26 | void IndicatorLed::set_is_flashing(bool is_flashing, uint32_t flash_color) 27 | { 28 | m_is_flashing = is_flashing; 29 | m_flash_color = flash_color; 30 | } 31 | void IndicatorLed::set_default_color(uint32_t color) 32 | { 33 | m_default_color = color; 34 | } 35 | -------------------------------------------------------------------------------- /lib/indicator_led/src/IndicatorLed.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class IndicatorLed 6 | { 7 | private: 8 | bool m_is_flashing = false; 9 | uint32_t m_default_color = 0; 10 | uint32_t m_flash_color = 0; 11 | 12 | protected: 13 | virtual void set_led_rgb(uint32_t color) = 0; 14 | 15 | public: 16 | void begin(); 17 | void set_is_flashing(bool is_flashing, uint32_t flash_color); 18 | void set_default_color(uint32_t color); 19 | 20 | friend void update_indicator_task(void *param); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/indicator_led_pico/src/TinyPICOIndicatorLed.cpp: -------------------------------------------------------------------------------- 1 | #include "TinyPICOIndicatorLed.h" 2 | 3 | #ifdef ARDUINO_TINYPICO 4 | 5 | TinyPICOIndicatorLed::TinyPICOIndicatorLed() 6 | { 7 | m_tp = new TinyPICO(); 8 | } 9 | void TinyPICOIndicatorLed::set_led_rgb(uint32_t color) 10 | { 11 | m_tp->DotStar_SetPixelColor(color); 12 | } 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /lib/indicator_led_pico/src/TinyPICOIndicatorLed.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IndicatorLed.h" 4 | 5 | #ifdef ARDUINO_TINYPICO 6 | #include 7 | 8 | class TinyPICO; 9 | class TinyPICOIndicatorLed : public IndicatorLed 10 | { 11 | private: 12 | TinyPICO *m_tp = NULL; 13 | 14 | protected: 15 | void set_led_rgb(uint32_t color); 16 | 17 | public: 18 | TinyPICOIndicatorLed(); 19 | }; 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /lib/transport/src/EspNowTransport.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "OutputBuffer.h" 6 | #include "EspNowTransport.h" 7 | 8 | const int MAX_ESP_NOW_PACKET_SIZE = 250; 9 | const uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; 10 | 11 | static EspNowTransport *instance = NULL; 12 | 13 | void receiveCallback(const uint8_t *macAddr, const uint8_t *data, int dataLen) 14 | { 15 | // annoyingly we can't pass an param into this so we need to do a bit of hack to access the EspNowTransport instance 16 | int header_size = instance->m_header_size; 17 | 18 | // first m_header_size bytes of m_buffer are the expected header 19 | if ((dataLen > header_size) && (dataLen<=MAX_ESP_NOW_PACKET_SIZE) && (memcmp(data,instance->m_buffer,header_size) == 0)) 20 | { 21 | instance->m_output_buffer->add_samples(data + header_size, dataLen - header_size); 22 | } 23 | } 24 | 25 | bool EspNowTransport::begin() 26 | { 27 | // Set Wifi channel 28 | esp_wifi_set_promiscuous(true); 29 | esp_wifi_set_channel(m_wifi_channel, WIFI_SECOND_CHAN_NONE); 30 | esp_wifi_set_promiscuous(false); 31 | 32 | esp_err_t result = esp_now_init(); 33 | if (result == ESP_OK) 34 | { 35 | Serial.println("ESPNow Init Success"); 36 | esp_now_register_recv_cb(receiveCallback); 37 | } 38 | else 39 | { 40 | Serial.printf("ESPNow Init failed: %s\n", esp_err_to_name(result)); 41 | return false; 42 | } 43 | // this will broadcast a message to everyone in range 44 | esp_now_peer_info_t peerInfo = {}; 45 | memcpy(&peerInfo.peer_addr, broadcastAddress, 6); 46 | if (!esp_now_is_peer_exist(broadcastAddress)) 47 | { 48 | result = esp_now_add_peer(&peerInfo); 49 | if (result != ESP_OK) 50 | { 51 | Serial.printf("Failed to add broadcast peer: %s\n", esp_err_to_name(result)); 52 | return false; 53 | } 54 | } 55 | return true; 56 | } 57 | 58 | EspNowTransport::EspNowTransport(OutputBuffer *output_buffer, uint8_t wifi_channel) : Transport(output_buffer, MAX_ESP_NOW_PACKET_SIZE) 59 | { 60 | instance = this; 61 | m_wifi_channel = wifi_channel; 62 | } 63 | 64 | void EspNowTransport::send() 65 | { 66 | 67 | esp_err_t result = esp_now_send(broadcastAddress, m_buffer, m_index + m_header_size); 68 | if (result != ESP_OK) 69 | { 70 | Serial.printf("Failed to send: %s\n", esp_err_to_name(result)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/transport/src/EspNowTransport.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Transport.h" 4 | 5 | class OutputBuffer; 6 | 7 | class EspNowTransport: public Transport { 8 | private: 9 | uint8_t m_wifi_channel; 10 | protected: 11 | void send(); 12 | public: 13 | EspNowTransport(OutputBuffer *output_buffer, uint8_t wifi_channel); 14 | virtual bool begin() override; 15 | friend void receiveCallback(const uint8_t *macAddr, const uint8_t *data, int dataLen); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/transport/src/Transport.cpp: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | #include "Transport.h" 3 | 4 | Transport::Transport(OutputBuffer *output_buffer, size_t buffer_size) 5 | { 6 | m_output_buffer = output_buffer; 7 | m_buffer_size = buffer_size; 8 | m_buffer = (uint8_t *)malloc(m_buffer_size); 9 | m_index = 0; 10 | m_header_size = 0; 11 | } 12 | 13 | void Transport::add_sample(int16_t sample) 14 | { 15 | m_buffer[m_index+m_header_size] = (sample + 32768) >> 8; 16 | m_index++; 17 | // have we reached a full packet? 18 | if ((m_index+m_header_size) == m_buffer_size) 19 | { 20 | send(); 21 | m_index = 0; 22 | } 23 | } 24 | 25 | void Transport::flush() 26 | { 27 | if (m_index >0 ) 28 | { 29 | send(); 30 | m_index = 0; 31 | } 32 | } 33 | 34 | int Transport::set_header(const int header_size, const uint8_t *header) 35 | { 36 | if ((header_size 3 | #include 4 | 5 | class OutputBuffer; 6 | 7 | class Transport 8 | { 9 | protected: 10 | // audio buffer for samples we need to send 11 | uint8_t *m_buffer = NULL; 12 | int m_buffer_size = 0; 13 | int m_index = 0; 14 | int m_header_size; 15 | 16 | OutputBuffer *m_output_buffer = NULL; 17 | 18 | virtual void send() = 0; 19 | 20 | public: 21 | Transport(OutputBuffer *output_buffer, size_t buffer_size); 22 | int set_header(const int header_size, const uint8_t *header); 23 | void add_sample(int16_t sample); 24 | void flush(); 25 | virtual bool begin() = 0; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/transport/src/UdpTransport.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "UdpTransport.h" 4 | #include "OutputBuffer.h" 5 | #include "config.h" 6 | 7 | const int MAX_UDP_SIZE = 1436; 8 | 9 | UdpTransport::UdpTransport(OutputBuffer *output_buffer) : Transport(output_buffer, MAX_UDP_SIZE) 10 | { 11 | } 12 | 13 | unsigned long last_packet; 14 | bool UdpTransport::begin() 15 | { 16 | udp = new AsyncUDP(); 17 | last_packet = millis(); 18 | if (udp->listen(8192)) 19 | { 20 | udp->onPacket([this](AsyncUDPPacket packet) 21 | { 22 | uint8_t broadcast_header[] = {0x00}; 23 | // our packets contain unsigned 8 bit PCM samples 24 | // so we can push them straight into the output buffer 25 | if ((packet.length() > this->m_header_size) && (packet.length() <= MAX_UDP_SIZE) && ((memcmp(packet.data(), this->m_buffer, this->m_header_size) == 0) || (memcmp(packet.data(), broadcast_header, this->m_header_size) == 0) )) 26 | { 27 | this->m_output_buffer->add_samples(packet.data() + m_header_size, packet.length() - m_header_size); 28 | } 29 | }); 30 | return true; 31 | } 32 | Serial.println("Failed to listen"); 33 | return false; 34 | } 35 | 36 | void UdpTransport::send() 37 | { 38 | udp->broadcast(m_buffer, m_index); 39 | } 40 | -------------------------------------------------------------------------------- /lib/transport/src/UdpTransport.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Transport.h" 4 | 5 | class OutputBuffer; 6 | class AsyncUDP; 7 | 8 | class UdpTransport : public Transport 9 | { 10 | private: 11 | AsyncUDP *udp; 12 | 13 | protected: 14 | void send(); 15 | 16 | public: 17 | UdpTransport(OutputBuffer *output_buffer); 18 | bool begin() override; 19 | }; -------------------------------------------------------------------------------- /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 | [platformio] 12 | default_envs = tinypico, lolin32 13 | extra_configs = 14 | platformio_override.ini 15 | 16 | [env] 17 | framework = arduino 18 | platform = espressif32 19 | ; upload_port = /dev/cu.SLAB_USBtoUART 20 | ; monitor_port = /dev/cu.SLAB_USBtoUART 21 | monitor_speed = 115200 22 | monitor_filters = esp32_exception_decoder 23 | build_flags = -Ofast 24 | 25 | [env:tinypico] 26 | board = tinypico 27 | lib_deps = tinypico/TinyPICO Helper Library@^1.4.0 28 | build_flags = -Ofast -D USE_I2S_MIC_INPUT -D USE_ESP_NOW 29 | 30 | [env:lolin32] 31 | board = lolin32 32 | build_flags = -Ofast -D USE_I2S_MIC_INPUT -D USE_ESP_NOW 33 | lib_ignore = indicator_led_pico 34 | -------------------------------------------------------------------------------- /platformio_override.example.ini: -------------------------------------------------------------------------------- 1 | # Example PlatformIO Project Configuration Override 2 | # ------------------------------------------------------------------------------ 3 | # Copy to platformio_override.ini to activate overrides 4 | # ------------------------------------------------------------------------------ 5 | # Please visit documentation: https://docs.platformio.org/page/projectconf.html 6 | 7 | [platformio] 8 | default_envs = tinypico,esp32dev 9 | 10 | [env] 11 | upload_speed = 921600 12 | 13 | [env:esp32dev] 14 | board = esp32dev 15 | build_flags = -Ofast -D USE_ESP_NOW -D USE_I2S_MIC_INPUT 16 | -------------------------------------------------------------------------------- /src/Application.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "Application.h" 6 | #include "I2SMEMSSampler.h" 7 | #include "ADCSampler.h" 8 | #include "I2SOutput.h" 9 | #include "DACOutput.h" 10 | #include "UdpTransport.h" 11 | #include "EspNowTransport.h" 12 | #include "OutputBuffer.h" 13 | #include "config.h" 14 | 15 | #ifdef ARDUINO_TINYPICO 16 | #include "TinyPICOIndicatorLed.h" 17 | #else 18 | #include "GenericDevBoardIndicatorLed.h" 19 | #endif 20 | 21 | static void application_task(void *param) 22 | { 23 | // delegate onto the application 24 | Application *application = reinterpret_cast(param); 25 | application->loop(); 26 | } 27 | 28 | Application::Application() 29 | { 30 | m_output_buffer = new OutputBuffer(300 * 16); 31 | #ifdef USE_I2S_MIC_INPUT 32 | m_input = new I2SMEMSSampler(I2S_NUM_0, i2s_mic_pins, i2s_mic_Config,128); 33 | #else 34 | m_input = new ADCSampler(ADC_UNIT_1, ADC1_CHANNEL_7, i2s_adc_config); 35 | #endif 36 | 37 | #ifdef USE_I2S_SPEAKER_OUTPUT 38 | m_output = new I2SOutput(I2S_NUM_0, i2s_speaker_pins); 39 | #else 40 | m_output = new DACOutput(I2S_NUM_0); 41 | #endif 42 | 43 | #ifdef USE_ESP_NOW 44 | m_transport = new EspNowTransport(m_output_buffer,ESP_NOW_WIFI_CHANNEL); 45 | #else 46 | m_transport = new UdpTransport(m_output_buffer); 47 | #endif 48 | 49 | m_transport->set_header(TRANSPORT_HEADER_SIZE,transport_header); 50 | 51 | #ifdef ARDUINO_TINYPICO 52 | m_indicator_led = new TinyPICOIndicatorLed(); 53 | #else 54 | m_indicator_led = new GenericDevBoardIndicatorLed(); 55 | #endif 56 | 57 | if (I2S_SPEAKER_SD_PIN != -1) 58 | { 59 | pinMode(I2S_SPEAKER_SD_PIN, OUTPUT); 60 | } 61 | } 62 | 63 | void Application::begin() 64 | { 65 | // show a flashing indicator that we are trying to connect 66 | m_indicator_led->set_default_color(0); 67 | m_indicator_led->set_is_flashing(true, 0xff0000); 68 | m_indicator_led->begin(); 69 | // bring up WiFi 70 | WiFi.mode(WIFI_STA); 71 | #ifndef USE_ESP_NOW 72 | WiFi.begin(WIFI_SSID, WIFI_PSWD); 73 | if (WiFi.waitForConnectResult() != WL_CONNECTED) 74 | { 75 | Serial.println("Connection Failed! Rebooting..."); 76 | delay(5000); 77 | ESP.restart(); 78 | } 79 | // this has a dramatic effect on packet RTT 80 | WiFi.setSleep(WIFI_PS_NONE); 81 | Serial.print("My IP Address is: "); 82 | Serial.println(WiFi.localIP()); 83 | #else 84 | // but don't connect if we're using ESP NOW 85 | WiFi.disconnect(); 86 | #endif 87 | Serial.print("My MAC Address is: "); 88 | Serial.println(WiFi.macAddress()); 89 | // do any setup of the transport 90 | m_transport->begin(); 91 | // connected so show a solid green light 92 | m_indicator_led->set_default_color(0x00ff00); 93 | m_indicator_led->set_is_flashing(false, 0x00ff00); 94 | 95 | // setup the transmit buttons 96 | pinMode(GPIO_TRANSMIT_BUTTON0, INPUT_PULLUP); 97 | pinMode(GPIO_TRANSMIT_BUTTON1, INPUT_PULLUP); 98 | pinMode(GPIO_TRANSMIT_BUTTON2, INPUT_PULLUP); 99 | pinMode(GPIO_TRANSMIT_BUTTON3, INPUT_PULLUP); 100 | pinMode(GPIO_TRANSMIT_BUTTON4, INPUT_PULLUP); 101 | 102 | // start off with i2S output running 103 | m_output->start(SAMPLE_RATE); 104 | // start the main task for the application 105 | TaskHandle_t task_handle; 106 | xTaskCreate(application_task, "application_task", 8192, this, 1, &task_handle); 107 | } 108 | 109 | // application task - coordinates everything 110 | void Application::loop() 111 | { 112 | int16_t *samples = reinterpret_cast(malloc(sizeof(int16_t) * 128)); 113 | // continue forever 114 | while (true) 115 | { 116 | // do we need to start transmitting? 117 | if (digitalRead(GPIO_TRANSMIT_BUTTON0) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON1) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON2) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON3) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON4) == LOW) 118 | { 119 | uint8_t temp_header[TRANSPORT_HEADER_SIZE]; 120 | 121 | if (digitalRead(GPIO_TRANSMIT_BUTTON0) == LOW){ 122 | temp_header[0] = 0x00; 123 | } 124 | if (digitalRead(GPIO_TRANSMIT_BUTTON1) == LOW){ 125 | temp_header[0] = 0x01; 126 | } 127 | if (digitalRead(GPIO_TRANSMIT_BUTTON2) == LOW){ 128 | temp_header[0] = 0x02; 129 | } 130 | if (digitalRead(GPIO_TRANSMIT_BUTTON3) == LOW){ 131 | temp_header[0] = 0x03; 132 | } 133 | if (digitalRead(GPIO_TRANSMIT_BUTTON4) == LOW){ 134 | temp_header[0] = 0x04; 135 | } 136 | 137 | m_transport->set_header(TRANSPORT_HEADER_SIZE,temp_header); 138 | 139 | Serial.print("Started transmitting on "); 140 | Serial.println(temp_header[0],HEX); 141 | m_indicator_led->set_is_flashing(true, 0xff0000); 142 | // stop the output as we're switching into transmit mode 143 | m_output->stop(); 144 | // start the input to get samples from the microphone 145 | m_input->start(); 146 | // transmit for at least 1 second or while the button is pushed 147 | unsigned long start_time = millis(); 148 | while (millis() - start_time < 1000 || digitalRead(GPIO_TRANSMIT_BUTTON0) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON1) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON2) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON3) == LOW || digitalRead(GPIO_TRANSMIT_BUTTON4) == LOW) 149 | { 150 | // read samples from the microphone 151 | int samples_read = m_input->read(samples, 128); 152 | // and send them over the transport 153 | for (int i = 0; i < samples_read; i++) 154 | { 155 | m_transport->add_sample(samples[i]); 156 | } 157 | } 158 | m_transport->flush(); 159 | // finished transmitting stop the input and start the output 160 | Serial.println("Finished transmitting"); 161 | m_indicator_led->set_is_flashing(false, 0xff0000); 162 | m_input->stop(); 163 | m_output->start(SAMPLE_RATE); 164 | } 165 | // while the transmit button is not pushed and 1 second has not elapsed 166 | Serial.print("Started Receiving"); 167 | digitalWrite(I2S_SPEAKER_SD_PIN, HIGH); 168 | unsigned long start_time = millis(); 169 | while (millis() - start_time < 1000 && digitalRead(GPIO_TRANSMIT_BUTTON0) == HIGH && digitalRead(GPIO_TRANSMIT_BUTTON1) == HIGH && digitalRead(GPIO_TRANSMIT_BUTTON2) == HIGH && digitalRead(GPIO_TRANSMIT_BUTTON3) == HIGH && digitalRead(GPIO_TRANSMIT_BUTTON4) == HIGH) 170 | { 171 | // read from the output buffer (which should be getting filled by the transport) 172 | m_output_buffer->remove_samples(samples, 128); 173 | // and send the samples to the speaker 174 | m_output->write(samples, 128); 175 | } 176 | digitalWrite(I2S_SPEAKER_SD_PIN, LOW); 177 | Serial.println("Finished Receiving"); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Application.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Output; 4 | class I2SSampler; 5 | class Transport; 6 | class OutputBuffer; 7 | class IndicatorLed; 8 | 9 | class Application 10 | { 11 | private: 12 | Output *m_output; 13 | I2SSampler *m_input; 14 | Transport *m_transport; 15 | IndicatorLed *m_indicator_led; 16 | OutputBuffer *m_output_buffer; 17 | 18 | public: 19 | Application(); 20 | void begin(); 21 | void loop(); 22 | }; 23 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | // In case each transport packet needs to start with a specific header, define transport_header here. 4 | // TRANSPORT_HEADER_SIZE needs to be defined in config.h 5 | // For example, when TRANSPORT_HEADER_SIZE is defined as 3, define transport_header for example as {0x1F, 0xCD, 0x01}; 6 | uint8_t transport_header[TRANSPORT_HEADER_SIZE] = {}; 7 | 8 | 9 | // i2s config for using the internal ADC 10 | #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 11 | i2s_config_t i2s_adc_config = { 12 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), 13 | .sample_rate = SAMPLE_RATE, 14 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, 15 | .channel_format = I2S_MIC_CHANNEL, 16 | .communication_format = I2S_COMM_FORMAT_I2S_LSB, 17 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 18 | .dma_buf_count = 4, 19 | .dma_buf_len = 64, 20 | .use_apll = false, 21 | .tx_desc_auto_clear = false, 22 | .fixed_mclk = 0}; 23 | #endif 24 | 25 | // i2s config for reading from I2S 26 | i2s_config_t i2s_mic_Config = { 27 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), 28 | .sample_rate = SAMPLE_RATE, 29 | .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, 30 | .channel_format = I2S_MIC_CHANNEL, 31 | .communication_format = I2S_COMM_FORMAT_I2S, 32 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, 33 | .dma_buf_count = 4, 34 | .dma_buf_len = 64, 35 | .use_apll = false, 36 | .tx_desc_auto_clear = false, 37 | .fixed_mclk = 0}; 38 | 39 | // i2s microphone pins 40 | i2s_pin_config_t i2s_mic_pins = { 41 | .bck_io_num = I2S_MIC_SERIAL_CLOCK, 42 | .ws_io_num = I2S_MIC_LEFT_RIGHT_CLOCK, 43 | .data_out_num = I2S_PIN_NO_CHANGE, 44 | .data_in_num = I2S_MIC_SERIAL_DATA}; 45 | 46 | // i2s speaker pins 47 | i2s_pin_config_t i2s_speaker_pins = { 48 | .bck_io_num = I2S_SPEAKER_SERIAL_CLOCK, 49 | .ws_io_num = I2S_SPEAKER_LEFT_RIGHT_CLOCK, 50 | .data_out_num = I2S_SPEAKER_SERIAL_DATA, 51 | .data_in_num = I2S_PIN_NO_CHANGE}; 52 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // WiFi credentials 6 | #define WIFI_SSID "MyWifi" 7 | #define WIFI_PSWD "Mypassword" 8 | 9 | // sample rate for the system 10 | #define SAMPLE_RATE 16000 11 | 12 | // are you using an I2S microphone - comment this if you want to use an analog mic and ADC input 13 | #define USE_I2S_MIC_INPUT 14 | 15 | // I2S Microphone Settings 16 | 17 | // Which channel is the I2S microphone on? I2S_CHANNEL_FMT_ONLY_LEFT or I2S_CHANNEL_FMT_ONLY_RIGHT 18 | // Generally they will default to LEFT - but you may need to attach the L/R pin to GND 19 | #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT 20 | // #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT 21 | #define I2S_MIC_SERIAL_CLOCK GPIO_NUM_18 22 | #define I2S_MIC_LEFT_RIGHT_CLOCK GPIO_NUM_19 23 | #define I2S_MIC_SERIAL_DATA GPIO_NUM_21 24 | 25 | // Analog Microphone Settings - ADC1_CHANNEL_7 is GPIO35 26 | #define ADC_MIC_CHANNEL ADC1_CHANNEL_7 27 | 28 | // speaker settings 29 | #define USE_I2S_SPEAKER_OUTPUT 30 | #define I2S_SPEAKER_SERIAL_CLOCK GPIO_NUM_18 31 | #define I2S_SPEAKER_LEFT_RIGHT_CLOCK GPIO_NUM_19 32 | #define I2S_SPEAKER_SERIAL_DATA GPIO_NUM_5 33 | // Shutdown line if you have this wired up or -1 if you don't 34 | #define I2S_SPEAKER_SD_PIN GPIO_NUM_22 35 | 36 | // transmit buttons 37 | #define GPIO_TRANSMIT_BUTTON0 GPIO_NUM_25 //ID0 38 | #define GPIO_TRANSMIT_BUTTON1 GPIO_NUM_26 //ID1 39 | #define GPIO_TRANSMIT_BUTTON2 GPIO_NUM_27 //ID2 40 | #define GPIO_TRANSMIT_BUTTON3 GPIO_NUM_32 //ID3 41 | #define GPIO_TRANSMIT_BUTTON4 GPIO_NUM_33 //ID4 42 | 43 | // Which LED pin do you want to use? TinyPico LED or the builtin LED of a generic ESP32 board? 44 | // Comment out this line to use the builtin LED of a generic ESP32 board 45 | // #define USE_LED_GENERIC 46 | 47 | // Which transport do you want to use? ESP_NOW or UDP? 48 | // comment out this line to use UDP 49 | // #define USE_ESP_NOW 50 | 51 | // On which wifi channel (1-11) should ESP-Now transmit? The default ESP-Now channel on ESP32 is channel 1 52 | #define ESP_NOW_WIFI_CHANNEL 1 53 | 54 | // In case all transport packets need a header (to avoid interference with other applications or walkie talkie sets), 55 | // specify TRANSPORT_HEADER_SIZE (the length in bytes of the header) in the next line, and define the transport header in config.cpp 56 | #define TRANSPORT_HEADER_SIZE 1 57 | extern uint8_t transport_header[TRANSPORT_HEADER_SIZE]; 58 | //extern uint8_t broadcast_header[TRANSPORT_HEADER_SIZE]; 59 | 60 | 61 | // i2s config for using the internal ADC 62 | extern i2s_config_t i2s_adc_config; 63 | // i2s config for reading from of I2S 64 | extern i2s_config_t i2s_mic_Config; 65 | // i2s microphone pins 66 | extern i2s_pin_config_t i2s_mic_pins; 67 | // i2s speaker pins 68 | extern i2s_pin_config_t i2s_speaker_pins; 69 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Application.h" 3 | 4 | // our application 5 | Application *application; 6 | 7 | void setup() 8 | { 9 | Serial.begin(115200); 10 | // start up the application 11 | application = new Application(); 12 | application->begin(); 13 | Serial.println("Application started"); 14 | } 15 | 16 | void loop() 17 | { 18 | // nothing to do - the application is doing all the work 19 | vTaskDelay(pdMS_TO_TICKS(1000)); 20 | } 21 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Unit Testing 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/page/plus/unit-testing.html 12 | --------------------------------------------------------------------------------