The engine graph, block by block
Engine::process(input, in_ch, output, out_ch):
- Apply every queued
Cmd(apply), counting them incmds_done. - Collect live MIDI from the ring into
live(delivered with the first sub-block only). - Split the device buffer into sub-blocks of at most
MAX_BLOCKframes, also cutting at the cycle end so a loop wraps on the exact sample. For each sub-block runprocess_block, copy the stereo bus to the output (mono devices get the average), advance the position and wrap at the cycle end. - Publish position, play/record state and the CPU load (smoothed ratio of time used to the
buffer’s duration), with the worst recent block’s load and the count of blocks over 100%
(
BlockLoad; not during offline renders).
process_block does this for every track, in processing order:
flowchart TD
A["Automation (Read/Touch/Latch):<br/>volume, pan, mute, send levels at mix time;<br/>plug-in params at fx time"] --> B
B{"track kind"}
B -- Folder --> Z["skip (folders only shape their subtracks' params)"]
B -- "Summing / Aux" --> C["take sum_l / sum_r (what feeders wrote), clear them"]
B -- Audio --> D["play_audio(RegionDsp list)<br/>+ input monitoring<br/>+ push input to the recorder ring"]
B -- Instrument --> F["note flush, CC chase, region notes and CC,<br/>live MIDI (thru, record)<br/>-> instrument plug-in<br/>+ play_audio (freeze file)"]
C --> G
D --> G
F --> G
G["Channel EQ"] --> H["Insert plug-ins, VST3 or built-in<br/>(bypass / Low Latency skip; contain() after each)"]
H --> I["PDC delay line write<br/>(side-chain key tapped here)"]
I --> J["Pre-fader sends (own tap)"]
J --> K["Fader + pan, smoothed; meter"]
K --> L["Output: parent/aux sum_l/sum_r or the Stereo Out bus"]
L --> M["Post-fader sends (own tap, same gains)"]
A track that is off (Track::off, or a stack above it: Project::is_off, sent as
TrackParams::off) skips all of this. It first fades through its fader like a muted one, then
goes dormant (dsp/track_off.rs): its VST3s get setProcessing(0) (allowed on the audio
thread, and meant to clear tails; setActive stays on and the plug-in loaded), built-in effects
and synths reset, the EQ, delay line and key buffers empty, and each block only empties what
feeders wrote into its bus. Its meter stops being fed, so it falls. A VST3’s editor edits still
reach its processor, through one block of silence when any are pending. Back on, the source fades
in over 5 ms before the plug-ins and the delay line, notes are released and chased, and
automation is resent. The session leaves it out of delay compensation (session/pdc.rs). A
track in the middle of a take stays on until the take ends; the session refuses the switch while
recording anyway (session/track_off.rs), so only an undo can get there.
After every track:
- Stereo Out: Channel EQ, its inserts, its volume (smoothed), its meter. Skipped (unity, no FX) in an isolated render.
- The click is synthesized after the Stereo Out fader, delayed by
pdc.totalso it lines up with the compensated music. The count-in always clicks; otherwise the metronome followsClickParams(while_playing,while_recording,only_count_in). Subdivisions, accent and the four sounds (Classic, Click, Woodblock, Beep) are all generated without allocation inClick::next.
Details of each stage:
- RegionDsp and newer-hides-older.
Session::regions_dspflattens a track’s regions into a sorted list of non-overlapping audio segments (plus MIDI regions). It lays loop repetitions first and region bodies second, each one cutting away the older audio under it, so a newer region hides older audio like Logic’s “No Overlap” playback without deleting anything, and a region body always wins over a loop repetition. Muted regions are left out. Take folders lay their comp pieces side by side so their crossfades overlap. Fades are stored as timeline(start, length)plus aFadeLawper side, and shaped inplay_audio. - Fades and crossfades (
fades.rs, session side insession/crossfades.rs). Each audio region’sfade_shape: FadeShapeholds the curves and types (Speed Up, Slow Down, and the crossfade types X, EqP and X S); the default is the old quarter sine.fades::crossfadespairs a region whose fade-out type crossfades with thefollowingregion on the track (touching or overlapping) and places the crossfade where both files have audio (sliding and shortening it, or falling back to butt fades).regions_dspthen stretches both regions over the crossfade and lays them without cutting each other (keep), so both sound there. A take folder takes part like a plain audio region (the comp pieces at its ends carry the crossfade), and a looping region crossfades from its last repetition at its loop end. WithFadeShape::loop_xfade(Crossfade Loop Repetitions)fades::loop_seamsalso crossfades each repetition into the next. Settings › Editing › Crossfade Time and Curve (EditingPrefs::crossfade_ms,crossfade_curve) set those seams, the comp boundaries, the X-Fade mode’s crossfades between regions that only touch, and the Crossfade menu’s first entry; changing them rebuilds the playback lists (set_prefs). Speed fades carry a table of how far the read position lags (speed_table), whichplay_audioreads with linear interpolation; no allocation on the audio thread. Every render (playback, bounce, export, freeze, bounce in place) goes throughplay_audio, and Join mixes through it too (dsp::mix_regions). The X-Fade drag mode (Project::drag_mode) is applied byauto_crossfadeat the end ofmove_region(s),resize_region,nudge_regionsandpaste_regions. The snapshot carriescrossfadesfor drawing. - No Overlap and Shuffle drag modes are pure functions on a lane of
(id, start, sounding length)spans indrag_modes.rs(the rules are written out there; Shuffle R is Shuffle L run on mirrored time).session/drag_modes.rsapplies them on top of the plain edits: the Tauri commands for user edits calldrag_move_regions,drag_nudge_regions,drag_resize_region,drag_delete_regionsanddrag_paste_regions, which snapshot the lanes, run the plain edit and then rearrange or shorten regions. Internal edits (range edits, Bounce in Place, Repeat, Move to New Track) call the plain ones and are left alone.src/dragModes.tsmirrors the rules so the Tracks area can draw a drag’s outcome. - Dragging regions past the last track (
src/newTrackDrag.tsdecides which source lanes get a new track,session/new_track_drag.rsapplies it):move_regions_to_new_tracksandcopy_regions_to_new_tracksmake the tracks as Move to New Track does (duplicate_trackswithout content), put them last and outside any stack, then rundrag_move_regionsordrag_paste_regionsonce with everything, so the drag modes apply and it’s one undo step. - Move Automation with Regions (
prefs.editing.move_automation) is applied by the same move and nudge calls:auto_move.rscarries each moved region’s stretch of every lane on its track to the new place, bridging the old one, and the track’s lanes are resent withpush_automation; undo restores them throughrestore()’s automation diff. The commands take an optionalautomationanswer, which the frontend’s Ask dialog (src/moveAutomation.ts) passes. - Audio tracks being recorded are silent (or, with Autopunch, silent only inside the punch range, so the old take is heard before and after). Input is heard when the I button is on, or on armed tracks when Auto Input Monitoring is on.
- Mono sources use a -3 dB compensated sine pan law (unity in the centre); stereo sources use balance.
- Summing stacks and auxes: a feeder writes into its destination’s
sum_l/sum_rthroughout_idx(output) andsend_idx(sends), indices thatresolve_routeskeeps current. Because receivers come later in the list, the split borrowtracks.split_at_mut(ti + 1)gives the current track mutable access to every track after it. - Instrument outputs: an aux with
TrackParams::instrument_outputalso pulls that output bus from its instrument (dsp/multi_out.rs), which played earlier in the block becauseProject::feedscounts the edge inprocess_order. It reads the plug-in’s own buffers, through a line of its own (PdcParams::feed) if the aux’s other feeders arrive later, with a gain that fades when the instrument goes off. An instrument that didn’t run this block (instrument_ran), or has no such bus, gives nothing. - Sends: pre-fader sends tap after the inserts; post-fader sends after the fader and pan.
Mute and solo silence both. With delay compensation, each send reads the track’s delay line
at its own tap (
pdc.sends[k]); a post-fader send at a different tap than the output re-applies the same per-sample fader gains (gain_l/gain_r). - Automation timing: plug-in lanes are evaluated
pdc.input_timelate and fader, pan and send lanespdc.output_timelate, so automation follows the compensated audio. Plug-in values are sent twice per block (start and end) so the plug-in can ramp; unchanged values are not resent (auto_sent), except after a jump or mode change. Parameters the user is holding (touched) are skipped so the live value wins. - MIDI: notes outside regions are cut at the region end;
soundingtracks held notes so a flush (stop, locate, a change of the track’s regions or instrument, which chase afterwards) can send note-offs.Cmd::MidiTargetandCmd::MidiThruare sent after many edits and only act on a real change, and then only on the old target with a key down (live_held): where live MIDI goes is nothing to do with what the regions play. CC chase: when playback starts or jumps, the latest value of every controller before the playhead (in the region that covers it) is sent first, so a region started in the middle sounds right. A flush after controllers were played also releases sustain and centres pitch bend. Note chase: right after, the notes held over the playhead (midi::sounding_at, a slice of the region’s sorted notes, no allocation) are started at offset 0, so a long note sounds when playback starts, jumps or wraps a cycle inside it; the region loop sends their note-off as usual. Both chases follow Settings › MIDI › Chase (Cmd::Chase, sent bysync_globalsandset_prefs; offline renders use the same engine, so they follow it too). - Live MIDI goes to
midi_target(the selected instrument track, else the first armed one). With MIDI Thru off it is recorded but not played. - Built-in effects and side-chains: a built-in effect runs in its insert slot like a VST3,
given its key signal when it has one (see Built-in effects).
A VST3 with an aux input gets the same signal there, as a second input bus in
ProcessData(a mono aux takes a mix-down; see The VST3 host).InsertDsp::keyholds the route for both. A track that keys another (keyed) copies its signal as it leaves its plug-ins, before its compensation delay, intokey_l/key_rand applies the fader gains to it, so the key is post-fader. A pre-fader key (keyed_pre) is copied at the same point intopre_key_l/pre_key_r, silenced by mute but not scaled by the fader.