Bedtime. /Audio Engine

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:

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:

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.

void* engine_get_ctx(engine_t* engine)

Get callback context from engine (to access your model in callbacks)

engine_t*engineEngine 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
}
void engine_set_callbacks(os_app_t* app, void* cb_ctx)

Set audio engine callbacks for the app

os_app_t*appApp instance
void*cb_ctxCallback context (usually model pointer)
void engine_set_active(bool state)

Set audio engine active state

boolstatetrue to enable processing, false to disable
void engine_clear_callbacks(const engine_callbacks_t* cb)

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.

engine_t* engine_get(void)

Get the global engine handle

uint_fast8_t engine_is_active(void)

Is the audio engine currently processing? (non-zero = yes)

mixer_t* mixer_get(void)

Get the global mixer handle

uint8_t os_audio_switch_input(bool line_mic_only)

Switch which external source the codec listens to, and apply it

boolline_mic_onlyVestigial — line and mic are the only sources; both modes toggle

Returns — The source now selected: 0 = line/aux, 1 = mic

uint8_t os_audio_get_input(void)

Current input source WITHOUT changing it: 0 = line/aux, 1 = mic

bool os_audio_get_monitor(void)

Is the dry monitor on? (the live input you hear while idle/recording)

bool os_audio_set_monitor(bool on)

Turn the dry monitor on/off — the user-facing toggle.

boolon

Returns — the state actually in effect afterwards