initial commit
This commit is contained in:
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [atomic14]
|
||||
ko_fi: atomic14
|
||||
30
.github/workflows/build.yml
vendored
Normal file
30
.github/workflows/build.yml
vendored
Normal 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
8
.gitignore
vendored
Normal 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
10
.vscode/extensions.json
vendored
Normal 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
10
.vscode/settings.json
vendored
Normal 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
21
LICENSE
Normal 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
49
README.md
Normal 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)
|
||||
|
||||
[](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
39
include/README
Normal 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
46
lib/README
Normal 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
|
||||
5
lib/audio_input/library.json
Normal file
5
lib/audio_input/library.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"build": {
|
||||
"flags": "-Ofast"
|
||||
}
|
||||
}
|
||||
32
lib/audio_input/src/ADCSampler.cpp
Normal file
32
lib/audio_input/src/ADCSampler.cpp
Normal 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
|
||||
18
lib/audio_input/src/ADCSampler.h
Normal file
18
lib/audio_input/src/ADCSampler.h
Normal 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);
|
||||
};
|
||||
52
lib/audio_input/src/I2SMEMSSampler.cpp
Normal file
52
lib/audio_input/src/I2SMEMSSampler.cpp
Normal 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;
|
||||
}
|
||||
25
lib/audio_input/src/I2SMEMSSampler.h
Normal file
25
lib/audio_input/src/I2SMEMSSampler.h
Normal 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);
|
||||
};
|
||||
21
lib/audio_input/src/I2SSampler.cpp
Normal file
21
lib/audio_input/src/I2SSampler.cpp
Normal 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);
|
||||
}
|
||||
28
lib/audio_input/src/I2SSampler.h
Normal file
28
lib/audio_input/src/I2SSampler.h
Normal 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;
|
||||
}
|
||||
};
|
||||
5
lib/audio_output/library.json
Normal file
5
lib/audio_output/library.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"build": {
|
||||
"flags": "-Ofast"
|
||||
}
|
||||
}
|
||||
30
lib/audio_output/src/DACOutput.cpp
Normal file
30
lib/audio_output/src/DACOutput.cpp
Normal 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
|
||||
20
lib/audio_output/src/DACOutput.h
Normal file
20
lib/audio_output/src/DACOutput.h
Normal 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;
|
||||
}
|
||||
};
|
||||
35
lib/audio_output/src/I2SOutput.cpp
Normal file
35
lib/audio_output/src/I2SOutput.cpp
Normal 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);
|
||||
}
|
||||
16
lib/audio_output/src/I2SOutput.h
Normal file
16
lib/audio_output/src/I2SOutput.h
Normal 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);
|
||||
};
|
||||
51
lib/audio_output/src/Output.cpp
Normal file
51
lib/audio_output/src/Output.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
27
lib/audio_output/src/Output.h
Normal file
27
lib/audio_output/src/Output.h
Normal 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);
|
||||
};
|
||||
99
lib/audio_output/src/OutputBuffer.h
Normal file
99
lib/audio_output/src/OutputBuffer.h
Normal 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);
|
||||
}
|
||||
};
|
||||
26
lib/indicator_led/src/GenericDevBoardIndicatorLed.cpp
Normal file
26
lib/indicator_led/src/GenericDevBoardIndicatorLed.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
12
lib/indicator_led/src/GenericDevBoardIndicatorLed.h
Normal file
12
lib/indicator_led/src/GenericDevBoardIndicatorLed.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "IndicatorLed.h"
|
||||
|
||||
class GenericDevBoardIndicatorLed : public IndicatorLed
|
||||
{
|
||||
protected:
|
||||
void set_led_rgb(uint32_t color);
|
||||
|
||||
public:
|
||||
GenericDevBoardIndicatorLed();
|
||||
};
|
||||
34
lib/indicator_led/src/IndicatorLed.cpp
Normal file
34
lib/indicator_led/src/IndicatorLed.cpp
Normal 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;
|
||||
}
|
||||
21
lib/indicator_led/src/IndicatorLed.h
Normal file
21
lib/indicator_led/src/IndicatorLed.h
Normal 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);
|
||||
};
|
||||
14
lib/indicator_led_pico/src/TinyPICOIndicatorLed.cpp
Normal file
14
lib/indicator_led_pico/src/TinyPICOIndicatorLed.cpp
Normal 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
|
||||
21
lib/indicator_led_pico/src/TinyPICOIndicatorLed.h
Normal file
21
lib/indicator_led_pico/src/TinyPICOIndicatorLed.h
Normal 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
|
||||
72
lib/transport/src/EspNowTransport.cpp
Normal file
72
lib/transport/src/EspNowTransport.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
16
lib/transport/src/EspNowTransport.h
Normal file
16
lib/transport/src/EspNowTransport.h
Normal 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);
|
||||
};
|
||||
46
lib/transport/src/Transport.cpp
Normal file
46
lib/transport/src/Transport.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
26
lib/transport/src/Transport.h
Normal file
26
lib/transport/src/Transport.h
Normal 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;
|
||||
};
|
||||
37
lib/transport/src/UdpTransport.cpp
Normal file
37
lib/transport/src/UdpTransport.cpp
Normal 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);
|
||||
}
|
||||
19
lib/transport/src/UdpTransport.h
Normal file
19
lib/transport/src/UdpTransport.h
Normal 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
33
platformio.ini
Normal 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
|
||||
15
platformio_override.example.ini
Normal file
15
platformio_override.example.ini
Normal 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
152
src/Application.cpp
Normal 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
22
src/Application.h
Normal 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
51
src/config.cpp
Normal 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
63
src/config.h
Normal 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
20
src/main.cpp
Normal 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
11
test/README
Normal 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
|
||||
Reference in New Issue
Block a user