# Audio System Architecture This document describes the architecture of Lightningbeam's audio engine (`daw-backend`), including real-time constraints, lock-free design patterns, and how to extend the system with new effects and features. ## Table of Contents - [Overview](#overview) - [Architecture](#architecture) - [Real-Time Constraints](#real-time-constraints) - [Lock-Free Communication](#lock-free-communication) - [Audio Processing Pipeline](#audio-processing-pipeline) - [Adding Effects](#adding-effects) - [Adding Synthesizers](#adding-synthesizers) - [MIDI System](#midi-system) - [Performance Optimization](#performance-optimization) - [Debugging Audio Issues](#debugging-audio-issues) ## Overview The `daw-backend` crate is a standalone real-time audio engine designed for: - **Multi-track audio playback and recording** - **Real-time audio effects processing** - **MIDI input/output and sequencing** - **Modular audio routing** (node graph system) - **Audio export** (WAV, MP3, AAC) ### Key Features - Lock-free design for real-time safety - Cross-platform audio I/O via cpal - Audio decoding via symphonia (MP3, FLAC, WAV, Ogg, AAC) - Node-based audio graph processing - Comprehensive effects library - Multiple synthesizer types - Zero-allocation audio thread ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ UI Thread │ │ (lightningbeam-editor or other application) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ AudioSystem::new() ─────> Creates audio stream │ │ │ │ │ ├─> command_sender (rtrb::Producer) │ │ └─> state_receiver (rtrb::Consumer) │ │ │ │ Commands sent: │ │ - Play / Stop / Seek │ │ - Add / Remove tracks │ │ - Load audio files │ │ - Add / Remove effects │ │ - Update parameters │ │ │ └──────────────────────┬──────────────────────────────────────┘ │ │ Lock-free queues (rtrb) │ ┌──────────────────────▼──────────────────────────────────────┐ │ Audio Thread (Real-Time) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Engine::process(output_buffer) │ │ │ │ │ ├─> Receive commands from queue │ │ ├─> Update playhead position │ │ ├─> For each track: │ │ │ ├─> Read audio samples at playhead │ │ │ ├─> Apply effects chain │ │ │ └─> Mix to output │ │ ├─> Apply master effects │ │ └─> Write samples to output_buffer │ │ │ │ Send state updates back to UI thread │ │ - Playhead position │ │ - Meter levels │ │ - Overrun warnings │ │ │ └──────────────────────┬──────────────────────────────────────┘ │ ▼ ┌───────────┐ │ cpal │ │ (Audio │ │ I/O) │ └───────────┘ │ ▼ ┌──────────────┐ │ Audio Output │ │ (Speakers) │ └──────────────┘ ``` ### Core Components #### AudioSystem (`src/lib.rs`) - Entry point for the audio engine - Creates the audio stream - Sets up lock-free communication channels - Manages audio device configuration #### Engine (`src/audio/engine.rs`) - The main audio callback - Runs on the real-time audio thread - Processes commands, mixes tracks, applies effects - Must complete in ~5ms (at 44.1kHz, 256 frame buffer) #### Project (`src/audio/project.rs`) - Top-level audio state - Contains tracks, tempo, time signature - Manages global settings #### Track (`src/audio/track.rs`) - Individual audio track - Contains audio clips and effects chain - Handles track-specific state (volume, pan, mute, solo) ## Real-Time Constraints ### The Golden Rule **The audio thread must NEVER block.** Audio callbacks run with strict timing deadlines: - **Buffer size**: 256 frames (default) = ~5.8ms at 44.1kHz - **ALSA on Linux**: May provide smaller buffers (64-75 frames = ~1.5ms) - **Deadline**: Audio callback must complete before next buffer is needed If the audio callback takes too long: - **Audio dropout**: Audible glitch/pop in output - **Buffer underrun**: Missing samples - **System instability**: Priority inversion, thread starvation ### Forbidden Operations in Audio Thread ❌ **Never do these in the audio callback:** - **Locking**: `Mutex`, `RwLock`, or any blocking synchronization - **Allocation**: `Vec::push()`, `Box::new()`, `String` operations - **I/O**: File operations, network, print statements - **System calls**: Most OS operations - **Unbounded loops**: Must have guaranteed completion time ✅ **Safe operations:** - Reading/writing lock-free queues (rtrb) - Fixed-size array operations - Arithmetic and DSP calculations - Pre-allocated buffer operations ### Optimized Debug Builds To meet real-time deadlines, audio code is compiled with optimizations even in debug builds: ```toml # In lightningbeam-ui/Cargo.toml [profile.dev.package.daw-backend] opt-level = 2 [profile.dev.package.symphonia] opt-level = 2 # ... other audio libraries ``` This allows fast iteration while maintaining audio performance. ## Lock-Free Communication ### Command Queue (UI → Audio) The UI thread sends commands to the audio thread via a lock-free ringbuffer: ```rust // UI Thread let command = AudioCommand::Play; command_sender.push(command).ok(); // Audio Thread (in Engine::process) while let Ok(command) = command_receiver.pop() { match command { AudioCommand::Play => self.playing = true, AudioCommand::Stop => self.playing = false, AudioCommand::Seek(time) => self.playhead = time, // ... handle other commands } } ``` ### State Updates (Audio → UI) The audio thread sends state updates back to the UI: ```rust // Audio Thread let state = AudioState { playhead: self.playhead, is_playing: self.playing, meter_levels: self.compute_meters(), }; state_sender.push(state).ok(); // UI Thread if let Ok(state) = state_receiver.pop() { // Update UI with new state } ``` ### Design Pattern: Command-Response 1. **UI initiates action**: Send command to audio thread 2. **Audio thread executes**: In `Engine::process()`, between buffer fills 3. **Audio thread confirms**: Send state update back to UI 4. **UI updates**: Reflect new state in user interface This pattern ensures: - No blocking on either side - UI remains responsive - Audio thread never waits ## Audio Processing Pipeline ### Per-Buffer Processing Every audio buffer (typically 256 frames), the `Engine::process()` callback: ```rust pub fn process(&mut self, output: &mut [f32]) -> Result<(), AudioError> { // 1. Process commands from UI thread self.process_commands(); // 2. Update playhead if self.playing { self.playhead += buffer_duration; } // 3. Clear output buffer output.fill(0.0); // 4. Process each track for track in &mut self.tracks { if track.muted { continue; } // Read audio samples at playhead position let samples = track.read_samples(self.playhead, output.len()); // Apply track effects chain let mut processed = samples; for effect in &mut track.effects { processed = effect.process(processed); } // Mix to output with volume/pan mix_to_output(output, &processed, track.volume, track.pan); } // 5. Apply master effects for effect in &mut self.master_effects { effect.process_in_place(output); } // 6. Send state updates to UI self.send_state_update(); Ok(()) } ``` ### Sample Rate and Buffer Size - **Sample rate**: 44.1kHz (default) or 48kHz - **Buffer size**: 256 frames (configurable) - **Channels**: Stereo (2 channels) Buffer is interleaved: `[L, R, L, R, L, R, ...]` ### Time Representation - **Playhead position**: Stored as `f64` seconds - **Sample index**: `(playhead * sample_rate) as usize` - **Frame index**: `sample_index / channels` ## Node Graph System ### Overview Tracks use a node graph architecture powered by `dasp_graph` for flexible audio routing. Unlike simple serial effect chains, the node graph allows: - **Parallel processing**: Multiple effects processing the same input - **Complex routing**: Effects feeding into each other in arbitrary configurations - **Modular synthesis**: Build synthesizers from oscillators, filters, and modulators - **Send/return chains**: Shared effects (reverb, delay) fed by multiple tracks - **Sidechain processing**: One signal controlling another (compression, vocoding) ### Node Graph Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ Track Node Graph │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ │ │ │ Input │ (Audio clip or synthesizer) │ │ └────┬────┘ │ │ │ │ │ ├──────┬──────────────┬─────────────┐ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌─────────┐ │ │ │Filter │ │Distort │ │ EQ │ │ Reverb │ │ │ │(Node 1)│ │(Node 2)│ │(Node 3)│ │(Node 4) │ │ │ └───┬────┘ └───┬────┘ └───┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ └────┬─────┴──────┬───┘ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ Mixer │ │Compress │ │ │ │ │(Node 5) │ │(Node 6) │◄──────────┘ │ │ └────┬────┘ └────┬────┘ (sidechain) │ │ │ │ │ │ └─────┬──────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ Output │ │ │ └──────────┘ │ │ │ └────────────────────────────────────────────────────────┘ ``` ### Node Types #### Input Nodes - **Audio Clip Reader**: Reads samples from audio file - **Oscillator**: Generates waveforms (sine, saw, square, triangle) - **Noise Generator**: White/pink noise - **External Input**: Microphone or line-in #### Processing Nodes - **Effects**: Any audio effect (see [Adding Effects](#adding-effects)) - **Filters**: Low-pass, high-pass, band-pass, notch - **Mixers**: Combine multiple inputs with gain control - **Splitters**: Duplicate signal to multiple outputs #### Output Nodes - **Track Output**: Sends to mixer or master bus - **Send Output**: Feeds auxiliary effects ### Building a Node Graph ```rust use dasp_graph::{Node, NodeData, Input, BoxedNode}; use petgraph::graph::NodeIndex; pub struct TrackGraph { graph: dasp_graph::Graph, input_node: NodeIndex, output_node: NodeIndex, } impl TrackGraph { pub fn new() -> Self { let mut graph = dasp_graph::Graph::new(); // Create input and output nodes let input_node = graph.add_node(NodeData::new1( Input::default(), PassThrough, // Simple input node )); let output_node = graph.add_node(NodeData::new1( Input::default(), PassThrough, // Simple output node )); Self { graph, input_node, output_node, } } pub fn add_effect(&mut self, effect: BoxedNode) -> NodeIndex { // Add effect node between input and output let effect_node = self.graph.add_node(NodeData::new1( Input::default(), effect, )); // Connect: input -> effect -> output self.graph.add_edge(self.input_node, effect_node, ()); self.graph.add_edge(effect_node, self.output_node, ()); effect_node } pub fn connect(&mut self, from: NodeIndex, to: NodeIndex) { self.graph.add_edge(from, to, ()); } pub fn process(&mut self, input: &[f32], output: &mut [f32]) { // Set input samples self.graph.set_input(self.input_node, input); // Process entire graph self.graph.process(); // Read output samples self.graph.get_output(self.output_node, output); } } ``` ### Example: Serial Effect Chain Simple effects chain (the most common case): ```rust // Input -> Distortion -> EQ -> Reverb -> Output let mut graph = TrackGraph::new(); let distortion = graph.add_effect(Box::new(Distortion::new(0.5))); let eq = graph.add_effect(Box::new(EQ::new())); let reverb = graph.add_effect(Box::new(Reverb::new())); // Connect in series graph.connect(graph.input_node, distortion); graph.connect(distortion, eq); graph.connect(eq, reverb); graph.connect(reverb, graph.output_node); ``` ### Example: Parallel Processing Split signal into parallel paths: ```rust // Input -> Split -> [Distortion + Clean] -> Mix -> Output let mut graph = TrackGraph::new(); // Create parallel paths let distortion = graph.add_effect(Box::new(Distortion::new(0.7))); let clean = graph.add_effect(Box::new(Gain::new(1.0))); let mixer = graph.add_effect(Box::new(Mixer::new(2))); // 2 inputs // Connect parallel paths graph.connect(graph.input_node, distortion); graph.connect(graph.input_node, clean); graph.connect(distortion, mixer); graph.connect(clean, mixer); graph.connect(mixer, graph.output_node); ``` ### Example: Modular Synthesizer Build a synthesizer from basic components: ```rust // ┌─ LFO ────┐ (modulation) // │ ▼ // Oscillator -> Filter -> Envelope -> Output let mut graph = TrackGraph::new(); // Sound source let oscillator = graph.add_effect(Box::new(Oscillator::new(440.0))); // Modulation source let lfo = graph.add_effect(Box::new(LFO::new(5.0))); // 5 Hz // Filter with LFO modulation let filter = graph.add_effect(Box::new(Filter::new_modulated())); // Envelope let envelope = graph.add_effect(Box::new(ADSREnvelope::new())); // Connect sound path graph.connect(oscillator, filter); graph.connect(filter, envelope); graph.connect(envelope, graph.output_node); // Connect modulation path graph.connect(lfo, filter); // LFO modulates filter cutoff ``` ### Example: Sidechain Compression One signal controls another: ```rust // Input (bass) ──────────────────┐ // ▼ // Kick drum ────> Compressor (sidechain) -> Output let mut graph = TrackGraph::new(); // Main signal input (bass) let bass_input = graph.add_effect(Box::new(PassThrough)); // Sidechain signal input (kick drum) let kick_input = graph.add_effect(Box::new(PassThrough)); // Compressor with sidechain let compressor = graph.add_effect(Box::new(SidechainCompressor::new())); // Connect main signal graph.connect(bass_input, compressor); // Connect sidechain signal (port 1 = main, port 2 = sidechain) graph.connect_to_port(kick_input, compressor, 1); graph.connect(compressor, graph.output_node); ``` ### Node Interface All nodes implement the `dasp_graph::Node` trait: ```rust pub trait Node { /// Process audio for this node fn process(&mut self, inputs: &[Input], output: &mut [f32]); /// Number of input ports fn num_inputs(&self) -> usize; /// Number of output ports fn num_outputs(&self) -> usize; /// Reset internal state fn reset(&mut self); } ``` ### Multi-Channel Processing Nodes can have multiple input/output channels: ```rust pub struct StereoEffect { left_processor: Processor, right_processor: Processor, } impl Node for StereoEffect { fn process(&mut self, inputs: &[Input], output: &mut [f32]) { // Split stereo input let (left_in, right_in) = inputs[0].as_stereo(); // Process each channel let left_out = self.left_processor.process(left_in); let right_out = self.right_processor.process(right_in); // Interleave output for i in 0..left_out.len() { output[i * 2] = left_out[i]; output[i * 2 + 1] = right_out[i]; } } fn num_inputs(&self) -> usize { 1 } // One stereo input fn num_outputs(&self) -> usize { 1 } // One stereo output fn reset(&mut self) { self.left_processor.reset(); self.right_processor.reset(); } } ``` ### Parameter Modulation Nodes can expose parameters for automation or modulation: ```rust pub struct ModulatableFilter { filter: Filter, cutoff: f32, resonance: f32, } impl Node for ModulatableFilter { fn process(&mut self, inputs: &[Input], output: &mut [f32]) { let audio_in = &inputs[0]; // Port 0: audio input // Port 1 (optional): cutoff modulation if inputs.len() > 1 { let mod_signal = &inputs[1]; // Modulate cutoff: base + modulation self.filter.set_cutoff(self.cutoff + mod_signal[0] * 1000.0); } // Process audio self.filter.process(audio_in, output); } fn num_inputs(&self) -> usize { 2 } // Audio + modulation fn num_outputs(&self) -> usize { 1 } fn reset(&mut self) { self.filter.reset(); } } ``` ### Graph Execution Order `dasp_graph` automatically determines execution order using topological sort: 1. Nodes with no dependencies execute first (inputs, oscillators) 2. Nodes execute when all inputs are ready 3. Cycles are detected and prevented 4. Output nodes execute last This ensures: - No node processes before its inputs are ready - Efficient CPU cache usage - Deterministic execution ### Performance Considerations #### Graph Overhead Node graphs have small overhead: - **Topological sort**: Done once when graph changes, not per-buffer - **Buffer copying**: Minimized by reusing buffers - **Indirection**: Virtual function calls (unavoidable with trait objects) For simple serial chains, the overhead is negligible (<1% CPU). #### When to Use Node Graphs vs Simple Chains **Use node graphs when:** - Complex routing (parallel, feedback, modulation) - Building synthesizers from components - User-configurable effect routing - Sidechain processing **Use simple chains when:** - Just a few effects in series - Performance is critical - Graph structure never changes **Note**: In Lightningbeam, audio layers always use node graphs to provide maximum flexibility for users. This allows any track to have complex routing, modular synthesis, or effect configurations without requiring different track types. ```rust // Simple chain (no graph overhead) pub struct SimpleChain { effects: Vec>, } impl SimpleChain { fn process(&mut self, buffer: &mut [f32]) { for effect in &mut self.effects { effect.process_in_place(buffer); } } } ``` ### Debugging Node Graphs Enable graph visualization: ```rust // Print graph structure println!("{:?}", graph); // Export to DOT format for visualization let dot = graph.to_dot(); std::fs::write("graph.dot", dot)?; // Then: dot -Tpng graph.dot -o graph.png ``` Trace signal flow: ```rust // Add probe nodes to inspect signals let probe = graph.add_effect(Box::new(SignalProbe::new("After Filter"))); graph.connect(filter, probe); graph.connect(probe, output); // Probe prints min/max/RMS of signal ``` ## Adding Effects ### Effect Trait All effects implement the `AudioEffect` trait: ```rust pub trait AudioEffect: Send { fn process(&mut self, input: &[f32], output: &mut [f32]); fn process_in_place(&mut self, buffer: &mut [f32]); fn reset(&mut self); } ``` ### Example: Simple Gain Effect ```rust pub struct Gain { gain: f32, } impl Gain { pub fn new(gain: f32) -> Self { Self { gain } } } impl AudioEffect for Gain { fn process(&mut self, input: &[f32], output: &mut [f32]) { for (i, &sample) in input.iter().enumerate() { output[i] = sample * self.gain; } } fn process_in_place(&mut self, buffer: &mut [f32]) { for sample in buffer.iter_mut() { *sample *= self.gain; } } fn reset(&mut self) { // No state to reset for gain } } ``` ### Example: Delay Effect (with state) ```rust pub struct Delay { buffer: Vec, write_pos: usize, delay_samples: usize, feedback: f32, mix: f32, } impl Delay { pub fn new(sample_rate: f32, delay_time: f32, feedback: f32, mix: f32) -> Self { let delay_samples = (delay_time * sample_rate) as usize; let buffer_size = delay_samples.next_power_of_two(); Self { buffer: vec![0.0; buffer_size], write_pos: 0, delay_samples, feedback, mix, } } } impl AudioEffect for Delay { fn process_in_place(&mut self, buffer: &mut [f32]) { for sample in buffer.iter_mut() { // Read delayed sample let read_pos = (self.write_pos + self.buffer.len() - self.delay_samples) % self.buffer.len(); let delayed = self.buffer[read_pos]; // Write new sample with feedback self.buffer[self.write_pos] = *sample + delayed * self.feedback; self.write_pos = (self.write_pos + 1) % self.buffer.len(); // Mix dry and wet signals *sample = *sample * (1.0 - self.mix) + delayed * self.mix; } } fn reset(&mut self) { self.buffer.fill(0.0); self.write_pos = 0; } } ``` ### Adding Effects to Tracks ```rust // UI Thread let command = AudioCommand::AddEffect { track_id: track_id, effect: Box::new(Delay::new(44100.0, 0.5, 0.3, 0.5)), }; command_sender.push(command).ok(); ``` ### Built-In Effects Located in `daw-backend/src/effects/`: - **reverb.rs**: Reverb - **delay.rs**: Delay - **eq.rs**: Equalizer - **compressor.rs**: Dynamic range compressor - **distortion.rs**: Distortion/overdrive - **chorus.rs**: Chorus - **flanger.rs**: Flanger - **phaser.rs**: Phaser - **limiter.rs**: Brick-wall limiter ## Adding Synthesizers ### Synthesizer Trait ```rust pub trait Synthesizer: Send { fn process(&mut self, output: &mut [f32], sample_rate: f32); fn note_on(&mut self, note: u8, velocity: u8); fn note_off(&mut self, note: u8); fn reset(&mut self); } ``` ### Example: Simple Oscillator ```rust pub struct Oscillator { phase: f32, frequency: f32, amplitude: f32, sample_rate: f32, } impl Oscillator { pub fn new(sample_rate: f32) -> Self { Self { phase: 0.0, frequency: 440.0, amplitude: 0.0, sample_rate, } } } impl Synthesizer for Oscillator { fn process(&mut self, output: &mut [f32], _sample_rate: f32) { for sample in output.iter_mut() { // Generate sine wave *sample = (self.phase * 2.0 * std::f32::consts::PI).sin() * self.amplitude; // Advance phase self.phase += self.frequency / self.sample_rate; if self.phase >= 1.0 { self.phase -= 1.0; } } } fn note_on(&mut self, note: u8, velocity: u8) { // Convert MIDI note to frequency self.frequency = 440.0 * 2.0_f32.powf((note as f32 - 69.0) / 12.0); self.amplitude = velocity as f32 / 127.0; } fn note_off(&mut self, _note: u8) { self.amplitude = 0.0; } fn reset(&mut self) { self.phase = 0.0; self.amplitude = 0.0; } } ``` ### Built-In Synthesizers Located in `daw-backend/src/synth/`: - **oscillator.rs**: Basic waveform generator (sine, saw, square, triangle) - **fm_synth.rs**: FM synthesis - **wavetable.rs**: Wavetable synthesis - **sampler.rs**: Sample-based synthesis ## MIDI System ### MIDI Input ```rust // Setup MIDI input (UI thread) let midi_input = midir::MidiInput::new("Lightningbeam")?; let port = midi_input.ports()[0]; midi_input.connect(&port, "input", move |_timestamp, message, _| { // Parse MIDI message match message[0] & 0xF0 { 0x90 => { // Note On let note = message[1]; let velocity = message[2]; command_sender.push(AudioCommand::NoteOn { note, velocity }).ok(); } 0x80 => { // Note Off let note = message[1]; command_sender.push(AudioCommand::NoteOff { note }).ok(); } _ => {} } }, ())?; ``` ### MIDI File Parsing ```rust use midly::{Smf, TrackEventKind}; let smf = Smf::parse(&midi_data)?; for track in smf.tracks { for event in track { match event.kind { TrackEventKind::Midi { channel, message } => { // Process MIDI message } _ => {} } } } ``` ## Performance Optimization ### Pre-Allocation Allocate all buffers before audio thread starts: ```rust // Good: Pre-allocated pub struct Track { buffer: Vec, // Allocated once in constructor // ... } // Bad: Allocates in audio thread fn process(&mut self) { let mut temp = Vec::new(); // ❌ Allocates! // ... } ``` ### Memory-Mapped Audio Files Large audio files use memory-mapped I/O for zero-copy access: ```rust use memmap2::Mmap; let file = File::open(path)?; let mmap = unsafe { Mmap::map(&file)? }; // Audio samples can be read directly from mmap ``` ### SIMD Optimization For portable SIMD operations, use the `fearless_simd` crate: ```rust use fearless_simd::*; fn process_simd(samples: &mut [f32], gain: f32) { // Automatically uses best available SIMD instructions // (SSE, AVX, NEON, etc.) without unsafe code for chunk in samples.chunks_exact_mut(f32x8::LEN) { let simd_samples = f32x8::from_slice(chunk); let simd_gain = f32x8::splat(gain); let result = simd_samples * simd_gain; result.write_to_slice(chunk); } // Handle remainder let remainder = samples.chunks_exact_mut(f32x8::LEN).into_remainder(); for sample in remainder { *sample *= gain; } } ``` This approach is: - **Portable**: Works across x86, ARM, and other architectures - **Safe**: No unsafe code required - **Automatic**: Uses best available SIMD instructions for the target - **Fallback**: Gracefully degrades on platforms without SIMD ### Avoid Branching in Inner Loops ```rust // Bad: Branch in inner loop for sample in samples.iter_mut() { if self.gain > 0.5 { *sample *= 2.0; } } // Good: Branch outside loop let multiplier = if self.gain > 0.5 { 2.0 } else { 1.0 }; for sample in samples.iter_mut() { *sample *= multiplier; } ``` ## Debugging Audio Issues ### Enable Debug Logging ```bash DAW_AUDIO_DEBUG=1 cargo run ``` Output includes: ``` [AUDIO] Buffer size: 256 frames (5.8ms at 44100 Hz) [AUDIO] Processing time: avg=0.8ms, worst=2.1ms [AUDIO] Playhead: 1.234s [AUDIO] WARNING: Audio overrun detected! ``` ### Common Issues #### Audio Dropouts **Symptoms**: Clicks, pops, glitches in audio output **Causes**: - Audio callback taking too long - Blocking operation in audio thread - Insufficient CPU resources **Solutions**: - Increase buffer size (reduces CPU pressure, increases latency) - Optimize audio processing code - Remove debug prints from audio thread - Check `DAW_AUDIO_DEBUG=1` output for timing info #### Crackling/Distortion **Symptoms**: Harsh, noisy audio **Causes**: - Samples exceeding [-1.0, 1.0] range (clipping) - Incorrect sample rate conversion - Denormal numbers in filters **Solutions**: - Add limiter to master output - Use hard clipping: `sample.clamp(-1.0, 1.0)` - Enable flush-to-zero for denormals #### No Audio Output **Symptoms**: Silence, but no errors **Causes**: - Audio device not found - Wrong device selected - All tracks muted - Volume set to zero **Solutions**: - Check `cpal` device enumeration - Verify track volumes and mute states - Check master volume - Test with simple sine wave ### Profiling Audio Performance ```bash # Use perf on Linux perf record --call-graph dwarf cargo run --release perf report # Look for hot spots in Engine::process() ``` ## Related Documentation - [ARCHITECTURE.md](../ARCHITECTURE.md) - Overall system architecture - [docs/UI_SYSTEM.md](UI_SYSTEM.md) - UI integration with audio system - [docs/BUILDING.md](BUILDING.md) - Build troubleshooting