Repository navigation
Quick Start
The AOOSender is an audio output: everything you write to it is encoded (PCM by default) and sent via UDP to the receiver with the indicated IP address and port. In this example a sine wave is generated; since a generator delivers the data much faster than real time, a Throttle limits it to the audio rate. With a real audio source (e.g. a microphone via I2S) the throttle is not needed. The sender uses source id 1; the format is announced to the receivers with a /start message.
#include "AOO.h"
AOOStreamUDP udp(IPAddress(192, 168, 1, 44), 9999); // receiver IP and port
AOOSender sender(1, udp);
SineWaveGenerator<int16_t> sineWave;
GeneratedSoundStream<int16_t> sound(sineWave);
Throttle throttled(sound);
StreamCopy copier(sender, throttled);
void setup() {
AudioInfo info(44100, 2, 16);
sineWave.begin(info, N_B4);
throttled.begin(info);
// when sender and receiver run on the same host, they need different ports
// udp.setLocalPort(9998);
sender.begin(info);
}
void loop() { copier.copy(); }The AOOReceiver is an audio source: it listens on the indicated UDP port, buffers the received data of each sender, decodes it and provides it in the output format which you define in the configuration (senders with a different format are converted). Pull the audio with a StreamCopy and write it to any output, e.g. I2S. The receiver uses sink id 1, which must match the sink id the sender is sending to. How the data is buffered is described in Architecture.
#include "AOO.h"
AOOStreamUDP udp(9999);
AOOReceiver receiver(1, udp);
I2SStream i2s;
StreamCopy copier(i2s, receiver); // receiver is the audio SOURCE
AudioInfo info(44100, 2, 16);
void setup() {
auto cfg_i2s = i2s.defaultConfig(TX_MODE);
cfg_i2s.copyFrom(info);
i2s.begin(cfg_i2s);
// the output format: all sources are converted to it
auto cfg = receiver.defaultConfig();
cfg.copyFrom(info);
receiver.begin(cfg);
}
void loop() { copier.copy(); }The AOOReceiverTask is a drop-in replacement for the AOOReceiver which receives the UDP messages in a separate task (details). Use setTaskConfig() to define the stack size (at least 8192 bytes), the priority and the core of the task.
#include "AOO.h"
AOOStreamUDP udp(9999);
AOOReceiverTask receiver(1, udp); // UDP processing on core 0
I2SStream i2s;
StreamCopy copier(i2s, receiver);
void setup() {
i2s.begin(i2s.defaultConfig(TX_MODE));
receiver.setTaskConfig(8192, 1, 0); // stack, priority, core
receiver.begin(); // output format: see Receiver above
}
void loop() { copier.copy(); }PCM needs a lot of bandwidth (e.g. 96 KB/s for 48 kHz mono, 16 bit), Opus only about 10-20 KB/s and is more robust on WiFi. You need the arduino-libopus library and on an ESP32 a bigger stack for the loop (e.g. SET_LOOP_TASK_STACK_SIZE(30000)).
PCM is built-in. Register Opus for compressed streaming. Like the official aoo library we use one Opus stream per channel (multistream with separate channels):
#include "AudioTools/AudioCodecs/CodecOpusMultiStream.h"
// Sender: Opus encoding (each sender needs its own encoder)
OpusMultiStreamAudioEncoder opus;
opus.config().default_channel_mapping = OPUS_CHANNEL_MAPPING_SEPARATE;
opus.config().frame_sizes_ms_x2 = OPUS_FRAMESIZE_20_MS;
sender.setEncoder("opus", opus);
// Receiver: Opus decoding (a new decoder for each source)
receiver.addDecoder("opus", []() {
auto* decoder = new OpusMultiStreamAudioDecoder();
decoder->config().default_channel_mapping = OPUS_CHANNEL_MAPPING_SEPARATE;
return (AudioDecoder*)decoder;
});Instead of just the audio format you can provide a full AOOSenderConfig: the sinks (id, IP address and port) to send to, the redundancy, the maximum packet size (bigger blocks are split into several UDP packets) and the ping interval which is used to measure the round trip time.
AOOSender sender(1, udp);
auto cfg = sender.defaultConfig();
cfg.sample_rate = 44100;
cfg.channels = 2;
cfg.bits_per_sample = 16;
cfg.sink_targets = {{1, IPAddress(192,168,1,10), 7000}};
cfg.redundancy = 2; // send each block twice for loss tolerance
cfg.max_packet_size = 960; // split blocks: must fit the network MTU
cfg.ping_interval_ms = 1000;
sender.begin(cfg);The AOOReceiverConfig defines the output format and how the receiver deals with the network: the buffer depth (jitter buffer: more blocks give more time for resends but add latency), the adaptive resampling which compensates the clock drift between sender and receiver, the resend requests for lost packets, when a source is removed and when a warning is logged because a source sends no audio.
AOOReceiver receiver(1, udp);
auto cfg = receiver.defaultConfig();
cfg.sample_rate = 48000;
cfg.channels = 2;
cfg.bits_per_sample = 16;
cfg.buffer_depth = 10; // encoded segments to buffer
cfg.adaptive_resampling = true; // compensate clock drift
cfg.max_resample_percent = 0.5; // max. playback rate adjustment
cfg.recovery_wait_ms = 10; // resend request timing
cfg.recovery_max_requests = 1; // max retries per gap, 0 = no resends
cfg.min_quality_percent = 80; // suppress resends below this quality
cfg.min_source_quality = 50; // remove sources below this quality
cfg.stream_timeout_ms = 5000; // remove silent sources
cfg.no_data_warning_ms = 1000; // warn: /start but no audio data
cfg.output_follows_source = false; // true: use the format of the 1st source
receiver.begin(cfg);See Choosing the Error Correction for the recommended settings.