Repository navigation
Communication Serial
A serial (UART) connection is the simplest way to send audio from one microcontroller to another: connect TX of the sender with RX of the receiver (and GND). Every HardwareSerial is an Arduino Stream, so it can be used directly with StreamCopy. The same applies to any other link that is a Stream, e.g. RS485, a Bluetooth serial connection or a radio module in transparent mode. See Communication for the general considerations.
From send-receive.ino: sender and receiver run on the same ESP32, with RX connected to TX of Serial2:
#include "AudioTools.h"
AudioInfo info(22000, 1, 16);
SineGenerator<int16_t> sineWave(32000);
GeneratedSoundStream<int16_t> sound(sineWave);
I2SStream out;
auto &serial = Serial2;
Throttle throttle(serial);
StreamCopy copierOut(throttle, sound, 256); // sound -> serial
StreamCopy copierIn(out, serial, 256); // serial -> I2S
void setup() {
Serial2.begin(3000000, SERIAL_8N1);
sineWave.begin(info, N_B4);
throttle.begin(info);
auto config = out.defaultConfig(TX_MODE);
config.copyFrom(info);
out.begin(config);
}
void loop() {
copierOut.copy();
copierIn.copy();
}The generator provides the data as fast as possible: the Throttle slows it down to the audio rate, so that the receiver is not overrun.
Baud rate: with 8N1 each byte needs 10 bits: 22000 Hz, mono, 16 bit needs 440 kbit/s, 44100 Hz stereo 16 bit needs 1.76 Mbit/s. The ESP32 supports up to about 5 Mbit/s, but the higher the speed, the shorter the cable must be and the more errors you get.
A UART does not detect lost or wrong bytes. With PCM data this is critical: if a single byte is lost, all following samples are misaligned and you only hear noise. You have the following options:
-
8 bit samples: with
EncoderL8/DecoderL8a sample is one byte, so a lost byte is just a short click - see send-8bit-receive:
EncoderL8 enc;
DecoderL8 dec;
EncodedAudioStream enc_stream(&serial, &enc); // sender
EncodedAudioStream dec_stream(&out, &dec); // receiver- Codec: compressing the data reduces the needed baud rate - e.g. IMA ADPCM (4:1) in send-adpcm-receive. An encoded stream however is even more sensitive to data loss.
-
Container: the
BinaryContainerEncoder/BinaryContainerDecoder(AudioTools/AudioCodecs/ContainerBinary.h) put each frame into a record with a header and a checksum, so the receiver can detect broken frames and resynchronize - see send-adpcm_framed-receive:
ADPCMEncoder enc(AV_CODEC_ID_ADPCM_IMA_WAV);
ADPCMDecoder dec(AV_CODEC_ID_ADPCM_IMA_WAV);
BinaryContainerEncoder bin_enc(&enc);
BinaryContainerDecoder bin_dec(&dec);
EncodedAudioOutput enc_stream(&serial, &bin_enc);
EncodedAudioOutput dec_stream(&out, &bin_dec);-
HDLC framing: HDLCStream (
AudioTools/Communication/HDLCStream.h) packs each write into a frame with flag bytes (0x7E), byte stuffing and a CRC-16. The receiver only returns complete and valid frames:
HDLCStream hdlc_enc(Serial1, 256); // stream, max frame size
HDLCStream hdlc_dec(Serial1, 256);
StreamCopy copierOut(hdlc_enc, throttle, 256); // write frames
StreamCopy copierIn(out, hdlc_dec, 256); // read valid frames only-
Forward error correction:
HammingFEC<bytecount, block_t>(AudioTools/Communication/HammingFEC.h) andReedSolomonFEC<bytecount, additional_bytes>(AudioTools/Communication/ReedSolomonFEC.h) add redundant data, so that the receiver can correct wrong bits. They are streams that wrap the serial:ReedSolomonFEC<200, 20> fec(Serial1);and then copy to / fromfec. -
Objects: ObjectStream (
AudioTools/Communication/ObjectStream.h) writes each write with a size prefix, so that the receiver reads exactly the same blocks again.
If you send encoded data (e.g. an MP3 stream from the internet), a Throttle is not possible: the sender does not know the audio rate. The receiver must tell the sender when to stop. The serial examples show three ways:
- mp3: hardware flow control with RTS/CTS of the ESP32 UART:
HardwareSerial MP3Serial(1);
...
MP3Serial.begin(115200, SERIAL_8N1);
MP3Serial.setPins(rx, tx, cts, rts);
MP3Serial.setHwFlowCtrlMode(UART_HW_FLOWCTRL_CTS_RTS);- mp3-custom: a flow control pin: the receiver collects the data in a buffer and sets the pin HIGH when the buffer is 90% full and LOW below 10%; the sender only copies while the pin is LOW:
// receiver: fill the buffer in a separate task
BufferRTOS<uint8_t> buffer(0);
QueueStream<uint8_t> queue(buffer);
StreamCopy copierFill(queue, Serial1);
StreamCopy copierPlay(dec, queue);
...
task.begin([]() {
copierFill.copy();
if (buffer.levelPercent() >= 90) digitalWrite(flowControlPin, HIGH); // stop
else if (buffer.levelPercent() <= 10) digitalWrite(flowControlPin, LOW); // continue
});
// sender
void loop() {
if (digitalRead(flowControlPin) == LOW) copier.copy();
}- mp3-xon-xoff: software flow control: the same logic, but the receiver sends XON (17) / XOFF (19) characters back on its TX line, so no extra pin is needed.
- Connect GND of both devices. Use a level shifter between 5 V and 3.3 V boards.
- A big
setRxBufferSize()/setTxBufferSize()(call it beforebegin()) helps against short delays. - For a faster and more reliable wired link between two ESP32 see SPI.