Skip to content

Commit 00437bc

Browse files
committed
Documentation
1 parent d8911c1 commit 00437bc

File tree

5 files changed

+106
-44
lines changed

5 files changed

+106
-44
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Some basic __header-only C++ classes__ that can be used for __Audio Processing__
44

55
- a simple I2S class (to read and write to the internal I2S)
66
- a simple ADC class (to read analog data with the help of I2S)
7-
- Additional Stream implementations: MemoryStream, URLStream, I2SStream, A2DPStream, PrintStream
7+
- a simple PWM class (to write audio data with the help of PWM)
8+
- Additional Stream implementations: MemoryStream, URLStream, I2SStream, A2DPStream, PrintStream,
89
- Converters
910
- Musical Notes (with frequencies of notes)
1011
- 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:
3031

3132
- external DAC – [I2SStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_i2_s_stream.html)
3233
- an Amplifier – [AnalogAudioStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_analog_audio_stream.html)
34+
- Earphones – [PWMAudioStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_pwm_audio_stream.html)
3335
- Bluetooth Speakers – [A2DPStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_a2_d_p_stream.html)
3436
- Serial to display the data as CSV – [CsvStream](https://pschatzmann.github.io/arduino-audio-tools/html/classaudio__tools_1_1_csv_stream.html)
3537
- Any other Arduino Classes implementing Streams: SD, Ethernet etc
@@ -69,6 +71,12 @@ void loop(){
6971
```
7072
A complete list of the supported Audio Stream classes and scenarios can be found in the [Scenarios Document](Scenarios.md)
7173

74+
### Sound Output
75+
76+
- __I2SStream__: The best quality can be achieved with the help of I2S and an external DAC. I2S is supporting 2 channels only.
77+
- __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.
78+
- __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.
79+
7280

7381
## Examples
7482

Scenarios.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
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:
66

77

8-
| Processor | I2SStream | ADCStream | A2DP | URLStream | Other |
9-
|----------------|-----------|-----------|--------|-----------|--------|
10-
| ESP32 | + | + | + | + | + |
11-
| ESP8266 | * | * | | | + |
12-
| SAMD21 | * | | | | + |
13-
| Raspberry Pico | | | | | + |
8+
| Processor | I2S | ADC/DAC | A2DP | URLStream | PWM | Other |
9+
|----------------|-----------|----------|--------|-----------|-------|--------|
10+
| ESP32 | + | + | + | + | * | + |
11+
| ESP8266 | * | * | | | | + |
12+
| SAMD21 | * | | | | | + |
13+
| Raspberry Pico | | | | | * | + |
1414

1515

1616
+ supported
@@ -24,6 +24,7 @@ Here are the related Stream classes with their supported operations that can be
2424
| Class | Read | Write | Comments |
2525
|-------------------------|------|-------|--------------------|
2626
| I2SStream | + | + | i2s |
27+
| PWMAduioStream | | + | pwm |
2728
| AnalogAudioStream | + | + | adc, dac |
2829
| MemoryStream | + | + | memory |
2930
| URLStream | + | | url |

src/AudioConfig.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
#define A2DP_BUFFER_SIZE 4096
4141
#define A2DP_BUFFER_COUNT 8
4242
#define DEFAUT_ADC_PIN 34
43+
#define PWM_BUFFER_SIZE 1024
44+
#define PWM_BUFFERS 4
4345

4446
/**
4547
* -------------------------------------------------------------------------

src/AudioPWM/PWMforESP32.h

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55
#include "AudioTools/AudioLogger.h"
66
#include "AudioTools/Vector.h"
77
#include "Stream.h"
8-
9-
#define PWM_BUFFER_LENGTH 512
8+
#include <math.h> /* pow */
109

1110
namespace audio_tools {
1211

13-
enum PWMResolution {Res8,Res9,Res10,Res11};
14-
void defaultAudioOutputCallback();
12+
// forward declaration
1513
class AudioPWM;
16-
static AudioPWM *accessAudioPWM;
14+
void defaultPWMAudioOutputCallback();
15+
static AudioPWM *accessAudioPWM = nullptr;
1716

1817
/**
1918
* @brief Configuration for PWM output
@@ -25,6 +24,7 @@ static AudioPWM *accessAudioPWM;
2524
* 11 | 2048 | 39.0625
2625
*
2726
* The default resolution is 8. The value must be between 8 and 11 and also drives the PWM frequency.
27+
*
2828
* @author Phil Schatzmann
2929
* @copyright GPLv3
3030
@@ -36,7 +36,7 @@ struct PWMConfig {
3636
int start_pin = 3;
3737
int buffer_size = 1024 * 8;
3838
int bits_per_sample = 16;
39-
int resolution = 8; // must be between 8 and 11
39+
int resolution = 8; // must be between 8 and 11 -> drives pwm frequency
4040
} default_config;
4141

4242
/**
@@ -50,17 +50,17 @@ struct PINInfo {
5050
};
5151

5252
/**
53-
* @brief Audio output to PWM pins for the ESP32.
53+
* @brief Audio output to PWM pins for the ESP32. The ESP32 supports up to 16 channels.
5454
* @author Phil Schatzmann
5555
* @copyright GPLv3
5656
*/
5757

58-
class AudioPWM : public Stream {
58+
class PWMAudioStream : public Stream {
5959
friend void defaultAudioOutputCallback();
6060

6161
public:
6262

63-
AudioPWM(){
63+
PWMAudioStream(){
6464
accessAudioPWM = this;
6565
}
6666

@@ -73,15 +73,30 @@ class AudioPWM : public Stream {
7373
}
7474

7575
// starts the processing
76-
virtual void begin(PWMConfig config){
76+
bool begin(PWMConfig config){
7777
LOGD(__FUNCTION__);
7878
this->audio_config = config;
7979
LOGI("sample_rate: %d", audio_config.sample_rate);
8080
LOGI("channels: %d", audio_config.channels);
81+
LOGI("bits_per_sample: %d", audio_config.bits_per_sample);
8182
LOGI("start_pin: %d", audio_config.start_pin);
83+
LOGI("resolution: %d", audio_config.resolution);
84+
85+
// controller has max 16 independent channels
86+
if (audio_config.channels>=16){
87+
LOGE("Only max 16 channels are supported");
88+
return false;
89+
}
90+
91+
// check selected resolution
92+
if (audio_config.resolution<8 || audio_config.resolution>11){
93+
LOGE("The resolution must be between 8 and 11!");
94+
return false;
95+
}
8296

8397
setupPWM();
8498
setupTimer();
99+
return true;
85100
}
86101

87102
// Ends the output
@@ -91,7 +106,7 @@ class AudioPWM : public Stream {
91106
for (int j=0;j<audio_config.channels;j++){
92107
ledcDetachPin(pins[j].gpio);
93108
}
94-
has_data = false;
109+
data_write_started = false;
95110
}
96111

97112
// not supported
@@ -125,37 +140,52 @@ class AudioPWM : public Stream {
125140
virtual void flush() {
126141
}
127142

143+
128144
// blocking write for a single byte
129145
virtual size_t write(uint8_t value) {
130146
if (buffer.availableToWrite()>1){
131147
buffer.write(value);
132-
has_data = true;
148+
setWriteStarted();
133149
}
134150
}
135151

136152
// blocking write for an array: we expect a singed value and convert it into a unsigned
137153
virtual size_t write(const uint8_t *wrt_buffer, size_t size){
154+
LOGI("write: %lu bytes", size)
138155
while(availableForWrite()<size){
139-
delay(5);
156+
LOGI("Buffer is full - waiting...");
157+
delay(10);
140158
}
141159
size_t result = buffer.writeArray(wrt_buffer, size);
142-
has_data = true;
160+
if (result!=size){
161+
LOGW("Could not write all data: %d -> %d", size, result);
162+
}
163+
setWriteStarted();
143164
return result;
144165
}
145166

146167

147168
protected:
148169
PWMConfig audio_config;
149170
Vector<PINInfo> pins;
150-
NBuffer<uint8_t> buffer = NBuffer<uint8_t>(DEFAULT_BUFFER_SIZE,4);
151-
int buffer_idx = 0;
152-
bool data_write_started = false;
153-
hw_timer_t * timer = NULL;
171+
NBuffer<uint8_t> buffer = NBuffer<uint8_t>(PWM_BUFFER_SIZE, PWM_BUFFERS);
172+
hw_timer_t * timer = nullptr;
154173
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
155-
bool has_data = false;
174+
bool data_write_started = false;
156175

176+
/// when we get the first write -> we activate the timer to start with the output of data
177+
void setWriteStarted(){
178+
if (!data_write_started){
179+
LOGI("timerAlarmEnable");
180+
data_write_started = true;
181+
timerAlarmEnable(timer);
182+
}
183+
}
157184

185+
/// Setup LED PWM
158186
void setupPWM(){
187+
LOGD(__FUNCTION__);
188+
159189
pins.resize(audio_config.channels);
160190
for (int j=0;j<audio_config.channels;j++){
161191
int pwmChannel = j;
@@ -166,23 +196,25 @@ class AudioPWM : public Stream {
166196
}
167197
}
168198

199+
/// Setup ESP32 timer with callback
169200
void setupTimer() {
201+
LOGD(__FUNCTION__);
170202
// Attach timer int at sample rate
171203
timer = timerBegin(0, 1, true); // Timer at full 40Mhz, no prescaling
172204
uint64_t counter = 40000000 / audio_config.sample_rate;
173205
LOGI("timer counter is %lu", counter);
174-
timerAttachInterrupt(timer, &defaultAudioOutputCallback, true);
206+
timerAttachInterrupt(timer, &defaultPWMAudioOutputCallback, true);
175207
timerAlarmWrite(timer, counter, true); // Timer fires at ~44100Hz [40Mhz / 907]
176-
timerAlarmEnable(timer);
177208
}
178209

210+
/// provides the max value for the configured resulution
179211
int maxUnsignedValue(){
180-
return maxUnsignedValue(audio_config.bits_per_sample);
212+
return maxUnsignedValue(audio_config.resolution);
181213
}
182-
183214

215+
/// provides the max value for the indicated resulution
184216
int maxUnsignedValue(int resolution){
185-
return 2^resolution;
217+
return pow(2,resolution);
186218
}
187219

188220
/// determiens the PWM frequency based on the requested resolution
@@ -198,10 +230,16 @@ class AudioPWM : public Stream {
198230

199231
/// writes the next frame to the output pins
200232
void playNextFrame(){
201-
int required = (audio_config.bits_per_sample / 8) * audio_config.channels;
202-
if (has_data && buffer.available() >= required){
203-
for (int j=0;j<audio_config.channels;j++){
204-
ledcWrite(pins[j].pwm_channel, nextValue());
233+
if (data_write_started){
234+
int required = (audio_config.bits_per_sample / 8) * audio_config.channels;
235+
if (buffer.available() >= required){
236+
for (int j=0;j<audio_config.channels;j++){
237+
int value = nextValue();
238+
//Serial.println(value);
239+
ledcWrite(pins[j].pwm_channel, value);
240+
}
241+
} else {
242+
LOGW("playNextFrame - underflow");
205243
}
206244
}
207245
}
@@ -210,23 +248,35 @@ class AudioPWM : public Stream {
210248
int nextValue() {
211249
switch(audio_config.bits_per_sample ){
212250
case 8: {
213-
int8_t value;
214-
buffer.readArray((uint8_t*)&value,1);
251+
int value = buffer.read();
252+
if (value<0){
253+
LOGE("Could not read full data");
254+
value = 0;
255+
}
215256
return map(value, -maxValue(8), maxValue(8), 0, maxUnsignedValue());
216257
}
217258
case 16: {
218259
int16_t value;
219-
buffer.readArray((uint8_t*)&value,2);
260+
if (buffer.readArray((uint8_t*)&value,2)!=2){
261+
LOGE("Could not read full data");
262+
}
263+
//Serial.print(value);
264+
//Serial.print(" -> ");
265+
//Serial.println(map(value, -maxValue(16), maxValue(16), 0, maxUnsignedValue()));
220266
return map(value, -maxValue(16), maxValue(16), 0, maxUnsignedValue());
221267
}
222268
case 24: {
223269
int24_t value;
224-
buffer.readArray((uint8_t*)&value,3);
270+
if (buffer.readArray((uint8_t*)&value,3)!=3){
271+
LOGE("Could not read full data");
272+
}
225273
return map((int32_t)value, -maxValue(24), maxValue(24), 0, maxUnsignedValue());
226274
}
227275
case 32: {
228276
int32_t value;
229-
buffer.readArray((uint8_t*)&value,4);
277+
if (buffer.readArray((uint8_t*)&value,4)!=4){
278+
LOGE("Could not read full data");
279+
}
230280
return map(value, -maxValue(32), maxValue(32), 0, maxUnsignedValue());
231281
}
232282
}
@@ -236,7 +286,8 @@ class AudioPWM : public Stream {
236286

237287
};
238288

239-
void IRAM_ATTR defaultAudioOutputCallback() {
289+
/// timer callback: write the next frame to the pins
290+
void IRAM_ATTR defaultPWMAudioOutputCallback() {
240291
if (accessAudioPWM!=nullptr){
241292
portENTER_CRITICAL_ISR(&(accessAudioPWM->timerMux));
242293
accessAudioPWM->playNextFrame();

src/AudioPWM/PWMforPico.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ struct PWMConfig {
5151
*/
5252

5353
template <class T>
54-
class AudioPWM : public Stream {
54+
class PWMAudioStream : public Stream {
5555
friend bool defaultAudioOutputCallback(repeating_timer* ptr);
5656

5757
public:
5858

59-
AudioPWM(){
59+
PWMAudioStream(){
6060
T amplitude_in = getDefaultAmplitude();
6161
audio_config.amplitude_in = amplitude_in;
6262
default_config.amplitude_in = amplitude_in;
@@ -157,7 +157,7 @@ class AudioPWM : public Stream {
157157
protected:
158158
PWMConfig audio_config;
159159
Vector<PicoChannelOut> pins;
160-
NBuffer<T> buffer = NBuffer<T>(DEFAULT_BUFFER_SIZE,4);
160+
NBuffer<T> buffer = NBuffer<T>(PWM_BUFFER_SIZE, PWM_BUFFERS);
161161
repeating_timer_t timer;
162162
uint64_t underflow_count = 0;
163163
bool data_write_started = false;

0 commit comments

Comments
 (0)