# DAW Backend Architecture & Implementation Roadmap **Version:** 1.0 **Date:** October 2025 **Language:** Rust **Audio I/O:** cpal --- ## Table of Contents 1. [Architecture Overview](#architecture-overview) 2. [Core Components](#core-components) 3. [Metatracks Architecture](#metatracks-architecture) 4. [Implementation Roadmap](#implementation-roadmap) 5. [Technical Specifications](#technical-specifications) 6. [Testing Strategy](#testing-strategy) --- ## Architecture Overview ### High-Level Design The DAW follows a **multi-threaded, message-passing architecture** that separates real-time audio processing from UI and control logic: ``` ┌─────────────┐ Commands ┌─────────────┐ Commands ┌─────────────┐ │ UI Thread │ ←──────────────→ │Control Thread│ ←──────────────→ │ Audio Thread│ └─────────────┘ Events └─────────────┘ Lock-free └─────────────┘ │ Queues │ ↓ │ ┌─────────────┐ │ │Project State│←────────────────────────┘ │(Triple-buf) │ Atomic reads └─────────────┘ ``` ### Design Principles 1. **Real-time Safety**: Audio thread is lock-free and allocation-free 2. **Hierarchical Composition**: Tracks can contain other tracks (metatracks) 3. **Message-Based Communication**: All cross-thread communication via lock-free queues 4. **Incremental Complexity**: Architecture supports simple flat tracks initially, scales to nested metatracks 5. **Separation of Concerns**: Audio processing, state management, and UI are decoupled --- ## Core Components ### 1. Audio Engine (Real-time Thread) **Responsibilities:** - Process audio graph in response to cpal callbacks - Execute commands from control thread - Maintain playback position and transport state - Send events back to control thread **Constraints:** - No memory allocations - No blocking operations (mutex, I/O) - No unbounded loops - Pre-allocated buffers only **Key Structures:** ```rust struct AudioEngine { tracks: Vec, playhead: u64, // Sample position playing: bool, sample_rate: u32, // Communication command_rx: rtrb::Consumer, event_tx: rtrb::Producer, // Pre-allocated resources mix_buffer: Vec, buffer_pool: BufferPool, } enum Command { Play, Stop, Seek(f64), SetTempo(f32), UpdateTrackVolume(TrackId, f32), UpdateTrackMute(TrackId, bool), AddEffect(TrackId, EffectType), // ... more commands } enum AudioEvent { PlaybackPosition(f64), PeakLevel(TrackId, f32), BufferUnderrun, // ... more events } ``` ### 2. Track Hierarchy **Track Node Types:** ```rust enum TrackNode { Audio(AudioTrack), Midi(MidiTrack), Metatrack(Metatrack), Bus(BusTrack), } struct AudioTrack { id: TrackId, name: String, clips: Vec, effects: Vec>, volume: f32, pan: f32, muted: bool, solo: bool, parent: Option, } struct MidiTrack { id: TrackId, name: String, clips: Vec, instrument: Box, // Virtual instrument effects: Vec>, volume: f32, pan: f32, muted: bool, solo: bool, parent: Option, } struct Metatrack { id: TrackId, name: String, children: Vec, effects: Vec>, // Metatrack-specific features time_stretch: f32, // Speed multiplier (0.5 = half speed) pitch_shift: f32, // Semitones offset: f64, // Time offset in seconds volume: f32, pan: f32, muted: bool, solo: bool, parent: Option, // UI hints collapsed: bool, color: Color, } struct BusTrack { id: TrackId, name: String, inputs: Vec, // Which tracks send to this bus effects: Vec>, volume: f32, pan: f32, } ``` ### 3. Clips and Regions ```rust struct Clip { id: ClipId, content: ClipContent, start_time: f64, // Position in parent track (seconds) duration: f64, // Clip duration (seconds) offset: f64, // Offset into content (seconds) // Clip-level processing gain: f32, fade_in: f64, fade_out: f64, reversed: bool, } enum ClipContent { AudioFile { pool_index: usize, // Index into AudioPool }, MidiData { events: Vec, }, MetatrackReference { track_id: TrackId, }, } struct MidiEvent { timestamp: u64, // Sample offset within clip status: u8, data1: u8, data2: u8, } ``` ### 4. Audio Pool Shared audio file storage: ```rust struct AudioPool { files: Vec, cache: LruCache>, } struct AudioFile { id: FileId, path: PathBuf, data: Vec, // Interleaved samples channels: u32, sample_rate: u32, frames: u64, } ``` ### 5. Effect System ```rust trait Effect: Send { fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32); fn set_parameter(&mut self, id: u32, value: f32); fn get_parameter(&self, id: u32) -> f32; fn reset(&mut self); } // Example implementations struct GainEffect { gain_db: f32, } struct SimpleEQ { low_gain: f32, mid_gain: f32, high_gain: f32, filters: [BiquadFilter; 3], } struct SimpleSynth { oscillators: Vec, adsr: AdsrEnvelope, } ``` ### 6. Render Context Carries time and tempo information through the track hierarchy: ```rust struct RenderContext { global_position: u64, // Absolute sample position local_position: u64, // Position within current scope sample_rate: u32, tempo: f32, time_signature: (u32, u32), time_stretch: f32, // Accumulated stretch factor } impl Metatrack { fn transform_context(&self, ctx: RenderContext) -> RenderContext { let offset_samples = (self.offset * ctx.sample_rate as f64) as u64; let local_pos = ((ctx.local_position.saturating_sub(offset_samples)) as f64 / self.time_stretch as f64) as u64; RenderContext { global_position: ctx.global_position, local_position: local_pos, sample_rate: ctx.sample_rate, tempo: ctx.tempo * self.time_stretch, time_signature: ctx.time_signature, time_stretch: ctx.time_stretch * self.time_stretch, } } } ``` ### 7. Project State ```rust struct Project { tracks: HashMap, root_tracks: Vec, audio_pool: AudioPool, // Global settings sample_rate: u32, tempo: f32, time_signature: (u32, u32), // Metadata name: String, created: SystemTime, modified: SystemTime, } impl Project { fn get_processing_order(&self) -> Vec { // Depth-first traversal for correct rendering order let mut order = Vec::new(); for root_id in &self.root_tracks { self.collect_depth_first(*root_id, &mut order); } order } fn collect_depth_first(&self, id: TrackId, order: &mut Vec) { if let Some(TrackNode::Metatrack(meta)) = self.tracks.get(&id) { for child_id in &meta.children { self.collect_depth_first(*child_id, order); } } order.push(id); } } ``` ### 8. Buffer Management ```rust struct BufferPool { buffers: Vec>, available: Vec, buffer_size: usize, } impl BufferPool { fn acquire(&mut self) -> Vec { if let Some(idx) = self.available.pop() { let mut buf = std::mem::take(&mut self.buffers[idx]); buf.fill(0.0); buf } else { vec![0.0; self.buffer_size] } } fn release(&mut self, buffer: Vec) { let idx = self.buffers.len(); self.buffers.push(buffer); self.available.push(idx); } } ``` --- ## Metatracks Architecture ### Processing Model Metatracks use a **pre-mix** model: 1. Mix all children into a temporary buffer 2. Apply metatrack's effects to the mixed buffer 3. Mix result into parent's output ```rust fn process_metatrack( meta: &Metatrack, project: &Project, output: &mut [f32], context: RenderContext, buffer_pool: &mut BufferPool, ) { // Transform context for children let child_context = meta.transform_context(context); // Acquire scratch buffer let mut submix = buffer_pool.acquire(); submix.resize(output.len(), 0.0); // Process all children into submix for child_id in &meta.children { if let Some(child) = project.tracks.get(child_id) { process_track_node( child, project, &mut submix, child_context, buffer_pool ); } } // Apply metatrack's effects for effect in &mut meta.effects { effect.process(&mut submix, 2, context.sample_rate); } // Mix into output with volume for (out, sub) in output.iter_mut().zip(submix.iter()) { *out += sub * meta.volume; } // Return buffer to pool buffer_pool.release(submix); } ``` ### Time Transformation Metatracks can manipulate time for all children: - **Time Stretch**: Speed up or slow down playback - **Offset**: Shift content in time - **Pitch Shift**: Transpose content (future feature, requires pitch-preserving time stretch) ### Metatrack Operations ```rust enum MetatrackOperation { // Creation CreateFromSelection(Vec), CreateEmpty, // Hierarchy manipulation AddToMetatrack(TrackId, Vec), RemoveFromMetatrack(TrackId, Vec), MoveToMetatrack { track: TrackId, new_parent: TrackId }, Ungroup(TrackId), Flatten(TrackId), // Transformation SetTimeStretch(TrackId, f32), SetOffset(TrackId, f64), // Rendering BounceToAudio(TrackId), Freeze(TrackId), Unfreeze(TrackId), } ``` ### Nesting Limits - **Recommended maximum depth**: 10 levels - **Reason**: Performance and UI complexity - **Implementation**: Check depth during metatrack creation --- ## Implementation Roadmap ### Phase 1: Single Audio File Playback (Week 1-2) **Goal**: Play one audio file through speakers **Deliverables:** - Basic cpal integration - Load audio file with symphonia - Simple playback loop - Press spacebar to play/pause **Core Implementation:** ```rust struct SimpleEngine { audio_data: Vec, playhead: usize, sample_rate: u32, playing: Arc, } // Main audio callback fn audio_callback(data: &mut [f32], engine: &mut SimpleEngine) { if engine.playing.load(Ordering::Relaxed) { let end = (engine.playhead + data.len()).min(engine.audio_data.len()); let available = end - engine.playhead; data[..available].copy_from_slice( &engine.audio_data[engine.playhead..end] ); engine.playhead = end; } else { data.fill(0.0); } } ``` **Dependencies:** - `cpal = "0.15"` - `symphonia = "0.5"` **Success Criteria:** - Audio plays without clicks or pops - Can start/stop playback - No audio thread panics --- ### Phase 2: Transport Control + UI Communication (Week 2-3) **Goal**: Start/stop/seek from a basic UI **Deliverables:** - Lock-free command queue - Atomic playhead position - Basic UI (terminal or simple window) - Play/pause/seek controls **Core Implementation:** ```rust enum Command { Play, Stop, Seek(f64), } struct Engine { audio_data: Vec, playhead: Arc, command_rx: rtrb::Consumer, playing: bool, sample_rate: u32, } fn audio_callback(data: &mut [f32], engine: &mut Engine) { // Process all pending commands while let Ok(cmd) = engine.command_rx.pop() { match cmd { Command::Play => engine.playing = true, Command::Stop => { engine.playing = false; engine.playhead.store(0, Ordering::Relaxed); } Command::Seek(seconds) => { let samples = (seconds * engine.sample_rate as f64) as u64; engine.playhead.store(samples, Ordering::Relaxed); } } } // Render audio... } ``` **New Dependencies:** - `rtrb = "0.3"` (lock-free ringbuffer) **Success Criteria:** - Commands execute within 1 buffer period - No audio glitches during seek - Playhead position updates smoothly --- ### Phase 3: Multiple Audio Tracks (Week 3-4) **Goal**: Play multiple audio files simultaneously **Deliverables:** - Track data structure - Per-track volume control - Mute/solo functionality - Mix multiple tracks **Core Implementation:** ```rust struct Track { id: u32, audio_data: Vec, volume: f32, muted: bool, solo: bool, } struct Engine { tracks: Vec, playhead: Arc, command_rx: rtrb::Consumer, sample_rate: u32, mix_buffer: Vec, } enum Command { Play, Stop, Seek(f64), SetTrackVolume(u32, f32), SetTrackMute(u32, bool), SetTrackSolo(u32, bool), } fn audio_callback(data: &mut [f32], engine: &mut Engine) { // Process commands... if engine.playing { // Clear mix buffer engine.mix_buffer.fill(0.0); // Check if any track is soloed let any_solo = engine.tracks.iter().any(|t| t.solo); // Mix all active tracks for track in &engine.tracks { let active = !track.muted && (!any_solo || track.solo); if active { mix_track(track, &mut engine.mix_buffer, engine.playhead, data.len()); } } // Copy mix to output data.copy_from_slice(&engine.mix_buffer[..data.len()]); } } fn mix_track(track: &Track, output: &mut [f32], playhead: u64, frames: usize) { let start = playhead as usize; let end = (start + frames).min(track.audio_data.len()); for (i, sample) in track.audio_data[start..end].iter().enumerate() { output[i] += sample * track.volume; } } ``` **Success Criteria:** - 4+ tracks play simultaneously without distortion - Volume changes are smooth (no clicks) - Solo/mute work correctly - CPU usage remains reasonable --- ### Phase 4: Clips & Timeline (Week 4-5) **Goal**: Place audio regions at different positions on timeline **Deliverables:** - Clip data structure - Timeline-based playback - Audio pool for shared audio data - Multiple clips per track **Core Implementation:** ```rust struct Clip { id: u32, audio_pool_index: usize, start_time: f64, // Seconds duration: f64, offset: f64, // Offset into audio file gain: f32, } struct Track { id: u32, clips: Vec, volume: f32, muted: bool, solo: bool, } struct AudioPool { files: Vec>, } fn render_track( track: &Track, output: &mut [f32], pool: &AudioPool, playhead_seconds: f64, sample_rate: u32, frames: usize, ) { for clip in &track.clips { let clip_start = clip.start_time; let clip_end = clip.start_time + clip.duration; // Check if clip is active in this time range if playhead_seconds < clip_end && playhead_seconds + (frames as f64 / sample_rate as f64) > clip_start { render_clip(clip, output, pool, playhead_seconds, sample_rate, frames); } } } fn render_clip( clip: &Clip, output: &mut [f32], pool: &AudioPool, playhead_seconds: f64, sample_rate: u32, frames: usize, ) { let audio = &pool.files[clip.audio_pool_index]; // Calculate position within clip let clip_position = playhead_seconds - clip.start_time + clip.offset; let start_sample = (clip_position * sample_rate as f64) as usize; // Calculate how many samples to copy let samples_available = audio.len().saturating_sub(start_sample); let samples_to_copy = samples_available.min(output.len()); // Mix into output for i in 0..samples_to_copy { output[i] += audio[start_sample + i] * clip.gain * clip.volume; } } ``` **Success Criteria:** - Clips play at correct timeline positions - Multiple clips per track work correctly - Clips can overlap - Audio pool prevents duplication **UI Requirements:** - Basic timeline view - Drag clips to position them - Visual representation of waveforms --- ### Phase 5: Effect Processing (Week 5-6) **Goal**: Add gain/pan/simple effects to tracks **Deliverables:** - Effect trait - Basic effects (gain, pan, simple EQ) - Per-track effect chain - Effect parameter control **Core Implementation:** ```rust trait Effect: Send { fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32); fn set_parameter(&mut self, id: u32, value: f32); fn get_parameter(&self, id: u32) -> f32; fn reset(&mut self); } struct GainEffect { gain_linear: f32, } impl Effect for GainEffect { fn process(&mut self, buffer: &mut [f32], _channels: usize, _sample_rate: u32) { for sample in buffer.iter_mut() { *sample *= self.gain_linear; } } fn set_parameter(&mut self, id: u32, value: f32) { if id == 0 { // Gain in dB self.gain_linear = 10.0_f32.powf(value / 20.0); } } fn get_parameter(&self, id: u32) -> f32 { if id == 0 { 20.0 * self.gain_linear.log10() } else { 0.0 } } fn reset(&mut self) {} } struct Track { id: u32, clips: Vec, effects: Vec>, volume: f32, muted: bool, } fn render_track( track: &mut Track, output: &mut [f32], // ... other params ) { // Render all clips... // Apply effect chain for effect in &mut track.effects { effect.process(output, 2, sample_rate); } // Apply track volume for sample in output.iter_mut() { *sample *= track.volume; } } ``` **Additional Effects to Implement:** ```rust struct PanEffect { pan: f32, // -1.0 (left) to 1.0 (right) } struct SimpleEQ { low_gain: f32, mid_gain: f32, high_gain: f32, low_filter: BiquadFilter, high_filter: BiquadFilter, } struct BiquadFilter { b0: f32, b1: f32, b2: f32, a1: f32, a2: f32, x1: f32, x2: f32, y1: f32, y2: f32, } ``` **Success Criteria:** - Effects process without distortion - Multiple effects can chain - Parameter changes are smooth - No performance degradation **Begin DSP Library:** - Basic filters (lowpass, highpass, bandpass) - Utilities (db to linear, frequency to coefficients) --- ### Phase 6: Hierarchical Tracks - Foundation (Week 6-7) **Goal**: Introduce track hierarchy (groups) without full metatracks **Deliverables:** - TrackNode enum - Group tracks - Recursive rendering - Parent-child relationships **Core Implementation:** ```rust enum TrackNode { Audio(AudioTrack), Group(GroupTrack), } struct AudioTrack { id: u32, clips: Vec, effects: Vec>, volume: f32, parent: Option, } struct GroupTrack { id: u32, children: Vec, effects: Vec>, volume: f32, } struct Project { tracks: HashMap, root_tracks: Vec, audio_pool: AudioPool, } fn render_track_node( node_id: u32, project: &Project, output: &mut [f32], context: &RenderContext, buffer_pool: &mut BufferPool, ) { match &project.tracks[&node_id] { TrackNode::Audio(track) => { render_audio_track(track, output, &project.audio_pool, context); } TrackNode::Group(group) => { // Get temp buffer from pool let mut group_buffer = buffer_pool.acquire(); group_buffer.resize(output.len(), 0.0); // Render all children for child_id in &group.children { render_track_node( *child_id, project, &mut group_buffer, context, buffer_pool ); } // Apply group effects for effect in &mut group.effects { effect.process(&mut group_buffer, 2, context.sample_rate); } // Mix into output for (out, group) in output.iter_mut().zip(group_buffer.iter()) { *out += group * group.volume; } buffer_pool.release(group_buffer); } } } struct RenderContext { playhead_seconds: f64, sample_rate: u32, tempo: f32, } ``` **Success Criteria:** - Can create groups of tracks - Groups can nest (test 3-4 levels) - Effects on groups affect all children - No audio glitches from recursion **Refactoring Required:** - Migrate from `Vec` to `HashMap` - Update all track access code - Add parent tracking --- ### Phase 7: MIDI Support (Week 7-8) **Goal**: Play MIDI through virtual instruments **Deliverables:** - MIDI data structures - MIDI clip rendering - Simple virtual instrument - MIDI track type **Core Implementation:** ```rust struct MidiEvent { timestamp: u64, // Sample position status: u8, data1: u8, // Note/CC number data2: u8, // Velocity/value } struct MidiClip { id: u32, events: Vec, start_time: f64, duration: f64, } struct MidiTrack { id: u32, clips: Vec, instrument: Box, // Synth as effect effects: Vec>, volume: f32, parent: Option, } enum TrackNode { Audio(AudioTrack), Midi(MidiTrack), Group(GroupTrack), } // Simple sine wave synth for testing struct SimpleSynth { voices: Vec, sample_rate: f32, } struct SynthVoice { active: bool, note: u8, velocity: u8, phase: f32, frequency: f32, } impl Effect for SimpleSynth { fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32) { // Process active voices for voice in &mut self.voices { if voice.active { for frame in buffer.chunks_mut(channels) { let sample = (voice.phase * 2.0 * PI).sin() * (voice.velocity as f32 / 127.0) * 0.3; for channel in frame.iter_mut() { *channel += sample; } voice.phase += voice.frequency / sample_rate as f32; if voice.phase >= 1.0 { voice.phase -= 1.0; } } } } } // Handle MIDI events via parameters fn set_parameter(&mut self, id: u32, value: f32) { match id { 0 => self.note_on(value as u8, 100), // Note on 1 => self.note_off(value as u8), // Note off _ => {} } } } fn render_midi_track( track: &mut MidiTrack, output: &mut [f32], context: &RenderContext, frames: usize, ) { // Collect MIDI events for this render period let mut events_to_process = Vec::new(); for clip in &track.clips { collect_events_in_range( clip, context.playhead_seconds, frames, context.sample_rate, &mut events_to_process ); } // Sort by timestamp events_to_process.sort_by_key(|e| e.timestamp); // Process events through instrument for event in events_to_process { handle_midi_event(&mut track.instrument, event); } // Generate audio track.instrument.process(output, 2, context.sample_rate); // Apply effect chain for effect in &mut track.effects { effect.process(output, 2, context.sample_rate); } } ``` **Success Criteria:** - Can load and play MIDI files - Notes trigger at correct times - Polyphony works (4+ notes) - Timing is sample-accurate **Dependencies:** - `midly = "0.5"` (for MIDI file parsing) --- ### Phase 8: Full Metatracks (Week 8-9) **Goal**: Add time transformation and metatrack-specific features **Deliverables:** - Metatrack type with transformations - Time stretch functionality - Offset capability - Transform context propagation **Core Implementation:** ```rust struct Metatrack { id: u32, children: Vec, effects: Vec>, // Transformation parameters time_stretch: f32, // 0.5 = half speed, 2.0 = double speed pitch_shift: f32, // Semitones (future feature) offset: f64, // Time offset in seconds volume: f32, parent: Option, } enum TrackNode { Audio(AudioTrack), Midi(MidiTrack), Metatrack(Metatrack), Group(GroupTrack), } struct RenderContext { global_position: u64, // Absolute sample position local_position: u64, // Position within current scope sample_rate: u32, tempo: f32, time_signature: (u32, u32), time_stretch: f32, // Accumulated stretch } impl Metatrack { fn transform_context(&self, ctx: RenderContext) -> RenderContext { let offset_samples = (self.offset * ctx.sample_rate as f64) as u64; let adjusted_position = ctx.local_position.saturating_sub(offset_samples); let stretched_position = (adjusted_position as f64 / self.time_stretch as f64) as u64; RenderContext { global_position: ctx.global_position, local_position: stretched_position, sample_rate: ctx.sample_rate, tempo: ctx.tempo * self.time_stretch, time_signature: ctx.time_signature, time_stretch: ctx.time_stretch * self.time_stretch, } } } fn render_metatrack( meta: &Metatrack, project: &Project, output: &mut [f32], context: RenderContext, buffer_pool: &mut BufferPool, ) { // Transform context for children let child_context = meta.transform_context(context); // Acquire buffer for submix let mut submix = buffer_pool.acquire(); submix.resize(output.len(), 0.0); // Render all children with transformed context for child_id in &meta.children { if let Some(child) = project.tracks.get(child_id) { render_track_node( *child_id, project, &mut submix, child_context, buffer_pool ); } } // Apply metatrack effects for effect in &mut meta.effects { effect.process(&mut submix, 2, context.sample_rate); } // Mix into output for (out, sub) in output.iter_mut().zip(submix.iter()) { *out += sub * meta.volume; } buffer_pool.release(submix); } ``` **Metatrack Operations:** ```rust impl Project { fn create_metatrack_from_selection(&mut self, track_ids: Vec) -> u32 { let metatrack_id = self.next_id(); let metatrack = Metatrack { id: metatrack_id, children: track_ids.clone(), effects: Vec::new(), time_stretch: 1.0, pitch_shift: 0.0, offset: 0.0, volume: 1.0, parent: None, }; // Update parent references for track_id in track_ids { if let Some(track) = self.tracks.get_mut(&track_id) { set_parent(track, Some(metatrack_id)); } } self.tracks.insert(metatrack_id, TrackNode::Metatrack(metatrack)); self.root_tracks.push(metatrack_id); metatrack_id } fn ungroup_metatrack(&mut self, metatrack_id: u32) { if let Some(TrackNode::Metatrack(meta)) = self.tracks.get(&metatrack_id) { let children = meta.children.clone(); // Remove parent from children for child_id in children { if let Some(track) = self.tracks.get_mut(&child_id) { set_parent(track, None); } self.root_tracks.push(child_id); } // Remove metatrack self.tracks.remove(&metatrack_id); self.root_tracks.retain(|&id| id != metatrack_id); } } } ``` **Success Criteria:** - Can create metatracks from track selection - Time stretch affects all children - Offset shifts children in time - Can nest metatracks 5+ levels deep - Performance remains acceptable --- ### Phase 9: Polish & Optimization (Week 9-11) #### 9a. Buffer Pool Optimization (Week 9) **Goal**: Eliminate allocations in audio thread ```rust struct BufferPool { buffers: Vec>, available: Vec, buffer_size: usize, total_allocations: AtomicUsize, } impl BufferPool { fn new(count: usize, size: usize) -> Self { let mut buffers = Vec::with_capacity(count); let mut available = Vec::with_capacity(count); for i in 0..count { buffers.push(vec![0.0; size]); available.push(i); } BufferPool { buffers, available, buffer_size: size, total_allocations: AtomicUsize::new(0), } } fn acquire(&mut self) -> Vec { if let Some(idx) = self.available.pop() { let mut buf = std::mem::take(&mut self.buffers[idx]); buf.fill(0.0); buf } else { self.total_allocations.fetch_add(1, Ordering::Relaxed); vec![0.0; self.buffer_size] } } fn release(&mut self, buffer: Vec) { if buffer.len() == self.buffer_size { let idx = self.buffers.len(); self.buffers.push(buffer); self.available.push(idx); } } } ``` **Success Criteria:** - Zero allocations during steady-state playback - Pool size auto-adjusts to actual usage - Metrics show allocation count #### 9b. Lock-Free State Updates (Week 10) **Goal**: Replace Mutex with triple-buffering for project state ```rust struct TripleBuffer { buffers: [T; 3], write_idx: AtomicUsize, read_idx: AtomicUsize, } impl TripleBuffer { fn new(initial: T) -> Self { TripleBuffer { buffers: [initial.clone(), initial.clone(), initial], write_idx: AtomicUsize::new(0), read_idx: AtomicUsize::new(0), } } // Called from control thread fn write(&mut self, value: T) { let write_idx = self.write_idx.load(Ordering::Acquire); let next_idx = (write_idx + 1) % 3; self.buffers[next_idx] = value; self.write_idx.store(next_idx, Ordering::Release); } // Called from audio thread fn read(&self) -> &T { let read_idx = self.read_idx.load(Ordering::Acquire); let write_idx = self.write_idx.load(Ordering::Acquire); if read_idx != write_idx { self.read_idx.store(write_idx, Ordering::Release); } &self.buffers[read_idx] } } ``` **Success Criteria:** - No locks in audio thread - State updates propagate within 1-2 buffers - No audio glitches during updates #### 9c. Disk Streaming (Week 11) **Goal**: Stream large audio files that don't fit in RAM ```rust struct StreamingFile { id: FileId, path: PathBuf, channels: u32, sample_rate: u32, total_frames: u64, // Streaming state buffer_rx: rtrb::Consumer, current_chunk: Option, chunk_offset: usize, } struct AudioChunk { start_frame: u64, data: Vec, } // Background streaming thread fn streaming_thread( file_path: PathBuf, request_rx: Receiver, chunk_tx: rtrb::Producer, ) { let mut file = File::open(file_path).unwrap(); let mut decoder = /* create decoder */; loop { if let Ok(request) = request_rx.try_recv() { // Seek to requested position decoder.seek(request.frame); } // Read chunk let chunk = decoder.read_frames(CHUNK_SIZE); // Send to audio thread let _ = chunk_tx.push(AudioChunk { start_frame: current_frame, data: chunk, }); current_frame += CHUNK_SIZE; } } ``` **Success Criteria:** - Can play files larger than RAM - No dropouts during streaming - Seek works smoothly - Multiple streaming files simultaneously --- ### Phase 10: Advanced Features (Week 11+) #### Automation ```rust struct AutomationLane { parameter_id: u32, points: Vec, } struct AutomationPoint { time: f64, value: f32, curve: CurveType, } enum CurveType { Linear, Exponential, SCurve, } ``` #### Plugin Hosting (VST/CLAP) ```rust struct PluginHost { scanner: PluginScanner, instances: HashMap, } struct PluginInstance { id: PluginId, plugin_type: PluginType, handle: *mut c_void, parameters: Vec, } enum PluginType { VST3, CLAP, } ``` #### Project Save/Load ```rust #[derive(Serialize, Deserialize)] struct ProjectFile { version: String, metadata: ProjectMetadata, tracks: Vec, audio_files: Vec, tempo_map: TempoMap, } ``` #### Undo/Redo System ```rust trait Command { fn execute(&mut self, project: &mut Project); fn undo(&mut self, project: &mut Project); } struct CommandHistory { commands: Vec>, position: usize, } ``` --- ## Technical Specifications ### Performance Targets - **Latency**: < 10ms (varies by buffer size and sample rate) - **CPU Usage**: < 50% for 32 tracks with effects at 44.1kHz - **Track Count**: Support 64+ tracks without performance degradation - **Nesting Depth**: 10 levels of metatrack nesting - **Plugin Count**: 8+ plugins per track ### Buffer Sizes - **Audio Callback**: 128-512 frames (adjustable) - **Streaming Chunk**: 8192 frames - **Ring Buffer**: 8192 samples (UI → Audio commands) ### Sample Rates - **Supported**: 44.1kHz, 48kHz, 88.2kHz, 96kHz - **Default**: 48kHz - **Internal Processing**: Always at project sample rate ### Memory Budget - **Audio Pool Cache**: 1GB default, configurable - **Buffer Pool**: 50 buffers × 4096 samples × 4 bytes = 800KB - **Per-Track Overhead**: < 1KB ### Thread Model 1. **UI Thread**: User interaction, visualization 2. **Control Thread**: Project state management, file I/O 3. **Audio Thread**: Real-time processing (cpal callback) 4. **Streaming Thread(s)**: Disk I/O for large files ### Data Format - **Internal Audio**: 32-bit float, interleaved - **File Support**: WAV, FLAC, MP3, OGG via symphonia - **MIDI**: Standard MIDI File Format - **Project Files**: JSON or MessagePack --- ## Testing Strategy ### Unit Tests ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_clip_rendering_at_position() { let clip = Clip { id: 0, audio_pool_index: 0, start_time: 1.0, // Starts at 1 second duration: 2.0, offset: 0.0, gain: 1.0, }; let pool = AudioPool { files: vec![vec![1.0; 96000]], // 2 seconds at 48kHz }; let mut output = vec![0.0; 4800]; // 0.1 seconds render_clip(&clip, &mut output, &pool, 1.5, 48000, 4800); assert!(output.iter().any(|&s| s != 0.0)); } #[test] fn test_metatrack_time_stretch() { let mut context = RenderContext { global_position: 48000, local_position: 48000, sample_rate: 48000, tempo: 120.0, time_signature: (4, 4), time_stretch: 1.0, }; let metatrack = Metatrack { id: 0, children: vec![], effects: vec![], time_stretch: 0.5, // Half speed pitch_shift: 0.0, offset: 0.0, volume: 1.0, parent: None, }; let child_context = metatrack.transform_context(context); assert_eq!(child_context.time_stretch, 0.5); assert_eq!(child_context.tempo, 60.0); } #[test] fn test_buffer_pool_no_allocations() { let mut pool = BufferPool::new(10, 1024); let buf1 = pool.acquire(); let buf2 = pool.acquire(); pool.release(buf1); pool.release(buf2); let buf3 = pool.acquire(); let buf4 = pool.acquire(); // Should reuse buffers, no new allocations assert_eq!(pool.total_allocations.load(Ordering::Relaxed), 0); } } ``` ### Integration Tests ```rust #[test] fn test_full_playback_pipeline() { // Setup let (cmd_tx, cmd_rx) = rtrb::RingBuffer::new(256); let (evt_tx, evt_rx) = rtrb::RingBuffer::new(256); let mut engine = AudioEngine::new(48000, cmd_rx, evt_tx); engine.load_audio_file("test.wav"); // Start playback cmd_tx.push(Command::Play).unwrap(); // Render some audio let mut output = vec![0.0; 4800]; engine.process(&mut output); // Verify audio was rendered assert!(output.iter().any(|&s| s.abs() > 0.001)); // Check events if let Ok(AudioEvent::PlaybackPosition(pos)) = evt_rx.pop() { assert!(pos > 0.0); } } #[test] fn test_nested_metatrack_rendering() { let mut project = Project::new(48000); // Create structure: Metatrack1 -> Metatrack2 -> AudioTrack let audio_id = project.add_audio_track(); let meta2_id = project.create_metatrack_from_selection(vec![audio_id]); let meta1_id = project.create_metatrack_from_selection(vec![meta2_id]); // Apply transformations project.set_metatrack_time_stretch(meta1_id, 0.5); project.set_metatrack_time_stretch(meta2_id, 2.0); // Render let mut output = vec![0.0; 4800]; project.render(&mut output, 0.0); // Effective stretch should be 0.5 * 2.0 = 1.0 (normal speed) assert!(output.iter().any(|&s| s != 0.0)); } ``` ### Performance Tests ```rust #[bench] fn bench_render_32_tracks(b: &mut Bencher) { let mut engine = create_engine_with_tracks(32); let mut output = vec![0.0; 512 * 2]; b.iter(|| { engine.process(&mut output); }); } #[bench] fn bench_metatrack_nesting_10_levels(b: &mut Bencher) { let mut project = create_nested_metatracks(10); let mut output = vec![0.0; 512 * 2]; b.iter(|| { project.render(&mut output, 0.0); }); } ``` ### Audio Quality Tests - **THD+N**: Total Harmonic Distortion + Noise < 0.01% - **Frequency Response**: Flat ±0.1dB 20Hz-20kHz - **Click Detection**: No clicks during parameter changes - **Timing Accuracy**: MIDI events within ±1 sample ### Stress Tests - **Long Sessions**: 8+ hours continuous playback - **Many Tracks**: 128 tracks, 8 effects each - **Deep Nesting**: 20 levels of metatrack nesting - **Rapid Commands**: 1000 commands/second - **Large Files**: 1GB+ audio files streaming --- ## Recommended Crates ### Core Audio - `cpal = "0.15"` - Audio I/O - `symphonia = "0.5"` - Audio decoding - `rubato = "0.14"` - Sample rate conversion ### Concurrency - `rtrb = "0.3"` - Lock-free ring buffers - `crossbeam = "0.8"` - Additional concurrency tools - `parking_lot = "0.12"` - Better mutexes (non-realtime) ### DSP - `realfft = "3.3"` - FFT for spectral processing - `biquad = "0.4"` - IIR filters ### Serialization - `serde = { version = "1.0", features = ["derive"] }` - `serde_json = "1.0"` or `rmp-serde = "1.1"` (MessagePack) ### File I/O - `midly = "0.5"` - MIDI file parsing - `hound = "3.5"` - WAV file writing ### Future - `vst3-sys` or `clack` - Plugin hosting - `egui = "0.24"` - Immediate mode GUI (if building UI in Rust) --- ## Project Structure ``` daw-backend/ ├── Cargo.toml ├── src/ │ ├── main.rs │ ├── lib.rs │ │ │ ├── audio/ │ │ ├── mod.rs │ │ ├── engine.rs # Audio engine, main processing loop │ │ ├── track.rs # Track types (Audio, MIDI, Metatrack) │ │ ├── clip.rs # Clip management │ │ ├── pool.rs # Audio pool │ │ ├── buffer_pool.rs # Buffer allocation pool │ │ └── render.rs # Rendering functions │ │ │ ├── project/ │ │ ├── mod.rs │ │ ├── project.rs # Project state │ │ ├── hierarchy.rs # Track hierarchy management │ │ ├── operations.rs # Project operations (add track, etc.) │ │ └── serialization.rs # Save/load │ │ │ ├── effects/ │ │ ├── mod.rs │ │ ├── trait.rs # Effect trait │ │ ├── gain.rs │ │ ├── pan.rs │ │ ├── eq.rs │ │ └── synth.rs # Simple synth for MIDI │ │ │ ├── dsp/ │ │ ├── mod.rs │ │ ├── filters.rs # Biquad, etc. │ │ ├── envelope.rs # ADSR │ │ ├── oscillator.rs │ │ └── utils.rs # DB conversion, etc. │ │ │ ├── io/ │ │ ├── mod.rs │ │ ├── audio_file.rs # Audio file loading │ │ ├── midi_file.rs # MIDI file loading │ │ └── streaming.rs # Disk streaming │ │ │ ├── command/ │ │ ├── mod.rs │ │ ├── types.rs # Command/Event enums │ │ └── queue.rs # Command queue management │ │ │ └── ui/ │ ├── mod.rs │ └── bridge.rs # UI-Audio communication │ ├── tests/ │ ├── integration_tests.rs │ └── audio_quality_tests.rs │ └── benches/ └── performance.rs ``` --- ## Conclusion This architecture provides: 1. **Incremental Development**: Each phase builds on the last without requiring rewrites 2. **Real-time Safety**: Lock-free, allocation-free audio thread from the start 3. **Flexibility**: Hierarchical tracks support simple projects and complex arrangements 4. **Scalability**: Architecture handles 64+ tracks with deep nesting 5. **Extensibility**: Effect trait makes plugin hosting straightforward The roadmap gets you from "hello audio" to a full-featured DAW in 11 weeks, with each phase delivering working, testable functionality. **Next Steps:** 1. Set up Rust project with cpal 2. Implement Phase 1 (single file playback) 3. Add comprehensive tests 4. Profile and optimize as needed 5. Continue through phases sequentially Good luck building your DAW!