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.
| TAPE_STATE_UNMOUNTED | 0 | no tape loaded |
| TAPE_STATE_IDLE | 1 | loaded, not playing or recording |
| TAPE_STATE_PLAYING | 2 | |
| TAPE_STATE_RECORDING | 3 |
Get the global tape instance
Returns — Pointer to tape (never NULL after system init)
Get current tape state
| const tape_t* | tape | Tape instance from tape_get() |
Returns — Current state (TAPE_STATE_UNMOUNTED, TAPE_STATE_IDLE, etc.)
Get tape length in tape positions (SD sectors)
| const tape_t* | tape | Tape 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.
Begin recording session
| tape_t* | tape | Tape instance from tape_get() |
| uint32_t | pos | Starting position in tape POSITIONS (SD sectors, 64 stereo frames each), NOT frames. |
| bool | reverse | True 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.
Commit recording session
| tape_rec_t* | rec | Recording handle from tape_rec_begin() |
| uint32_t | end_pos | End position of recorded region |
Creates marks for recorded region, commits undo state.
Returns — Final end position
Abort recording (discard without creating marks)
| tape_rec_t* | rec | Recording handle |
Check if undo is available for last recording
| const tape_t* | tape | Tape instance from tape_get() |
Returns — true if undo data is valid
Execute undo - restore original audio from last recording
| tape_t* | tape | Tape instance from tape_get() |
Returns — true on success
Number of recording marks on the tape
| const tape_t* | tape | Tape instance from tape_get() |
Returns — Mark count (0 if none / no tape)
Get the start/end of mark `idx`
| const tape_t* | tape | Tape instance from tape_get() |
| uint32_t | idx | Mark index (0 .. tape_marks_count()-1) |
| uint32_t* | start_frame | Output: region start, in stereo frames |
| uint32_t* | end_frame | Output: region end, in stereo frames |
Returns — true if idx is valid and outputs were written
Read recorded audio into a RAM buffer (stereo-interleaved L,R,...)
| const tape_t* | tape | Tape instance from tape_get() |
| uint32_t | pos_frame | Start position in stereo frames |
| float* | dst | Destination buffer (must hold 2*n_frames floats) |
| uint32_t | n_frames | Number 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().
Write audio to the tape (stereo-interleaved L,R,...)
| tape_t* | tape | Tape instance from tape_get() |
| uint32_t | pos_frame | Start position in stereo frames |
| const float* | src | Source buffer (2*n_frames floats) |
| uint32_t | n_frames | Number 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.
Claim a pair of DMA windows from the tape's pool
| tape_t* | tape | Tape instance from tape_get() |
Returns — NULL when the pool is exhausted - ALWAYS check, and fail init() if so
Release the windows back to the pool. Call from deinit().
| tape_buffer_t* | s |
Request the window needed NEXT, and swap the other one in to play now
| tape_buffer_t* | s | Window pair |
| uint32_t | pos | Tape POSITION, and it must sit on the TAPE_WINDOW_POS grid |
| bool | write | true 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
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.
The other window of the pair - the one being prefetched into.
| 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.
Drop both windows and force a re-read - after a seek that changes content.
| tape_buffer_t* | s |
Zero the resident window from `sample_offset` (in floats) to its end
| tape_buffer_t* | s | |
| uint32_t | sample_offset |
Note — What the decks use to keep the button click out of the tail of a take.
Add a cue (a navigable point of interest) spanning [start, end) frames
| tape_t* | tape | |
| uint32_t | start_frame | |
| uint32_t | end_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
Number of cues on the tape
| tape_t* | tape |
Read cue `idx` (0..tape_cues_count-1), in stereo frames
| tape_t* | tape | |
| uint32_t | idx | |
| 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
Erase [start_frame, end_frame): audio, marks, and cues
| tape_t* | tape | |
| uint32_t | start_frame | |
| uint32_t | end_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