Skip to content

The engine graph, block by block

Engine::process(input, in_ch, output, out_ch):

  1. Apply every queued Cmd (apply), counting them in cmds_done.
  2. Collect live MIDI from the ring into live (delivered with the first sub-block only).
  3. Split the device buffer into sub-blocks of at most MAX_BLOCK frames, also cutting at the cycle end so a loop wraps on the exact sample. For each sub-block run process_block, copy the stereo bus to the output (mono devices get the average), advance the position and wrap at the cycle end.
  4. 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.total so it lines up with the compensated music. The count-in always clicks; otherwise the metronome follows ClickParams (while_playing, while_recording, only_count_in). Subdivisions, accent and the four sounds (Classic, Click, Woodblock, Beep) are all generated without allocation in Click::next.

Details of each stage:

  • RegionDsp and newer-hides-older. Session::regions_dsp flattens 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 a FadeLaw per side, and shaped in play_audio.
  • Fades and crossfades (fades.rs, session side in session/crossfades.rs). Each audio region’s fade_shape: FadeShape holds 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::crossfades pairs a region whose fade-out type crossfades with the following region 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_dsp then 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. With FadeShape::loop_xfade (Crossfade Loop Repetitions) fades::loop_seams also 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), which play_audio reads with linear interpolation; no allocation on the audio thread. Every render (playback, bounce, export, freeze, bounce in place) goes through play_audio, and Join mixes through it too (dsp::mix_regions). The X-Fade drag mode (Project::drag_mode) is applied by auto_crossfade at the end of move_region(s), resize_region, nudge_regions and paste_regions. The snapshot carries crossfades for drawing.
  • No Overlap and Shuffle drag modes are pure functions on a lane of (id, start, sounding length) spans in drag_modes.rs (the rules are written out there; Shuffle R is Shuffle L run on mirrored time). session/drag_modes.rs applies them on top of the plain edits: the Tauri commands for user edits call drag_move_regions, drag_nudge_regions, drag_resize_region, drag_delete_regions and drag_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.ts mirrors the rules so the Tracks area can draw a drag’s outcome.
  • Dragging regions past the last track (src/newTrackDrag.ts decides which source lanes get a new track, session/new_track_drag.rs applies it): move_regions_to_new_tracks and copy_regions_to_new_tracks make the tracks as Move to New Track does (duplicate_tracks without content), put them last and outside any stack, then run drag_move_regions or drag_paste_regions once 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.rs carries 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 with push_automation; undo restores them through restore()’s automation diff. The commands take an optional automation answer, 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_r through out_idx (output) and send_idx (sends), indices that resolve_routes keeps current. Because receivers come later in the list, the split borrow tracks.split_at_mut(ti + 1) gives the current track mutable access to every track after it.
  • Instrument outputs: an aux with TrackParams::instrument_output also pulls that output bus from its instrument (dsp/multi_out.rs), which played earlier in the block because Project::feeds counts the edge in process_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_time late and fader, pan and send lanes pdc.output_time late, 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; sounding tracks held notes so a flush (stop, locate, a change of the track’s regions or instrument, which chase afterwards) can send note-offs. Cmd::MidiTarget and Cmd::MidiThru are 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 by sync_globals and set_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::key holds the route for both. A track that keys another (keyed) copies its signal as it leaves its plug-ins, before its compensation delay, into key_l/key_r and applies the fader gains to it, so the key is post-fader. A pre-fader key (keyed_pre) is copied at the same point into pre_key_l/pre_key_r, silenced by mute but not scaled by the fader.