Search guides and the APICtrl K
  1. API reference
  2. Applications

audio_source.h

The pull model: sources that are ASKED for their next block.

#include <sonore/audio_source.h>
On this page

Design notes

A plugin is pushed: the host hands it a block and expects it filled, and every format's process call is that shape. An application playing a file is the other shape. Something at the device end asks for the next block, and that request travels backwards through a chain -- a transport asks a resampler, the resampler asks a read-ahead buffer, the buffer asks a file -- each link pulling from the one behind it. The reference builds its playback on that model, and code ported from it (a file player, a DJ deck, a sampler's preview, a test harness) is written against these class names. This header is the target for that code: the reference's shapes, built on the SDK's own parts rather than beside them.

AudioFileReader (audiostream.h)  ->  AudioFormatReaderSource
WavData (wav.h / audiofile.h)    ->  MemoryAudioSource
Biquad (dsp.h)                   ->  IIRFilterAudioSource
Reverb (effects.h)               ->  ReverbAudioSource
WindowedSincKernel + SvfSection  ->  ResamplingAudioSource
ChangeBroadcaster                ->  AudioTransportSource's notifications
the backends' RenderFn           ->  AudioSourcePlayer

The one rule, and how each class keeps it

getNextAudioBlock() runs on the audio thread, and on the audio thread nothing allocates, locks, waits or touches a file. The reference keeps that rule for the samples and breaks it for the bookkeeping: its mixer, transport, remapper, filter and reverb sources each take a lock inside the callback so that another thread can change them, which is a priority inversion waiting for a busy message thread. Nothing here does that. What changes while audio runs is handed over in one of three ways, chosen by what it is:

A few numbers (a gain, a ratio, a frequency)   one atomic each.
A small table (coefficients, reverb settings,  a seqlock (SeqBox below): the
a channel map)                                 audio thread takes a
                                               consistent copy or, if a
                                               writer is mid-update, keeps
                                               the copy it had for one more
                                               block. It never waits.
Something with a LIFETIME (the mixer's input   published by atomic pointer,
list, the transport's chain, the player's      and the OLD one freed only
source)                                        after the audio thread is
                                               provably not inside a block
                                               that saw it (BlockEpoch).
                                               The thread making the change
                                               waits; the audio thread
                                               never does.

The MixerAudioSource case is the one the reference documents as needing a lock, so it is worth saying plainly: there is no lock and no try-lock. A try-lock that outputs silence on contention would turn every add or remove into a dropout of the whole mix, which is the failure the lock was there to avoid. The list is swapped whole, and the thread that swapped it waits out the block in flight before it frees anything.

Every function that runs on the audio thread says "[audio thread]" at the top of its comment, and scripts/audio-thread-rules.mjs reads those bodies for allocation, locks, waits and file calls. AudioFormatReaderSource is the exception by necessity and is marked [reading thread] instead: reading a file is a seek and an fread. Behind a BufferingAudioSource -- which is what AudioTransportSource puts it behind when given a read-ahead size -- that read happens on the ReadAheadThread, which is the point of having one.

Types 15

A region of a buffer for a source to fill: numSamples frames starting at startSample, across every channel of buffer.

The reference's struct, with the SDK's AudioBlock where it has its owning buffer: AudioBlock is a view over channel pointers somebody else owns, which is exactly what a device callback, a ring and a scratch area all are. A source must write EVERY channel of the region -- silence where it has nothing -- because the caller's buffer holds whatever the last block left in it, and a source that skipped a channel would replay that.

Something that can be asked for the next block.

The lifecycle is the reference's: prepareToPlay() before the first block, from a thread that is not running blocks at the same time; then any number of getNextAudioBlock() calls from the audio thread; then releaseResources(). prepareToPlay() is where everything a source needs on the audio thread is allocated, because getNextAudioBlock() will not.

class PositionableAudioSource : public AudioSource

A source with a position: a file, a buffer, a transport.

Positions are in the source's own samples. setNextReadPosition() may be called from a thread other than the one pulling blocks -- every source in this header keeps its position in an atomic for that reason -- but a source written elsewhere need not, which is why AudioTransportSource only calls it from the audio thread while playing.

class MemoryAudioSource : public PositionableAudioSource

Audio already in memory, played from a position, optionally looping.

Built from a decoded file -- readAudioFile() hands back WavData for WAV, AIFF, FLAC, MP3 and Ogg alike, so this is how a compressed file is played -- or from planar channels, copied or borrowed.

One deliberate difference from the reference: the position keeps counting through the silence after the end, as a file reader's does, instead of stopping at the length. A transport decides the stream has finished by watching the position pass the length, and a position that stopped AT the length would never pass it.

class AudioFormatReaderSource : public PositionableAudioSource

A file, read on demand through AudioFileReader.

The reader is a seek and an fread, so getNextAudioBlock() here is NOT for the audio thread -- in the reference it is not either, it just does not say so. Put it behind a BufferingAudioSource, or give AudioTransportSource a read-ahead size and it does that itself, and the read happens on the ReadAheadThread.

A mono file into a stereo buffer plays on both sides, as the reference's reader does it; a file narrower than a wider buffer leaves the extra channels silent. Compressed formats: AudioFileReader streams WAV and AIFF and refuses the rest by name, so decode those with readAudioFile() and use MemoryAudioSource.

class ToneGeneratorAudioSource : public AudioSource

A sine wave at a frequency and level: the reference's test tone.

Every channel gets the same sample. The phase is kept in CYCLES and wrapped, not in radians that grow without bound, so an hour in it is still in tune; and a level change glides across the next block instead of stepping.

class ChannelRemappingAudioSource : public AudioSource

Feeds a source from chosen channels of the incoming buffer and sends its channels to chosen channels of the result.

The reference's semantics, exactly, because a ported routing table has to mean the same thing: setInputChannelMapping(dest, src) says channel src of the INCOMING buffer feeds channel dest of the wrapped source; setOutputChannelMapping(src, dest) says channel src of the source's output is ADDED into channel dest of the result. An unmapped input is silence, an unmapped output goes nowhere, and two outputs mapped to one destination sum. The XML save and restore the reference offers is not here; the getters give a caller everything it needs to store the map its own way.

The map is a SeqBox: a change made while audio runs lands at the next block boundary, whole, never half-applied.

class IIRFilterAudioSource : public AudioSource

A source through a second-order IIR section per channel.

The coefficients come from a Biquad -- design it with any of its cookbook shapes, hand it over, and every channel's section copies the five numbers and keeps its own state. That is the reference's IIRCoefficients split without a second type: the design is the Biquad you configured, and the channels' filters are the ones in here.

A change lands at a block boundary with the filters' state kept, which is what an IIR filter whose knob moved does. makeInactive() passes the audio through untouched until coefficients are set again.

class ReverbAudioSource : public AudioSource

A source through the SDK's Reverb (effects.h).

Stereo is the reverb's own stereo frame. Mono feeds the left side alone and keeps the left output: the tank's input gain is applied to left plus right, so a mono sample fed to both sides would arrive twice as loud as the reference's mono path makes it. Channels past the second are left alone.

Bypassing resets the tank on the way in and out, as the reference does, so coming back from bypass does not replay a tail from before it.

class ResamplingAudioSource : public AudioSource

Plays an input at a different rate: samplesInPerOutputSample input samples per output sample, so 44100.0 / 48000.0 plays a 44.1 kHz source at its own pitch in a 48 kHz stream, and 2.0 plays it an octave up.

resample.h's Resampler is the SDK's rate converter, and it is the wrong tool inside a callback by its own description: it converts a whole buffer at once, evaluates its kernel with a sine per tap, and keeps nothing between calls. This is its streaming counterpart, and the test holds it to Resampler's output for the same material:

interpolation   interpolation.h's WindowedSincKernel<8>, read from its
                table: no trigonometry per sample, and the first output
                is aligned with the first input, because the kernel's
                eight samples of look-ahead are pulled AHEAD rather than
                paid for as latency.
anti-aliasing   reading faster than one-for-one folds whatever lies above
                the new Nyquist back down, and no interpolation kernel
                undoes that. So above a ratio of 1 the input goes through
                an eighth-order Butterworth lowpass (four of dsp.h's
                double-precision SvfSections, the form whose corner may
                move) at 0.45 of the OUTPUT rate first -- kept running at
                every ratio and crossfaded in, so that crossing 1 is not a
                click (see antiAlias()).
                The reference uses a single second-order section and
                linear interpolation; this measures better and costs a
                table read.
ratio changes   glide linearly across the block in which they are seen,
                sample by sample. A ratio that steps is a pitch that
                steps; the read position itself never jumps either way.
ratio 1         exact. The fractional position stays at zero and every
                output is the input sample, bit for bit.
class MixerAudioSource : public AudioSource

Sums any number of sources.

Inputs may be added and removed from any thread while audio runs, and the audio thread takes no lock for it: the list is an immutable snapshot behind an atomic pointer. A change copies the list, publishes the copy, then waits out the block in flight (BlockEpoch) before it frees the old list or releases a removed source -- so a removed source has finished its last block before releaseResources() is called on it, and the CALLER waits for that, never the audio thread. Removing a source from inside the mixer's own callback cannot wait for itself, so the old list and the source are kept and freed at the next change or when the mixer goes.

The waiting is done with the writers' lock RELEASED. A source removing itself from its own callback takes that lock on the audio thread, inside the very block another thread's change may be waiting out; a wait holding the lock is then a deadlock of both threads (the one-shot sample player that removes itself while the UI adds the next one did exactly that). Changes from other threads still go one at a time: the next one waits until the last has freed what it replaced.

The first input renders straight into the output and the rest are added from scratch, in order, the reference's arithmetic. Scratch holds AudioSource::kMaxChannels channels; a wider output gets only the first input on the channels past that.

One background thread that takes turns among the sources reading ahead.

The reference's BufferingAudioSource takes a TimeSliceThread: one thread, many clients, each called in turn and each saying how long it can wait before it next needs a turn. thread_pool.h's ThreadPool is the SDK's background work, and it is the wrong shape for this -- it runs jobs to COMPLETION, and reading ahead never completes. Parked on a pool worker it would take that worker for good, and the default pool has two.

So this is the cooperative shape, for exactly this job. A client returning 0 is called again at once (it has more to read); a positive number is a nap in milliseconds; a negative one waits until moveToFrontOfQueue(). The thread starts itself when the first client arrives -- a thread nobody started is a buffer that never fills, which sounds exactly like a broken file -- and must outlive every client registered with it.

class BufferingAudioSource : public PositionableAudioSource, private ReadAheadThread::Client

Reads a positionable source ahead of playback on a ReadAheadThread, into a ring the audio thread copies out of.

The audio thread never waits for the reader. Frames that are not in the ring yet come out as silence and are COUNTED (getUnderrunFrames()), the way SampleStreamer counts them, rather than hidden: a disk that cannot keep up should read as a number somebody can look at, not as a plugin that sounds broken. waitForNextAudioBlockReady() is the blocking question for the places that may block -- an offline render, a test.

The ring is addressed by ABSOLUTE position: frame f lives in slot f mod capacity, and the reader publishes which frames are valid as a range [start, end). The audio thread copies whatever part of its block falls in that range. The reader only ever overwrites a slot after raising start past the frame that was in it and waiting out any block that might have read the old range (BlockEpoch) -- so a slot is never written while it is being read, which is the whole of the ring's correctness, and the waiting is done by the reader thread.

The reference does this with a lock that the audio callback takes. A seek (setNextReadPosition) is a single atomic store here, from any thread, including the audio thread; the reader notices on its next turn, which is at most a few milliseconds while playback is moving.

class AudioTransportSource : public PositionableAudioSource, public ChangeBroadcaster

Start, stop, seek and gain over a positionable source -- with the read-ahead and the sample-rate correction set up for it -- as one source.

setSource() builds the chain the reference builds: the caller's source, then a BufferingAudioSource when a read-ahead size and a thread are given, then a ResamplingAudioSource when the source's rate is given, correcting it to whatever rate the transport is prepared at.

What moves is RAMPED, where the reference cuts:

start()          fades in over kFadeSamples. The reference starts at full
                 level on whatever sample it lands on.
stop()           fades out over kFadeSamples, across block boundaries if
                 the blocks are short, then stops pulling the source. It
                 returns at once; isPlaying() is the request and the
                 fade finishes on the audio thread.
a seek           while playing, is carried to the audio thread and done
                 there: kSeekFadeSamples of fade-out at the old position,
                 the jump, then the fade back in. getCurrentPosition()
                 reports the new position at once.
setGain()        ramps across the next block.

The position is counted in OUTPUT samples as they are produced, so with a resampler in the chain it is the position being heard rather than the position the resampler has read up to. Finishing -- a non-looping source played past its length -- stops the transport and sends a change message (ChangeBroadcaster: one atomic flag, dispatched on the UI tick).

Plays a source through an audio device: the adapter between the pull model and the SDK's device backends.

Every output backend here calls one render function with interleaved floats -- WasapiOutput, CoreAudioOutput, LinuxOutput, AlsaOutput and JackOutput all take a RenderFn of (float* interleaved, frames, channels), and AsioOutput the same with an input buffer in front. renderFunction() and renderWithInputFunction() hand those over directly:

standalone::WasapiOutput out;
out.open(player.renderFunction());
player.audioDeviceAboutToStart(out.sampleRate(), 1024, (int) out.channels());
out.run();

audioDeviceIOCallback() is the reference's planar form, for a caller that already has channel pointers. Every entry point declares ScopedNoDenormals, as the DSP entry points do.

A device may hand over more frames than it promised -- WASAPI's shared mode fills whatever is free -- so the interleaved paths go through scratch in pieces of the prepared size rather than trusting the promise. Input channels are copied onto the first outputs before the source is called, as the reference does, so a source can process them.

↑↓ to move↵ to openEvery public name in the headers, and every guide