initial commit

This commit is contained in:
2023-02-10 10:34:41 +01:00
commit fcad3a98b6
44 changed files with 1392 additions and 0 deletions

4
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [atomic14]
ko_fi: atomic14

30
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: PlatformIO CI
on: [push, pull_request]
jobs:
build:
name: Build Environments
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v2
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Set up Python
uses: actions/setup-python@v2
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade platformio
- name: Build firmware
run: pio run

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
package-lock.json
platformio_override.ini

10
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"files.associations": {
"*.rb": "ruby",
"SP_ENC.C": "cpp",
"SP_DEC.C": "cpp",
"DTX.C": "cpp",
"HOST.C": "cpp",
"VAD.C": "cpp"
}
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 atomic14
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# Overview
We've made a Walkie-Talkie using the ESP32.
[Explanatory video](https://www.youtube.com/watch?v=d_h38X4_eQQ)
[![Demo Video](https://img.youtube.com/vi/d_h38X4_eQQ/0.jpg)](https://www.youtube.com/watch?v=d_h38X4_eQQ)
Audio data is transmitted over either UDP broadcast or ESP-NOW. So the Walkie-Talkie will even work without a WiFi network!
I'm using my own microphone board (available on Tindie: https://www.tindie.com/products/21519/) but the code will work equally well with any I2S microphone (e.g. the INMP441) and you can easily modify it to use the built-in ADC for analogue microphones.
For output, I'm using an I2S amplifier breakout board which I'm using the drive a 4ohm speaker. Once again, you can modify the code to use the built-in DAC for output which will let you use headphones or an analogue amplifier board.
I've got a great series of videos on ESP32 Audio which are a great resource for anyone who wants to learn more about audio on the ESP32 which you can find here: https://www.youtube.com/playlist?list=PL5vDt5AALlRfGVUv2x7riDMIOX34udtKD
For this project I've 3D printed a case - you can access the Fusion 360 project here: https://a360.co/2PXgAUS
I've also created a custom PCB - you can access the schematic here: https://easyeda.com/chris_9044/esp32-walkie-talkie
The boards were manufactured by PCBWay and as always they've done a really great job. You can order the boards directly from PCBWay here: https://www.pcbway.com/project/shareproject/ESP32_Audio_Board_For_Walkie_Talkie.html
And you can help support the channel by using my referral link: https://www.pcbway.com/setinvite.aspx?inviteid=403566 for other PCBs.
However, you can also easily wire this up on breadboard - that's how I prototyped it. Everything is I2S based so it's just straightforward jumper wires.
# Setup
Everything is configured from the `src/config.h` file. To use UDP Broadcast comment out the line:
```
#define USE_ESP_NOW
```
Make sure you update the WiFi SSID and Password:
```
// WiFi credentials
#define WIFI_SSID << YOUR_SSID >>
#define WIFI_PSWD << YOUR_PASSWORD >>
```
The pins for the microphone and the amplifier board are all setup in the same `config.h` file.
# Building and Running
I'm using PlatformIO for this project so you will need to have that installed. Open up the project and connect your ESP32. You should be able to just hit build and run.
Obviously, you'll need two ESP32 boards and components to do anything :)

39
include/README Normal file
View File

@@ -0,0 +1,39 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View File

@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -0,0 +1,5 @@
{
"build": {
"flags": "-Ofast"
}
}

View File

@@ -0,0 +1,32 @@
#include "ADCSampler.h"
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
ADCSampler::ADCSampler(adc_unit_t adcUnit, adc1_channel_t adcChannel, const i2s_config_t &i2s_config) : I2SSampler(I2S_NUM_0, i2s_config)
{
m_adcUnit = adcUnit;
m_adcChannel = adcChannel;
}
void ADCSampler::configureI2S()
{
//init ADC pad
i2s_set_adc_mode(m_adcUnit, m_adcChannel);
// enable the adc
i2s_adc_enable(m_i2sPort);
}
int ADCSampler::read(int16_t *samples, int count)
{
// read from i2s
size_t bytes_read = 0;
i2s_read(m_i2sPort, samples, sizeof(int16_t) * count, &bytes_read, portMAX_DELAY);
int samples_read = bytes_read / sizeof(int16_t);
for (int i = 0; i < samples_read; i++)
{
samples[i] = (2048 - (uint16_t(samples[i]) & 0xfff)) * 15;
}
return samples_read;
}
#endif

View File

@@ -0,0 +1,18 @@
#pragma once
#include <driver/adc.h>
#include "I2SSampler.h"
class ADCSampler : public I2SSampler
{
private:
adc_unit_t m_adcUnit;
adc1_channel_t m_adcChannel;
protected:
void configureI2S();
public:
ADCSampler(adc_unit_t adc_unit, adc1_channel_t adc_channel, const i2s_config_t &i2s_config);
virtual int read(int16_t *samples, int count);
};

View File

@@ -0,0 +1,52 @@
#include "I2SMEMSSampler.h"
#include "soc/i2s_reg.h"
I2SMEMSSampler::I2SMEMSSampler(
i2s_port_t i2s_port,
i2s_pin_config_t &i2s_pins,
i2s_config_t i2s_config,
int raw_samples_size,
bool fixSPH0645) : I2SSampler(i2s_port, i2s_config)
{
m_i2sPins = i2s_pins;
m_fixSPH0645 = fixSPH0645;
m_raw_samples_size = raw_samples_size;
m_raw_samples = (int32_t *)malloc(sizeof(int32_t) * raw_samples_size);
}
I2SMEMSSampler::~I2SMEMSSampler()
{
free(m_raw_samples);
}
void I2SMEMSSampler::configureI2S()
{
if (m_fixSPH0645)
{
// FIXES for SPH0645
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
REG_SET_BIT(I2S_TIMING_REG(m_i2sPort), BIT(9));
REG_SET_BIT(I2S_CONF_REG(m_i2sPort), I2S_RX_MSB_SHIFT);
#endif
}
i2s_set_pin(m_i2sPort, &m_i2sPins);
}
int I2SMEMSSampler::read(int16_t *samples, int count)
{
// read from i2s
size_t bytes_read = 0;
if (count>m_raw_samples_size)
{
count = m_raw_samples_size; // Buffer is too small
}
i2s_read(m_i2sPort, m_raw_samples, sizeof(int32_t) * count, &bytes_read, portMAX_DELAY);
int samples_read = bytes_read / sizeof(int32_t);
for (int i = 0; i < samples_read; i++)
{
int32_t temp = m_raw_samples[i] >> 11;
samples[i] = (temp > INT16_MAX) ? INT16_MAX : (temp < -INT16_MAX) ? -INT16_MAX : (int16_t)temp;
}
return samples_read;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "I2SSampler.h"
class I2SMEMSSampler : public I2SSampler
{
private:
i2s_pin_config_t m_i2sPins;
bool m_fixSPH0645;
int32_t *m_raw_samples;
int m_raw_samples_size;
protected:
void configureI2S();
public:
I2SMEMSSampler(
i2s_port_t i2s_port,
i2s_pin_config_t &i2s_pins,
i2s_config_t i2s_config,
int raw_samples_size,
bool fixSPH0645 = false);
~I2SMEMSSampler();
virtual int read(int16_t *samples, int count);
};

View File

@@ -0,0 +1,21 @@
#include "I2SSampler.h"
#include "driver/i2s.h"
I2SSampler::I2SSampler(i2s_port_t i2sPort, const i2s_config_t &i2s_config) : m_i2sPort(i2sPort), m_i2s_config(i2s_config)
{
}
void I2SSampler::start()
{
//install and start i2s driver
i2s_driver_install(m_i2sPort, &m_i2s_config, 0, NULL);
// set up the I2S configuration from the subclass
configureI2S();
}
void I2SSampler::stop()
{
// stop the i2S driver
i2s_driver_uninstall(m_i2sPort);
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <driver/i2s.h>
/**
* Base Class for both the ADC and I2S sampler
**/
class I2SSampler
{
protected:
i2s_port_t m_i2sPort = I2S_NUM_0;
i2s_config_t m_i2s_config;
virtual void configureI2S() = 0;
virtual void processI2SData(void *samples, size_t count){
// nothing to do for the default case
};
public:
I2SSampler(i2s_port_t i2sPort, const i2s_config_t &i2sConfig);
void start();
virtual int read(int16_t *samples, int count) = 0;
void stop();
int sample_rate()
{
return m_i2s_config.sample_rate;
}
};

View File

@@ -0,0 +1,5 @@
{
"build": {
"flags": "-Ofast"
}
}

View File

@@ -0,0 +1,30 @@
#include "DACOutput.h"
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
void DACOutput::start(int sample_rate)
{
// i2s config for writing both channels of I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
.sample_rate = sample_rate,
.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_MSB),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = 1024,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0};
//install and start i2s driver
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
// enable the DAC channels
i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN);
// clear the DMA buffers
i2s_zero_dma_buffer(I2S_NUM_0);
i2s_start(I2S_NUM_0);
}
#endif

View File

@@ -0,0 +1,20 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <driver/i2s.h>
#include "Output.h"
/**
* Base Class for both the ADC and I2S sampler
**/
class DACOutput : public Output
{
public:
DACOutput(i2s_port_t i2s_port) : Output(i2s_port) {}
void start(int sample_rate);
virtual int16_t process_sample(int16_t sample)
{
// DAC needs unsigned 16 bit samples
return sample + 32768;
}
};

View File

@@ -0,0 +1,35 @@
#include "I2SOutput.h"
I2SOutput::I2SOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins) : Output(i2s_port), m_i2s_pins(i2s_pins)
{
}
void I2SOutput::start(int sample_rate)
{
// i2s config for writing both channels of I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = sample_rate,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
#else
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S),
#endif
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = 1024,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0};
//install and start i2s driver
i2s_driver_install(m_i2s_port, &i2s_config, 0, NULL);
// set up the i2s pins
i2s_set_pin(m_i2s_port, &m_i2s_pins);
// clear the DMA buffers
i2s_zero_dma_buffer(m_i2s_port);
i2s_start(m_i2s_port);
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "Output.h"
/**
* Base Class for both the ADC and I2S sampler
**/
class I2SOutput : public Output
{
private:
i2s_pin_config_t m_i2s_pins;
public:
I2SOutput(i2s_port_t i2s_port, i2s_pin_config_t &i2s_pins);
void start(int sample_rate);
};

View File

@@ -0,0 +1,51 @@
#include "Output.h"
#include <esp_log.h>
#include <driver/i2s.h>
static const char *TAG = "OUT";
// number of frames to try and send at once (a frame is a left and right sample)
const int NUM_FRAMES_TO_SEND = 256;
Output::Output(i2s_port_t i2s_port) : m_i2s_port(i2s_port)
{
// this will contain the prepared samples for sending to the I2S device
m_frames = (int16_t *)malloc(2 * sizeof(int16_t) * NUM_FRAMES_TO_SEND);
}
Output::~Output()
{
free(m_frames);
}
void Output::stop()
{
// stop the i2S driver
i2s_stop(m_i2s_port);
i2s_driver_uninstall(m_i2s_port);
}
void Output::write(int16_t *samples, int count)
{
int sample_index = 0;
while (sample_index < count)
{
int samples_to_send = 0;
for (int i = 0; i < NUM_FRAMES_TO_SEND && sample_index < count; i++)
{
int sample = process_sample(samples[sample_index]);
m_frames[i * 2] = sample; // left channel
m_frames[i * 2 + 1] = sample; // right channel
samples_to_send++;
sample_index++;
}
// write data to the i2s peripheral
size_t bytes_written = 0;
i2s_write(m_i2s_port, m_frames, samples_to_send * sizeof(int16_t) * 2, &bytes_written, portMAX_DELAY);
if (bytes_written != samples_to_send * sizeof(int16_t) * 2)
{
ESP_LOGE(TAG, "Did not write all bytes");
}
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <driver/i2s.h>
/**
* Base Class for both the DAC and I2S output
**/
class Output
{
private:
int16_t *m_frames;
protected:
i2s_port_t m_i2s_port = I2S_NUM_0;
public:
Output(i2s_port_t i2s_port);
~Output();
virtual void start(int sample_rate) = 0;
void stop();
// override this in derived classes to turn the sample into
// something the output device expects - for the default case
// this is simply a pass through
virtual int16_t process_sample(int16_t sample) { return sample; }
void write(int16_t *samples, int count);
};

View File

@@ -0,0 +1,99 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
/**
* @brief Circular buffer for 8 bit unsigned PCM samples
*
*/
class OutputBuffer
{
private:
// how many samples should we buffer before outputting data?
int m_number_samples_to_buffer;
// where are we reading from
int m_read_head;
// where are we writing to
int m_write_head;
// keep track of how many samples we have
int m_available_samples;
// the total size of the buffer
int m_buffer_size;
// are we currently buffering samples?
bool m_buffering;
// the sample buffer
uint8_t *m_buffer;
// thread safety
SemaphoreHandle_t m_semaphore;
public:
OutputBuffer(int number_samples_to_buffer) : m_number_samples_to_buffer(number_samples_to_buffer)
{
// create a semaphore and make it available for locking
m_semaphore = xSemaphoreCreateBinary();
xSemaphoreGive(m_semaphore);
// set reading and writing to the beginning of the buffer
m_read_head = 0;
m_write_head = 0;
m_available_samples = 0;
// we'll start off buffering data as we have no samples yet
m_buffering = true;
// make sufficient space for the bufferring and incoming data
m_buffer_size = 3 * number_samples_to_buffer;
m_buffer = (uint8_t *)malloc(m_buffer_size);
memset(m_buffer, 0, m_buffer_size);
if (!m_buffer)
{
Serial.println("Failed to allocate buffer");
}
}
// we're adding samples that are 8 bit as they are coming from the transport
void add_samples(const uint8_t *samples, int count)
{
xSemaphoreTake(m_semaphore, portMAX_DELAY);
// copy the samples into the buffer wrapping around as needed
for (int i = 0; i < count; i++)
{
m_buffer[m_write_head] = samples[i];
m_write_head = (m_write_head + 1) % m_buffer_size;
}
m_available_samples += count;
xSemaphoreGive(m_semaphore);
}
// convert the samples to 16 bit as they are going to the output
void remove_samples(int16_t *samples, int count)
{
xSemaphoreTake(m_semaphore, portMAX_DELAY);
for (int i = 0; i < count; i++)
{
samples[i] = 0;
// if we have no samples and we aren't already buffering then we need to start buffering
if (m_available_samples == 0 && !m_buffering)
{
Serial.println("Buffering");
m_buffering = true;
samples[i] = 0;
}
// are we buffering?
if (m_buffering && m_available_samples < m_number_samples_to_buffer)
{
// just return 0 as we don't have enough samples yet
samples[i] = 0;
}
else
{
// we've buffered enough samples so no need to buffer anymore
m_buffering = false;
// just send back the samples we've got and move the read head forward
int16_t sample = m_buffer[m_read_head];
samples[i] = (sample - 128) << 5;
m_read_head = (m_read_head + 1) % m_buffer_size;
m_available_samples--;
}
}
xSemaphoreGive(m_semaphore);
}
};

View File

@@ -0,0 +1,26 @@
#include "Arduino.h"
#include "GenericDevBoardIndicatorLed.h"
#ifdef LED_BUILTIN
const uint8_t BUILT_IN_LED = LED_BUILTIN;
#else
const uint8_t BUILT_IN_LED = GPIO_NUM_2;
#endif
GenericDevBoardIndicatorLed::GenericDevBoardIndicatorLed()
{
pinMode(BUILT_IN_LED, OUTPUT);
}
// we don't really have any colors so just use the built in LED
void GenericDevBoardIndicatorLed::set_led_rgb(uint32_t color)
{
if (color == 0)
{
digitalWrite(BUILT_IN_LED, LOW);
}
else
{
digitalWrite(BUILT_IN_LED, HIGH);
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "IndicatorLed.h"
class GenericDevBoardIndicatorLed : public IndicatorLed
{
protected:
void set_led_rgb(uint32_t color);
public:
GenericDevBoardIndicatorLed();
};

View File

@@ -0,0 +1,34 @@
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include "IndicatorLed.h"
void update_indicator_task(void *param)
{
IndicatorLed *indicator = reinterpret_cast<IndicatorLed *>(param);
while (true)
{
if (indicator->m_is_flashing)
{
indicator->set_led_rgb(indicator->m_flash_color);
vTaskDelay(100);
}
indicator->set_led_rgb(indicator->m_default_color);
vTaskDelay(100);
}
}
void IndicatorLed::begin()
{
TaskHandle_t task_handle;
xTaskCreate(update_indicator_task, "Indicator LED Task", 4096, this, 0, &task_handle);
}
void IndicatorLed::set_is_flashing(bool is_flashing, uint32_t flash_color)
{
m_is_flashing = is_flashing;
m_flash_color = flash_color;
}
void IndicatorLed::set_default_color(uint32_t color)
{
m_default_color = color;
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include <stdint.h>
class IndicatorLed
{
private:
bool m_is_flashing = false;
uint32_t m_default_color = 0;
uint32_t m_flash_color = 0;
protected:
virtual void set_led_rgb(uint32_t color) = 0;
public:
void begin();
void set_is_flashing(bool is_flashing, uint32_t flash_color);
void set_default_color(uint32_t color);
friend void update_indicator_task(void *param);
};

View File

@@ -0,0 +1,14 @@
#include "TinyPICOIndicatorLed.h"
#ifdef ARDUINO_TINYPICO
TinyPICOIndicatorLed::TinyPICOIndicatorLed()
{
m_tp = new TinyPICO();
}
void TinyPICOIndicatorLed::set_led_rgb(uint32_t color)
{
m_tp->DotStar_SetPixelColor(color);
}
#endif

View File

@@ -0,0 +1,21 @@
#pragma once
#include "IndicatorLed.h"
#ifdef ARDUINO_TINYPICO
#include <TinyPICO.h>
class TinyPICO;
class TinyPICOIndicatorLed : public IndicatorLed
{
private:
TinyPICO *m_tp = NULL;
protected:
void set_led_rgb(uint32_t color);
public:
TinyPICOIndicatorLed();
};
#endif

View File

@@ -0,0 +1,72 @@
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include "OutputBuffer.h"
#include "EspNowTransport.h"
const int MAX_ESP_NOW_PACKET_SIZE = 250;
const uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static EspNowTransport *instance = NULL;
void receiveCallback(const uint8_t *macAddr, const uint8_t *data, int dataLen)
{
// annoyingly we can't pass an param into this so we need to do a bit of hack to access the EspNowTransport instance
int header_size = instance->m_header_size;
// first m_header_size bytes of m_buffer are the expected header
if ((dataLen > header_size) && (dataLen<=MAX_ESP_NOW_PACKET_SIZE) && (memcmp(data,instance->m_buffer,header_size) == 0))
{
instance->m_output_buffer->add_samples(data + header_size, dataLen - header_size);
}
}
bool EspNowTransport::begin()
{
// Set Wifi channel
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(m_wifi_channel, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
esp_err_t result = esp_now_init();
if (result == ESP_OK)
{
Serial.println("ESPNow Init Success");
esp_now_register_recv_cb(receiveCallback);
}
else
{
Serial.printf("ESPNow Init failed: %s\n", esp_err_to_name(result));
return false;
}
// this will broadcast a message to everyone in range
esp_now_peer_info_t peerInfo = {};
memcpy(&peerInfo.peer_addr, broadcastAddress, 6);
if (!esp_now_is_peer_exist(broadcastAddress))
{
result = esp_now_add_peer(&peerInfo);
if (result != ESP_OK)
{
Serial.printf("Failed to add broadcast peer: %s\n", esp_err_to_name(result));
return false;
}
}
return true;
}
EspNowTransport::EspNowTransport(OutputBuffer *output_buffer, uint8_t wifi_channel) : Transport(output_buffer, MAX_ESP_NOW_PACKET_SIZE)
{
instance = this;
m_wifi_channel = wifi_channel;
}
void EspNowTransport::send()
{
esp_err_t result = esp_now_send(broadcastAddress, m_buffer, m_index + m_header_size);
if (result != ESP_OK)
{
Serial.printf("Failed to send: %s\n", esp_err_to_name(result));
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "Transport.h"
class OutputBuffer;
class EspNowTransport: public Transport {
private:
uint8_t m_wifi_channel;
protected:
void send();
public:
EspNowTransport(OutputBuffer *output_buffer, uint8_t wifi_channel);
virtual bool begin() override;
friend void receiveCallback(const uint8_t *macAddr, const uint8_t *data, int dataLen);
};

View File

@@ -0,0 +1,46 @@
#include "Arduino.h"
#include "Transport.h"
Transport::Transport(OutputBuffer *output_buffer, size_t buffer_size)
{
m_output_buffer = output_buffer;
m_buffer_size = buffer_size;
m_buffer = (uint8_t *)malloc(m_buffer_size);
m_index = 0;
m_header_size = 0;
}
void Transport::add_sample(int16_t sample)
{
m_buffer[m_index+m_header_size] = (sample + 32768) >> 8;
m_index++;
// have we reached a full packet?
if ((m_index+m_header_size) == m_buffer_size)
{
send();
m_index = 0;
}
}
void Transport::flush()
{
if (m_index >0 )
{
send();
m_index = 0;
}
}
int Transport::set_header(const int header_size, const uint8_t *header)
{
if ((header_size<m_buffer_size) && (header))
{
m_header_size = header_size;
memcpy(m_buffer, header, header_size);
return 0;
}
else
{
return -1;
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <stdlib.h>
#include <stdint.h>
class OutputBuffer;
class Transport
{
protected:
// audio buffer for samples we need to send
uint8_t *m_buffer = NULL;
int m_buffer_size = 0;
int m_index = 0;
int m_header_size;
OutputBuffer *m_output_buffer = NULL;
virtual void send() = 0;
public:
Transport(OutputBuffer *output_buffer, size_t buffer_size);
int set_header(const int header_size, const uint8_t *header);
void add_sample(int16_t sample);
void flush();
virtual bool begin() = 0;
};

View File

@@ -0,0 +1,37 @@
#include <Arduino.h>
#include <AsyncUDP.h>
#include "UdpTransport.h"
#include "OutputBuffer.h"
const int MAX_UDP_SIZE = 1436;
UdpTransport::UdpTransport(OutputBuffer *output_buffer) : Transport(output_buffer, MAX_UDP_SIZE)
{
}
unsigned long last_packet;
bool UdpTransport::begin()
{
udp = new AsyncUDP();
last_packet = millis();
if (udp->listen(8192))
{
udp->onPacket([this](AsyncUDPPacket packet)
{
// our packets contain unsigned 8 bit PCM samples
// so we can push them straight into the output buffer
if ((packet.length() > this->m_header_size) && (packet.length() <= MAX_UDP_SIZE) && (memcmp(packet.data(), this->m_buffer, this->m_header_size) == 0))
{
this->m_output_buffer->add_samples(packet.data() + m_header_size, packet.length() - m_header_size);
}
});
return true;
}
Serial.println("Failed to listen");
return false;
}
void UdpTransport::send()
{
udp->broadcast(m_buffer, m_index);
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "Transport.h"
class OutputBuffer;
class AsyncUDP;
class UdpTransport : public Transport
{
private:
AsyncUDP *udp;
protected:
void send();
public:
UdpTransport(OutputBuffer *output_buffer);
bool begin() override;
};

33
platformio.ini Normal file
View File

@@ -0,0 +1,33 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = tinypico
extra_configs =
platformio_override.ini
[env]
framework = arduino
platform = espressif32
; upload_port = /dev/cu.SLAB_USBtoUART
; monitor_port = /dev/cu.SLAB_USBtoUART
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
build_flags = -Ofast
[env:tinypico]
board = tinypico
lib_deps = tinypico/TinyPICO Helper Library@^1.4.0
build_flags = -Ofast -D USE_I2S_MIC_INPUT -D USE_ESP_NOW
[env:lolin32]
board = lolin32
build_flags = -Ofast -D USE_I2S_MIC_INPUT -D USE_ESP_NOW
lib_ignore = indicator_led_pico

View File

@@ -0,0 +1,15 @@
# Example PlatformIO Project Configuration Override
# ------------------------------------------------------------------------------
# Copy to platformio_override.ini to activate overrides
# ------------------------------------------------------------------------------
# Please visit documentation: https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = tinypico,esp32dev
[env]
upload_speed = 921600
[env:esp32dev]
board = esp32dev
build_flags = -Ofast -D USE_ESP_NOW -D USE_I2S_MIC_INPUT

152
src/Application.cpp Normal file
View File

@@ -0,0 +1,152 @@
#include <Arduino.h>
#include <driver/i2s.h>
#include <WiFi.h>
#include "Application.h"
#include "I2SMEMSSampler.h"
#include "ADCSampler.h"
#include "I2SOutput.h"
#include "DACOutput.h"
#include "UdpTransport.h"
#include "EspNowTransport.h"
#include "OutputBuffer.h"
#include "config.h"
#ifdef ARDUINO_TINYPICO
#include "TinyPICOIndicatorLed.h"
#else
#include "GenericDevBoardIndicatorLed.h"
#endif
static void application_task(void *param)
{
// delegate onto the application
Application *application = reinterpret_cast<Application *>(param);
application->loop();
}
Application::Application()
{
m_output_buffer = new OutputBuffer(300 * 16);
#ifdef USE_I2S_MIC_INPUT
m_input = new I2SMEMSSampler(I2S_NUM_0, i2s_mic_pins, i2s_mic_Config,128);
#else
m_input = new ADCSampler(ADC_UNIT_1, ADC1_CHANNEL_7, i2s_adc_config);
#endif
#ifdef USE_I2S_SPEAKER_OUTPUT
m_output = new I2SOutput(I2S_NUM_0, i2s_speaker_pins);
#else
m_output = new DACOutput(I2S_NUM_0);
#endif
#ifdef USE_ESP_NOW
m_transport = new EspNowTransport(m_output_buffer,ESP_NOW_WIFI_CHANNEL);
#else
m_transport = new UdpTransport(m_output_buffer);
#endif
m_transport->set_header(TRANSPORT_HEADER_SIZE,transport_header);
#ifdef ARDUINO_TINYPICO
m_indicator_led = new TinyPICOIndicatorLed();
#else
m_indicator_led = new GenericDevBoardIndicatorLed();
#endif
if (I2S_SPEAKER_SD_PIN != -1)
{
pinMode(I2S_SPEAKER_SD_PIN, OUTPUT);
}
}
void Application::begin()
{
// show a flashing indicator that we are trying to connect
m_indicator_led->set_default_color(0);
m_indicator_led->set_is_flashing(true, 0xff0000);
m_indicator_led->begin();
// bring up WiFi
WiFi.mode(WIFI_STA);
#ifndef USE_ESP_NOW
WiFi.begin(WIFI_SSID, WIFI_PSWD);
if (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
// this has a dramatic effect on packet RTT
WiFi.setSleep(WIFI_PS_NONE);
Serial.print("My IP Address is: ");
Serial.println(WiFi.localIP());
#else
// but don't connect if we're using ESP NOW
WiFi.disconnect();
#endif
Serial.print("My MAC Address is: ");
Serial.println(WiFi.macAddress());
// do any setup of the transport
m_transport->begin();
// connected so show a solid green light
m_indicator_led->set_default_color(0x00ff00);
m_indicator_led->set_is_flashing(false, 0x00ff00);
// setup the transmit button
pinMode(GPIO_TRANSMIT_BUTTON, INPUT_PULLUP);
// start off with i2S output running
m_output->start(SAMPLE_RATE);
// start the main task for the application
TaskHandle_t task_handle;
xTaskCreate(application_task, "application_task", 8192, this, 1, &task_handle);
}
// application task - coordinates everything
void Application::loop()
{
int16_t *samples = reinterpret_cast<int16_t *>(malloc(sizeof(int16_t) * 128));
// continue forever
while (true)
{
// do we need to start transmitting?
if (!digitalRead(GPIO_TRANSMIT_BUTTON))
{
Serial.println("Started transmitting");
m_indicator_led->set_is_flashing(true, 0xff0000);
// stop the output as we're switching into transmit mode
m_output->stop();
// start the input to get samples from the microphone
m_input->start();
// transmit for at least 1 second or while the button is pushed
unsigned long start_time = millis();
while (millis() - start_time < 1000 || !digitalRead(GPIO_TRANSMIT_BUTTON))
{
// read samples from the microphone
int samples_read = m_input->read(samples, 128);
// and send them over the transport
for (int i = 0; i < samples_read; i++)
{
m_transport->add_sample(samples[i]);
}
}
m_transport->flush();
// finished transmitting stop the input and start the output
Serial.println("Finished transmitting");
m_indicator_led->set_is_flashing(false, 0xff0000);
m_input->stop();
m_output->start(SAMPLE_RATE);
}
// while the transmit button is not pushed and 1 second has not elapsed
Serial.print("Started Receiving");
digitalWrite(I2S_SPEAKER_SD_PIN, HIGH);
unsigned long start_time = millis();
while (millis() - start_time < 1000 || digitalRead(GPIO_TRANSMIT_BUTTON))
{
// read from the output buffer (which should be getting filled by the transport)
m_output_buffer->remove_samples(samples, 128);
// and send the samples to the speaker
m_output->write(samples, 128);
}
digitalWrite(I2S_SPEAKER_SD_PIN, LOW);
Serial.println("Finished Receiving");
}
}

22
src/Application.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
class Output;
class I2SSampler;
class Transport;
class OutputBuffer;
class IndicatorLed;
class Application
{
private:
Output *m_output;
I2SSampler *m_input;
Transport *m_transport;
IndicatorLed *m_indicator_led;
OutputBuffer *m_output_buffer;
public:
Application();
void begin();
void loop();
};

51
src/config.cpp Normal file
View File

@@ -0,0 +1,51 @@
#include "config.h"
// In case each transport packet needs to start with a specific header, define transport_header here.
// TRANSPORT_HEADER_SIZE needs to be defined in config.h
// For example, when TRANSPORT_HEADER_SIZE is defined as 3, define transport_header for example as {0x1F, 0xCD, 0x01};
uint8_t transport_header[TRANSPORT_HEADER_SIZE] = {};
// i2s config for using the internal ADC
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
i2s_config_t i2s_adc_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_MIC_CHANNEL,
.communication_format = I2S_COMM_FORMAT_I2S_LSB,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0};
#endif
// i2s config for reading from I2S
i2s_config_t i2s_mic_Config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
.channel_format = I2S_MIC_CHANNEL,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0};
// i2s microphone pins
i2s_pin_config_t i2s_mic_pins = {
.bck_io_num = I2S_MIC_SERIAL_CLOCK,
.ws_io_num = I2S_MIC_LEFT_RIGHT_CLOCK,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_MIC_SERIAL_DATA};
// i2s speaker pins
i2s_pin_config_t i2s_speaker_pins = {
.bck_io_num = I2S_SPEAKER_SERIAL_CLOCK,
.ws_io_num = I2S_SPEAKER_LEFT_RIGHT_CLOCK,
.data_out_num = I2S_SPEAKER_SERIAL_DATA,
.data_in_num = I2S_PIN_NO_CHANGE};

63
src/config.h Normal file
View File

@@ -0,0 +1,63 @@
#include <freertos/FreeRTOS.h>
#include <driver/i2s.h>
#include <driver/gpio.h>
// WiFi credentials
#define WIFI_SSID "iot"
#define WIFI_PSWD "Rijnstraat214"
// sample rate for the system
#define SAMPLE_RATE 16000
// are you using an I2S microphone - comment this if you want to use an analog mic and ADC input
#define USE_I2S_MIC_INPUT
// I2S Microphone Settings
// Which channel is the I2S microphone on? I2S_CHANNEL_FMT_ONLY_LEFT or I2S_CHANNEL_FMT_ONLY_RIGHT
// Generally they will default to LEFT - but you may need to attach the L/R pin to GND
//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT
#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT
#define I2S_MIC_SERIAL_CLOCK GPIO_NUM_18
#define I2S_MIC_LEFT_RIGHT_CLOCK GPIO_NUM_19
#define I2S_MIC_SERIAL_DATA GPIO_NUM_23
// Analog Microphone Settings - ADC1_CHANNEL_7 is GPIO35
#define ADC_MIC_CHANNEL ADC1_CHANNEL_7
// speaker settings
#define USE_I2S_SPEAKER_OUTPUT
#define I2S_SPEAKER_SERIAL_CLOCK GPIO_NUM_18
#define I2S_SPEAKER_LEFT_RIGHT_CLOCK GPIO_NUM_19
#define I2S_SPEAKER_SERIAL_DATA GPIO_NUM_5
// Shutdown line if you have this wired up or -1 if you don't
#define I2S_SPEAKER_SD_PIN 22
// transmit button
#define GPIO_TRANSMIT_BUTTON 14
// Which LED pin do you want to use? TinyPico LED or the builtin LED of a generic ESP32 board?
// Comment out this line to use the builtin LED of a generic ESP32 board
// #define USE_LED_GENERIC
// Which transport do you want to use? ESP_NOW or UDP?
// comment out this line to use UDP
#define USE_ESP_NOW
// On which wifi channel (1-11) should ESP-Now transmit? The default ESP-Now channel on ESP32 is channel 1
#define ESP_NOW_WIFI_CHANNEL 1
// In case all transport packets need a header (to avoid interference with other applications or walkie talkie sets),
// specify TRANSPORT_HEADER_SIZE (the length in bytes of the header) in the next line, and define the transport header in config.cpp
#define TRANSPORT_HEADER_SIZE 0
extern uint8_t transport_header[TRANSPORT_HEADER_SIZE];
// i2s config for using the internal ADC
extern i2s_config_t i2s_adc_config;
// i2s config for reading from of I2S
extern i2s_config_t i2s_mic_Config;
// i2s microphone pins
extern i2s_pin_config_t i2s_mic_pins;
// i2s speaker pins
extern i2s_pin_config_t i2s_speaker_pins;

20
src/main.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include <Arduino.h>
#include "Application.h"
// our application
Application *application;
void setup()
{
Serial.begin(115200);
// start up the application
application = new Application();
application->begin();
Serial.println("Application started");
}
void loop()
{
// nothing to do - the application is doing all the work
vTaskDelay(pdMS_TO_TICKS(1000));
}

11
test/README Normal file
View File

@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html