Recipes
Add a project field
Section titled “Add a project field”Example: a per-project “Notes” field (it exists as Project::notes).
crates/engine/src/model/project.rs: add the field at the end ofProjectwith#[serde(default)], and initialise it inProject::new.src/api.ts: add it to theProjectinterface.- Add a
Sessionmethod that changes it and setsself.dirty = true(in the most fittingsession/module,options.rsfor project options). - Add an undoable command for it (next recipe).
- If the engine needs the value, add a
Cmd(recipe after next), send it fromsync_globals()(for project-wide values) orbuild_track()(per track), and make surerestore()resends it when it differs. - Build anything that constructs a
Projector the struct you changed by hand (tests, examples) and fix the literals.
Add an undoable command end to end
Section titled “Add an undoable command end to end”-
Session. Write the edit as a method on
Session. It changesself.project, sends what the engine needs (push_regions,Cmd::Paramsviapush_all_params,sync_tracksfor structural changes) and setsself.dirty = true. ReturnResultif it can fail. -
Command. At the end of the fitting
src-tauri/src/commands/<area>.rs(add a module incommands.rsif none fits, withmod <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_editwhen the method returnsResult, andedit_madewhen the UI needs the ids of what was created. The label is what Edit › Undo shows. Give akeywhen a drag or a typing burst should merge into one undo step. Passplugins = truewhen the edit can remove plug-ins, so undo can bring their state back. -
Register it at the end of the
invoke_handlerlist insrc-tauri/src/main.rs. -
Call it from the UI:
act("set_project_notes", { notes }), oractMade("cmd", { ... })to get the new ids. Argument names are camelCase in JS. -
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.
Add an engine Cmd
Section titled “Add an engine Cmd”- Add the variant at the end of
enum Cmdincrates/engine/src/dsp/cmd.rs, with a doc comment. Prefer smallCopypayloads; anything allocated is built by the session and moved in (Box,Arc). - Handle it in
Engine::apply(dsp/apply.rs). If it replaces an allocated object, send the old one back withself.discard(Garbage::...)(add aGarbagevariant if needed). Never drop allocations on the audio thread. - Use the new state in
process_block(dsp/process.rs) orrun_strip(dsp/strip.rs) without allocating. Preallocate buffers inTrackDsp::new(dsp/track.rs) orEngine::new(dsp.rs). - Send it from the session with
self.send(Cmd::...). - Make it survive everything that rebuilds or diffs the graph:
build_track()(new tracks and project load) andsync_globals()(global values, called after load and undo) insession/engine_sync.rs,restore()(undo and redo) insession/undo.rs, and, if offline renders must save and restore it,Engine::renderindsp/offline.rs. - Add a unit test in
dsp/tests.rs(see therig()helper) or a headless example.
Add a key command
Section titled “Add a key command”src/keymap.ts: add an entry toKEY_COMMANDSwith a uniqueid, the label shown in Settings › Key Commands, a group fromKEY_GROUPSand the default keys ("Mod+..."for ⌘/Ctrl,"Ctrl+..."for the macOS Control key, thenAlt,Shift). Check that the keys don’t clash withMENU_SHORTCUTSor another command.src/App.tsx: add the action toKEY_ACTIONSunder the same id. If a menu item does the same thing, callhandleMenu("<menu id>")so both stay in step.- 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. - 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.
Add a native menu item
Section titled “Add a native menu item”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.src/App.tsx: addcase "my-id":tohandleMenu.- If you gave it an accelerator, add it to
MENU_SHORTCUTSinsrc/keymap.tsso Settings › Key Commands shows it and refuses conflicting keys. - 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"). - List it in the menu bar table in
docs/USER_GUIDE.md.tests/commands.test.tschecks thathandleMenuhandles the id.
Add a setting
Section titled “Add a setting”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 theirDefaultimpl. - Interface only:
UiPrefsinsrc-tauri/src/settings.rs(add the default to itsDefaultimpl). - Saved with the project: a
Projectfield instead (see above).
Then:
- Mirror the field in
src/prefs.ts(PrefsorUiPrefs). - Add the control to the right pane in
src/components/settingsPanes/, using theForm.tsxcontrols, and change it withsetPrefsorsetUifromsettingsPanes/parts.tsx, which go throughupdateSettings. That callsset_settings, which writessettings.jsonand callsSession::set_prefs. - Apply it in
Session::set_prefs(session/options.rs) if the engine must react (send aCmd, callupdate_pdc, …), or readself.prefswhere it is used. - Add a case to the tests in
prefs.rsorsettings.rsif parsing could be affected.
Add a built-in effect
Section titled “Add a built-in effect”See ARCHITECTURE.md › Built-in effects and presets.
crates/engine/src/fx/<name>.rs(or a folder, asfx/reverb/is, when it’s big): a struct implementingEffect, allocating everything in its constructor;PARAMS(new ids only, never renumbered), meter names,PRESETS, andpub fn info() -> EffectInfowith the idbuiltin:<name>and a category.- List it in
fx::registry()andfx::factory_presets()infx/mod.rs. - 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.rsandreverb/tests.rs). - A panel, if the generic one isn’t enough:
src/components/fx/<Name>.tsx, built fromcommon.tsx, registered inFX_PANELSinsrc/fx.ts. Call the panel props’touch(id, on)around every drag so Touch and Latch automation record it. - Run
fx_check(it covers every registry entry) andcargo run --release -q -p daw-engine --example fx_benchto check the cost per block.
Add a built-in instrument
Section titled “Add a built-in instrument”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.
crates/engine/src/instrument/<name>/:mod.rswith a struct implementingInstrumentandpub fn info() -> InstrumentInfowith the idbuiltin:<name>;params.rsandpresets.rs.- List it in
instrument::registry()ininstrument/mod.rs, with apub mod. Nothing else is needed: menus, the panel (fromsections), presets, automation, MIDI learn, freezing and saving all work from the registry entry. tests.rs, built oninstrument/test_kit.rs, for whattre/tests.rsmeasures and what’s specific to the model.tests/builtinInstruments.test.tskeeps trademarks out of each model’s panel and presets; add the model there, with the names to keep out.- To have the real-time checks cover it, add it to
MODELSincrates/engine/examples/common/all_features.rs. Runinstrument_check,rt_alloc_checkandparallel_check, andcargo run --release -q -p daw-engine --example instrument_benchfor the cost per block.
Add a panel
Section titled “Add a panel”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.
- Add it to
BottomPaneinstore.ts, and show it withsetState({ bottom: "<pane>" })from a key command, a menu item orEditorTabs. - Render it in the layout in
App.tsx, next tos.bottom === "steps". - To let it detach, register it in
panelKinds.tsxwithbottomPanel("<pane>", { title, render, size, own: ownBottom("<pane>") }), and put<DetachButton panel="<pane>" />in its bar.
Add a theme colour
Section titled “Add a theme colour”For a CSS colour:
src/themes.ts: add"--my-colour": "..."tostudioCss. Flat, Midnight, Light and High Contrast inherit it by spreading, so override it only where it must differ (at least checklightCssandcontrastCss).- Use
var(--my-colour)in the area’s file insrc/styles/(new rules go at the end of it).
For a canvas colour:
src/theme.ts: add the field toCanvasThemeand a value to the defaultthemeobject.src/themes.ts: give it a value instudioCanvas, and override it in the other canvas themes where needed (they spreadstudioCanvasorflatCanvas).- Draw with
theme.myColour. Views redraw on a theme switch when they subscribe tothemeId.