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—inputlowers toNodeKind::Input { device };follower/tap/onsets/pitchOfaccept the reserved source-name string"input", so they need no new IR node.compiler/src/lower.rssetsModule.nondeterministicwhen any synth contains aNodeKind::Input, anOnsets/PitchOfreading"input", a playedMagentagenerator, a track triggered from"input", or a shredrecordwith nofrom; the field lives inir/src/ir.rs.compiler/src/refine.rsgivesInputa[-1, 1]range.
- Browser host (
runtime/src/audioinput.ts) —InputRegistry, peer ofSampleRegistry. Owns permission (requestAccess,getUserMediawith echo cancellation / AGC / noise suppression all disabled), device enumeration +devicechangehotplug, 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 tonode()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 insideprocess. AnAnalyserNodetap 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 anInputSink(the producer half of anrtrbSPSC ring) and aMidiSourcealongside the engine + handle;engine()is a thin wrapper that drops both (an unfed live/test engine reads silence).AudioEngineholds the consumer plus per-blockinput_l/input_randinput_level;drain_inputpulls one interleaved stereo frame per output frame at the top of everyprocessblock (zero-fill on underflow) and computes the input RMS with the same convention astrack_rms.engine/src/voice/mod.rs—LiveInput { l, r, level }, aCopyvalue forwarded through the whole render chain next toNoiseBufs.CompiledNode::Inputreads the hub L/R;Follower/Tapread it when theirSourceBusisInput.engine/src/graph.rs—K::Inputlowers toCompiledNode::Input(a stereo source);resolve_busmaps the reserved name"input"toSourceBus::InputforFollower/Tap.CompiledModule::uses_live_inputis 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 avolta-audio-inthread; 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 browserDynamicsCompressor), and pushes into the ring (a full ring drops the sample, capping latency; never blocks the callback).InputControlholds theInputSinkand opens the stream once, on the firstensure_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 inapp-tauri/until that shell was deleted 2026-07-20.)app-native/src/main.rs—wire_iocallsInputControl::ensure_startedwhenCompiledModule::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-gatedrequestAccess). The "audio in" header pill also callsensure_started, and the meter readsEngineHandle::input_level().- macOS permission (required). cpal input is silently denied without
NSMicrophoneUsageDescription; the packaged bundle supplies it (app-native/macos/Info.plist). An unbundledvolta-studiodev 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 sharedAudioRuntime.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.captureBufferstill exists but nothing calls it. The desktop equivalent is the workbench's arm input rec chip, which writes the live input to~/Documents/Volta/Recordingsrather than registering alive<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.