Skip to content

Cross-cutting patterns

A few patterns recur across features. Follow them when adding one.

The project stores what the user edited; what plays is derived from it when the session builds a track’s playback list (Session::regions_dsp), never on the audio thread. The engine only ever sees plain RegionDsp::Audio segments and RegionDsp::Midi regions, so a feature built this way needs no new Cmd and plays the same in every render. regions_dsp applies, in order:

  1. Freeze (freeze_dsp): a frozen track plays only its freeze file.
  2. Region playback parameters (params_map, delay_audio in session/region_params.rs; the transforms in region_params.rs): an audio region’s delay moves it, and a MIDI region with parameters becomes one transformed RegionDsp::Midi per pass (region_params::pass).
  3. Pattern regions (realize_patterns in session/patterns.rs, pattern::realize): each becomes the plain MIDI region its pattern plays.
  4. Comps and crossfades: take folders expand into comp pieces (takes::pass_pieces_edged), and fades::crossfades and fades::loop_seams place the crossfades between neighbours and loop repetitions.

MIDI take folders need no step of their own: the region’s content always holds the notes of the take it plays (see Take folders and comps). Code that reads regions directly instead of the playback list (Join, MIDI file export, render ranges) calls region_params::bake (which realizes a pattern first) or region_params::bounds. Transforms that read the tempo map are rebuilt on tempo edits by push_tempo_regions, and the snapshot’s played list (played_notes) gives the Tracks area the notes as they play, while the Piano Roll and Event List edit the stored ones. A new transform goes into regions_dsp and bake, and into push_tempo_regions if it follows the tempo.

Edits that move whole stretches of the timeline, the Arrangement track’s moves, copies and deletes and Edit › Section Between Locators, are described as a time map: a list of arrangement::Piece { start, end, dst, copy } saying where each stretch of the old timeline lands. The pure side builds it (arrangement::move_section, copy_section and delete_section return a Plan of pieces plus the new sections; arrange_cmds::section_pieces for Insert Silence, Cut and Repeat Section), and one apply step maps everything through it: regions with session/arrangement.rs remap_regions (a region inside one piece moves whole, one across a piece edge is cut with range_edit::sub_region), automation lanes with arrangement::remap_points, markers with arrangement::map_pos, and, for the section edits, the arrangement sections with arrange_cmds::remap_sections. The tempo and signature tracks never move. With a marquee, the section edits map only its tracks (session/arrange_cmds.rs section_edit takes a scope). Every apply refuses, changing nothing, when a frozen track’s regions would change.

Sheets (anything modal drawn over the window: Sheet in Dialogs.tsx, the Settings frame, the name sheet, the Project Chooser) register with useSheetKeys (src/sheetKeys.ts). installSheetKeys, called in main.tsx before any other listener, applies one rule (sheetKeysModel.ts, heldBack and outsideKey): keys typed in a sheet’s fields stay there (sheetKey in Form.tsx stops them); keys pressed with the focus outside it never reach the app, except that Esc closes the top sheet, Return presses its default button and Tab moves the focus. A "non-modal" sheet (Controller Assignments, a window in Logic) lets the transport and Control Surfaces commands through (NON_MODAL_COMMANDS) and holds back the rest. onKey and keys passed on from panel windows ask sheetBlocks(id). A new sheet should use Sheet or call useSheetKeys; tests/sheetKeys.test.ts checks that every sheet does.

Each window has its own store, and detached windows agree only on what is listed in SYNCED_FIELDS (detachModel.ts), sent as ui-sync patches, plus the settings (ui-settings) and the session’s snapshot (project-sync). Anything a user would expect to be the same everywhere, such as a selection or a clipboard, belongs in the store as a synced field rather than in a module variable: the region, channel strip, plug-in and Channel EQ clipboards (clipboard, stripClip, pluginClip, eqClip) are store fields for that reason. Synced values are compared by identity, so replace them, never mutate them. View state that is per window (which bottom pane shows, zoom, scroll) stays out.

Some rules are enforced by tests that read the sources rather than import them: tests/storeSelectors.test.ts (a useStore selector must not build a new array or object, or React re-renders forever and the window goes blank), tests/commands.test.ts (every key command and menu id is handled, no two default keys clash), tests/tauriCommands.test.ts (every command the frontend calls is registered, with its required arguments in camelCase), tests/browserDialogs.test.ts (no window.prompt, confirm or alert) and tests/sheetKeys.test.ts.