Creating New Audio Objects

The audio library makes creating your own audio processing objects fairly easy.

C++ Object Definition

The first step is defining your own C++ objects, inherited from AudioStream. Please read the naming convention guidelines for choosing your object's name.

The AudioStream base class provides all the memory management, communication with other audio objects, and CPU usage tracking common to the audio library.

Here is a minimal C++ object template using AudioStream.

#include <Arduino.h>
#include <AudioStream.h>

class AudioEffectTemplate : public AudioStream
{
public:
        AudioEffectTemplate() : AudioStream(1, inputQueueArray) {
          // any extra initialization
        }
        virtual void update(void);
private:
        audio_block_t *inputQueueArray[1];
};

The definition "class AudioEffectTemplate : public AudioStream" creates a new object class which inherits from the AudioStream class. This C++ inheritance provides all the audio library functionality to your object.

Your object's constructor must initialize the AudioStream constructor. This is done with ": AudioStream(number, inputQueueArray)" in the constructor definition. The first parameter is the number of inputs your object will support. The second is an array of audio block pointers used for the inputs. If your object is a playback or synthesis object, or simply doesn't need any inputs, use ": AudioStream(0, NULL)" to create you object without any inputs.

The heart of your object is it's update() function. The library will call your update function every 128 samples, or approximately every 2.9 milliseconds.

The final required component is inputQueueArray[], which should be a private variable. The size must match the number passed to the AudioStream constructor.

Of course, your object will likely need its own private variables for settings, saved state between updates, and other uses. Most objects also have additional public functions meant to be called from Arduino sketches, to configure the object or adjust its parameters while running. See the naming conventions for recommendations on naming your public functions.

Audio Data Functions

Your update() function needs to perform all your object's audio processing. The library provides these functions and data types to help.

AUDIO_BLOCK_SAMPLES

This constant, usually 128, tells the number of audio samples the library places in each block. If you use this name, rather than hard-coding 128, your code can adapt if the library is every changed to a different block size.

audio_block_t

Audio blocks are represented with this data type, which is a C struct. The only member intended for use in update() is "data", an array of 16 bit integers representing the audio. If "myblock" is a pointer to an audio_block_t, use myblock->data[0] to access the first audio sample, myblock->data[1] to access the second, and so on. The data[] array is always 32 bit aligned in memory, so you can fetch pairs of samples by type casting the address as a pointer to 32 bit data.

receiveReadOnly(channel);

Receive a block of incoming audio from an input channel. This read-only version is preferred if your object does not modify the data[] array, since it allows the library to avoid copying data when multiple objects receive the same data. Every run of your update() function should call a receive function once for each input. Only 1 block may be received from each channel during each run of update(), so there is no need to attempt receiving from any channel more than once. You get a pointer to an audio_block_t struct. The pointer may be NULL, which should be treated as if a block of silent zeros was received.

receiveWritable(channel);

Receive a block of incoming audio from an input channel, with access to modify its data[] contents. This writable version requires the library to make a unique copy if other objects are receiving the same data, so it should only be used when you must change the data[] array. Every input channel should have one of these receive functions used on every run of your update() function. The audio_block_t pointer may be NULL, which should be treated as if silent data was received.

allocate();

For synthesis where you create audio, or playback where you obtain audio from external media, or effects where building the new sound in a different data[] array is more convenient, you can use allocate() to obtain another block. Like the receive functions, allocate() may return NULL, which your update function should usually handle by releasing all blocks it currently holds and sending no output, or a default output if at least one writable block is held. The data[] array of a newly allocated block is likely to contain old data. If you require the block to be zero (silent), you must write all 128 samples.

transmit(block, channel);

Send an audio_block_t to one of your object's output channels. You can send the same block to more than one channel, which is handy for stereo effects running in a mono mode, and perhaps other uses. You may transmit any block, even if it was obtained from receiveReadOnly. Transmitting does not release your ownership of the memory.

release(block);

Your update function has ownership of all audio blocks obtained by receiveReadOnly(), receiveWritable(), and allocate(). You must call release() for every non-NULL pointer obtained from those 3 functions. You can temporarily store audio_block_t pointers in your object's private variables, to buffer data for short times, but every non-NULL pointer must later be released to avoid leaking memory. Most object designs simply put pointers into local variables and release any non-NULL pointers at the end of the update function.

Interrupt Safety

Most audio objects implement functions to allow Arduino sketches to adjust parameters, trigger output, read status, or otherwise interact with the object as it processes audio data. The update() function is run from a low priority interrupt, so special care is needed when exchanging data between the sketch and interrupt-based code.

Normally sketch-called function should use __disable_irq() and __enable_irq() to temporarily disable all interrupts while reading or writing a set of variables that are used used by update(). Because these disable ALL interrupts, as much preparatory work should be done as possible, to minimize the time interrupts are disabled. Moving data to or from local variable gives best performance. Usually disabling interrupts once for all necessary data exchange is best, since update() may run at any moment while interrupts are enabled.

Shared variables, especially those which are read by the Arduino sketch-called functions, many need to be defined with "volatile".

The special AudioNoInterrupts() and AudioInterrupts() functions should NOT be used within your object's functions. These are meant to allow the Arduino sketch to disable audio library updates across multiple function calls, perhaps to several different objects. They work separate from __disable_irq(), and are meant only to be used from the user's Arduino sketch code.

Examples

AudioPlaySdRaw is one of the simplest objects in the audio library. It reads a file from the SD card and treats the data as raw 16 bit samples.

The update() function uses only 3 member variables, "playing" to quickly determine if sound should be played, "rawfile" as a SD library File object to read from the SD card, and "file_offset" which is updated with the playback position as the file plays.

void AudioPlaySdRaw::update(void)
{
        unsigned int i, n;
        audio_block_t *block;

        // only update if we're playing
        if (!playing) return;

        // allocate the audio blocks to transmit
        block = allocate(); 
        if (block == NULL) return;

        if (rawfile.available()) {
                // we can read more data from the file...
                n = rawfile.read(block->data, AUDIO_BLOCK_SAMPLES*2);
                file_offset += n;
                for (i=n/2; i < AUDIO_BLOCK_SAMPLES; i++) {
                        block->data[i] = 0;
                }
                transmit(block);
        } else {
                rawfile.close();
                playing = false;
        }
        release(block);
}

Many other object have more complex update() functions. The audio library source contains many objects with comments that explain their implementation.

Extra Tips

Every audio library object must have an update() function. If update() is not defined in your .h header file, the compiler will produce a helpful error explaning that update() is required. If update() is not implemented in your .cpp file, the compiler will produce a very unhelpful vtable error. If you see this error, odds are good it means you're missing the update function.

AUDIO_SAMPLE_RATE is a constant that give you the library's nominal sample rate as an integer. Normally this will be 44100.

AUDIO_SAMPLE_RATE_EXACT is a constant that gives you the library's actual precise sample rate as a floating point number. Normally this will be 44117.64706, which can be used to account for the slightly faster speed due to rounding error in creating the clocks from the 96 MHz time base.