Documentation

This commit is contained in:
Phil Schatzmann 2021-06-27 22:23:31 +02:00
parent d8911c1126
commit 00437bc4fe
5 changed files with 106 additions and 44 deletions

View File

@ -4,7 +4,8 @@ Some basic __header-only C++ classes__ that can be used for __Audio Processing__
- 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
- a simple PWM class (to write audio data with the help of PWM)
- Additional Stream implementations: MemoryStream, URLStream, I2SStream, A2DPStream, PrintStream,
- Converters
- Musical Notes (with frequencies of notes)
- SineWaveGenerator (to generate a sine tone) and [Mozzi](https://sensorium.github.io/Mozzi/) for more complex scenario
@ -30,6 +31,7 @@ As “Audio Sinks” we will have e.g:
- external DAC [I2SStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_i2_s_stream.html)
- an Amplifier [AnalogAudioStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_analog_audio_stream.html)
- Earphones [PWMAudioStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_pwm_audio_stream.html)
- Bluetooth Speakers [A2DPStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_a2_d_p_stream.html)
- Serial to display the data as CSV [CsvStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_csv_stream.html)
- Any other Arduino Classes implementing Streams: SD, Ethernet etc
@ -69,6 +71,12 @@ void loop(){
```
A complete list of the supported Audio Stream classes and scenarios can be found in the [Scenarios Document](Scenarios.md)
### Sound Output
- __I2SStream__: The best quality can be achieved with the help of I2S and an external DAC. I2S is supporting 2 channels only.
- __AnalogAudioStream__: Some processors are providing an analog output, this is usually an easy and good approach: The number of pins (and herewith output channels) however is usually very limited.
- __PWMAudioStream__: The last possibility is to simulate an analog output with the help of PWM by using a frequency which is beyond the audible range of 20 KHz. This method is supported by all processor and usually supports a bigger number of output pins. In terms of audio quality this is usually the worst option.
## Examples

View File

@ -5,12 +5,12 @@
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 | | | | | + |
| Processor | I2S | ADC/DAC | A2DP | URLStream | PWM | Other |
|----------------|-----------|----------|--------|-----------|-------|--------|
| ESP32 | + | + | + | + | * | + |
| ESP8266 | * | * | | | | + |
| SAMD21 | * | | | | | + |
| Raspberry Pico | | | | | * | + |
+ supported
@ -24,6 +24,7 @@ Here are the related Stream classes with their supported operations that can be
| Class | Read | Write | Comments |
|-------------------------|------|-------|--------------------|
| I2SStream | + | + | i2s |
| PWMAduioStream | | + | pwm |
| AnalogAudioStream | + | + | adc, dac |
| MemoryStream | + | + | memory |
| URLStream | + | | url |

View File

@ -40,6 +40,8 @@
#define A2DP_BUFFER_SIZE 4096
#define A2DP_BUFFER_COUNT 8
#define DEFAUT_ADC_PIN 34
#define PWM_BUFFER_SIZE 1024
#define PWM_BUFFERS 4
/**
* -------------------------------------------------------------------------

View File

@ -5,15 +5,14 @@
#include "AudioTools/AudioLogger.h"
#include "AudioTools/Vector.h"
#include "Stream.h"
#define PWM_BUFFER_LENGTH 512
#include <math.h> /* pow */
namespace audio_tools {
enum PWMResolution {Res8,Res9,Res10,Res11};
void defaultAudioOutputCallback();
// forward declaration
class AudioPWM;
static AudioPWM *accessAudioPWM;
void defaultPWMAudioOutputCallback();
static AudioPWM *accessAudioPWM = nullptr;
/**
* @brief Configuration for PWM output
@ -25,6 +24,7 @@ static AudioPWM *accessAudioPWM;
* 11 | 2048 | 39.0625
*
* The default resolution is 8. The value must be between 8 and 11 and also drives the PWM frequency.
*
* @author Phil Schatzmann
* @copyright GPLv3
@ -36,7 +36,7 @@ struct PWMConfig {
int start_pin = 3;
int buffer_size = 1024 * 8;
int bits_per_sample = 16;
int resolution = 8; // must be between 8 and 11
int resolution = 8; // must be between 8 and 11 -> drives pwm frequency
} default_config;
/**
@ -50,17 +50,17 @@ struct PINInfo {
};
/**
* @brief Audio output to PWM pins for the ESP32.
* @brief Audio output to PWM pins for the ESP32. The ESP32 supports up to 16 channels.
* @author Phil Schatzmann
* @copyright GPLv3
*/
class AudioPWM : public Stream {
class PWMAudioStream : public Stream {
friend void defaultAudioOutputCallback();
public:
AudioPWM(){
PWMAudioStream(){
accessAudioPWM = this;
}
@ -73,15 +73,30 @@ class AudioPWM : public Stream {
}
// starts the processing
virtual void begin(PWMConfig config){
bool begin(PWMConfig config){
LOGD(__FUNCTION__);
this->audio_config = config;
LOGI("sample_rate: %d", audio_config.sample_rate);
LOGI("channels: %d", audio_config.channels);
LOGI("bits_per_sample: %d", audio_config.bits_per_sample);
LOGI("start_pin: %d", audio_config.start_pin);
LOGI("resolution: %d", audio_config.resolution);
// controller has max 16 independent channels
if (audio_config.channels>=16){
LOGE("Only max 16 channels are supported");
return false;
}
// check selected resolution
if (audio_config.resolution<8 || audio_config.resolution>11){
LOGE("The resolution must be between 8 and 11!");
return false;
}
setupPWM();
setupTimer();
return true;
}
// Ends the output
@ -91,7 +106,7 @@ class AudioPWM : public Stream {
for (int j=0;j<audio_config.channels;j++){
ledcDetachPin(pins[j].gpio);
}
has_data = false;
data_write_started = false;
}
// not supported
@ -125,21 +140,27 @@ class AudioPWM : public Stream {
virtual void flush() {
}
// blocking write for a single byte
virtual size_t write(uint8_t value) {
if (buffer.availableToWrite()>1){
buffer.write(value);
has_data = true;
setWriteStarted();
}
}
// blocking write for an array: we expect a singed value and convert it into a unsigned
virtual size_t write(const uint8_t *wrt_buffer, size_t size){
LOGI("write: %lu bytes", size)
while(availableForWrite()<size){
delay(5);
LOGI("Buffer is full - waiting...");
delay(10);
}
size_t result = buffer.writeArray(wrt_buffer, size);
has_data = true;
if (result!=size){
LOGW("Could not write all data: %d -> %d", size, result);
}
setWriteStarted();
return result;
}
@ -147,15 +168,24 @@ class AudioPWM : public Stream {
protected:
PWMConfig audio_config;
Vector<PINInfo> pins;
NBuffer<uint8_t> buffer = NBuffer<uint8_t>(DEFAULT_BUFFER_SIZE,4);
int buffer_idx = 0;
bool data_write_started = false;
hw_timer_t * timer = NULL;
NBuffer<uint8_t> buffer = NBuffer<uint8_t>(PWM_BUFFER_SIZE, PWM_BUFFERS);
hw_timer_t * timer = nullptr;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
bool has_data = false;
bool data_write_started = false;
/// when we get the first write -> we activate the timer to start with the output of data
void setWriteStarted(){
if (!data_write_started){
LOGI("timerAlarmEnable");
data_write_started = true;
timerAlarmEnable(timer);
}
}
/// Setup LED PWM
void setupPWM(){
LOGD(__FUNCTION__);
pins.resize(audio_config.channels);
for (int j=0;j<audio_config.channels;j++){
int pwmChannel = j;
@ -166,23 +196,25 @@ class AudioPWM : public Stream {
}
}
/// Setup ESP32 timer with callback
void setupTimer() {
LOGD(__FUNCTION__);
// Attach timer int at sample rate
timer = timerBegin(0, 1, true); // Timer at full 40Mhz, no prescaling
uint64_t counter = 40000000 / audio_config.sample_rate;
LOGI("timer counter is %lu", counter);
timerAttachInterrupt(timer, &defaultAudioOutputCallback, true);
timerAttachInterrupt(timer, &defaultPWMAudioOutputCallback, true);
timerAlarmWrite(timer, counter, true); // Timer fires at ~44100Hz [40Mhz / 907]
timerAlarmEnable(timer);
}
/// provides the max value for the configured resulution
int maxUnsignedValue(){
return maxUnsignedValue(audio_config.bits_per_sample);
return maxUnsignedValue(audio_config.resolution);
}
/// provides the max value for the indicated resulution
int maxUnsignedValue(int resolution){
return 2^resolution;
return pow(2,resolution);
}
/// determiens the PWM frequency based on the requested resolution
@ -198,10 +230,16 @@ class AudioPWM : public Stream {
/// writes the next frame to the output pins
void playNextFrame(){
int required = (audio_config.bits_per_sample / 8) * audio_config.channels;
if (has_data && buffer.available() >= required){
for (int j=0;j<audio_config.channels;j++){
ledcWrite(pins[j].pwm_channel, nextValue());
if (data_write_started){
int required = (audio_config.bits_per_sample / 8) * audio_config.channels;
if (buffer.available() >= required){
for (int j=0;j<audio_config.channels;j++){
int value = nextValue();
//Serial.println(value);
ledcWrite(pins[j].pwm_channel, value);
}
} else {
LOGW("playNextFrame - underflow");
}
}
}
@ -210,23 +248,35 @@ class AudioPWM : public Stream {
int nextValue() {
switch(audio_config.bits_per_sample ){
case 8: {
int8_t value;
buffer.readArray((uint8_t*)&value,1);
int value = buffer.read();
if (value<0){
LOGE("Could not read full data");
value = 0;
}
return map(value, -maxValue(8), maxValue(8), 0, maxUnsignedValue());
}
case 16: {
int16_t value;
buffer.readArray((uint8_t*)&value,2);
if (buffer.readArray((uint8_t*)&value,2)!=2){
LOGE("Could not read full data");
}
//Serial.print(value);
//Serial.print(" -> ");
//Serial.println(map(value, -maxValue(16), maxValue(16), 0, maxUnsignedValue()));
return map(value, -maxValue(16), maxValue(16), 0, maxUnsignedValue());
}
case 24: {
int24_t value;
buffer.readArray((uint8_t*)&value,3);
if (buffer.readArray((uint8_t*)&value,3)!=3){
LOGE("Could not read full data");
}
return map((int32_t)value, -maxValue(24), maxValue(24), 0, maxUnsignedValue());
}
case 32: {
int32_t value;
buffer.readArray((uint8_t*)&value,4);
if (buffer.readArray((uint8_t*)&value,4)!=4){
LOGE("Could not read full data");
}
return map(value, -maxValue(32), maxValue(32), 0, maxUnsignedValue());
}
}
@ -236,7 +286,8 @@ class AudioPWM : public Stream {
};
void IRAM_ATTR defaultAudioOutputCallback() {
/// timer callback: write the next frame to the pins
void IRAM_ATTR defaultPWMAudioOutputCallback() {
if (accessAudioPWM!=nullptr){
portENTER_CRITICAL_ISR(&(accessAudioPWM->timerMux));
accessAudioPWM->playNextFrame();

View File

@ -51,12 +51,12 @@ struct PWMConfig {
*/
template <class T>
class AudioPWM : public Stream {
class PWMAudioStream : public Stream {
friend bool defaultAudioOutputCallback(repeating_timer* ptr);
public:
AudioPWM(){
PWMAudioStream(){
T amplitude_in = getDefaultAmplitude();
audio_config.amplitude_in = amplitude_in;
default_config.amplitude_in = amplitude_in;
@ -157,7 +157,7 @@ class AudioPWM : public Stream {
protected:
PWMConfig audio_config;
Vector<PicoChannelOut> pins;
NBuffer<T> buffer = NBuffer<T>(DEFAULT_BUFFER_SIZE,4);
NBuffer<T> buffer = NBuffer<T>(PWM_BUFFER_SIZE, PWM_BUFFERS);
repeating_timer_t timer;
uint64_t underflow_count = 0;
bool data_write_started = false;