Skip to content

Recipes

Example: a per-project “Notes” field (it exists as Project::notes).

  1. crates/engine/src/model/project.rs: add the field at the end of Project with #[serde(default)], and initialise it in Project::new.
  2. src/api.ts: add it to the Project interface.
  3. Add a Session method that changes it and sets self.dirty = true (in the most fitting session/ module, options.rs for project options).
  4. Add an undoable command for it (next recipe).
  5. If the engine needs the value, add a Cmd (recipe after next), send it from sync_globals() (for project-wide values) or build_track() (per track), and make sure restore() resends it when it differs.
  6. Build anything that constructs a Project or the struct you changed by hand (tests, examples) and fix the literals.
  1. Session. Write the edit as a method on Session. It changes self.project, sends what the engine needs (push_regions, Cmd::Params via push_all_params, sync_tracks for structural changes) and sets self.dirty = true. Return Result if it can fail.

  2. Command. At the end of the fitting src-tauri/src/commands/<area>.rs (add a module in commands.rs if none fits, with mod <area>; pub use <area>::*;):

    /// Sets the project notes (Project Settings).
    #[tauri::command]
    pub fn set_project_notes(state: State<AppState>, notes: String) -> Snapshot {
    edit(&state, "Project Notes", Some("notes".into()), false, |s| s.set_notes(notes))
    }

    Use try_edit when the method returns Result, and edit_made when the UI needs the ids of what was created. The label is what Edit › Undo shows. Give a key when a drag or a typing burst should merge into one undo step. Pass plugins = true when the edit can remove plug-ins, so undo can bring their state back.

  3. Register it at the end of the invoke_handler list in src-tauri/src/main.rs.

  4. Call it from the UI: act("set_project_notes", { notes }), or actMade("cmd", { ... }) to get the new ids. Argument names are camelCase in JS.

  5. Check undo: make the edit, undo, redo, and check that the engine plays the right thing (a headless example is the best way). If the engine holds state for it, restore() must resend it.

  1. Add the variant at the end of enum Cmd in crates/engine/src/dsp/cmd.rs, with a doc comment. Prefer small Copy payloads; anything allocated is built by the session and moved in (Box, Arc).
  2. Handle it in Engine::apply (dsp/apply.rs). If it replaces an allocated object, send the old one back with self.discard(Garbage::...) (add a Garbage variant if needed). Never drop allocations on the audio thread.
  3. Use the new state in process_block (dsp/process.rs) or run_strip (dsp/strip.rs) without allocating. Preallocate buffers in TrackDsp::new (dsp/track.rs) or Engine::new (dsp.rs).
  4. Send it from the session with self.send(Cmd::...).
  5. Make it survive everything that rebuilds or diffs the graph: build_track() (new tracks and project load) and sync_globals() (global values, called after load and undo) in session/engine_sync.rs, restore() (undo and redo) in session/undo.rs, and, if offline renders must save and restore it, Engine::render in dsp/offline.rs.
  6. Add a unit test in dsp/tests.rs (see the rig() helper) or a headless example.
  1. src/keymap.ts: add an entry to KEY_COMMANDS with a unique id, the label shown in Settings › Key Commands, a group from KEY_GROUPS and the default keys ("Mod+..." for ⌘/Ctrl, "Ctrl+..." for the macOS Control key, then Alt, Shift). Check that the keys don’t clash with MENU_SHORTCUTS or another command.
  2. src/App.tsx: add the action to KEY_ACTIONS under the same id. If a menu item does the same thing, call handleMenu("<menu id>") so both stay in step.
  3. Settings › Key Commands lists the new command automatically, and user overrides work through settings.json › ui.keymap. npm test (tests/commands.test.ts) fails if the id has no action or its default key is already taken.
  4. If an editor (Piano Roll, Audio Track Editor) should handle the key differently while it has focus, handle it in that editor’s own key listener and stop propagation there.
  1. src-tauri/src/menu.rs: add .item(&item("my-id", "My Command…", None)?) next to related items. Use an accelerator (Some("CmdOrCtrl+Shift+X")) only for ⌘ shortcuts; plain keys belong in the keymap so they can be changed.
  2. src/App.tsx: add case "my-id": to handleMenu.
  3. If you gave it an accelerator, add it to MENU_SHORTCUTS in src/keymap.ts so Settings › Key Commands shows it and refuses conflicting keys.
  4. If the command should also run from a key that the user can change, add a key command (above) whose action calls handleMenu("my-id").
  5. List it in the menu bar table in docs/USER_GUIDE.md. tests/commands.test.ts checks that handleMenu handles the id.

Decide where it belongs:

  • Engine behaviour (General, Recording, MIDI, Editing, latency): a field in the matching struct in crates/engine/src/prefs.rs (GeneralPrefs, RecordingPrefs, …). The structs use #[serde(default)]; add the default in their Default impl.
  • Interface only: UiPrefs in src-tauri/src/settings.rs (add the default to its Default impl).
  • Saved with the project: a Project field instead (see above).

Then:

  1. Mirror the field in src/prefs.ts (Prefs or UiPrefs).
  2. Add the control to the right pane in src/components/settingsPanes/, using the Form.tsx controls, and change it with setPrefs or setUi from settingsPanes/parts.tsx, which go through updateSettings. That calls set_settings, which writes settings.json and calls Session::set_prefs.
  3. Apply it in Session::set_prefs (session/options.rs) if the engine must react (send a Cmd, call update_pdc, …), or read self.prefs where it is used.
  4. Add a case to the tests in prefs.rs or settings.rs if parsing could be affected.

See ARCHITECTURE.md › Built-in effects and presets.

  1. crates/engine/src/fx/<name>.rs (or a folder, as fx/reverb/ is, when it’s big): a struct implementing Effect, allocating everything in its constructor; PARAMS (new ids only, never renumbered), meter names, PRESETS, and pub fn info() -> EffectInfo with the id builtin:<name> and a category.
  2. List it in fx::registry() and fx::factory_presets() in fx/mod.rs.
  3. Unit tests in the module: silence stays clean, the reported latency is the real one, the presets set real parameters within range, and it stays stable at extreme settings (see limiter.rs and reverb/tests.rs).
  4. A panel, if the generic one isn’t enough: src/components/fx/<Name>.tsx, built from common.tsx, registered in FX_PANELS in src/fx.ts. Call the panel props’ touch(id, on) around every drag so Touch and Latch automation record it.
  5. Run fx_check (it covers every registry entry) and cargo run --release -q -p daw-engine --example fx_bench to check the cost per block.

The contract is at the top of crates/engine/src/instrument/mod.rs: the same real-time rules as an effect, the same bits after a reset, parameter ids stable forever. instrument/uno/ is a small complete model and tre/ a larger one.

  1. crates/engine/src/instrument/<name>/: mod.rs with a struct implementing Instrument and pub fn info() -> InstrumentInfo with the id builtin:<name>; params.rs and presets.rs.
  2. List it in instrument::registry() in instrument/mod.rs, with a pub mod. Nothing else is needed: menus, the panel (from sections), presets, automation, MIDI learn, freezing and saving all work from the registry entry.
  3. tests.rs, built on instrument/test_kit.rs, for what tre/tests.rs measures and what’s specific to the model. tests/builtinInstruments.test.ts keeps trademarks out of each model’s panel and presets; add the model there, with the names to keep out.
  4. To have the real-time checks cover it, add it to MODELS in crates/engine/examples/common/all_features.rs. Run instrument_check, rt_alloc_check and parallel_check, and cargo run --release -q -p daw-engine --example instrument_bench for the cost per block.

There are four kinds, from least to most wiring. New CSS goes in src/styles/ (see Conventions); use the theme variables.

A sheet. Build it on Sheet from components/Dialogs.tsx, which registers its keys with useSheetKeys (tests/sheetKeys.test.ts fails on a sheet that doesn’t). A flag or a state.dialog value in store.ts shows it, and App.tsx renders it next to the others.

A Settings pane. A file in components/settingsPanes/ exporting a component that takes { close }, ending in Foot, and an entry in tabs in SettingsWindow (Settings.tsx) with an id added to SettingsTab in store.ts.

A built-in effect’s or instrument’s panel. components/fx/<Name>.tsx exporting a component that takes FxPanelProps: info and values to read, set and touch to change a parameter, meters to draw (start from fx/Gain.tsx; fx/common.tsx has the controls). Register it by plug-in id in FX_PANELS (src/fx.ts). That is all: fx/FxPanel.tsx shows it docked, floating or in its own window, with the header controls. Without an entry an effect gets the generic panel, and an instrument the sectioned one from its spec.

A pane in the bottom section.

  1. Add it to BottomPane in store.ts, and show it with setState({ bottom: "<pane>" }) from a key command, a menu item or EditorTabs.
  2. Render it in the layout in App.tsx, next to s.bottom === "steps".
  3. To let it detach, register it in panelKinds.tsx with bottomPanel("<pane>", { title, render, size, own: ownBottom("<pane>") }), and put <DetachButton panel="<pane>" /> in its bar.

For a CSS colour:

  1. src/themes.ts: add "--my-colour": "..." to studioCss. Flat, Midnight, Light and High Contrast inherit it by spreading, so override it only where it must differ (at least check lightCss and contrastCss).
  2. Use var(--my-colour) in the area’s file in src/styles/ (new rules go at the end of it).

For a canvas colour:

  1. src/theme.ts: add the field to CanvasTheme and a value to the default theme object.
  2. src/themes.ts: give it a value in studioCanvas, and override it in the other canvas themes where needed (they spread studioCanvas or flatCanvas).
  3. Draw with theme.myColour. Views redraw on a theme switch when they subscribe to themeId.