Audio Engine
Real-time audio processing
The audio engine allows TAPPs to process audio in real-time. Audio callbacks run in a high-priority context.
Audio Buffers
Use the mixer getter functions for stable ABI:
mixer_get_out(mix)- Output buffer (interleaved stereo: L, R, L, R, ...)mixer_get_in(mix)- Input buffermixer_get_fx(mix)- FX send buffermixer_get_fs(mix)- Frame size (total samples, L+R combined)- Sample rate: 48kHz
Example
static void my_process(engine_t* engine, mixer_t* mix) {
float* out = mixer_get_out(mix);
uint32_t fs = mixer_get_fs(mix);
for (uint32_t i = 0; i < fs; i += 2) {
out[i] *= 0.5f; // Left
out[i+1] *= 0.5f; // Right
}
}
static engine_callbacks_t my_engine = {
.process = my_process,
};Source tapps
Declare the role once at file scope: TAPP_DECLARE_TYPE(AppTypeSrc); (tapp-build stamps it into the manifest). No declaration = Standalone, the classic engine-replacing tapp everything above describes.
A source tapp is an audio SOURCE: the firmware keeps its own tapedecks engine running and records your process() output plus the live input, exactly like the built-in radio (the device's input-volume param scales your output). Write the WHOLE buffer from mixer_get_in() every block — overwrite it, don't add. What you find in it on entry: ZEROS normally, or the LIVE INPUT when the user turns the tapedecks' WET switch on, which is available to every source tapp — transform it in place and your render replaces it. Ignore it and you simply overwrite it.
mixer_get_fx() carries the decks' FX sends — whatever the four tapedecks are feeding their Send knobs into, one block behind (2.667 ms), zero when no deck has Send up. You OWN that buffer for the length of your process() call, the same way the built-in FX chain does, and whatever you leave in it is what the user hears:
- leave it alone -> the sends pass through dry (the default; a tapp that ignores them doesn't silence them)
- overwrite it -> the user hears your processed version instead
- mix it into your own output, then ZERO it -> heard once, through you Mixing it in without zeroing means the sends are heard twice.
One hazard: mixing mixer_get_fx() into mixer_get_in() closes a real feedback loop (sends -> you -> in_bus -> recorded to tape -> played back -> sends). Route sends to your output, or duck/analyse them, but think twice before folding them into the signal the decks are recording.
Your samplerate is your own business — you always get the real 48 kHz bus. To work internally at 24 kHz (the built-in radio's own trick, which halves your per-block budget) render into your own half-length scratch and convert with ups2x_f32_add(); for 12 kHz cascade two stages through a 24 kHz intermediate. Both dull the top end — noticeably from ~10 kHz at 24 kHz, and from ~2.5 kHz cascaded from 12 kHz, so treat 12 kHz as a lo-fi/pad rate. To PROCESS the live input at a lower rate, dec2x_f32() is the matching decimator. Q15-internal DSP (Q15_MUL/Q15_LERP/lutsin_q15) pairs well with 24 kHz; float is fully supported too.
UI policy: source tapps have NO statusbar and the hint band is ALWAYS shown. The firmware enforces both — ui_statusbar_* and ui_hints_show(false) are inert for the session.
MIDI: declaring any midi_* handler in engine_callbacks_t diverts note/CC/pitch-bend/aftertouch/prog-change from the tapedecks to you while resident; MIDI clock and transport ALWAYS stay with the decks, so the tape stays DAW-syncable. No handlers = the decks keep all MIDI.
Lifecycle: your exit gesture backgrounds the tapp — you keep sounding and being recordable — and the user unloads you from the input-source picker. engine_set_callbacks/engine_set_active/engine_clear_callbacks work unchanged. Install order in init(): call engine_set_callbacks() BEFORE engine_set_active() — a set_active before your callbacks are installed is ignored.
Note — Firmware older than this feature ignores the declaration and loads the tapp as a plain engine tapp: audio goes to the monitor, not the record path.
Get callback context from engine (to access your model in callbacks)
| engine_t* | engine | Engine pointer passed to your callback |
Returns — Your callback context (typically your model pointer)
static void my_process(engine_t* engine, mixer_t* mix) {
my_model_t* m = engine_get_ctx(engine);
// Now use m->your_data
}Set audio engine callbacks for the app
| os_app_t* | app | App instance |
| void* | cb_ctx | Callback context (usually model pointer) |
Set audio engine active state
| bool | state | true to enable processing, false to disable |
Clear audio engine callbacks
| const engine_callbacks_t* | cb |
Warning — Call this in your deinit() callback before the TAPP unloads to prevent the engine from calling freed TAPP code.
Get the global engine handle
Is the audio engine currently processing? (non-zero = yes)
Get the global mixer handle
Switch which external source the codec listens to, and apply it
| bool | line_mic_only | Vestigial — line and mic are the only sources; both modes toggle |
Returns — The source now selected: 0 = line/aux, 1 = mic
Current input source WITHOUT changing it: 0 = line/aux, 1 = mic
Is the dry monitor on? (the live input you hear while idle/recording)
Turn the dry monitor on/off — the user-facing toggle.
| bool | on |
Returns — the state actually in effect afterwards