Stream support

This commit is contained in:
Phil Schatzmann 2021-05-05 19:17:13 +02:00
parent b3891a80dd
commit 9e9f0c8593
76 changed files with 112722 additions and 864 deletions

BIN
.DS_Store vendored

Binary file not shown.

126
README.md
View File

@ -1,44 +1,120 @@
# Arduino Audio Tools
Some basic C++ classes that can be used for Audio Processing privided as Arduino Library
Some basic __header-only C++ classes__ that can be used for __Audio Processing__ provided as __Arduino Library__:
- Additional Stream implementations: MemoryStream - ESP32 only: UrlStream, I2SStream
- a simple I2S class (to read and write to the internal I2S) [ESP32 only]
- a simple ADC class (to read analog data with the help of I2S) [ESP32 only]
- a simple I2S class (to read and write to the internal I2S)
- a simple ADC class (to read analog data with the help of I2S)
- Additional Stream implementations: MemoryStream, UrlStream, I2SStream, A2DPStream, PrintStream
- Converters
- Musical Notes (with Frequencies of notes)
- Musical Notes (with frequencies of notes)
- SineWaveGenerator (to generate some sine tone)
- NBuffer (Multi buffer for writing and reading of (audio) data)
- TimerAlarmRepeating (e.g. for sampling audio data using exact times) [ESP32 only]
- A Wav Encoder and Decoder
- AudioOutputWithCallback class to provide callback integration with ESP8266Audio
- AudioOutputWithCallback class to provide callback integration e.g. with ESP8266Audio
This functionality provides the glue which makes different audio processing components and libraries work together.
We also provide plenty of examples that demonstrate how to implement the different scenarios.
We also provide plenty of examples that demonstrate how to implement the different scenarios. The __design philosophy__ is based on the Arduino conventions: we use the ```begin()``` and ```end()``` methods to start and stop the processing and we propagate the __use of Streams__. We all know the Arduino Streams. We use them to write out print messages and sometimes we use them to read the output from Serial devices. The same thing applies to my “Audio Streams”: You can read audio data from “Audio Sources” and you write them to “Audio Sinks”.
As “Audio Sources” we will have e.g.:
- Analog Microphones AnalogStream
- Digital Microphonse I2SStream
- Files on the Internet UrlStream
- Generated Sound GeneratedSoundStream
- Mobile Phone A2DP Bluetooth A2DPStream
- Binary Data in Flash Memory MemoryStream
- SD Files
As “Audio Sinks” we will have e.g:
- external DAC I2SStream
- an Amplifier AnalogStream
- Bluetooth Speakers A2DPStream
- Serial to display the data as CSV CsvStream.
- SD Files
Here is an simple example which streams a file from the Flash Memory and writes it to I2S:
```
#include "AudioTools.h"
#include "StarWars30.h"
using namespace audio_tools;
uint8_t channels = 2;
uint16_t sample_rate = 22050;
MemoryStream music(StarWars30_raw, StarWars30_raw_len);
I2SStream i2s; // Output to I2S
StreamCopyT<int16_t> copier(i2s, music); // copies sound into i2s
void setup(){
Serial.begin(115200);
I2SConfig config = i2s.defaultConfig(TX_MODE);
config.sample_rate = sample_rate;
config.channels = channels;
config.bits_per_sample = 16;
i2s.begin(config);
}
void loop(){
if (!copier.copy2()){
i2s.end();
stop();
}
}
```
A complete list of the supported Audio Stream classes and scenarios can be found in the [Scenarios Document](Scenarios.md)
## Examples
The examples follow the following naming convention: "scenario type"-"source"-"destination". For the scenario types we might have __base__ (using basic api functionality), __stream__ for examples using Streams and __test__ for the test cases.
For the __source__ we currently have __adc__ for analog input devices like analog microphones, __i2s__ for digital input devices (e.g. digital microphones), __file__ for SD files and __a2dp__ for input from Bluetooth A2DP (e.g. from a Mobile Phone).
For the __destination__ we use __dac__ for analog output (e.g. to an amplifier), __i2s__ for digital output devices (e.g. an external DAC), __file__ for SD files and __a2dp__ for output to Bluetooth A2DP (e.g. a Bluetooth Speaker).
Here is the list of examples:
#### Stream API
- [streams-url_raw-serial](/examples/streams-url_raw-serial) Displaying a music file from the internet on the Serial Plotter
- [streams-generator-serial](/examples/streams-generator-serial) Displaying generated sound on the Serial Plotter
- [streams-adc-serial](/examples/streams-adc-serial) Displaying input from analog microphone on the Serial Plotter
- [streams-i2s-a2dp](examples/streams-i2s-a2dp) - Sample analog sound and write it to a A2DP Bluetooth source
- [streams-file_raw-a2dp](examples/streams-file_raw-a2dp) - Read Raw File from SD card write it A2DP Bluetooth
- [streams-adc-a2dp](examples/streams-adc-a2dp) - Sample analog sound from analog microphone and send it to Bluetooth Speaker
- [streams-memory_raw-i2s_external_dac](examples/streams-memory_raw-i2s_external_dac) - Play music form Flash Memory via I2S to External DAC
... these are just a few examples, but you can combine any Input Stream with any Output Stream as you like...
#### Basic API
- [base-adc-serial](examples/base-adc-serial) - Sample analog sound and write it to Serial
- [base-adc-a2dp](examples/base-adc-a2dp) - Sample analog sound and write it to a A2DP Bluetooth source
- [base-file_raw-serial](examples/base-file_raw-serial) - Read Raw File from SD card to and write it to Serial
- [base-file_raw-a2dp](examples/base-file_raw-a2dp) - Read Raw File from SD card write it A2DP Bluetooth
- [base-file_mp3-a2dp](examples/base-file_mp3-a2dp) - Stream MP3 File from SD card to A2DP Bluetooth using the ESP8266Audio library
- [base-i2s-serial](examples/base-i2s-serial) - Sample digital sound and write it to Serial
- [base-i2s-a2dp](examples/base-i2s-a2dp) - Sample analog sound and write it to a A2DP Bluetooth source
The design philosophy is based on the Arduino conventions: we use the ```begin()``` and ```end()``` methods to start and stop the processing and we propagate the use of Streams.
## Optional Libraries
Dependent on the example you might need to install some of the following libraries:
- [ESP32-A2DP Library](https://github.com/pschatzmann/ESP32-A2DP)
- [ESP8266Audio](https://github.com/earlephilhower/ESP8266Audio)
- [SD Library](https://www.arduino.cc/en/reference/SD)
- [arduino-fdk-aac](https://github.com/pschatzmann/arduino-fdk-aac)
- [ESP32-A2DP Library](https://github.com/pschatzmann/ESP32-A2DP) to support A2DP Bluetooth Audio
- [ESP8266Audio]( ) to play different audio Formats
- [SD Library](https://www.arduino.cc/en/reference/SD) to read and write files.
- [arduino-fdk-aac](https://github.com/pschatzmann/arduino-fdk-aac) to encode or decode AAC
# Examples
- [adc-a2dp](examples/adc-a2dp) - Stream Analog input to A2DP Bluetooth
- [adc-serial](examples/adc-serial) - Stream Analog input to Serial
- [file_raw-a2dp](examples/file_raw-a2dp) - Stream Row File from SD card to A2DP Bluetooth
- [file_raw-serial](examples/file_raw-serial) - Stream Raw File from SD card to Serial
- [file_mp3-a2dp](examples/file_mp3-a2dp) - Stream MP3 File from SD card to A2DP Bluetooth using the ESP8266Audio library
- [i2s-a2dp](examples/i2s-a2dp) - Stream I2S Input to A2DP Bluetooth
- [i2s-serial](examples/i2s-serial) - Stream I2S Input to Serial
## Installation
@ -52,7 +128,9 @@ git clone pschatzmann/arduino-audio-tools.git
## Documentation
Here is the generated [Class documentation](https://pschatzmann.github.io/arduino-audio-tools/html/annotated.html). You might find further information in [one of my blogs](https://www.pschatzmann.ch/home/category/machine-sound/)
Here is the generated [Class documentation](https://pschatzmann.github.io/arduino-audio-tools/html/annotated.html).
You also might find further information in [one of my blogs](https://www.pschatzmann.ch/home/category/machine-sound/)
## Project Status
@ -63,7 +141,7 @@ This is currently work in progress:
| Analog input - ADC | tested |
| I2S | tested |
| Files (RAW, MP3...) | tested |
| Streams | open |
| Streams | tested |
| WAV encoding/deconding | open |
| AAC encoding/deconding | open |
| int24_t | tested |

53
Scenarios.md Normal file
View File

@ -0,0 +1,53 @@
# Schenarios
### Supported Architectures
Unfortunatly Arduino does not provide an I2S functionality which is standrdized acress the different processors. There is only an official documentation for SAMD21 processors. The full functionality of the library is currently only available on the ESP32:
| Processor | I2SStream | ADCStream | A2DP | UrlStream | Other |
|----------------|-----------|-----------|--------|-----------|--------|
| ESP32 | + | + | + | + | + |
| ESP8266 | * | * | | | + |
| SAMD21 | * | | | | + |
| Raspberry Pico | | | | | + |
+ supported
* not tested
### Supported Schenarios
Here are the related Stream classes with their supported operations that can be used:
| Class | Read | Write | Comments |
|-------------------------|------|-------|--------------------|
| I2SStream | + | + | i2s |
| AnalogAudioStream | + | + | adc, dac |
| MemoryStream | + | + | memory |
| UrlStream | + | | url |
| A2DPStream | + | + | a2dp |
| GeneratedSoundStream | + | | gen |
| AudioOutputWithCallback | + | + | |
| CsvStream | | + | csv |
| File | + | + | From SD library |
| Serial, ... | + | + | Std Arduino |
In theory we should be able to support the following scenarios:
| Input | dac | i2s | file | a2dp | Serial | csv |
|--------|------|-----|------|------|--------|------|
| adc | + | + | + | + | + | + |
| i2s | + | + | + | + | + | + |
| file | + | + | + | + | + | + |
| a2dp | + | + | + | - | + | + |
| Serial | + | + | + | + | + | + |
| memory | + | + | + | + | + | + |
| url | + | + | + | - | + | + |
| gen | + | + | + | + | + | + |

BIN
docs/.DS_Store vendored

Binary file not shown.

View File

@ -17,17 +17,17 @@ using namespace audio_tools;
* @brief We use a mcp6022 analog microphone as input and send the data to A2DP
*/
ADC adc;
AnalogAudio adc;
BluetoothA2DPSource a2dp_source;
// The data has a center of around 26427, so we we need to shift it down to bring the center to 0
FilterScaler<int16_t> scaler(1.0, -26427, 32700 );
ConverterScaler<int16_t> scaler(1.0, -26427, 32700 );
// callback used by A2DP to provide the sound data
int32_t get_sound_data(Channels* data, int32_t len) {
arrayOf2int16_t *data_arrays = (arrayOf2int16_t *) data;
// the ADC provides data in 16 bits
size_t result_len = adc.read(data_arrays, len);
scaler.process(data_arrays, result_len);
scaler.convert(data_arrays, result_len);
return result_len;
}
@ -37,7 +37,7 @@ void setup(void) {
// start i2s input with default configuration
Serial.println("starting I2S-ADC...");
adc.begin(adc.defaultConfig());
adc.begin(adc.defaultConfig(RX_MODE));
// start the bluetooth
Serial.println("starting A2DP...");

View File

@ -17,11 +17,11 @@ using namespace audio_tools;
* @brief We use a mcp6022 analog microphone on GPIO34 and write it to Serial
*/
ADC adc;
AnalogAudio adc;
const int32_t max_buffer_len = 512;
int16_t buffer[max_buffer_len][2];
// The data has a center of around 26427, so we we need to shift it down to bring the center to 0
FilterScaler<int16_t> scaler(1.0, -26427, 32700 );
ConverterScaler<int16_t> scaler(1.0, -26427, 32700 );
// Arduino Setup
void setup(void) {
@ -29,7 +29,7 @@ void setup(void) {
// start i2s input with default configuration
Serial.println("starting I2S-ADC...");
adc.begin(adc.defaultConfig());
adc.begin(adc.defaultConfig(RX_MODE));
}
@ -37,7 +37,7 @@ void setup(void) {
void loop() {
size_t len = adc.read(buffer, max_buffer_len);
// move center to 0 and scale the values
scaler.process(buffer, len);
scaler.convert(buffer, len);
for (int j=0;j<len;j++){
Serial.print(buffer[j][0]);

View File

@ -11,7 +11,7 @@
#include <SD.h>
#include "AudioFileSourceSD.h"
#include "AudioGeneratorMP3.h"
#include "ESP8266AudioSupport.h"
#include "AudioESP8266.h"
#include "BluetoothA2DPSource.h"
#include "AudioTools.h"

View File

@ -1,7 +1,7 @@
/**
* @file i2s-a2dp.ino
* @file base-i2s-a2dp.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/i2s-a2dp/README.md
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/base-i2s-a2dp/README.md
*
* @author Phil Schatzmann
* @copyright GPLv3
@ -10,14 +10,15 @@
#include "Arduino.h"
#include "AudioTools.h"
#include "BluetoothA2DPSource.h"
using namespace audio_tools;
BluetoothA2DPSource a2dp_source;
I2S<int32_t> i2s;
ChannelConverter<int32_t> converter(&convertFrom32To16);
FilterFillLeftAndRight<int32_t> bothChannels;
CallbackConverter<int32_t,int16_t> converter(&convertFrom32To16);
ConverterFillLeftAndRight<int32_t> bothChannels;
const size_t max_buffer_len = 1024;
int32_t buffer[max_buffer_len][2];
@ -29,10 +30,10 @@ int32_t get_sound_data(Channels* data, int32_t len) {
size_t result_len = i2s.read(buffer, req_len);
// we have data only in 1 channel but we want to fill both
bothChannels.process(buffer, result_len);
bothChannels.convert(buffer, result_len);
// convert buffer to int16 for A2DP
converter.convert(buffer, data, result_len);
converter.convert(buffer, (int16_t(*)[2]) data, result_len);
return result_len;
}

View File

@ -1,4 +1,4 @@
/**
* @file i2s-serial.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/i2s-serial/README.md
@ -28,8 +28,8 @@ void setup(void) {
// start i2s input with default configuration
Serial.println("starting I2S...");
I2SConfig<int32_t> config = i2s.defaultConfig(RX_MODE);
config.i2s.sample_rate = 16000;
I2SConfig config = i2s.defaultConfig(RX_MODE);
config.sample_rate = 16000;
i2s.begin(config);
}

View File

@ -0,0 +1,22 @@
# Reading an Analog Signal
We can read an analog signal from a microphone. To test the functionality I am using a MCP6022 microphone module.
![MCP6022](https://pschatzmann.github.io/arduino-audio-tools/resources/mcp6022.jpeg)
![MCP6022](https://pschatzmann.github.io/arduino-audio-tools/resources/mcp6022-1.jpeg)
The MCP6022 is a anlog microphone which operates at 3.3 V
We sample the sound signal with the help of the ESP32 I2S ADC input functionality.
### Pins:
| MCP6022 | ESP32
|---------|---------------
| VCC | 3.3
| GND | GND
| OUT | GPIO34
Here is the result on the Arduino Serial Plotter:
![serial-plotter](https://pschatzmann.github.io/arduino-audio-tools/resources/serial-plotter-sine.png)

View File

@ -0,0 +1,32 @@
/**
* @file streams-adc-serial.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/streams-adc-serial/README.md
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "AudioTools.h"
using namespace audio_tools;
uint8_t channels = 2;
AnalogAudioStream microphone; // analog microphone
CsvStream<int16_t> printer(Serial, channels); // ASCII output stream
StreamCopy copier(printer, microphone); // copies microphone into printer
ConverterAutoCenter<int16_t> center; // make sure the avg of the signal is 0
// Arduino Setup
void setup(void) {
// Open Serial
Serial.begin(115200);
AnalogConfig cfg = microphone.defaultConfig(RX_MODE);
microphone.begin(cfg);
}
// Arduino loop - repeated processing
void loop() {
copier.copy(center);
}

View File

@ -0,0 +1,32 @@
# Digital output via I2S to a external DAC
To test the I2S output I'm using the generate sine signal and write it to I2S.
For my tests I am using the 24-bit PCM5102 PCM5102A Stereo DAC Digital-to-analog Converter PLL Voice Module pHAT
![DAC](https://pschatzmann.github.io/arduino-audio-tools/resources/dac.jpeg)
### External DAC:
I am just using the default pins defined by the framework. However I could change them with the help of the config object. The mute pin can be defined in the constructor of the I2SStream - by not defining anything we use the default which is GPIO23
| DAC | ESP32
| --------| ---------------
| VDD | 5V
| GND | GND
| SD | OUT (GPIO22)
| L/R | GND
| WS | WS (GPIO15)
| SCK | BCK (GPIO14)
| FMT | GND
| XSMT | GPIO23
- DEMP - De-emphasis control for 44.1kHz sampling rate(1): Off (Low) / On (High)
- FLT - Filter select : Normal latency (Low) / Low latency (High)
- SCK - System clock input (probably SCL on your board).
- FMT - Audio format selection : I2S (Low) / Left justified (High)
- XSMT - Soft mute control(1): Soft mute (Low) / soft un-mute (High)

View File

@ -0,0 +1,40 @@
/**
* @file streams-generator-i2s.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/streams-generator-i2s/README.md
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "AudioTools.h"
using namespace audio_tools;
typedef int16_t sound_t; // sound will be represented as int16_t (with 2 bytes)
uint16_t sample_rate=44100;
uint8_t channels = 2; // The stream will have 2 channels
SineWaveGenerator<sound_t> sineWave(32000); // subclass of SoundGenerator with max amplitude of 32000
GeneratedSoundStream<sound_t> sound(sineWave, channels); // Stream generated from sine wave
I2SStream out; // Output to I2S
StreamCopy copier(out, sound); // copies sound into i2s
// Arduino Setup
void setup(void) {
// Open Serial
Serial.begin(115200);
// Setup sine wave
sineWave.begin(sample_rate, B4);
// Open I2S
I2SConfig config = out.defaultConfig(TX_MODE);
config.sample_rate = sample_rate;
config.channels = channels;
config.bits_per_sample = sizeof(sound_t)*8;
out.begin(config);
}
// Arduino loop - copy sound to out
void loop() {
copier.copy();
}

View File

@ -0,0 +1,12 @@
# Display Generated Sound
Sometimes it is quite useful to be able to generate a test tone.
We can use the GeneratedSoundStream class together with a SoundGenerator class. In my example I use a SineWaveGenerator.
In order to make sure that we use a consistent data type in the sound processing I also used a ```typedef int16_t sound_t;```. You could experiment with some other data types.
To be able to verify the result I used ```CsvStream<sound_t> printer(Serial, channels);``` which generates CSV output and sends it to Serial.
Here is the result on the Arduino Serial Plotter:
![serial-plotter](https://pschatzmann.github.io/arduino-audio-tools/resources/serial-plotter-sine.png)

View File

@ -0,0 +1,34 @@
/**
* @file streams-url_raw-serial.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/streams-url_raw-serial/README.md
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "AudioTools.h"
using namespace audio_tools;
typedef int16_t sound_t; // sound will be represented as int16_t (with 2 bytes)
uint16_t sample_rate=44100;
uint8_t channels = 2; // The stream will have 2 channels
SineWaveGenerator<sound_t> sineWave(32000); // subclass of SoundGenerator with max amplitude of 32000
GeneratedSoundStream<sound_t> sound(sineWave, channels); // Stream generated from sine wave
CsvStream<sound_t> printer(Serial, channels); // ASCII stream
StreamCopy copier(printer, sound); // copies sound into printer
// Arduino Setup
void setup(void) {
// Open Serial
Serial.begin(115200);
// Setup sine wave
sineWave.begin(sample_rate, B4);
}
// Arduino loop - repeated processing
void loop() {
copier.copy();
}

View File

@ -0,0 +1,38 @@
# Digital output via I2S to a external DAC
Somtimes we want to store the sound file in memory. [Audacity](https://www.audacityteam.org/) might help you out here: export with the file name audio.raw as RAW signed 16 bit PCM and copy it to the SD card. In the example I was just using one channel to save memory!.
Then you can convert the file with xxd into a C file that contains the data in an array. In the Sketch I am using the __MemoryStream class__ which turns the array into a Stream.
Unlike in the other examples I am using the typed __StreamCopyT<int16_t>__ class together with the __copy2()__ method. This reads the one channel input and copies it as 2 channels to the destination I2S stream.
Please note that you must compile this sketch with the __Partition Scheme: Huge App__!
For my tests I am using the 24-bit PCM5102 PCM5102A Stereo DAC Digital-to-analog Converter PLL Voice Module pHAT
![DAC](https://pschatzmann.github.io/arduino-audio-tools/resources/dac.jpeg)
### External DAC:
I am just using the default pins defined by the framework. However I could change them with the help of the config object. The mute pin can be defined in the constructor of the I2SStream - by not defining anything we use the default which is GPIO23
| DAC | ESP32
| --------| ---------------
| VDD | 5V
| GND | GND
| SD | OUT (GPIO22)
| L/R | GND
| WS | WS (GPIO15)
| SCK | BCK (GPIO14)
| FMT | GND
| XSMT | GPIO23
- DEMP - De-emphasis control for 44.1kHz sampling rate(1): Off (Low) / On (High)
- FLT - Filter select : Normal latency (Low) / Low latency (High)
- SCK - System clock input (probably SCL on your board).
- FMT - Audio format selection : I2S (Low) / Left justified (High)
- XSMT - Soft mute control(1): Soft mute (Low) / soft un-mute (High)

View File

@ -0,0 +1,40 @@
/**
* @file streams-memory_raw-i2s_external_dac.ino
* @author Phil Schatzmann
* @brief Compile with Partition Scheme Hughe APP!
* @version 0.1
* @date 2021-01-24
*
* @copyright Copyright (c) 2021
*
*/
#include "AudioTools.h"
#include "StarWars30.h"
using namespace audio_tools;
uint8_t channels = 2;
uint16_t sample_rate = 22050;
I2SStream i2s; // Output to I2S
MemoryStream music(StarWars30_raw, StarWars30_raw_len);
StreamCopyT<int16_t> copier(i2s, music); // copies sound into i2s
void setup(){
Serial.begin(115200);
I2SConfig config = i2s.defaultConfig(TX_MODE);
config.sample_rate = sample_rate;
config.channels = channels;
config.bits_per_sample = 16;
i2s.begin(config);
}
void loop(){
if (!copier.copy2()){
i2s.end();
stop();
}
}

View File

@ -0,0 +1,8 @@
# Display RAW Stream
Sometimes it is handy to check out the data on the screen with the help of the Arduino Serial Monitor and Serial Plotter.
We read the raw binary data from an UrlStream.
As output stream we use a CsvStream: this class transforms the data into CSV and prints the result to Serial. Finally we can use the Arduino Serial Plotter to view the result as chart:
![serial-plotter](https://pschatzmann.github.io/arduino-audio-tools/resources/serial-plotter-01.png)

View File

@ -0,0 +1,39 @@
/**
* @file streams-url_raw-serial.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/streams-url_raw-serial/README.md
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "WiFi.h"
#include "AudioTools.h"
using namespace audio_tools;
UrlStream music; // Music Stream
int channels = 2; // The stream has 2 channels
CsvStream<int16_t> printer(Serial, channels); // ASCII stream
StreamCopy copier(printer, music); // copies music into printer
// Arduino Setup
void setup(void) {
// Open Serial
Serial.begin(115200);
// connect to WIFI
WiFi.begin("network-name", "password");
while (WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(500);
}
// open music stream - it contains 2 channels of int16_t data
music.begin("https://pschatzmann.github.io/arduino-audio-tools/resources/audio.raw");
}
// Arduino loop - repeated processing
void loop() {
copier.copy();
}

View File

@ -1,3 +1,3 @@
These folder contains some experiments, tests and work in progress, so do not expect them to work.
These folder contains some work in progress, experiments and tests, so do not expect them to work.
But it might still serve as a source of inspiration...

View File

@ -1,22 +0,0 @@
# SD Module
![sd](https://pschatzmann.github.io/arduino-audio-tools/doc/resources/sd-module.jpeg)
We are reading a raw audio file from the SD card and send it to a Bluetooth A2DP device. The audio file must be available using 16 bit integers with 2 channels.
The SD module is connected with the help of the SPI bus
### Pins:
We use SPI2 (HSPI):
| MCP6022 | ESP32
|---------|---------------
| VCC | 5V
| GND | GND
| CS | CS GP15
| SCK | SCK GP14
| MOSI | MOSI GP13
| MISO | MISO GP12

View File

@ -1,34 +0,0 @@
#include "AudioTools.h"
#include <SPI.h>
#include <SD.h>
I2S i2s; // I2S output destination
File sound_file;
AACDecoder decoder(i2s);
const int sd_ss_pin = 4;
const char* file_name = "audio.aac"
uint8_t buffer[512];
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// Setup SD and open file
SD.begin(sd_ss_pin);
sound_file = SD.open(file_name, FILE_READ);
i2s.begin();
decoder.begin();
}
// Arduino loop - repeated processing
void loop() {
if (sound_file.available()>0){
int len = sound_file.readBytes(buffer, 512);
decoder.write(buffer, len);
}
}

View File

@ -1,46 +0,0 @@
/**
* @file file_raw-external_dac.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/file_raw-external_dac/README.md
*
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "AudioTools.h"
#include <SPI.h>
#include <SD.h>
using namespace audio_tools;
File sound_file;
I2S<int16_t> i2s;
I2SStream i2s_stream(i2s);
StreamCopy streamCopy(i2s_stream, sound_file, 1024);
const char* file_name = "/audio.raw";
const int sd_ss_pin = 5;
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// Setup SD and open file
SD.begin(sd_ss_pin);
sound_file = SD.open(file_name, FILE_READ);
// start I2S with external DAC
Serial.println("starting I2S...");
i2s.begin(i2s.defaultConfig(TX_MODE));
}
// Arduino loop - repeated processing
void loop() {
if (streamCopy.copy()){
Serial.print(".");
} else {
Serial.println();
Serial.println("Copy ended");
delay(10000);
}
}

View File

@ -1,41 +0,0 @@
# Stream SD File to I2S internal DAC
We are reading a raw audio file from the SD card and write the data to the analog pins of the ESP32 using the I2S interface. The audio file must be available using 16 bit integers with 2 channels.
[Audacity](https://www.audacityteam.org/) might help you out here: export with the file name audio.raw as RAW signed 16 bit PCM and copy it to the SD card. In my example I was using the file [audio.raw](https://pschatzmann.github.io/arduino-audio-tools/resources/audio.raw).
### SD Pins:
The SD module is connected with the help of the SPI bus
![sd](https://pschatzmann.github.io/arduino-audio-tools/resources/sd-module.jpeg)
We connect the SD to the ESP32:
| SD | ESP32
|---------|---------------
| VCC | 5V
| GND | GND
| CS | CS GP5
| SCK | SCK GP18
| MOSI | MOSI GP23
| MISO | MISO GP19
### Amplifier Pins:
To hear the sound we connect the ESP32 to an amplifier module: The analog output is available on GPIO25 & GPIO26. You could also use some earphones.
| Amp | ESP32
|---------|---------------
| + | 5V
| - | GND
| L | GPIO25
| R | GPIO26
| T | GND

View File

@ -1,48 +0,0 @@
/**
* @file file_raw-internal_dac.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/file_raw-internal_dac/README.md
*
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "AudioTools.h"
#include <SPI.h>
#include <SD.h>
using namespace audio_tools;
File sound_file;
I2S<int16_t> i2s;
I2SStream i2s_stream(i2s);
StreamCopy streamCopy(i2s_stream, sound_file, 1024);
const char* file_name = "/audio.raw";
const int sd_ss_pin = 5;
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// Setup SD and open file
SD.begin(sd_ss_pin);
sound_file = SD.open(file_name, FILE_READ);
// start I2S with internal DAC -> GPIO25 & GPIO26
Serial.println("starting I2S...");
I2SConfig<int16_t> config = i2s.defaultConfig(TX_MODE);
config.i2s.mode = static_cast<i2s_mode_t>( I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN);
i2s.begin(config);
}
// Arduino loop - repeated processing
void loop() {
if (streamCopy.copy()){
Serial.print(".");
} else {
Serial.println();
Serial.println("Copy ended");
delay(10000);
}
}

View File

@ -0,0 +1,21 @@
# Stream Analog input to A2DP Bluetooth
We can read an analog signal from a microphone and and send it to a Bluetooth A2DP device. To test the functionality I am using a MCP6022 microphone module.
![MCP6022](https://pschatzmann.github.io/arduino-audio-tools/resources/mcp6022.jpeg)
![MCP6022](https://pschatzmann.github.io/arduino-audio-tools/resources/mcp6022-1.jpeg)
The MCP6022 is a anlog microphone which operates at 3.3 V
We sample the sound signal with the help of the ESP32 I2S ADC input functionality.
In this implementation we are using the Streams !
### Pins:
| MCP6022 | ESP32
|---------|---------------
| VCC | 3.3
| GND | GND
| OUT | GPIO34

View File

@ -0,0 +1,44 @@
/**
* @file adc-a2dp.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/adc-a2dp/README.md
*
* @author Phil Schatzmann
* @copyright GPLv3
*
*/
#include "Arduino.h"
#include "BluetoothA2DPSource.h"
#include "AudioTools.h"
using namespace audio_tools;
/**
* @brief We use a mcp6022 analog microphone as input and send the data to A2DP
*/
ADC adc;
ADCStream adcStream(adc);
A2DPStream a2dpStream;
StreamCopy copier(a2dp, adcStream, 512);
// The data has a center of around 26427, so we we need to shift it down to bring the center to 0
ConverterScaler<int16_t> scaler(1.0, -26427, 32700 );
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// start i2s input with default configuration
Serial.println("starting ADC...");
adc.begin(adc.defaultConfig());
// start the bluetooth
Serial.println("starting A2DP...");
a2dpStream.start("MyMusic");
}
// Arduino loop - repeated processing
void loop() {
copier.copy(scaler);
}

View File

@ -0,0 +1,33 @@
# Stream I2S Input to A2DP Bluetooth
## General Description:
We implement a A2DP source: We stream the sound input which we read in from the I2S interface to a A2DP sink. We can use any device which provides the sound data via I2S. In order to test the functionality we use the INMP441 microphone.
In this Sketch we are using Streams!
![INMP441](https://pschatzmann.github.io/arduino-audio-tools/resources/inmp441.jpeg)
The INMP441 is a high-performance, low power, digital-output, omnidirectional MEMS microphone with a bottom port. The complete INMP441 solution consists of a MEMS sensor, signal conditioning, an analog-to-digital converter, anti-aliasing filters, power management, and an industry-standard 24-bit I²S interface. The I²S interface allows the INMP441 to connect directly to digital processors, such as DSPs and microcontrollers, without the need for an audio codec in the system.
## Pins
| INMP441 | ESP32
| --------| ---------------
| VDD | 3.3
| GND | GND
| SD | IN (GPIO32)
| L/R | GND
| WS | WS (GPIO15)
| SCK | BCK (GPIO14)
SCK: Serial data clock for I²S interface
WS: Select serial data words for the I²S interface
L/R: Left / right channel selection
When set to low, the microphone emits signals on the left channel of the I²S frame.
When the high level is set, the microphone will send signals on the right channel.
ExSD: Serial data output of the I²S interface
VCC: input power 1.8V to 3.3V
GND: Power groundHigh PSR: -75 dBFS.

View File

@ -0,0 +1,39 @@
/**
* @file i2s-a2dp.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/i2s-a2dp/README.md
*
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "Arduino.h"
#include "AudioTools.h"
using namespace audio_tools;
I2S<int32_t> i2s;
I2SStream i2sStream(i2s); // Access I2S as stream
const A2DPStream &a2dpStream = A2DPStream.instance(); // access A2DP as stream
ChannelConverter<int32_t> converter(&convertFrom32To16);
ConverterFillLeftAndRight<int32_t> bothChannels;
StreamCopy copier(i2sStream, a2dpStream, 1024); // copy a2dpStream to i2sStream
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// start i2s input with default configuration
Serial.println("starting I2S...");
i2s.begin(i2s.defaultConfig(RX_MODE));
// start the bluetooth
Serial.println("starting A2DP...");
a2dpStream.begin("MyMusic");
}
// Arduino loop - copy data
void loop() {
copier.copy(bothChannels, converter)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,9 @@
# Stream SD File to I2S external DAC
# Stream URL to I2S external DAC
We are reading a raw audio file from the SD card and write the data to the I2S interface. The audio file must be available using 16 bit integers with 2 channels.
We are reading a raw audio file from the Intenet and write the data to the I2S interface. The audio file must be available using 16 bit integers with 2 channels.
[Audacity](https://www.audacityteam.org/) might help you out here: export with the file name audio.raw as RAW signed 16 bit PCM and copy it to the SD card. In my example I was using the file [audio.raw](https://pschatzmann.github.io/arduino-audio-tools/resources/audio.raw).
### SD Pins:
The SD module is connected with the help of the SPI bus
![sd](https://pschatzmann.github.io/arduino-audio-tools/resources/sd-module.jpeg)
We connect the SD to the ESP32:
| SD | ESP32
|---------|---------------
| VCC | 5V
| GND | GND
| CS | CS GP5
| SCK | SCK GP18
| MOSI | MOSI GP23
| MISO | MISO GP19
### External DAC:
@ -34,7 +17,7 @@ We connect the SD to the ESP32:
| SCK | BCK (GPIO14)
Comments - The Playing is breaking up most likely because we receive the data not fast enough. Try to make an example with a lower sampes per second....

View File

@ -0,0 +1,51 @@
/**
* @file url_raw-I2S_external_dac.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/url_raw-I2S_externel_dac/README.md
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "WiFi.h"
#include "AudioTools.h"
using namespace audio_tools;
UrlStream music; // Music Stream
I2S<int16_t> i2s; // I2S
I2SStream i2s_stream(i2s);// I2S as Stream
StreamCopy copier(i2s_stream, music, 1024); // copy music to i2s_stream
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// connect to WIFI
WiFi.begin("network", "pwd");
while (WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(500);
}
// open music stream
music.begin("https://pschatzmann.github.io/arduino-audio-tools/resources/audio-8000.raw");
// start I2S with external DAC
Serial.println("\nstarting I2S...");
I2SConfig cfg = i2s.defaultConfig(TX_MODE);
cfg.sample_rate = 8000;
i2s.begin(cfg);
}
// Arduino loop - repeated processing: copy input stream to output stream
void loop() {
int len = copier.copy();
if (len){
Serial.print(".");
} else {
i2s.end();
Serial.println("\nCopy ended");
delay(10000);
}
}

View File

@ -0,0 +1,24 @@
# Stream URL to I2S internal DAC
I am reading a raw audio file from the Intenet and write the data to the analog pins of the ESP32 using the I2S interface. The audio file must be available using 16 bit integers with 2 channels.
[Audacity](https://www.audacityteam.org/) might help you out here: export with the file name audio.raw as RAW signed 16 bit PCM and copy it to the SD card. In my example I was using the file [audio.raw](https://pschatzmann.github.io/arduino-audio-tools/resources/audio.raw).
### Amplifier Pins:
To hear the sound I connected the ESP32 to an amplifier module: The analog output is available on GPIO25 & GPIO26. But you could also use some earphones...
| Amp | ESP32
|---------|---------------
| + | 5V
| - | GND
| L | GPIO25
| R | GPIO26
| T | GND

View File

@ -0,0 +1,40 @@
/**
* @file url_raw-I2S_internal_dac.ino
* @author Phil Schatzmann
* @brief see https://github.com/pschatzmann/arduino-audio-tools/blob/main/examples/url_raw-I2S_internal_dac/README.md
* @author Phil Schatzmann
* @copyright GPLv3
*/
#include "AudioTools.h"
using namespace audio_tools;
UrlStream music; // Music Stream
AnalogAudio dac;
StreamCopy streamCopy(dac, music);
ConverterToInternalDACFormat<int16_t> converter;
// Arduino Setup
void setup(void) {
Serial.begin(115200);
// open music stream
music.begin("https://pschatzmann.github.io/arduino-audio-tools/resources/audio-8000.raw");
// start I2S with internal DAC -> GPIO25 & GPIO26
Serial.println("starting I2S...");
I2SConfig cfg = i2s.defaultConfig(TX_MODE);
cfg.sample_rate = 8000;
i2s.begin(cfg);
}
// Arduino loop - repeated processing: copy input stream to output stream
void loop() {
if (!streamCopy.copy(converter)){
Serial.println("Copy ended");
dac.end();
music.end();
stop();
}
}

View File

@ -2,17 +2,37 @@
using namespace audio_tools;
void setup(){
int24_t value;
Serial.begin(115200);
void testMath() {
int24_t value(1);
Serial.println("Expecting 2:")
Serial.println(value+value);
Serial.println(1+value);
Serial.println(value+1);
Serial.println("Expecting true")
Serial.println(1==value);
Serial.println(value==1);
Serial.println(value>=1);
Serial.println(value<=1);
}
void testCompare() {
int24_t value;
for (int32_t j=8388607;j>-8388606;j++){
value = j;
Serial.print(j);
Serial.print(", ");
Serial.print((int32_t)value);
Serial.println((int32_t)value == j ? ", OK" : ", ERROR");
Serial.print(value);
Serial.println(static_cast<int32_t<(value) == j ? ", OK" : ", ERROR");
}
}
void setup(){
Serial.begin(115200);
testMath();
testCompare();
}

130
src/AudioA2DP.h Normal file
View File

@ -0,0 +1,130 @@
/**
* @file AudioA2DP.h
* @author Phil Schatzmann
* @brief A2DP Support via Arduino Streams
* @copyright GPLv3
*
*/
#pragma once
#include "AudioTools.h"
#ifdef USE_A2DP
#include "BluetoothA2DPSink.h"
#include "BluetoothA2DPSource.h"
#include "AudioESP8266.h"
namespace audio_tools {
/**
* @brief Covnerts the data from T src[][2] to a Channels array
* @author Phil Schatzmann
* @copyright GPLv3
*
* @tparam T
*/
template<typename T>
class ChannelConverter {
public:
ChannelConverter( int16_t (*convert_ptr)(T from)){
this->convert_ptr = convert_ptr;
}
// The data is provided as int24_t tgt[][2] but returned as int24_t
void convert(T src[][2], Channels* channels, size_t size) {
for (int i=size; i>0; i--) {
channels[i].channel1 = (*convert_ptr)(src[i][0]);
channels[i].channel2 = (*convert_ptr)(src[i][1]);
}
}
private:
int16_t (*convert_ptr)(T from);
};
AudioOutputWithCallback a2dp_stream_out(A2DP_BUFFER_SIZE, A2DP_BUFFER_COUNT);
// callback used by A2DP to provide the sound data
int32_t a2dp_stream_source_sound_data(Channels* data, int32_t len) {
// the data in the file must be in int16 with 2 channels
size_t result_len_bytes = a2dp_stream_out.read(data, len);
// result is in number of frames
int32_t result_len = result_len_bytes / sizeof(Channels);
ESP_LOGD("get_sound_data", "%d -> %d", result_len );
return result_len;
}
// callback used by A2DP to write the sound data
void a2dp_stream_sink_sound_data(const uint8_t* data, uint32_t len) {
a2dp_stream_out.write(data, len);
}
/**
* @brief Stream support for A2DP: When calling write we start a a2dp_source - when calling read we start a a2dp_sink
* The data is in int16_t with 2 channels at 44100 hertz.
*
* Because we support only one instance the class is implemented as singleton!
*/
class A2DPStream : public BufferedStream {
public:
/// Just defines the bluetooth name
void begin(char* name) {
this->name = name;
}
/// Provides the A2DPStream singleton instance
static A2DPStream &instance() {
static A2DPStream *ptr;
if (ptr==nullptr){
ptr = new A2DPStream();
}
return *ptr;
}
/// provides access to the
BluetoothA2DPSource &source() {
return a2dp_source;
}
/// provides access to the BluetoothA2DPSink
BluetoothA2DPSink &sink(){
return a2dp_sink;
}
protected:
BluetoothA2DPSource a2dp_source;
BluetoothA2DPSink a2dp_sink;
bool is_setup = false;
char* name = nullptr;
A2DPStream() : BufferedStream(A2DP_BUFFER_SIZE){
}
virtual size_t writeExt(const uint8_t* data, size_t len) {
if (name==nullptr) return 0;
if (!is_setup){
a2dp_source.start(name, a2dp_stream_source_sound_data);
is_setup = true;
}
return a2dp_stream_out.write(data, len);
}
virtual size_t readExt( uint8_t *data, size_t len) {
if (name==nullptr) return 0;
if (!is_setup){
a2dp_sink.set_stream_reader(&a2dp_stream_sink_sound_data, false);
a2dp_sink.start(name);
is_setup = true;
}
return a2dp_stream_out.readBytes(data, len);
}
};
}
#endif

65
src/AudioConfig.h Normal file
View File

@ -0,0 +1,65 @@
/**
* @author Phil Schatzmann
* @brief AutioTools Configuration
* @copyright GPLv3
*
*/
#pragma once
/**
* @brief Optional Functionality - comment out if not wanted
*/
#define USE_ESP8266_AUDIO
#ifdef ESP32
#define USE_A2DP
#endif
/**
* @brief Common Default Settings that can usually be changed in the API
*/
#ifndef LED_BUILTIN
#define LED_BUILTIN 13 // pin number is specific to your esp32 board
#endif
#define DEFAULT_BUFFER_SIZE 1024
#define I2S_DEFAULT_SAMPLE_RATE 44100
#define I2S_DEFAULT_CHANNELS 2
#define I2S_DEFAULT_BITS_PER_SAMPLE 16
#define I2S_DEFAULT_PORT 0
#define I2S_BUFFER_SIZE 1024
#define I2S_BUFFER_COUNT 6
#define A2DP_BUFFER_SIZE 512
#define A2DP_BUFFER_COUNT 5
#define DEFAUT_ADC_PIN 34
#define I2S_TAG "I2S"
/**
* @brief Platform specific Settings
*
*/
#ifdef ESP32
#define USE_ESP32_LOGGER
#define PIN_I2S_BCK 14
#define PIN_I2S_WS 15
#define PIN_I2S_DATA_IN 32
#define PIN_I2S_DATA_OUT 22
#define I2S_USE_APLL false
// Default Setting: The mute pin can be switched off by setting it to -1. Or you could drive the LED by assigning LED_BUILTIN
#define PIN_I2S_MUTE 23
#define SOFT_MUTE_VALUE LOW
#endif
#ifdef ESP8266
#define PIN_I2S_BCK -1
#define PIN_I2S_WS -1
#define PIN_I2S_DATA_IN -1
#define PIN_I2S_DATA_OUT -1
#define I2S_USE_APLL false
#define PIN_I2S_MUTE 23
#define SOFT_MUTE_VALUE LOW
#endif

98
src/AudioESP8266.h Normal file
View File

@ -0,0 +1,98 @@
#pragma once
#include "AudioTools.h"
#ifdef USE_ESP8266_AUDIO
#include "AudioOutput.h"
#include "SoundData.h"
namespace audio_tools {
/**
* @brief ESP8266Audio AudioOutput class which stores the data in a temporary buffer.
* The buffer can be consumed e.g. by a callback function by calling read();
* @author Phil Schatzmann
* @copyright GPLv3
*/
class AudioOutputWithCallback : public AudioOutput, public BufferedStream {
public:
// Default constructor
AudioOutputWithCallback(int bufferSize, int bufferCount ):BufferedStream(bufferSize) {
callback_buffer_ptr = new NBuffer<Channels>(bufferSize, bufferCount);
}
virtual ~AudioOutputWithCallback() {
delete callback_buffer_ptr;
}
/// Activates the output
virtual bool begin() {
active = true;
return true;
}
/// puts the sample into a buffer
virtual bool ConsumeSample(int16_t sample[2]) {
Channels c;
c.channel1 = sample[0];
c.channel2 = sample[1];
return callback_buffer_ptr->write(c);
};
/// stops the processing
virtual bool stop() {
active = false;
return true;
};
/// Provides the data from the internal buffer to the callback
size_t read(Channels *src, size_t len){
return active ? this->callback_buffer_ptr->readArray(src, len) : 0;
}
protected:
NBuffer<Channels> *callback_buffer_ptr;
bool active;
virtual size_t writeExt(const uint8_t* data, size_t len) {
return callback_buffer_ptr->writeArray((Channels*)data, len/sizeof(Channels));
}
virtual size_t readExt( uint8_t *data, size_t len) {
return callback_buffer_ptr->readArray((Channels*)data, len/sizeof(Channels));;
}
};
// /**
// * @brief ESP8266Audio Output to Stream
// *
// */
// class SerialOutputStream : public AudioOutput {
// public:
// SerialOutputStream(const Stream &out){
// this->out = &out;
// }
// virtual bool begin() {
// active = true;
// }
// virtual bool ConsumeSample(int16_t sample[2]){
// if (active) out->write((uint8_t*)sample, 2*sizeof(int16_t));
// }
// virtual bool stop() {
// active = false;
// }
// protected:
// Stream *out;
// bool active;
// };
}
#endif

View File

@ -7,13 +7,14 @@
* @copyright GPLv3
*
*/
#include "AudioConfig.h"
#include "AudioTools/AudioTypes.h"
#include "AudioTools/Buffers.h"
#include "AudioTools/Converter.h"
#include "AudioTools/MusicalNotes.h"
#include "AudioTools/SoundGenerator.h"
#include "AudioTools/I2S.h"
#include "AudioTools/ADC.h"
#include "AudioTools/AudioI2S.h"
#include "AudioTools/AnalogAudio.h"
#include "AudioTools/AudioLogger.h"
#include "AudioTools/Buffers.h"
#include "AudioTools/Streams.h"

View File

@ -2,13 +2,16 @@
#ifdef ESP32
#include "esp_a2dp_api.h"
#pragma once
#include "AudioConfig.h"
#if(defined ESP32)
#include "driver/i2s.h"
#include "freertos/queue.h"
#include "driver/adc.h"
#ifndef DEFAUT_ADC_PIN
#define DEFAUT_ADC_PIN 34
#include "esp_a2dp_api.h"
//#include "freertos/queue.h"
#endif
#if(defined ESP8266)
#include "i2s.h"
#define I2S_NUM_0 0
#endif
namespace audio_tools {
@ -17,30 +20,46 @@ typedef int16_t arrayOf2int16_t[2];
const char* ADC_TAG = "ADC";
/**
* @brief ESP32 specific configuration for i2s input via adc. The default input pin is GPIO34.
* @brief ESP32 specific configuration for i2s input via adc. The default input pin is GPIO34. We always use int16_t values on 2 channels
*
* @author Phil Schatzmann
* @copyright GPLv3
*
*
*/
class ADCConfig {
class AnalogConfig {
public:
// allow ADC to access the protected methods
friend class ADC;
friend class AnalogAudio;
// public config parameters
RxTxMode mode;
int sample_rate = 44100;
int dma_buf_count = 10;
int dma_buf_len = 512;
int dma_buf_count = I2S_BUFFER_COUNT;
int dma_buf_len = I2S_BUFFER_SIZE;
bool use_apll = false;
int mode_internal;
AnalogConfig() {
this->mode = RX_MODE;
setPin(DEFAUT_ADC_PIN);
mode_internal = (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN);
}
/// Default constructor
ADCConfig() {
setPin(DEFAUT_ADC_PIN);
AnalogConfig(RxTxMode mode) {
this->mode = mode;
if (mode == RX_MODE) {
setPin(DEFAUT_ADC_PIN);
mode_internal = (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN);
} else {
mode_internal = (I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN);
}
}
/// Copy constructor
ADCConfig(const ADCConfig &cfg) = default;
AnalogConfig(const AnalogConfig &cfg) = default;
/// provides the current ADC pin
int pin() {
@ -98,32 +117,37 @@ class ADCConfig {
/**
* @brief A very fast Analog to Digital Converter which is using the ESP32 I2S interface
* @brief A very fast ADC and DAC using the ESP32 I2S interface.
* @author Phil Schatzmann
* @copyright GPLv3
*/
class ADC {
class AnalogAudio {
friend class AnalogAudioStream;
public:
/// Default constructor
ADC() {
AnalogAudio() {
}
/// Destructor
~ADC() {
stop();
~AnalogAudio() {
end();
}
/// Provides the default configuration
ADCConfig defaultConfig() {
AnalogConfig defaultConfig(RxTxMode mode) {
ESP_LOGD(ADC_TAG, "%s", __func__);
ADCConfig config;
AnalogConfig config(mode);
return config;
}
/// starts the DAC
void begin(ADCConfig cfg) {
void begin(AnalogConfig cfg) {
ESP_LOGI(ADC_TAG, "%s", __func__);
adc_config = cfg;
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
.mode = (i2s_mode_t) cfg.mode_internal,
.sample_rate = cfg.sample_rate,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
@ -146,42 +170,66 @@ class ADC {
ESP_LOGE(ADC_TAG, "%s - %s", __func__, "i2s_zero_dma_buffer");
}
//init ADC pad
if (i2s_set_adc_mode(cfg.unit, cfg.channel)!=ESP_OK) {
ESP_LOGE(ADC_TAG, "%s - %s", __func__, "i2s_driver_install");
}
switch (cfg.mode) {
case RX_MODE:
//init ADC pad
if (i2s_set_adc_mode(cfg.unit, cfg.channel)!=ESP_OK) {
ESP_LOGE(ADC_TAG, "%s - %s", __func__, "i2s_driver_install");
}
// enable the ADC
if (i2s_adc_enable(i2s_num)!=ESP_OK) {
ESP_LOGE(ADC_TAG, "%s - %s", __func__, "i2s_adc_enable");
// enable the ADC
if (i2s_adc_enable(i2s_num)!=ESP_OK) {
ESP_LOGE(ADC_TAG, "%s - %s", __func__, "i2s_adc_enable");
}
break;
case TX_MODE:
i2s_set_pin(i2s_num, NULL);
break;
}
ESP_LOGI(ADC_TAG, "%s - %s", __func__,"end");
}
/// stops the I2C and unistalls the driver
void stop(){
void end(){
ESP_LOGD(ADC_TAG, "%s", __func__);
i2s_driver_uninstall(i2s_num);
}
/// Reads data from I2S
size_t read(int16_t (*src)[2], size_t sizeFrames, TickType_t ticks_to_wait=portMAX_DELAY){
size_t len = readBytes(src, sizeFrames * sizeof(int16_t)*2, ticks_to_wait); // 2 bytes * 2 channels
size_t read(int16_t (*src)[2], size_t sizeFrames){
size_t len = readBytes(src, sizeFrames * sizeof(int16_t)*2); // 2 bytes * 2 channels
size_t result = len / (sizeof(int16_t) * 2);
ESP_LOGD(ADC_TAG, "%s - len: %d -> %d", __func__,sizeFrames, result);
return result;
}
AnalogConfig &config() {
return adc_config;
}
protected:
const i2s_port_t i2s_num =(i2s_port_t) 0; // Analog input only supports 0!
const i2s_port_t i2s_num = I2S_NUM_0; // Analog input only supports 0!
AnalogConfig adc_config;
size_t readBytes(void *dest, size_t size_bytes, TickType_t ticks_to_wait=portMAX_DELAY){
size_t result = 0;
if (i2s_read(i2s_num, dest, size_bytes, &result, ticks_to_wait)!=ESP_OK){
ESP_LOGE(ADC_TAGƒ, "%s", __func__);
/// writes the data to the I2S interface
size_t writeBytes(const void *src, size_t size_bytes){
size_t result = 0;
if (i2s_write(i2s_num, src, size_bytes, &result, portMAX_DELAY)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s", __func__);
}
return result;
}
size_t readBytes(void *dest, size_t size_bytes){
size_t result = 0;
if (i2s_read(i2s_num, dest, size_bytes, &result, portMAX_DELAY)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s", __func__);
}
ESP_LOGD(ADC_TAG, "%s - len: %d -> %d", __func__, size_bytes, result);
return result;
}
};
}

50
src/AudioTools/AudioI2S.h Normal file
View File

@ -0,0 +1,50 @@
#pragma once
#include "AudioConfig.h"
#include "AudioTypes.h"
#include "I2S_ESP32.h"
#include "I2S_ESP8266.h"
#include "I2S_SAMD.h"
namespace audio_tools {
/**
* @brief A Simple I2S interface class for the ESP32 which supports the reading and writing with a defined data type
* @author Phil Schatzmann
* @copyright GPLv3
*/
template<typename T>
class I2S : public I2SBase {
public:
/// Default Constructor
I2S() {
}
/// Destructor
~I2S() {
end();
}
void begin(I2SConfig cfg) {
// define bits per sampe from data type
cfg.bits_per_sample = sizeof(T) * 8;
I2SBase::begin(cfg);
}
/// writes the data to the I2S interface
size_t write(T (*src)[2], size_t frameCount){
return writeBytes(src, frameCount * sizeof(T) * 2); // 2 bytes * 2 channels
}
/// Reads data from I2S
size_t read(T (*src)[2], size_t frameCount){
size_t len = readBytes(src, frameCount * sizeof(T) * 2); // 2 bytes * 2 channels
size_t result = len / (sizeof(T) * 2);
return result;
}
};
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "AudioConfig.h"
#include "Stream.h"
#ifndef SOUND_LOG_LEVEL
@ -50,59 +51,66 @@ class AudioLogger {
}
/// logs an error
void error(const char *str, const char* str1=nullptr, const char* str2=nullptr) const {
log(Error, str, str1, str2);
void error(const char *str) const {
printf(Error,"%s\n", str);
}
/// logs an info message
void info(const char *str, const char* str1=nullptr, const char* str2=nullptr) const {
log(Info, str, str1, str2);
void info(const char *str) const {
printf(Info,"%s\n", str);
}
/// logs an warning
void warning(const char *str, const char* str1=nullptr, const char* str2=nullptr) const {
log(Warning, str, str1, str2);
void warning(const char *str) const {
printf(Warning,"%s\n", str);
}
/// writes an debug message
void debug(const char *str, const char* str1=nullptr, const char* str2=nullptr) const {
log(Debug, str, str1, str2);
void debug(const char *str) const {
printf(Debug,"%s\n", str);
}
/// printf support
int printf(LogLevel current_level, const char* fmt, ...) const {
char serial_printf_buffer[PRINTF_BUFFER_SIZE] = {0};
int len = 0;
va_list args;
#ifdef USE_ESP32_LOGGER
va_start(args,fmt);
len = vsnprintf(serial_printf_buffer, PRINTF_BUFFER_SIZE, fmt, args);
va_end(args);
Serial.println(serial_printf_buffer);
// TODO why is the following not working
switch(current_level){
case Error:
esp_log_write(ESP_LOG_ERROR, TAG, (const char*)serial_printf_buffer);
break;
case Info:
esp_log_write(ESP_LOG_INFO, TAG, (const char*)serial_printf_buffer);
break;
case Warning:
esp_log_write(ESP_LOG_WARN,TAG, (const char*)serial_printf_buffer);
break;
case Debug:
esp_log_write(ESP_LOG_DEBUG, TAG, (const char*)serial_printf_buffer);
break;
default:
esp_log_write(ESP_LOG_INFO, TAG, (const char*)serial_printf_buffer);
break;
}
#else
if (this->active && log_stream_ptr!=nullptr && current_level >= log_level){
char serial_printf_buffer[PRINTF_BUFFER_SIZE] = {0};
va_list args;
va_start(args,fmt);
len = vsnprintf(serial_printf_buffer,PRINTF_BUFFER_SIZE, fmt, args);
log_stream_ptr->print(serial_printf_buffer);
va_end(args);
}
#endif
return len;
}
/// write an message to the log
void log(LogLevel current_level, const char *str, const char* str1=nullptr, const char* str2=nullptr) const {
if (this->active && log_stream_ptr!=nullptr){
if (current_level >= log_level){
log_stream_ptr->print((char*)str);
if (str1!=nullptr){
log_stream_ptr->print(" ");
log_stream_ptr->print(str1);
}
if (str2!=nullptr){
log_stream_ptr->print(" ");
log_stream_ptr->print(str2);
}
log_stream_ptr->println();
log_stream_ptr->flush();
}
}
}
/// provides the singleton instance
static AudioLogger &instance(){
static AudioLogger *ptr;
@ -114,15 +122,14 @@ class AudioLogger {
protected:
Stream *log_stream_ptr;
LogLevel log_level;
bool active;
Stream *log_stream_ptr = nullptr;
const char* TAG = "AudioTools";
LogLevel log_level = Error;
bool active = false;
AudioLogger(){
}
};
}

View File

@ -1,5 +1,7 @@
#pragma once
#include "AudioConfig.h"
namespace audio_tools {
#define INT24_MAX 0x7FFFFF
@ -10,7 +12,7 @@ namespace audio_tools {
* @copyright GPLv3
*
*/
class int24_t {
class int24_t : public Printable {
public:
@ -21,7 +23,10 @@ class int24_t {
}
int24_t(void *ptr){
int24_t(static_cast<uint8_t*>(ptr));
uint8_t *ptrChar = static_cast<uint8_t*>(ptr);
value[0]=ptrChar[0];
value[1]=ptrChar[1];
value[2]=ptrChar[2];
}
int24_t(uint8_t *ptr){
@ -30,19 +35,19 @@ class int24_t {
value[2]=ptr[2];
}
int24_t(int16_t in){
int24_t(const int16_t &in) {
value[2] = in > 0 ? 0 : 0xFF;
value[1] = (in >> 8) & 0xFF;
value[0] = in & 0xFF;
}
int24_t(int32_t in){
int24_t(const int32_t &in){
value[2] = (in >> 16) & 0xFF;;
value[1] = (in >> 8) & 0xFF;
value[0] = in & 0xFF;
}
operator int32_t() {
operator int32_t() const {
uint8_t tmp[4];
tmp[0] = value[0];
tmp[1] = value[1];
@ -52,25 +57,31 @@ class int24_t {
return *ptr;
}
operator float() {
return static_cast<int32_t>(*this);
operator float() const {
return static_cast<float>(*this);
}
/// provides value between -32767 and 32767
int16_t scale16() {
int16_t scale16() const {
return static_cast<float>(*this) * INT16_MAX / INT24_MAX ;
}
/// provides value between -2,147,483,647 and 2,147,483,647
int32_t scale32() {
int32_t scale32() const {
return static_cast<float>(*this) / static_cast<float>(INT24_MAX) * static_cast<float>(INT32_MAX) ;
}
/// provides value between -1.0 and 1.0
float scaleFloat() {
float scaleFloat() const {
return static_cast<float>(*this) / INT24_MAX ;
}
virtual size_t printTo(Print& p) const {
// just print as int32_t
const int32_t v = *this;
return p.print(v);
}
private:
uint8_t value[3];
};
@ -94,7 +105,62 @@ class AudioBaseInfoDependent {
public:
virtual ~AudioBaseInfoDependent(){}
virtual void setAudioBaseInfo(AudioBaseInfo info) {};
virtual bool validate(AudioBaseInfo &info){
return true;
}
};
enum RxTxMode { TX_MODE, RX_MODE };
enum I2SMode {
I2S_PHILIPS_MODE,
I2S_RIGHT_JUSTIFIED_MODE,
I2S_LEFT_JUSTIFIED_MODE
};
/**
* @brief configuration for all common i2s settings
* @author Phil Schatzmann
* @copyright GPLv3
*/
class I2SConfig {
public:
I2SConfig() {
}
/// Constructor
I2SConfig(RxTxMode mode) {
this->rx_tx_mode = mode;
pin_data = rx_tx_mode == TX_MODE ? PIN_I2S_DATA_OUT : PIN_I2S_DATA_IN;
}
/// Default Copy Constructor
I2SConfig(const I2SConfig &cfg) = default;
/// public settings
RxTxMode rx_tx_mode = TX_MODE;
int port_no = 0; // processor dependent port
int channels = I2S_DEFAULT_CHANNELS;
int sample_rate = I2S_DEFAULT_SAMPLE_RATE;
int bits_per_sample = I2S_DEFAULT_BITS_PER_SAMPLE;
int pin_ws = PIN_I2S_WS;
int pin_bck = PIN_I2S_BCK;
int pin_data = PIN_I2S_DATA_OUT;
I2SMode i2s_mode = I2S_PHILIPS_MODE;
bool is_digital = true;
};
/// stops any further processing by spinning in an endless loop
void stop() {
while(true){
delay(1000);
}
}
}

View File

@ -24,6 +24,17 @@ public:
return lenResult;
}
int writeArray(T data[], int len){
int result = 0;
for (int j=0;j<len;j++){
result = j;
if (write(data[j])==0){
break;
}
}
return result;
}
// reads multiple values for array of 2 dimensional frames
int readFrames(T data[][2], int len) {
int result = min(len, available());
@ -48,7 +59,6 @@ public:
return lenResult;
}
// peeks the actual entry from the buffer
virtual T peek() = 0;

View File

@ -1,6 +1,6 @@
#pragma once
#include "AudioTypes.h"
#include "BluetoothA2DPSource.h"
#include "Vector.h"
namespace audio_tools {
@ -23,19 +23,20 @@ static int16_t convertFrom32To16(int32_t value) {
/**
* @brief Abstract Base class for Filters
* A filter is processing the data in the indicated array
* @brief Abstract Base class for Converters
* A converter is processing the data in the indicated array
* @author Phil Schatzmann
* @copyright GPLv3
* @tparam T
*/
template<typename T>
class BaseFilter {
class BaseConverter {
public:
virtual void process(T (*src)[2], size_t size) = 0;
virtual void convert(T (*src)[2], size_t size) = 0;
};
/**
* @brief Multiplies the values with the indicated factor adds the offset and clips at maxValue. To mute use a factor of 0.0!
* @author Phil Schatzmann
@ -44,15 +45,15 @@ class BaseFilter {
* @tparam T
*/
template<typename T>
class FilterScaler : public BaseFilter<T> {
class ConverterScaler : public BaseConverter<T> {
public:
FilterScaler(float factor, T offset, T maxValue){
ConverterScaler(float factor, T offset, T maxValue){
this->factor = factor;
this->maxValue = maxValue;
this->offset = offset;
}
void process(T (*src)[2], size_t size) {
void convert(T (*src)[2], size_t size) {
for (size_t j=0;j<size;j++){
src[j][0] = (src[j][0] + offset) * factor;
if (src[j][0]>maxValue){
@ -74,6 +75,53 @@ class FilterScaler : public BaseFilter<T> {
T offset;
};
/**
* @brief Makes sure that the avg of the signal is set to 0
*
* @tparam T
*/
template<typename T>
class ConverterAutoCenter : public BaseConverter<T> {
public:
ConverterAutoCenter(){
}
void convert(T (*src)[2], size_t size) {
setup(src, size);
if (is_setup){
for (size_t j=0; j<size; j++){
src[j][0] = src[j][0] - offset;
src[j][1] = src[j][1] - offset;
}
}
}
protected:
T offset;
float left;
float right;
bool is_setup = false;
void setup(T (*src)[2], size_t size){
if (!is_setup) {
for (size_t j=0;j<size;j++){
left += src[j][0];
right += src[j][1];
}
left = left / size;
right = right / size;
if (left>0){
offset = left;
is_setup = true;
} else if (right>0){
offset = right;
is_setup = true;
}
}
}
};
/**
* @brief Switches the left and right channel
* @author Phil Schatzmann
@ -82,11 +130,11 @@ class FilterScaler : public BaseFilter<T> {
* @tparam T
*/
template<typename T>
class FilterSwitchLeftAndRight : public BaseFilter<T> {
class ConverterSwitchLeftAndRight : public BaseConverter<T> {
public:
FilterSwitchLeftAndRight(){
ConverterSwitchLeftAndRight(){
}
void process(T (*src)[2], size_t size) {
void convert(T (*src)[2], size_t size) {
for (size_t j=0;j<size;j++){
src[j][1] = src[j][0];
src[j][0] = src[j][1];
@ -102,11 +150,11 @@ class FilterSwitchLeftAndRight : public BaseFilter<T> {
* @tparam T
*/
template<typename T>
class FilterFillLeftAndRight : public BaseFilter<T> {
class ConverterFillLeftAndRight : public BaseConverter<T> {
public:
FilterFillLeftAndRight(){
ConverterFillLeftAndRight(){
}
void process(T (*src)[2], size_t size) {
void convert(T (*src)[2], size_t size) {
setup(src, size);
if (left_empty && !right_empty){
for (size_t j=0;j<size;j++){
@ -156,12 +204,12 @@ class FilterFillLeftAndRight : public BaseFilter<T> {
* @tparam T
*/
template<typename T>
class FilterToInternalDACFormat : public BaseFilter<T> {
class ConverterToInternalDACFormat : public BaseConverter<T> {
public:
FilterToInternalDACFormat(){
ConverterToInternalDACFormat(){
}
void process(T (*src)[2], size_t size) {
void convert(T (*src)[2], size_t size) {
for (int i=0; i<size; i++) {
src[i][0] = src[i][0] + 0x8000;
src[i][1] = src[i][1] + 0x8000;
@ -169,8 +217,33 @@ class FilterToInternalDACFormat : public BaseFilter<T> {
}
};
/**
* @brief Combines multiple converters
*
* @tparam T
*/
template<typename T>
class MultiConverter : public BaseConverter<T> {
public:
MultiConverter(){
}
// adds a converter
void add(BaseConverter<T> &converter){
converters.push_back(converter);
}
// The data is provided as int24_t tgt[][2] but returned as int24_t
void convert(T (*src)[2], size_t size) {
for(int i=0; i < converters.size(); i++){
converters[i].convert(src);
}
}
private:
Vector<T> converters;
};
/**
* @brief Converts e.g. 24bit data to the indicated bigger data type
@ -180,9 +253,9 @@ class FilterToInternalDACFormat : public BaseFilter<T> {
* @tparam T
*/
template<typename FromType, typename ToType>
class Converter {
class CallbackConverter {
public:
Converter(ToType (*converter)(FromType v)){
CallbackConverter(ToType (*converter)(FromType v)){
this->convert_ptr = converter;
}
@ -200,37 +273,5 @@ class Converter {
};
// static int16[2][] toArray(Channel *c){
// return (static_cast<(int16[2])*>(c));
// }
/**
* @brief Covnerts the data from T src[][2] to a Channels array
* @author Phil Schatzmann
* @copyright GPLv3
*
* @tparam T
*/
template<typename T>
class ChannelConverter {
public:
ChannelConverter( int16_t (*convert_ptr)(T from)){
this->convert_ptr = convert_ptr;
}
// The data is provided as int24_t tgt[][2] but returned as int24_t
void convert(T src[][2], Channels* channels, size_t size) {
for (int i=size; i>0; i--) {
channels[i].channel1 = (*convert_ptr)(src[i][0]);
channels[i].channel2 = (*convert_ptr)(src[i][1]);
}
}
private:
int16_t (*convert_ptr)(T from);
};
}

View File

@ -1,225 +0,0 @@
#pragma once
#ifdef ESP32
#include "esp_a2dp_api.h"
#include "driver/i2s.h"
#include "freertos/queue.h"
#include "AudioTypes.h"
namespace audio_tools {
enum I2SMode { TX_MODE, RX_MODE };
const char* I2S_TAG = "I2S";
/**
* @brief ESP32 specific configuration for all i2s settings
* @author Phil Schatzmann
* @copyright GPLv3
*/
template<typename T>
class I2SConfig {
public:
i2s_port_t port_no = I2S_NUM_0;
i2s_config_t i2s;
i2s_pin_config_t pin;
int channels = 2;
I2SConfig() {
i2s = defaultConfig(TX_MODE);
pin = defaultPinConfig(TX_MODE);
}
/// Default Constructor
I2SConfig(I2SMode mode) {
i2s = defaultConfig(mode);
pin = defaultPinConfig(mode);
}
/// Copy constructor
I2SConfig(const I2SConfig<T> &cfg) {
port_no = cfg.port_no;
i2s = cfg.i2s;
pin = cfg.pin;
}
protected:
i2s_config_t defaultConfig(I2SMode mode) {
ESP_LOGD(I2S_TAG, "%s", __func__);
i2s_config_t config = {
.mode = (i2s_mode_t) ((mode == TX_MODE) ? (I2S_MODE_MASTER | I2S_MODE_TX) : (I2S_MODE_MASTER | I2S_MODE_RX)) ,
.sample_rate = 44100,
.bits_per_sample = (i2s_bits_per_sample_t) (sizeof(T) * 8),
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = static_cast<i2s_comm_format_t>(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // default interrupt priority
.dma_buf_count = 8,
.dma_buf_len = 1024,
.use_apll = false,
};
return config;
}
i2s_pin_config_t defaultPinConfig(I2SMode mode = TX_MODE) {
ESP_LOGD(I2S_TAG, "%s - mode: %s", __func__, mode==TX_MODE ? "TX" : "RX");
i2s_pin_config_t config = {
.bck_io_num = 14,
.ws_io_num = 15,
.data_out_num = mode == TX_MODE ? 22 : I2S_PIN_NO_CHANGE,
.data_in_num = mode == RX_MODE ? 32 : I2S_PIN_NO_CHANGE
};
return config;
}
};
/**
* @brief A Simple I2S interface class.
* @author Phil Schatzmann
* @copyright GPLv3
*/
template<typename T>
class I2S : public AudioBaseInfoDependent {
friend class I2SStream;
public:
/// Default Constructor
I2S() {
}
/// Destructor
~I2S() {
stop();
}
/// Provides the default configuration
I2SConfig<T> defaultConfig(I2SMode mode) {
ESP_LOGD(I2S_TAG, "%s", __func__);
I2SConfig<T> c(mode);
return c;
}
/// starts the DAC
void begin(I2SConfig<T> cfg) {
ESP_LOGD(I2S_TAG, "%s", __func__);
this->cfg = cfg;
this->i2s_num = cfg.port_no;
// We make sure that we can reconfigure
if (is_started) {
stop();
ESP_LOGD(I2S_TAG, "%s", "I2S restarting");
}
ESP_LOGD(I2S_TAG, "sample rate: %d", cfg.i2s.sample_rate);
ESP_LOGD(I2S_TAG, "bits per sample: %d", cfg.i2s.bits_per_sample);
ESP_LOGD(I2S_TAG, "number of channels: %d", cfg.channels);
// setup config
if (i2s_driver_install(i2s_num, &cfg.i2s, 0, NULL)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s - %s", __func__, "i2s_driver_install");
}
// setup pin config
if (this->cfg.i2s.mode & I2S_MODE_DAC_BUILT_IN ) {
ESP_LOGD(I2S_TAG, "Using built in DAC");
//for internal DAC, this will enable both of the internal channels
i2s_set_pin(i2s_num, NULL);
} else {
if (i2s_set_pin(i2s_num, &cfg.pin)!= ESP_OK){
ESP_LOGD(I2S_TAG, "pin bck_io_num: %d", cfg.pin.bck_io_num);
ESP_LOGD(I2S_TAG, "pin ws_io_num: %d", cfg.pin.ws_io_num);
ESP_LOGD(I2S_TAG, "pin data_out_num: %d", cfg.pin.data_out_num);
ESP_LOGD(I2S_TAG, "pin data_in_num: %d", cfg.pin.data_in_num);
ESP_LOGE(I2S_TAG, "%s - %s", __func__, "i2s_set_pin");
}
}
// clear initial buffer
i2s_zero_dma_buffer(i2s_num);
is_started = true;
}
/// stops the I2C and unistalls the driver
void stop(){
ESP_LOGD(I2S_TAG, "%s", __func__);
i2s_driver_uninstall(i2s_num);
is_started = false;
}
/// writes the data to the I2S interface
size_t write(T (*src)[2], size_t sizeFrames, TickType_t ticks_to_wait=portMAX_DELAY){
ESP_LOGD(I2S_TAG, "%s", __func__);
return writeBytes(src, sizeFrames * sizeof(T) * 2, ticks_to_wait); // 2 bytes * 2 channels
}
/// Reads data from I2S
size_t read(T (*src)[2], size_t sizeFrames, TickType_t ticks_to_wait=portMAX_DELAY){
size_t len = readBytes(src, sizeFrames * sizeof(T) * 2, ticks_to_wait); // 2 bytes * 2 channels
size_t result = len / (sizeof(T) * 2);
ESP_LOGD(I2S_TAG, "%s - len: %d -> %d", __func__,sizeFrames, result);
return result;
}
/// provides the actual configuration
I2SConfig<T> config() {
return cfg;
}
/// updates the sample rate dynamically
virtual void setAudioBaseInfo(AudioBaseInfo info) {
bool is_update = false;
if (cfg.i2s.sample_rate != info.sample_rate
|| cfg.i2s.bits_per_sample != info.bits_per_sample) {
cfg.i2s.sample_rate = info.sample_rate;
cfg.i2s.bits_per_sample = static_cast<i2s_bits_per_sample_t>(info.bits_per_sample);
is_update = true;
}
if (cfg.channels != info.channels){
if (info.channels==2){
cfg.i2s.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
} else if (info.channels==1){
cfg.i2s.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT;
}
cfg.channels = info.channels;
is_update = true;
}
if (is_update){
// restart
begin(config());
}
}
protected:
I2SConfig<T> cfg;
i2s_port_t i2s_num;
bool is_started = false;
/// writes the data to the I2S interface
size_t writeBytes(const void *src, size_t size_bytes, TickType_t ticks_to_wait=portMAX_DELAY){
size_t result = 0;
if (i2s_write(i2s_num, src, size_bytes, &result, ticks_to_wait)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s", __func__);
}
return result;
}
size_t readBytes(void *dest, size_t size_bytes, TickType_t ticks_to_wait=portMAX_DELAY){
size_t result = 0;
if (i2s_read(i2s_num, dest, size_bytes, &result, ticks_to_wait)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s", __func__);
}
return result;
}
};
}
#endif

148
src/AudioTools/I2S_ESP32.h Normal file
View File

@ -0,0 +1,148 @@
#pragma once
#ifdef ESP32
#include "driver/i2s.h"
#include "esp_a2dp_api.h"
namespace audio_tools {
/**
* @brief Basic I2S API - for the ESP32
*
*/
class I2SBase {
friend class I2SStream;
public:
/// Provides the default configuration
I2SConfig defaultConfig(RxTxMode mode) {
ESP_LOGD(I2S_TAG, "%s", __func__);
I2SConfig c(mode);
return c;
}
/// starts the DAC
void begin(I2SConfig cfg) {
ESP_LOGD(I2S_TAG, "%s", __func__);
this->cfg = cfg;
this->i2s_num = (i2s_port_t) cfg.port_no;
setChannels(cfg.channels);
i2s_config_t i2s_config_new = {
.mode = (i2s_mode_t) ((cfg.rx_tx_mode == TX_MODE) ? (I2S_MODE_MASTER | I2S_MODE_TX) : (I2S_MODE_MASTER | I2S_MODE_RX)) ,
.sample_rate = cfg.sample_rate,
.bits_per_sample = (i2s_bits_per_sample_t) cfg.bits_per_sample,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = static_cast<i2s_comm_format_t>(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // default interrupt priority
.dma_buf_count = I2S_BUFFER_COUNT,
.dma_buf_len = I2S_BUFFER_SIZE,
.use_apll = I2S_USE_APLL,
};
i2s_config = i2s_config_new;
logConfig();
// We make sure that we can reconfigure
if (is_started) {
end();
ESP_LOGD(I2S_TAG, "%s", "I2S restarting");
}
// setup config
if (i2s_driver_install(i2s_num, &i2s_config, 0, NULL)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s - %s", __func__, "i2s_driver_install");
}
// setup pin config
if (this->cfg.is_digital ) {
i2s_pin_config_t pin_config = {
.bck_io_num = cfg.pin_bck,
.ws_io_num = cfg.pin_ws,
.data_out_num = cfg.rx_tx_mode == TX_MODE ? cfg.pin_data : I2S_PIN_NO_CHANGE,
.data_in_num = cfg.rx_tx_mode == RX_MODE ? cfg.pin_data : I2S_PIN_NO_CHANGE
};
logConfigPins(pin_config);
if (i2s_set_pin(i2s_num, &pin_config)!= ESP_OK){
ESP_LOGE(I2S_TAG, "%s - %s", __func__, "i2s_set_pin");
}
} else {
ESP_LOGD(I2S_TAG, "Using built in DAC");
//for internal DAC, this will enable both of the internal channels
i2s_set_pin(i2s_num, NULL);
}
// clear initial buffer
i2s_zero_dma_buffer(i2s_num);
is_started = true;
ESP_LOGD(I2S_TAG, "%s - %s", __func__, "started");
}
/// stops the I2C and unistalls the driver
void end(){
ESP_LOGD(I2S_TAG, "%s", __func__);
i2s_driver_uninstall(i2s_num);
is_started = false;
}
/// provides the actual configuration
I2SConfig config() {
return cfg;
}
protected:
I2SConfig cfg;
i2s_port_t i2s_num;
i2s_config_t i2s_config;
bool is_started = false;
// update the cfg.i2s.channel_format based on the number of channels
void setChannels(int channels){
if (channels==2){
i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
} else if (channels==1){
i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT;
}
cfg.channels = channels;
}
/// writes the data to the I2S interface
size_t writeBytes(const void *src, size_t size_bytes){
size_t result = 0;
if (i2s_write(i2s_num, src, size_bytes, &result, portMAX_DELAY)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s", __func__);
}
return result;
}
size_t readBytes(void *dest, size_t size_bytes){
size_t result = 0;
if (i2s_read(i2s_num, dest, size_bytes, &result, portMAX_DELAY)!=ESP_OK){
ESP_LOGE(I2S_TAG, "%s", __func__);
}
return result;
}
void logConfig() {
ESP_LOGD(I2S_TAG, "mode: %s", cfg.rx_tx_mode == TX_MODE ? "TX":"RX");
ESP_LOGD(I2S_TAG, "sample rate: %d", cfg.sample_rate);
ESP_LOGD(I2S_TAG, "bits per sample: %d", cfg.bits_per_sample);
ESP_LOGD(I2S_TAG, "number of channels: %d", cfg.channels);
}
void logConfigPins(i2s_pin_config_t pin_config){
ESP_LOGD(I2S_TAG, "pin bck_io_num: %d", cfg.pin_bck);
ESP_LOGD(I2S_TAG, "pin ws_io_num: %d", cfg.pin_ws);
ESP_LOGD(I2S_TAG, "pin data_num: %d", cfg.pin_data);
}
};
}
#endif

View File

@ -0,0 +1,78 @@
#pragma once
#ifdef ESP8266
#include "AudioTools/AudioLogger.h"
#include "i2s.h"
namespace audio_tools {
/**
* @brief Basic I2S API - for the ESP8266
* Only 16 bits are supported !
*
*/
class I2SBase {
friend class I2SStream;
public:
/// Provides the default configuration
I2SConfig defaultConfig(RxTxMode mode) {
I2SConfig c(mode);
return c;
}
/// starts the DAC
void begin(I2SConfig cfg) {
i2s_set_rate(cfg.sample_rate);
cfg.bits_per_sample = 16;
AudioLogger logger = AudioLogger::instance();
if(!i2s_rxtx_begin(cfg.rx_tx_mode == RX_MODE, cfg.rx_tx_mode == TX_MODE)){
logger.error("i2s_rxtx_begin failed");
}
}
/// stops the I2C and unistalls the driver
void end(){
i2s_end();
}
/// provides the actual configuration
I2SConfig config() {
return cfg;
}
protected:
I2SConfig cfg;
/// writes the data to the I2S interface
size_t writeBytes(const void *src, size_t size_bytes){
size_t frame_size = cfg.channels * (cfg.bits_per_sample/8);
uint16_t frame_count = size_bytes / frame_size;
return i2s_write_buffer(( int16_t *)src, frame_count ) * frame_size;
}
/// reads the data from the I2S interface
size_t readBytes(void *dest, size_t size_bytes){
size_t result_bytes = 0;
uint16_t frame_size = cfg.channels * (cfg.bits_per_sample/8);
size_t frames = size_bytes / frame_size;
int16_t *ptr = (int16_t *)dest;
for (int j=0;j<frames;j++){
int16_t *left = ptr;
int16_t *right = ptr+1;
bool ok = i2s_read_sample(left, right, false); // RX data returned in both 16-bit outputs.
if(!ok){
break;
}
ptr += 2;
result_bytes += frame_size;
}
return result_bytes;
}
};
}
#endif

57
src/AudioTools/I2S_SAMD.h Normal file
View File

@ -0,0 +1,57 @@
#pragma once
#ifdef __SAMD21G18A__
namespace audio_tools {
/**
* @brief Basic I2S API - for the SAMD
*
*/
class I2SBase {
friend class I2SStream;
public:
/// Provides the default configuration
I2SConfig defaultConfig(RxTxMode mode) {
I2SConfig c(mode);
return c;
}
/// starts the DAC
void begin(I2SConfig cfg) {
I2S.begin(cfg.i2s_mode, cfg.sample_rate, csg.bits_per_sample, true);
if (cfg.mode = TX_MODE){
I2S.enableTransmitter();
} else {
I2S.enableReceiver();
}
}
/// stops the I2C and unistalls the driver
void end(){
I2S.end();
}
/// provides the actual configuration
I2SConfig config() {
return cfg;
}
size_t writeBytes(const void *src, size_t size_bytes){
return I2S.write((const uint8_t *)src, size_bytes);
}
size_t readBytes(void *dest, size_t size_bytes){
return I2S.read(src, size_bytes);
}
protected:
I2SConfig cfg;
};
#endif

View File

@ -0,0 +1,53 @@
#pragma once
#ifdef SPECIFIC_PROCESSOR_ARCHITECTURE
namespace audio_tools {
/**
* @brief Basic I2S API - for the ...
*
*/
class I2SBase {
friend class I2SStream;
public:
/// Provides the default configuration
I2SConfig defaultConfig(RxTxMode mode) {
I2SConfig c(mode);
return c;
}
/// starts the DAC
void begin(I2SConfig cfg) {
}
/// stops the I2C and unistalls the driver
void end(){
}
/// provides the actual configuration
I2SConfig config() {
return cfg;
}
protected:
I2SConfig cfg;
/// writes the data to the I2S interface
size_t writeBytes(const void *src, size_t size_bytes){
size_t result = 0;
return result;
}
size_t readBytes(void *dest, size_t size_bytes){
size_t result = 0;
return result;
}
};
#endif

View File

@ -1,5 +1,7 @@
#pragma once
#include "AudioLogger.h"
namespace audio_tools {
/**
@ -11,19 +13,78 @@ namespace audio_tools {
*/
template <class T>
class SoundGenerator {
virtual size_t readBytes(uint8_t *buffer, size_t bytes) = 0;
virtual size_t readSamples(T* data, size_t size)=0;
virtual size_t readSamples(T src[][2], size_t size) {
T tmp[size];
int len = readSamples(tmp, size);
for (int j=0;j<len;j++) {
T value = tmp[j];
src[j][1] = src[j][0] = value;
public:
/// Provides the samples into simple array - which represents 1 channel
virtual size_t readSamples(T* data, size_t sampleCount=512){
for (int j=0;j<sampleCount;j++){
data[j] = readSample();
}
return sampleCount;
}
return len;
}
/// Provides the samples into a 2 channel array
virtual size_t readSamples(T src[][2], size_t frameCount) {
T tmp[frameCount];
int len = readSamples(tmp, frameCount);
for (int j=0;j<len;j++) {
T value = tmp[j];
src[j][1] = src[j][0] = value;
}
return frameCount;
}
/// Provides a single sample
virtual T readSample() = 0;
/// Provides the data as byte array
virtual size_t readBytes( uint8_t *buffer, size_t lengthBytes, uint8_t channels=1){
size_t result = 0;
int frame_size = sizeof(T) * channels;
if (active){
switch (channels){
case 1:
result = readSamples((T*) buffer, lengthBytes / frame_size) ;
break;
case 2:
result = readSamples((T(*)[2]) buffer, lengthBytes / frame_size);
break;
default:
logger().printf(AudioLogger::Error, "SoundGenerator::readBytes -> number of channels %d is not supported (use 1 or 2)\n", channels);
result = 0;
break;
}
} else {
logger().debug("SoundGenerator::readBytes -> inactive");
// when inactive did not generate anything
result = 0;
}
logger().printf(AudioLogger::Debug, "SoundGenerator::readBytes (channels: %d) %d -> %d\n",channels, lengthBytes,result);
return result * frame_size;
}
void begin() {
logger().info("SoundGenerator::begin");
active = true;
}
/// ends the processing
void end(){
logger().info("SoundGenerator::end");
active = false;
}
/// provides the logger
AudioLogger &logger() {
return AudioLogger::instance();
}
bool isActive() {
return active;
}
protected:
bool active = false;
};
@ -37,38 +98,50 @@ template <class T>
class SineWaveGenerator : public SoundGenerator<T> {
public:
// the scale defines the max value which is generated
SineWaveGenerator(double scale=1.0) {
this->scale = scale;
SineWaveGenerator(float amplitude = 32767.0, float phase = 0) {
m_amplitude = amplitude;
m_phase = phase;
}
/// Starts the processing by defining the sample rate and frequency
void begin(uint16_t sample_rate=44100, uint16_t frequency=0){
this->frequency = frequency;
this->sample_rate = sample_rate;
SoundGenerator<T>::logger().info("SineWaveGenerator::begin");
this->m_frequency = frequency;
this->m_sample_rate = sample_rate;
this->m_deltaTime = 1.0 / m_sample_rate;
SoundGenerator<T>::active = true;
logStatus();
}
/// Defines the frequency - after the processing has been started
void setFrequency(uint16_t frequency) {
this->frequency = frequency;
}
virtual size_t readBytes(uint8_t *buffer, size_t length){
return readSamples((T*)buffer, length*sizeof(T)) * sizeof(T);
/// Provides a single sample
virtual T readSample() {
float angle = double_Pi * m_frequency * m_time + m_phase;
T result = m_amplitude * sin(angle);
m_time += m_deltaTime;
return result;
}
virtual size_t readSamples(T* data, size_t len=512){
for (int j=0;j<len;j++){
data[j] = readSample();;
}
return len;
}
protected:
uint64_t sample_step;
uint16_t frequency;
double scale;
uint64_t sample_rate;
uint16_t m_sample_rate;
T readSample() {
return sin(2.0 * PI * sample_step++ * frequency / sample_rate) * (double)scale;
float m_frequency = 0;
float m_time = 0.0;
float m_amplitude = 1.0;
float m_deltaTime = 1.0 / m_sample_rate;
float m_phase = 0.0;
float double_Pi = PI * 2.0;
void logStatus() {
AudioLogger &log = SoundGenerator<T>::logger();
log.printf(AudioLogger::Info, "frequency: %f", this->m_frequency );
log.printf(AudioLogger::Info, "sample rate: %u", this->m_sample_rate );
log.printf(AudioLogger::Info, "amplitude: %f", this->m_amplitude );
log.printf(AudioLogger::Info, "active: %s", SoundGenerator<T>::active ? "true" : "false" );
}
};
@ -88,27 +161,14 @@ class NoiseGenerator : public SoundGenerator<T> {
this->scale = scale;
}
void begin(){
}
virtual size_t readBytes(uint8_t *buffer, size_t length){
return readSamples((T*)buffer, length*sizeof(T)) * sizeof(T);
}
virtual size_t readSamples(T* data, size_t len=512){
for (int j=0;j<len;j++){
data[j] = readSample();
}
return len;
}
protected:
double scale;
/// Provides a single sample
T readSample() {
return ((rand() % (static_cast<T>(2 * scale)) - scale)); // generate number between -scale / scale
}
protected:
double scale;
};
}

View File

@ -1,10 +1,17 @@
#pragma once
#include "Arduino.h"
#include "AudioConfig.h"
#include "AudioTypes.h"
#include "Buffers.h"
#include "AudioI2S.h"
#ifdef ESP32
#include <esp_http_client.h>
#include "i2s.h"
#endif
namespace audio_tools {
const AudioLogger &log = AudioLogger::instance();
/**
* @brief A simple Stream implementation which is backed by allocated memory
@ -42,21 +49,43 @@ class MemoryStream : public Stream {
return result;
}
virtual size_t write(const uint8_t *buffer, size_t size){
size_t result = 0;
for (int j=0;j<size;j++){
if(!write(buffer[j])){
break;
}
result = j;
}
}
virtual int available() {
return write_pos - read_pos;
}
virtual int read() {
int result = peek();
if (result>0){
if (result>=0){
read_pos++;
}
return result;
}
size_t readBytes(char *buffer, size_t length){
size_t count = 0;
while (count < length) {
int c = read();
if (c < 0) break;
*buffer++ = (char)c;
count++;
}
return count;
}
virtual int peek() {
int result = -1;
if (available()>0 && read_pos < write_pos){
if (available()>0){
result = buffer[read_pos];
}
return result;
@ -74,16 +103,85 @@ class MemoryStream : public Stream {
}
}
protected:
int write_pos;
int read_pos;
int buffer_size;
uint8_t *buffer;
bool owns_buffer;
};
/**
* @brief Source for reading generated tones. Please note
* - that the output is for one channel only!
* - we do not support reading of individual characters!
* - we do not support any write operations
* @param generator
*/
template <class T>
class GeneratedSoundStream : public Stream {
public:
GeneratedSoundStream(SoundGenerator<T> &generator, uint8_t channels=2){
this->generator_ptr = &generator;
this->channels = channels;
}
/// unsupported operations
virtual size_t write(uint8_t) {
return not_supported();
};
/// unsupported operations
virtual int availableForWrite() {
return not_supported();
}
/// unsupported operations
virtual size_t write(const uint8_t *buffer, size_t size) {
return not_supported();
}
/// unsupported operations
virtual int read() {
return -1;
}
/// unsupported operations
virtual int peek() {
return -1;
}
/// This is unbounded so we just return the buffer size
virtual int available() {
return DEFAULT_BUFFER_SIZE;
}
/// privide the data as byte stream
size_t readBytes( char *buffer, size_t length) {
return generator_ptr->readBytes((uint8_t*)buffer, length, channels);
}
/// start the processing
void begin() {
generator_ptr->begin();
}
/// stop the processing
void stop() {
generator_ptr->stop();
}
void flush(){
}
protected:
SoundGenerator<T> *generator_ptr;
uint8_t channels;
int not_supported() {
log.error("GeneratedSoundStream-unsupported operation!");
return 0;
}
};
/**
@ -93,7 +191,7 @@ class MemoryStream : public Stream {
*/
class StreamCopy {
public:
StreamCopy(Stream &to, Stream &from, int buffer_size){
StreamCopy(Print &to, Stream &from, int buffer_size=DEFAULT_BUFFER_SIZE){
this->from = &from;
this->to = &to;
this->buffer_size = buffer_size;
@ -104,16 +202,31 @@ class StreamCopy {
delete[] buffer;
}
/// copies the available bytes from the input stream to the ouptut stream
/// copies a buffer length of data - repeat this call until everthing is copied
size_t copy() {
size_t total_bytes = available();
size_t result = total_bytes;
while (total_bytes>0){
size_t bytes_to_read = min(total_bytes,static_cast<size_t>(buffer_size) );
from->readBytes(buffer, bytes_to_read);
to->write(buffer, bytes_to_read);
total_bytes -= bytes_to_read;
}
log.printf(AudioLogger::Info, "StreamCopy::copy");
size_t result = available();
if (result>0){
size_t bytes_to_read = min(result,static_cast<size_t>(buffer_size) );
result = from->readBytes(buffer, bytes_to_read);
to->write(buffer, result);
}
log.printf(AudioLogger::Info, "StreamCopy::copy %d bytes\n", result);
return result;
}
/// copies a buffer length of data and applies the converter
template<typename T>
size_t copy(BaseConverter<T> &converter) {
size_t result = available();
BaseConverter<T> *coverter_ptr = &converter;
if (result>0){
size_t bytes_to_read = min(result, static_cast<size_t>(buffer_size) );
result = from->readBytes(buffer, bytes_to_read);
// convert to pointer to array of 2
coverter_ptr->convert((T(*)[2])buffer, result / (sizeof(T)*2) );
to->write(buffer, result);
}
return result;
}
@ -123,15 +236,298 @@ class StreamCopy {
protected:
Stream *from;
Stream *to;
Print *to;
uint8_t *buffer;
int buffer_size;
};
/**
* @brief Typed Stream Copy which supports the conversion from channel to 2 channels. We make sure that we
* allways copy full samples
* @tparam T
*/
template <class T>
class StreamCopyT {
public:
StreamCopyT(Print &to, Stream &from, int buffer_size=DEFAULT_BUFFER_SIZE){
this->from = &from;
this->to = &to;
this->buffer_size = buffer_size;
buffer = new uint8_t[buffer_size];
}
~StreamCopyT(){
delete[] buffer;
}
// copies the data from one channel from the source to 2 channels on the destination
size_t copy(){
size_t result = available();
if (result>0){
size_t bytes_to_read = min(result, static_cast<size_t>(buffer_size));
size_t samples = bytes_to_read / sizeof(T);
size_t bytes = samples * sizeof(T);
from->readBytes(buffer, bytes);
result = to->write(buffer, bytes);
}
log.printf(AudioLogger::Info, "StreamCopyT::copy %d bytes\n", result);
return result;
}
// copies the data from one channel from the source to 2 channels on the destination
size_t copy2(){
size_t result = available();
size_t bytes_read;
log.printf(AudioLogger::Info, "StreamCopyT::copy2 %u\n", result);
if (result>0){
size_t bytes_to_read = min(result, static_cast<size_t>(buffer_size / 2));
size_t frames = bytes_to_read / sizeof(T);
T temp_data[frames];
bytes_read = from->readBytes((uint8_t*)temp_data, frames * sizeof(T));
T* bufferT = (T*) buffer;
for (int j=0;j<frames;j++){
*bufferT = temp_data[j];
bufferT++;
*bufferT = temp_data[j];
bufferT++;
}
result = to->write(buffer, frames * sizeof(T)*2);
}
log.printf(AudioLogger::Info, "StreamCopyT::copy2 %u -> %u bytes / available %u\n", bytes_read, result, available());
return result;
}
/// available bytes in the data source
int available() {
return from->available();
}
protected:
Stream *from;
Print *to;
uint8_t *buffer;
int buffer_size;
};
/**
* @brief The Arduino Stream supports operations on single characters. This is usually not the best way to push audio information, but we
* will support it anyway - by using a buffer. On reads: if the buffer is empty it gets refilled - for writes
* if it is full it gets flushed.
*
*/
class BufferedStream : public Stream {
public:
BufferedStream(size_t buffer_size){
buffer = new SingleBuffer<uint8_t>(buffer_size);
}
~BufferedStream() {
delete [] buffer;
}
/// writes a byte to the buffer
virtual size_t write(uint8_t c) {
if (buffer->isFull()){
flush();
}
return buffer->write(c);
}
/// Use this method: write an array
virtual size_t write(const uint8_t* data, size_t len) {
flush();
return writeExt(data, len);
}
/// empties the buffer
virtual void flush() {
// just dump the memory of the buffer and clear it
if (buffer->available()>0){
writeExt(buffer->address(), buffer->available());
buffer->reset();
}
}
/// reads a byte - to be avoided
virtual int read() {
if (buffer->isEmpty()){
refill();
}
return buffer->read();
}
/// peeks a byte - to be avoided
virtual int peek() {
if (buffer->isEmpty()){
refill();
}
return buffer->peek();
};
/// Use this method !!
size_t readBytes( uint8_t *data, size_t length) {
if (buffer->isEmpty()){
return readExt(data, length);
} else {
return buffer->readArray(data, length);
}
}
// refills the buffer with data from i2s
void refill() {
size_t result = readExt(buffer->address(), buffer->size());
buffer->setAvailable(result);
}
/// Returns the available bytes in the buffer: to be avoided
virtual int available() {
if (buffer->isEmpty()){
refill();
}
return buffer->available();
}
protected:
SingleBuffer<uint8_t> *buffer;
virtual size_t writeExt(const uint8_t* data, size_t len) = 0;
virtual size_t readExt( uint8_t *data, size_t length) = 0;
};
/**
* @brief Stream Wrapper which can be used to print the values as readable ASCII to the screen to be analyzed in the Serial Plotter
* The frames are separated by a new line. The channels in one frame are separated by a ,
* @tparam T
*/
template<typename T>
class CsvStream : public BufferedStream, public AudioBaseInfoDependent {
public:
/// Constructor
CsvStream(Print &out, int channels, int buffer_size=DEFAULT_BUFFER_SIZE, bool active=true) : BufferedStream(buffer_size){
this->channels = channels;
this->out_ptr = &out;
this->active = active;
}
/// Sets the CsvStream as active
void begin(){
active = true;
}
/// Sets the CsvStream as inactive
void end() {
active = false;
}
protected:
T *data_ptr;
Print *out_ptr;
int channels;
bool active;
virtual size_t writeExt(const uint8_t* data, size_t len) {
if (!active) return 0;
size_t lenChannels = len / (sizeof(T)*channels);
data_ptr = (T*)data;
for (int j=0;j<lenChannels;j++){
for (int ch=0;ch<channels;ch++){
out_ptr->print(*data_ptr);
data_ptr++;
if (ch<channels-1) Serial.print(", ");
}
Serial.println();
}
return len;
}
virtual size_t readExt( uint8_t *data, size_t length) {
return 0;
}
};
/**
* @brief We support the Stream interface for the I2S access. In addition we allow a separate mute pin which might also be used
* to drive a LED...
*
* @tparam T
*/
class I2SStream : public BufferedStream, public AudioBaseInfoDependent {
public:
I2SStream(int mute_pin=PIN_I2S_MUTE) : BufferedStream(DEFAULT_BUFFER_SIZE){
this->mute_pin = mute_pin;
if (mute_pin>0) {
pinMode(mute_pin, OUTPUT);
mute(true);
}
}
/// Provides the default configuration
I2SConfig defaultConfig(RxTxMode mode) {
return i2s.defaultConfig(mode);
}
void begin(I2SConfig cfg) {
i2s.begin(cfg);
// unmute
mute(false);
}
void end() {
mute(true);
i2s.end();
}
/// updates the sample rate dynamically
virtual void setAudioBaseInfo(AudioBaseInfo info) {
bool is_update = false;
I2SConfig cfg = i2s.config();
if (cfg.sample_rate != info.sample_rate
|| cfg.channels != info.channels
|| cfg.bits_per_sample != info.bits_per_sample) {
cfg.sample_rate = info.sample_rate;
cfg.bits_per_sample = info.bits_per_sample;
cfg.channels = info.channels;
i2s.end();
i2s.begin(cfg);
}
}
protected:
I2SBase i2s;
int mute_pin;
/// set mute pin on or off
void mute(bool is_mute){
if (mute_pin>0) {
digitalWrite(mute_pin, is_mute ? SOFT_MUTE_VALUE : !SOFT_MUTE_VALUE );
}
}
virtual size_t writeExt(const uint8_t* data, size_t len) {
return i2s.writeBytes(data, len);
}
virtual size_t readExt( uint8_t *data, size_t length) {
return i2s.readBytes(data, length);
}
};
#ifdef ESP32
/**
* @brief Represents the content of a URL as Stream. We use the ESP32 ESP HTTP Client API
* @author Phil Schatzmann
@ -140,21 +536,20 @@ class StreamCopy {
*/
class UrlStream : public Stream {
public:
UrlStream(int readBufferSize=512){
UrlStream(int readBufferSize=DEFAULT_BUFFER_SIZE){
read_buffer = new uint8_t[readBufferSize];
}
~UrlStream(){
delete[] read_buffer;
esp_http_client_close(client);
esp_http_client_cleanup(client);
end();
}
int begin(const char* url) {
int result = -1;
config.url = url;
config.method = HTTP_METHOD_GET;
Logger.info("UrlStream.begin ",url);
Logger.printf(AudioLogger::Info, "UrlStream.begin %s\n",url);
// cleanup last begin if necessary
if (client==nullptr){
@ -189,16 +584,19 @@ class UrlStream : public Stream {
}
virtual int available() {
return size - read_pos;
return size - total_read;
}
virtual size_t readBytes(uint8_t *buffer, size_t length){
return esp_http_client_read(client, (char*)buffer, length);
size_t read = esp_http_client_read(client, (char*)buffer, length);
total_read+=read;
return read;
}
virtual int read() {
fillBuffer();
return isEOS() ? -1 : read_buffer[read_pos++];
total_read++;
return isEOS() ? -1 :read_buffer[read_pos++];
}
virtual int peek() {
@ -213,11 +611,17 @@ class UrlStream : public Stream {
Logger.error("UrlStream write - not supported");
}
void end(){
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
protected:
esp_http_client_handle_t client;
esp_http_client_config_t config;
long size;
long total_read;
// buffered read
uint8_t *read_buffer;
uint16_t read_buffer_size;
@ -225,9 +629,9 @@ class UrlStream : public Stream {
uint16_t read_size;
const AudioLogger &Logger = AudioLogger::instance();
inline void fillBuffer() {
if (isEOS()){
// if we consumed all bytes we refill the buffer
read_size = readBytes(read_buffer,read_buffer_size);
read_pos = 0;
}
@ -241,95 +645,51 @@ class UrlStream : public Stream {
/**
* @brief We support the Stream interface on I2S
* @brief We support the Stream interface for the ADC class
*
* @tparam T
*/
class I2SStream : public Stream, public AudioBaseInfoDependent {
class AnalogAudioStream : public BufferedStream, public AudioBaseInfoDependent {
public:
template<typename T>
I2SStream(I2S<T> &i2s){
this->i2s = (I2S<uint8_t>*) &i2s;
size_t buffer_size = this->i2s->config().i2s.dma_buf_len;
buffer = new SingleBuffer<uint8_t>(buffer_size);
}
public:
AnalogAudioStream() : BufferedStream(DEFAULT_BUFFER_SIZE){
}
~I2SStream() {
delete [] buffer;
}
/// Provides the default configuration
AnalogConfig defaultConfig(RxTxMode mode) {
return adc.defaultConfig(mode);
}
/// writes a byte to the buffer
virtual size_t write(uint8_t c) {
if (buffer->isFull()){
flush();
}
return buffer->write(c);
}
void begin(AnalogConfig cfg) {
adc.begin(cfg);
// unmute
mute(false);
}
/// Use this method: write an array
virtual size_t write(const uint8_t* data, size_t len) {
flush();
return i2s->writeBytes(data, len);
}
void end() {
mute(true);
adc.end();
}
/// empties the buffer
virtual void flush() {
// just dump the memory of the buffer and clear it
if (buffer->available()>0){
i2s->writeBytes(buffer->address(), buffer->available());
buffer->reset();
}
}
protected:
AnalogAudio adc;
int mute_pin;
/// reads a byte - to be avoided
virtual int read() {
if (buffer->isEmpty()){
refill();
}
return buffer->read();
}
/// set mute pin on or off
void mute(bool is_mute){
if (mute_pin>0) {
digitalWrite(mute_pin, is_mute ? SOFT_MUTE_VALUE : !SOFT_MUTE_VALUE );
}
}
/// peeks a byte - to be avoided
virtual int peek() {
if (buffer->isEmpty()){
refill();
}
return buffer->peek();
};
/// Use this method !!
size_t readBytes( uint8_t *data, size_t length) {
if (buffer->isEmpty()){
return i2s->readBytes(data, length);
} else {
return buffer->readArray(data, length);
}
}
virtual size_t writeExt(const uint8_t* data, size_t len) {
return adc.writeBytes(data, len);
}
/// Returns the available bytes in the buffer: to be avoided
virtual int available() {
if (buffer->isEmpty()){
refill();
}
return buffer->available();
}
/// updates the sample rate dynamically
virtual void setAudioBaseInfo(AudioBaseInfo info) {
i2s->setAudioBaseInfo(info);
}
protected:
I2S<uint8_t> *i2s;
SingleBuffer<uint8_t> *buffer;
// refills the buffer with data from i2s
void refill() {
size_t result = i2s->readBytes(buffer->address(), buffer->size());
buffer->setAvailable(result);
}
virtual size_t readExt( uint8_t *data, size_t length) {
return adc.readBytes(data, length);
}
};

290
src/AudioTools/Vector.h Normal file
View File

@ -0,0 +1,290 @@
#pragma once
namespace audio_tools {
/**
* @brief Vector implementation which provides the most important methods as defined by std::vector. This is neither part of
* Pico nor of the Arduino framwork but nevertheless it is quite handy to have and most of the times quite better then dealing
* with raw c arrays.
*
* @author Phil Schatzmann
* @copyright GPLv3
**/
template <class T>
class Vector {
public:
/**
* @brief Iterator for the Vector class
*
* by Phil Schatzmann
**/
class iterator {
protected:
T *ptr;
size_t pos_;
public:
inline iterator(){
}
inline iterator(T* parPtr, size_t pos){
this->ptr = parPtr;
this->pos_ = pos;
}
// copy constructor
inline iterator(const iterator &copyFrom){
ptr = copyFrom.ptr;
pos_ = copyFrom.pos_;
}
inline iterator operator++(int n) {
ptr++;
pos_++;
return *this;
}
inline iterator operator++() {
ptr++;
pos_++;
return *this;
}
inline iterator operator--(int n) {
ptr--;
pos_--;
return *this;
}
inline iterator operator--() {
ptr--;
pos_--;
return *this;
}
inline iterator operator+(int offset) {
pos_ += offset;
return iterator(ptr+offset, offset);
}
inline bool operator==(iterator it) {
return ptr == it.getPtr();
}
inline bool operator<(iterator it) {
return ptr < it.getPtr();
}
inline bool operator<=(iterator it) {
return ptr <= it.getPtr();
}
inline bool operator>(iterator it) {
return ptr > it.getPtr();
}
inline bool operator>=(iterator it) {
return ptr >= it.getPtr();
}
inline bool operator!=(iterator it) {
return ptr != it.getPtr();
}
inline T &operator*() {
return *ptr;
}
inline T *getPtr() {
return ptr;
}
inline size_t pos() {
return pos_;
}
inline size_t operator-(iterator it) {
return (ptr - it.getPtr());
}
};
// default constructor
inline Vector(int len = 20) {
resize_internal(len, false);
}
// allocate size and initialize array
inline Vector(int size, T value) {
resize(size);
for (int j=0;j< size;j++){
data[j] = value;
}
}
// copy constructor
inline Vector( Vector<T> &copyFrom) {
resize_internal(copyFrom.size(), false);
for (int j=0;j<copyFrom.size();j++){
data[j] = copyFrom[j];
}
this->len = copyFrom.size();
}
// legacy constructor with pointer range
inline Vector(T *from, T *to) {
this->len = to - from;
resize_internal(this->len, false);
for (size_t j=0;j<this->len;j++){
data[j] = from[j];
}
}
inline ~Vector() {
clear();
shrink_to_fit();
delete [] this->data;
}
inline void clear() {
len = 0;
}
inline int size() {
return len;
}
inline bool empty() {
return size()==0;
}
inline void push_back(T value){
resize_internal(len+1, true);
data[len] = value;
len++;
}
inline void pop_back(){
if (len>0) {
len--;
}
}
inline void assign(iterator v1, iterator v2) {
size_t newLen = v2 - v1;
resize_internal(newLen, false);
this->len = newLen;
int pos = 0;
for (auto ptr = v1; ptr != v2; ptr++) {
data[pos++] = *ptr;
}
}
inline void assign(size_t number, T value) {
resize_internal(number, false);
this->len = number;
for (int j=0;j<number;j++){
data[j]=value;
}
}
inline void swap(Vector<T> &in){
// save data
T *dataCpy = data;
int bufferLenCpy = bufferLen;
int lenCpy = len;
// swap this
data = in.data;
len = in.len;
bufferLen = in.bufferLen;
// swp in
in.data = dataCpy;
in.len = lenCpy;
in.bufferLen = bufferLenCpy;
}
inline T &operator[](int index) {
return data[index];
}
inline Vector<T> &operator=(Vector<T> &copyFrom) {
resize_internal(copyFrom.size(), false);
for (int j=0;j<copyFrom.size();j++){
data[j] = copyFrom[j];
}
this->len = copyFrom.size();
}
inline T &operator[] (const int index) const {
return data[index];
}
inline bool resize(int newSize, T value){
if (resize(newSize)){
for (int j=0;j<newSize;j++){
data[j]=value;
}
return true;
}
return false;
}
inline void shrink_to_fit() {
resize_internal(this->len, true, true);
}
int capacity(){
return this->bufferLen;
}
inline bool resize(int newSize){
int oldSize = this->len;
resize_internal(newSize, true);
this->len = newSize;
return this->len!=oldSize;
}
inline iterator begin(){
return iterator(data, 0);
}
inline T& back(){
return *iterator(data+(len-1), len-1);
}
inline iterator end(){
return iterator(data+len, len);
}
// removes a single element
inline void erase(iterator it) {
int pos = it.pos();
if (pos<len){
int lenToEnd = len - pos - 1;
// call destructor on data to be erased
data[pos].~T();
// shift values by 1 position
memmove((void*) &data[pos],(void*)(&data[pos+1]),lenToEnd*sizeof(T));
// make sure that we have a valid object at the end
data[len-1] = T();
len--;
}
}
protected:
int bufferLen;
int len = 0;
T *data = nullptr;
inline void resize_internal(int newSize, bool copy, bool shrink=false) {
bool withNewSize = false;
if (newSize>bufferLen || this->data==nullptr ||shrink){
withNewSize = true;
T* oldData = data;
int oldBufferLen = this->bufferLen;
this->data = new T[newSize+1];
this->bufferLen = newSize;
if (oldData != nullptr) {
if(copy && this->len > 0){
memcpy((void*)data,(void*) oldData, this->len*sizeof(T));
}
if (shrink){
cleanup(oldData, newSize, oldBufferLen);
}
delete [] oldData;
}
}
}
void cleanup(T*data, int from, int to){
for (int j=from;j<to;j++){
data[j].~T();
}
}
};
}

View File

@ -226,7 +226,7 @@ class WAVDecoder {
public:
WAVDecoder(Print &out_stream, AudioBaseInfoDependent &bi = AudioBaseInfoDependentNone){
this->out = &out_stream;
this->audioBaseInfoSupport = &bi;
this->audioBaseInfoSupport = bi;
}
void begin() {
@ -260,17 +260,19 @@ class WAVDecoder {
isValid = false;
} else {
// update sampling rate if the target supports it
if (audioBaseInfoSupport!=nullptr){
AudioBaseInfo bi;
bi.sample_rate = header.audioInfo().sample_rate;
bi.channels = header.audioInfo().channels;
bi.bits_per_sample = header.audioInfo().bits_per_sample;
AudioBaseInfo bi;
bi.sample_rate = header.audioInfo().sample_rate;
bi.channels = header.audioInfo().channels;
bi.bits_per_sample = header.audioInfo().bits_per_sample;
// we provide some functionality so that we could check if the destination supports the requested format
isValid = audioBaseInfoSupport.validate(bi);
WAVLogger.printf(AudioLogger::Error,"isValid: ", isValid ? "true":"false");
if (isValid){
audioBaseInfoSupport->setAudioBaseInfo(bi);
// write prm data from first record
WAVLogger.printf(AudioLogger::Info,"WAVDecoder writing first sound data");
out->write(sound_ptr, len);
}
// write prm data from first record
WAVLogger.printf(AudioLogger::Info,"WAVDecoder writing first sound data");
out->write(sound_ptr, len);
}
}
} else {
@ -281,7 +283,7 @@ class WAVDecoder {
protected:
WAVHeader header;
Print *out;
AudioBaseInfoDependent *audioBaseInfoSupport;
const AudioBaseInfoDependent &audioBaseInfoSupport;
bool isFirst = true;
bool isValid = true;

View File

@ -1,84 +0,0 @@
#pragma once
#include "AudioOutput.h"
#include "SoundData.h"
#include "AudioTools.h"
namespace audio_tools {
/**
* @brief ESP8266Audio AudioOutput class which stores the data in a temporary buffer.
* The buffer can be consumed e.g. by a callback function by calling read();
* @author Phil Schatzmann
* @copyright GPLv3
*/
class AudioOutputWithCallback : public AudioOutput {
public:
// Default constructor
AudioOutputWithCallback(int bufferSize, int bufferCount ){
buffer_ptr = new NBuffer<Channels>(bufferSize, bufferCount);
}
virtual ~AudioOutputWithCallback() {
delete buffer_ptr;
}
/// Activates the output
virtual bool begin() {
active = true;
return true;
}
/// puts the sample into a buffer
virtual bool ConsumeSample(int16_t sample[2]) {
Channels c;
c.channel1 = sample[0];
c.channel2 = sample[1];
return buffer_ptr->write(c);
};
/// stops the processing
virtual bool stop() {
active = false;
return true;
};
/// Provides the data from the internal buffer to the callback
size_t read(Channels *src, size_t len){
return active ? this->buffer_ptr->readArray(src, len) : 0;
}
protected:
NBuffer<Channels> *buffer_ptr;
bool active;
};
/**
* @brief ESP8266Audio Output to Stream
*
*/
class SerialOutputStream(): public AudioOutput {
public:
SerialOutputStream(Stream &out){
this->out = out;
}
virtual bool begin() {
active = start;
}
virtual bool ConsumeSample(int16_t sample[2]){
if (active) out.write((uint8_t*)sample, const_exptr(2*sizeof(int16_t)));
}
virtual bool stop() {
active = false;
}
protected:
const Stream &out;
bool active;
};
}