Bedtime. /Tape API

Tape API

Access to tape state and transport controls

The tape API provides access to the global tape storage and transport state. TAPPs can query tape state and control playback/recording.

enum tape_state_t
TAPE_STATE_UNMOUNTED0no tape loaded
TAPE_STATE_IDLE1loaded, not playing or recording
TAPE_STATE_PLAYING2
TAPE_STATE_RECORDING3
tape_t* tape_get(void)

Get the global tape instance

Returns — Pointer to tape (never NULL after system init)

tape_state_t tape_get_state(const tape_t* tape)

Get current tape state

const tape_t*tapeTape instance from tape_get()

Returns — Current state (TAPE_STATE_UNMOUNTED, TAPE_STATE_IDLE, etc.)

uint32_t tape_size(const tape_t* tape)

Get tape length in tape positions (SD sectors)

const tape_t*tapeTape instance from tape_get()

Returns — Tape length in positions. 1 position = 1 sector = 64 stereo frames, so multiply by 64 to compare against tape_read()/tape_write() frames.

tape_rec_t* tape_rec_begin(tape_t* tape, uint32_t pos, bool reverse)

Begin recording session

tape_t*tapeTape instance from tape_get()
uint32_tposStarting position in tape POSITIONS (SD sectors, 64 stereo frames each), NOT frames.
boolreverseTrue for reverse direction

No undo is captured for the take, and the tape's previous undo/redo is dropped. Marks are created on commit.

Returns — Recording handle. A take still open is aborted first.

Note — This only brackets the take (timeline, mark). It writes NO audio - the tapp's engine callback has replaced the one that normally does. Stream the tapp's output to the tape with tape_write() between begin and commit.

uint32_t tape_rec_commit(tape_rec_t* rec, uint32_t end_pos)

Commit recording session

tape_rec_t*recRecording handle from tape_rec_begin()
uint32_tend_posEnd position of recorded region

Creates marks for recorded region, commits undo state.

Returns — Final end position

void tape_rec_abort(tape_rec_t* rec)

Abort recording (discard without creating marks)

tape_rec_t*recRecording handle
bool tape_rec_undo_available(const tape_t* tape)

Check if undo is available for last recording

const tape_t*tapeTape instance from tape_get()

Returns — true if undo data is valid

bool tape_rec_undo_execute(tape_t* tape)

Execute undo - restore original audio from last recording

tape_t*tapeTape instance from tape_get()

Returns — true on success

uint32_t tape_marks_count(const tape_t* tape)

Number of recording marks on the tape

const tape_t*tapeTape instance from tape_get()

Returns — Mark count (0 if none / no tape)

bool tape_marks_get(const tape_t* tape, uint32_t idx, uint32_t* start_frame, uint32_t* end_frame)

Get the start/end of mark `idx`

const tape_t*tapeTape instance from tape_get()
uint32_tidxMark index (0 .. tape_marks_count()-1)
uint32_t*start_frameOutput: region start, in stereo frames
uint32_t*end_frameOutput: region end, in stereo frames

Returns — true if idx is valid and outputs were written

uint32_t tape_read(const tape_t* tape, uint32_t pos_frame, float* dst, uint32_t n_frames)

Read recorded audio into a RAM buffer (stereo-interleaved L,R,...)

const tape_t*tapeTape instance from tape_get()
uint32_tpos_frameStart position in stereo frames
float*dstDestination buffer (must hold 2*n_frames floats)
uint32_tn_framesNumber of stereo frames to read

Returns — Number of stereo frames actually read

Warning — Blocks on SD DMA. Call from tick()/init() (main thread), NEVER from the audio process() callback. The tapp sequences the slices itself in process().

uint32_t tape_write(tape_t* tape, uint32_t pos_frame, const float* src, uint32_t n_frames)

Write audio to the tape (stereo-interleaved L,R,...)

tape_t*tapeTape instance from tape_get()
uint32_tpos_frameStart position in stereo frames
const float*srcSource buffer (2*n_frames floats)
uint32_tn_framesNumber of stereo frames to write

Clamped to the tape length and addressed from the tape's own data area, so it cannot reach any other file. Partial sectors are read-modify-written.

This is how a TAPP records: ring the process() output in RAM and drain it here from tick(), bracketed by tape_rec_begin()/tape_rec_commit() so a mark is created. Undo covers only what tape_rec_begin() armed - writes outside a begin/commit pair overwrite tape audio permanently.

While a recording, frames written also drive the timeline widget's live-record waveform automatically (anchored at the position passed to tape_rec_begin(); writes below that anchor are not drawn).

Returns — Number of stereo frames actually written

Warning — Blocks on SD DMA. Call from tick()/init() (main thread), NEVER from the audio process() callback.

tape_buffer_t* tape_buffer_init(tape_t* tape)

Claim a pair of DMA windows from the tape's pool

tape_t*tapeTape instance from tape_get()

Returns — NULL when the pool is exhausted - ALWAYS check, and fail init() if so

void tape_buffer_deinit(tape_buffer_t* s)

Release the windows back to the pool. Call from deinit().

tape_buffer_t*s
bool tape_buffer_update(tape_buffer_t* s, uint32_t pos, bool write)

Request the window needed NEXT, and swap the other one in to play now

tape_buffer_t*sWindow pair
uint32_tposTape POSITION, and it must sit on the TAPE_WINDOW_POS grid
boolwritetrue to write the evicted window back on the way out (recording)

This is a two-slot pipeline, and the naming hides it. On a miss the DMA is started into the slot that was current, and the OTHER slot - the one a previous call already filled - becomes what tape_buffer_get_ptr() returns. So the caller does not request the window it is about to play; it requests the one AFTER it, and reads back the one it requested last time. On a hit (either slot already holds pos) nothing is issued and that slot simply becomes current.

Practically: at each window boundary call tape_buffer_update() with pos + TAPE_WINDOW_POS, and play tape_buffer_get_ptr(). After a seek the pipeline is empty, so prime it with TWO calls - the first has nothing valid to hand back, which is why tape_buffer_clear_all_buffers() zeroes both slots: the result is a moment of silence rather than whatever was resident.

Safe from process(): a hit does no I/O, a miss posts one command and returns.

Returns — false only if the SD queue was full - and the slot is relabelled anyway, so it will replay stale audio: treat false as a dropped window, not a retry

float* tape_buffer_get_ptr(tape_buffer_t* s)

The resident window: TAPE_WINDOW_FR stereo frames, interleaved L,R

tape_buffer_t*s

Read it, and overdub it in place while recording. Valid until the next tape_buffer_update() evicts it.

float* tape_buffer_get_next_ptr(tape_buffer_t* s)

The other window of the pair - the one being prefetched into.

tape_buffer_t*s
void tape_buffer_flush(tape_buffer_t* s)

Write both resident windows back to tape

tape_buffer_t*s

Recording persists only through eviction, so at the end of a take the resident window(s) still hold audio that nothing has written. Call this once when the take ends, or up to two windows of it are lost.

void tape_buffer_clear_all_buffers(tape_buffer_t* s)

Drop both windows and force a re-read - after a seek that changes content.

tape_buffer_t*s
void tape_buffer_mute_tail(tape_buffer_t* s, uint32_t sample_offset)

Zero the resident window from `sample_offset` (in floats) to its end

tape_buffer_t*s
uint32_tsample_offset

Note — What the decks use to keep the button click out of the tail of a take.

bool tape_cue_add(tape_t* tape, uint32_t start_frame, uint32_t end_frame)

Add a cue (a navigable point of interest) spanning [start, end) frames

tape_t*tape
uint32_tstart_frame
uint32_tend_frame

Cues are what the device's skip-to-cue navigation steps through. Duplicates within one sector are merged, the list is kept in tape order, and the save is flagged automatically. A cue at position 0 is legal.

Returns — false if the span is empty, shorter than one 64-frame sector, ends past the end of the tape, or the tape already holds 255 cues

uint32_t tape_cues_count(tape_t* tape)

Number of cues on the tape

tape_t*tape
bool tape_cue_get(tape_t* tape, uint32_t idx, uint32_t* start_frame, uint32_t* end_frame)

Read cue `idx` (0..tape_cues_count-1), in stereo frames

tape_t*tape
uint32_tidx
uint32_t*start_frame
uint32_t*end_frame

Cues are stored in tape order, so stepping between them is index +/- 1.

Returns — false if idx is out of range

bool tape_erase(tape_t* tape, uint32_t start_frame, uint32_t end_frame)

Erase [start_frame, end_frame): audio, marks, and cues

tape_t*tape
uint32_tstart_frame
uint32_tend_frame

The audio blocks are zeroed (asynchronously, on the storage task), the span is punched out of the recorded-region marks (a longer mark is split around it), every cue overlapping the span is deleted, and the timeline waveform is refreshed. Everything persists automatically.

Cue indices shift on deletion - re-read tape_cues_count() and re-select afterwards. The erase is captured by the single-level record undo (tape_rec_undo_execute() restores it) for spans up to ~136 s and until the next tape_rec_begin(); longer spans erase without undo. Refused while a recording in progress - commit or abort first.

Returns — false if the span is empty, off-tape, or a take is open