Provide `amy_config.write_samples_fn` to accommodate audio output that doesn't use the built-in I2S
For typical AMY deployments on common MCUs using common DACs (i.e., I2S DACs like the PCM510x), AMY comes with built-in drivers which, by setting a couple of config parameters to define the I2S pins, provide audio output out-of-the-box.
#include <AMY-Arduino.h>
void setup() {
amy_config_t amy_config = amy_default_config();
// Pins for i2s board.
amy_config.i2s_bclk = 8;
amy_config.i2s_lrc = 9;
amy_config.i2s_dout = 10;
// Start the AMY engine.
amy_start(amy_config);
// Start the I2S output running.
amy_live_start();
// Set a drum loop going, as an example.
example_sequencer_drums_synth(2000);
}
void loop() {
// Let AMY do its processing for this moment. Will block while the audio output buffer is full,
// leading to a cycle time of around 5.8 ms (256 / 44.1).
amy_update();
}
However, I2S audio isn't always what people want. For instance, the RP2040 Pico can write audio out with no DAC via PWM (see PWM Audio Library). For these scenarios, we provide an alternative API which returns a block of 16 bit samples to pass to the alternative audio output (see AMY_pico_PWM.ino):
#include <AMY-Arduino.h>
#include <PWMAudio.h>
PWMAudio pwm(0, true); // PWM Stereo out on pins 0 and 1.
void setup() {
amy_config_t amy_config = amy_default_config();
// Setup PWM
pwm.setBuffers(4, AMY_BLOCK_SIZE * AMY_NCHANS * sizeof(int16_t) / sizeof(int32_t));
pwm.begin(44100);
// Start the AMY engine.
amy_start(amy_config);
// Set a drum loop going, as an example.
example_sequencer_drums_synth(2000);
}
void loop() {
int16_t *block = amy_simple_fill_buffer(); // AMY calculates next block of audio.
size_t wrote = 0;
do {
// Call the application-specific function that accepts blocks of samples.
wrote = pwm.write((const uint8_t *)block, AMY_BLOCK_SIZE * AMY_NCHANS * sizeof(int16_t));
} while (wrote == 0);
}
It seems clumsy to have these two different configurations depending on the details of the output function. It would be nicer if the differences in audio output were localized to the startup() function (e.g., incorporated as far as possible into amy_config), and the loop() function is the same in both cases.
For this scenario at least, we could accommodate this by providing a amy_config.write_samples_fn with a signature size_t (*write_samples_fn)(const uint8_t *buffer, size_t buffer_size) and with the behavior that it blocks if the audio output is full (or returns zero if no samples are accepted). If the write_samples_fn is non-null, AMY will pass it the samples block on each call to amy_update().
I also think we can dispense with amy_live_start() -- amy_start() can include starting the I2S interface if it is configured. Under this setup, the PWM example would look much more like the first:
#include <AMY-Arduino.h>
#include <PWMAudio.h>
PWMAudio pwm(0, true); // PWM Stereo out on pins 0 and 1.
void setup() {
amy_config_t amy_config = amy_default_config();
// Configure AMY to write samples directly to the PWM output.
amy_config.write_samples_fn = pwm.write;
// Start PWM
pwm.setBuffers(4, AMY_BLOCK_SIZE * AMY_NCHANS * sizeof(int16_t) / sizeof(int32_t));
pwm.begin(44100);
// Start the AMY engine.
amy_start(amy_config);
// Set a drum loop going, as an example.
example_sequencer_drums_synth(2000);
}
void loop() {
amy_update();
}