Follow the audio path to investigate why a part is silent or why an expected signal is missing.
What it is#
An on-demand, advisory analysis over the compiled IR (compiler/src/doctor.rs). It reuses refine.rs's interval arithmetic and re-parameterizes it: "what does this synth do as loaded?" is answered by pinning every modulation-free macro to its declared initial and re-computing intervals; "can any knob position make it sound?" by leaving macros free. Findings never appear during a normal compile — no diagnostics-channel noise, no golden-snapshot churn, nothing for @strict to trip on. (The two sample detectors are the one exception, and only for a host that passes a sample manifest — see As compile warnings below.)
Severities are honesty-graded:
- silent — provable: an exact-zero amp interval, a pattern with no sounding events, a structurally muted strip, a missing sample bank.
- suspect — heuristic with honest error bars: silent until a knob moves, a closed filter (filters leak), an empty scene, a solo shadowing the rest — and the "sounds wrong, not silent" class (a railed resonance, a model at the wrong rate).
Each finding names its owner (synth/track/song/mixer), carries a source span and a culprit chain — a walk from the field into the zero:
suspect: `pad` is silent as loaded: macro "level" (initial 0) multiplies `gain` — turn the knob or raise the initial
--> line 4, col 12
|
| gain = vel * level
| ^^^^^^^^^^^
↳ a factor of this product is 0 — zero times anything is silence (line 4)
↳ macro "level" sits at 0 (its declared initial) — the knob hasn't moved (line 4)
Carets render only for entry-bound owners (imported bindings carry offsets into their own module source — the standard provenance gate).
Detector catalogue#
Silent: DeadOutput (amp path [0,0] with macros free), VelocityZero (|> velocity 0 with no gain floor), GainTrackZero (constant zero), StripMuted, StripLevelZero, MasterZero, EmptyPattern (all rests / combinator ate everything), UnknownSample (needs a manifest; bd:3 variant suffixes strip to the bank; bounce handles are always known), UnknownVariant (needs a manifest: a bank:N at or past the bank's file count — a sound event's own :N, a |> variantP lane value, or a sample "bd:7" node; the desktop engine plays silence for it and the browser plays files[N % count], so the message names the wrapped index to write. bd:? / bd:* are never out of range). UnresolvedRemoteSample (a remoteSample reference this machine's capture cache lacks — volta doctor and MCP doctor read ~/.volta/captures; it is Suspect when no host was asked; Remote samples: a streaming track as a sample).
Suspect: GainTrackZero when a live macro starts at zero but can raise the level; MacroSilence (as-loaded zero, attributed to the zero-init macro by single re-pin), FilterKill (lp cutoff wholly < 25 Hz / hp wholly > 16 kHz, potential or as-loaded; a macro declared in physical units — macro 800Hz, a patch site's minted mix: knob — spans its declared range in the potential model, not the [0, 1] a normalized macro does), SendZero, SoloShadow, EmptyScene (with bar count from the arrangement), NotInSong, SceneMacroKill (a with { m = 0 } that zeroes a synth in that scene), ResonanceUnit (a value never below 1 on the Normalized 0..1 resonance of ladder/diode/korg35; a Q-written one is refused at check time — not silence but a filter railed toward self-oscillation; the finding sits on the literal and names the filter, the value and the fix: write the normalized value, or use svf/lpf, whose resonance is a Q. It is refine's compile warning of the same name (refine::resonance_unit_hits, one detection), so the workbench and LSP show it once, as that warning, not again as a doctor: row); LatencyMismatch (a track insert that declares latency N — a user effect … on track latency N — whose wet path, rendered bypassed vs active through the engine and cross-correlated, lines up at a different lag; the only detector that renders audio, so it runs only when some insert declares a latency, in the CLI and the MCP doctor tool — not in the LSP or workbench diagnostics; engine/src/latency_probe.rs, see Write a custom audio processor § Live effect parameters). Opt-in via --all: NeverPlayed (off by default — morph endpoints and template bases are healthy unplayed synths).
Surfaces#
CLI
volta doctor song.volta
volta doctor --json song.volta # structured findings
volta doctor --samples=runtime/public/samples/index.json song.volta
volta doctor --all song.volta # + NeverPlayed
Always exits 0. --samples takes the manifest (index.json; its top-level keys are the bank names, each entry's file list — or zones — its variants) and enables UnknownSample and UnknownVariant.
As compile warnings — the two sample detectors are the only findings that also reach the ordinary warning channel, and only where a host holds a manifest (doctor::SampleIndex; doctor::sample_diagnostics / apply_sample_index append them to module.diagnostics after lowering, as warn rows of kind UnknownSample / UnknownVariant, located at the event's token when it lies inside the track's declaration, else at the declaration). volta --samples=<index.json> song.volta prints them as warning[UnknownVariant]; the desktop workbench reads the open song's bank names (app-native/src/samples.rs::sample_index) and every analysis passes them as ProjectAnalysis.samples, so they land in the problems panel; the browser workbench does the same once the page calls the UI wasm's set_sample_manifest(text). Without a host manifest every compile still checks UnknownSample against the STOCK manifest's bank names (runtime/public/samples/index.json, embedded by compiler/build.rs; doctor::stock_sample_index) plus the module's own bakes, bounces, remote samples and record targets, so sound "kik snar hatt" warns in a plain compile, the CLI, the LSP and the golden IR. A host's own index supersedes that check: apply_sample_index drops the stock rows before appending its own, so a bank in a song's samples/ folder never warns there. A host-less compile (no --samples) cannot see private sample folders, so a song that plays one warns UnknownSample there. UnknownVariant stays host-only. An unindexed stock-root folder's UnknownSample says to merge its entry. Lowering also warns SampleNamesOnSynth (no manifest needed) when a synth track's pattern holds sample-name tokens (play kick !"x x x x"): a synth voice triggers only on pitch and number events, so the engine skips those events; onTrigger tracks are exempt; fs2 is respelt f#2. @strict never sees either (both are added after the strict pass).
Workbench + LSP — one path, ide::analyze_identified_with_project with ProjectAnalysis.doctor = true (compiler/src/ide.rs). Findings arrive as Information-severity diagnostics prefixed doctor:, span-gated to the owner's own file, and land in the workbench's problems panel and squiggles alongside every other advisory. The LSP disables them with initializationOptions: { "doctor": false }; the workbench passes doctor: false only for the code-action pass (ui/src/intel.rs).
The TypeScript workbench's automatic SilenceWatch — a report fired after >2 s of playing silence — went with the TypeScript workbench on 2026-08-09; see git history.
Guarantees & limits#
- A
silentfinding on a sounding song is a bug — enforced bycompiler/tests/doctor_corpus.rsover the wholeexamples/songs/corpus againstruntime/public/samples/index.json(zero tolerance, dir-driven), so a corpus song with an unknown bank or an out-of-rangebank:Nfails it. - The interval model is conservative: absence of findings never proves a song is audible; presence of
silentproves that piece renders silence. - Macros with modulation graphs are never pinned (the graph can lift a 0 base), so no false MacroSilence on modulated knobs.
- Doctor exposed real corpus bugs on its first runs (missing sample banks in tapestop/fmtest/toccata/halcyon, later glassraver/neon) — held in the corpus test's
KNOWN_CORPUS_BUGSallowlist until fixed.
Compile warnings#
Doctor is on demand; the ordinary compile carries its own advisories in Module::diagnostics at level warn. Plain volta <file>.volta prints them to stderr before the declaration dump — refinement verdicts as refinement:, everything else as warning[Kind]: (grep ^warning) — and the workbench and LSP show them as warnings. The common kinds:
| Kind | Says |
|---|---|
TrackHeadroom | a track's worst-case peak (voice peak × concurrent voices × gainTrack × velocity, pre-track-effects) is over 0 dBFS. When the patch's ampEnv gates its output, the voice count is the track's own notes weighted by where each note's envelope stands when the next one peaks (a 30 ms hat counts ≈ 1 voice however large its pool). A circuit drum (kick808 … cymbal606) reached from out through products, sums of drums or a plain unbiased drive gates the same way by its own decay: each model's T60 from its decay knob's upper bound, times a per-model overshoot measured offline (the output saturator's small-signal gain; a plain drive multiplies in its own), so a 909 kick on four to the floor counts 1 voice. The drums' voice peak stays the full-scale [-1, 1] — every model reaches it — and engine/tests/drum_voices.rs pins both the peak and the tail table against a knob sweep. Otherwise the count is the patch's voice bound. The estimate is in voice units: playback scales every voice by VOICE_GAIN 0.5 and the master by 0.7, so a track at 1.0 here meters ≈ −9 dBFS on the master. The estimate is pre-fader; the mixer strip's fader only shapes the advice — past +6 dB it says "lower the fader" rather than "trim the patch", and on a track that fits it suggests raising the patch and lowering the fader. |
OutputPeak | one voice's own peak bound exceeds full scale. Most stages bound structurally; four are calibrated against the engine's DSP by engine/tests/peak_calibration.rs in both directions (never beaten, never loose past a stated factor): tremolo (its exact curve maximum, ×1.27 at depth 0.5), zFilter (a per-mode cutoff gain table under a hard 1.52·10·level ceiling — The effects reference), and a modal bank struck by strike/pick/scrape (a seeded-noise model of the bank's real gain, 3.3 σ at 48 kHz, over the MIDI pitches the song's tracks play — Strings, bells and resonating objects). A modal patch no track plays is bounded down to MIDI 0 and reported at ok ("not statically proven") instead of warned. |
PolyphonyBound | more simultaneous voices than the pool allows; the pool is per PATCH, shared by every track that plays it. |
InAudibleRange | a cutoff's range leaves (or cannot be shown inside) 20 Hz–20 kHz. On a morph patch this is an analyzer artifact (Morph between patches § Diagnostics). |
ResonanceUnit | a value always above 1 reaches the Normalized 0..1 resonance of ladder/diode/korg35, which clamps and rails it (Filters: shape brightness and resonance); a Q-written value there is a check-time error. |
BareNumberUnit | a bare number in a seconds, milliseconds or Hz port reads as a duration or frequency the port cannot use — outside its declared clamp, or a narrow per-unit default (over 10 s, under 1 ms, an audio-band frequency under 20 Hz) — and names the suffix that fits: "duck's attack reads 5 as 5 seconds — did you mean 5ms?" (Named options and parameter values). |
SceneMacroInert | a scene automates a macro nothing reads, so the automation is silent. |
GroupLevelMacroInert | a group level bound to a macro only sets the fader's starting position; the macro does not ride it (Mixing tracks and buses). |
PatternRefinement | a Pattern P | preds obligation does not hold over the pattern's events (Explore choices with constraints). |
ArrangeCellScale | a timing-v1 arrange cell's fast/slow is erased. |
NudgeCycleSpan | a bare or c nudge, a hair against one bar, that the track's long cycle makes ≥ 1/16 beat; names the offset and the absolute spelling (Control patterns for individual events). |
GlideWithoutPitch | a patch sets glide but its graph never reads the note pitch (a chop/slice read or a grain-motion cloud does not glide), so the engine drops the glide and the persistent voice with it: cross-note state (accent, bowed) restarts per note (Glide between notes). voices = 1 keeps the mono voice without the dead directive. |
UnemittedSong | a song binding produced no arrangement (Arrange a song). |
DspLoad | the module is heavy against the realtime budget (Understand audio processing cost); advisory, and spent on purpose by much of the corpus. |
Every message names its own fix. compiler/tests/warning_corpus.rs gates the song corpus on them: each examples/songs/*.volta must produce exactly the (song, kind) pairs listed in compiler/tests/fixtures/warning_allowlist.txt — a new warning fails until the song is fixed or the line is added with a reason, and a listed pair that stops firing fails until its line is deleted. DspLoad, and InAudibleRange on a binding written morph a b t, are exempt by rule in the test.
Files#
compiler/src/doctor.rs (detectors, chains, render_finding, SampleIndex, sample_diagnostics) · compiler/src/refine.rs (pub(crate) Interval/compute_intervals) · compiler/tests/doctor_corpus.rs (honesty audit) · cli/src/main.rs (doctor subcommand) · wasm/src/lib.rs (doctor() export) · compiler/src/ide.rs (the doctor: Information diagnostics both the workbench and the LSP read) · lsp/src/analysis.rs (the LSP toggle).