VOLTAreference

Reference / Connect and share

Live audio input

Connect and share

Open in the workbench →
On this page · 5 sections

Bring a microphone or line input into Volta and process the incoming sound with a patch.

What it is#

Capture a microphone or line-in stream and use it as a live signal source inside Volta. The same capture feeds an audio-rate synth source (input "device"), analysis sources for sidechaining and triggering (follower "input", tap "input", onsets "input", pitchOf "input"), and the desktop's input-lane recorder. Because external audio is not reproducible, any module that uses live input is flagged non-deterministic by the compiler, and the capture chain carries its own feedback safety limiter.

Surface#

input "device" is a one-string audio source ("" = system default; a non-empty string is a best-effort preferred-device hint matched against device labels). follower "input" attack release points the follower at the live hub instead of a track bus. A captured buffer registers under live1, live2, … and is referenced like any other sample.

voc = patch { source = input "" |> lpf (const 800Hz) (const 1) |> drive 0.4, gain = vel }
pad = patch { source = saw (freq pitch), gain = vel * follower "input" 8ms 180ms }

How it's implemented#

  • Compiler:
    • compiler/src/builtins.rs — BuiltinKind::Input (Str -> Sig Audio Bipolar).
    • compiler/src/lower/expr.rs — input lowers to NodeKind::Input { device }; follower/tap/onsets/pitchOf accept the reserved source-name string "input", so they need no new IR node.
    • compiler/src/lower.rs sets Module.nondeterministic when any synth contains a NodeKind::Input, an Onsets/PitchOf reading "input", a played Magenta generator, a track triggered from "input", or a shred record with no from; the field lives in ir/src/ir.rs. compiler/src/refine.rs gives Input a [-1, 1] range.
  • Browser host (runtime/src/audioinput.ts) — InputRegistry, peer of SampleRegistry. Owns permission (requestAccess, getUserMedia with echo cancellation / AGC / noise suppression all disabled), device enumeration + devicechange hotplug, the preferred-device match (setPreferredDevice), and the shared capture graph: MediaStreamAudioSourceNode → DynamicsCompressor (limiter) → hub (GainNode). The hub is built eagerly when the context binds, so consumers wire to node() exactly once and a device swap only re-points the source→limiter edge; the hub feeds the engine worklet node's input, which the engine drains inside process. An AnalyserNode tap drives the UI meter (level()). Offline export requires captured input instead of silently substituting a microphone.

Native (desktop engine)#

The engine reaches every live-input surface through a lock-free live-input hub; on the desktop cpal fills it.

  • engine/src/lib.rs — engine_with_input(sr) returns an InputSink (the producer half of an rtrb SPSC ring) and a MidiSource alongside the engine + handle; engine() is a thin wrapper that drops both (an unfed live/test engine reads silence). AudioEngine holds the consumer plus per-block input_l/input_r and input_level; drain_input pulls one interleaved stereo frame per output frame at the top of every process block (zero-fill on underflow) and computes the input RMS with the same convention as track_rms.
  • engine/src/voice/mod.rs — LiveInput { l, r, level }, a Copy value forwarded through the whole render chain next to NoiseBufs. CompiledNode::Input reads the hub L/R; Follower/Tap read it when their SourceBus is Input.
  • engine/src/graph.rs — K::Input lowers to CompiledNode::Input (a stereo source); resolve_bus maps the reserved name "input" to SourceBus::Input for Follower/Tap. CompiledModule::uses_live_input is set when any compiled template touches the hub — the host's lazy-open trigger.
  • engine-host/src/audio.rs — InputControl: opens the default cpal input device on a volta-audio-in thread; the callback resamples to the engine rate (linear), downmixes/spreads to stereo, runs a feedback-safety peak limiter (−1 dBFS ceiling — the native analogue of the browser DynamicsCompressor), and pushes into the ring (a full ring drops the sample, capping latency; never blocks the callback). InputControl holds the InputSink and opens the stream once, on the first ensure_started, which only spawns the thread: the device lookup, config query and stream build all run on it, so a machine with no microphone logs once and the song reads silence. (This lived in app-tauri/ until that shell was deleted 2026-07-20.)
  • app-native/src/main.rs — wire_io calls InputControl::ensure_started when CompiledModule::uses_live_input, so the OS microphone-permission prompt appears only for songs that actually use live input (the desktop analogue of the browser's gesture-gated requestAccess). The "audio in" header pill also calls ensure_started, and the meter reads EngineHandle::input_level().
  • macOS permission (required). cpal input is silently denied without NSMicrophoneUsageDescription; the packaged bundle supplies it (app-native/macos/Info.plist). An unbundled volta-studio dev binary relies on the OS attributing the prompt to the launching terminal.
  • The browser "audio in" pill. In engine mode, NativeRuntime.ensureAudioInput (runtime/src/native-runtime.ts) bridges the pill to the worklet engine's input path and polls the live-input RMS into the shared AudioRuntime.inputLevel() meter accessor. Loading a live-input song ALSO opens capture lazily, so either path works.

The device hint is not acted on anywhere: the desktop always captures from the default cpal input device, and the browser's preferred-device match is best-effort label matching. Round-trip latency is measured on the desktop — InputControl::latency_samples plus the output latency, forwarded via set_input_latency — and take capture shifts by it (Recording and takes §2.3); direct hardware monitoring is still deferred.

Notes & limits#

  • Non-deterministic. Live input cannot be reproduced; the compiler flags the module (Module.nondeterministic). Export/bounce requires recorded sample material. The Rust checked offline APIs also accept captured stereo PCM at the render rate, starting at transport sample zero and zero-filling after EOF. Missing capture, non-finite samples and mismatched rates produce errors.
  • Feedback safety. The capture chain runs through a brickwall-ish DynamicsCompressorNode (threshold −6 dB, ratio 20, 3 ms attack) before the hub, in addition to the master limiter, so an open-mic feedback path can't run away.
  • Permission is gesture-gated. requestAccess() must be called from a user gesture; the hub stays silent until granted. The preferred-device hint is best-effort label matching — the UI device picker is the live authority.
  • Live sampling (live1, live2, …) has no UI. The old browser "rec 1 bar" button went with the TypeScript workbench on 2026-08-09; InputRegistry.captureBuffer still exists but nothing calls it. The desktop equivalent is the workbench's arm input rec chip, which writes the live input to ~/Documents/Volta/Recordings rather than registering a live<N> sample. Re-landing a one-gesture capture-to-sample is open work.
  • Deferred: direct hardware monitoring. See ideas.md §9 and Live audio input §5.