diff --git a/daw-backend/Cargo.lock b/daw-backend/Cargo.lock index 5097d43..c62b395 100644 --- a/daw-backend/Cargo.lock +++ b/daw-backend/Cargo.lock @@ -196,6 +196,31 @@ dependencies = [ "windows", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "dasp_sample" version = "0.11.0" @@ -207,6 +232,7 @@ name = "daw-backend" version = "0.1.0" dependencies = [ "cpal", + "midly", "rtrb", "symphonia", ] @@ -372,6 +398,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "midly" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207d755f4cb882d20c4da58d707ca9130a0c9bc5061f657a4f299b8e36362b7a" +dependencies = [ + "rayon", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -527,6 +562,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.12.2" diff --git a/daw-backend/Cargo.toml b/daw-backend/Cargo.toml index dc83d9f..e4fdf9f 100644 --- a/daw-backend/Cargo.toml +++ b/daw-backend/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" cpal = "0.15" symphonia = { version = "0.5", features = ["all"] } rtrb = "0.3" +midly = "0.5" [dev-dependencies] diff --git a/daw-backend/darude-sandstorm.mid b/daw-backend/darude-sandstorm.mid new file mode 100644 index 0000000..f9a99b0 Binary files /dev/null and b/daw-backend/darude-sandstorm.mid differ diff --git a/daw-backend/examples/midi_debug.rs b/daw-backend/examples/midi_debug.rs new file mode 100644 index 0000000..208a5d5 --- /dev/null +++ b/daw-backend/examples/midi_debug.rs @@ -0,0 +1,72 @@ +use daw_backend::load_midi_file; + +fn main() { + let clip = load_midi_file("darude-sandstorm.mid", 0, 44100).unwrap(); + + println!("Clip duration: {:.2}s", clip.duration); + println!("Total events: {}", clip.events.len()); + println!("\nEvent summary:"); + + let mut note_on_count = 0; + let mut note_off_count = 0; + let mut other_count = 0; + + for event in &clip.events { + if event.is_note_on() { + note_on_count += 1; + } else if event.is_note_off() { + note_off_count += 1; + } else { + other_count += 1; + } + } + + println!(" Note On events: {}", note_on_count); + println!(" Note Off events: {}", note_off_count); + println!(" Other events: {}", other_count); + + // Show events around 28 seconds + println!("\nEvents around 28 seconds (27-29s):"); + let sample_rate = 44100.0; + let start_sample = (27.0 * sample_rate) as u64; + let end_sample = (29.0 * sample_rate) as u64; + + for (i, event) in clip.events.iter().enumerate() { + if event.timestamp >= start_sample && event.timestamp <= end_sample { + let time_sec = event.timestamp as f64 / sample_rate; + let event_type = if event.is_note_on() { + "NoteOn" + } else if event.is_note_off() { + "NoteOff" + } else { + "Other" + }; + println!(" [{:4}] {:.3}s: {} ch={} note={} vel={}", + i, time_sec, event_type, event.channel(), event.data1, event.data2); + } + } + + // Check for stuck notes - note ons without corresponding note offs + println!("\nChecking for unmatched notes..."); + let mut active_notes = std::collections::HashMap::new(); + + for (i, event) in clip.events.iter().enumerate() { + if event.is_note_on() { + let key = (event.channel(), event.data1); + active_notes.insert(key, i); + } else if event.is_note_off() { + let key = (event.channel(), event.data1); + active_notes.remove(&key); + } + } + + if !active_notes.is_empty() { + println!("Found {} notes that never got note-off events:", active_notes.len()); + for ((ch, note), event_idx) in active_notes.iter().take(10) { + let time_sec = clip.events[*event_idx].timestamp as f64 / sample_rate; + println!(" Note {} on channel {} at {:.2}s (event #{})", note, ch, time_sec, event_idx); + } + } else { + println!("All notes have matching note-off events!"); + } +} diff --git a/daw-backend/examples/midi_end_debug.rs b/daw-backend/examples/midi_end_debug.rs new file mode 100644 index 0000000..2d072f9 --- /dev/null +++ b/daw-backend/examples/midi_end_debug.rs @@ -0,0 +1,74 @@ +use daw_backend::load_midi_file; + +fn main() { + let clip = load_midi_file("darude-sandstorm.mid", 0, 44100).unwrap(); + + println!("Clip duration: {:.3}s", clip.duration); + println!("Total events: {}", clip.events.len()); + + // Show the last 30 events + println!("\nLast 30 events:"); + let sample_rate = 44100.0; + let start_idx = clip.events.len().saturating_sub(30); + + for (i, event) in clip.events.iter().enumerate().skip(start_idx) { + let time_sec = event.timestamp as f64 / sample_rate; + let event_type = if event.is_note_on() { + "NoteOn " + } else if event.is_note_off() { + "NoteOff" + } else { + "Other " + }; + println!(" [{:4}] {:.3}s: {} ch={} note={:3} vel={:3}", + i, time_sec, event_type, event.channel(), event.data1, event.data2); + } + + // Find notes that are still active at the end of the clip + println!("\nNotes active at end of clip ({:.3}s):", clip.duration); + let mut active_notes = std::collections::HashMap::new(); + + for event in &clip.events { + let time_sec = event.timestamp as f64 / sample_rate; + + if event.is_note_on() { + let key = (event.channel(), event.data1); + active_notes.insert(key, time_sec); + } else if event.is_note_off() { + let key = (event.channel(), event.data1); + active_notes.remove(&key); + } + } + + if !active_notes.is_empty() { + println!("Found {} notes still active after all events:", active_notes.len()); + for ((ch, note), start_time) in &active_notes { + println!(" Channel {} Note {} started at {:.3}s (no note-off before clip end)", + ch, note, start_time); + } + } else { + println!("All notes are turned off by the end!"); + } + + // Check maximum polyphony + println!("\nAnalyzing polyphony..."); + let mut max_polyphony = 0; + let mut current_notes = std::collections::HashSet::new(); + + for event in &clip.events { + if event.is_note_on() { + let key = (event.channel(), event.data1); + current_notes.insert(key); + max_polyphony = max_polyphony.max(current_notes.len()); + } else if event.is_note_off() { + let key = (event.channel(), event.data1); + current_notes.remove(&key); + } + } + + println!("Maximum simultaneous notes: {}", max_polyphony); + println!("Available synth voices: 16"); + if max_polyphony > 16 { + println!("WARNING: Polyphony exceeds available voices! Voice stealing will occur."); + } +} diff --git a/daw-backend/src/audio/buffer_pool.rs b/daw-backend/src/audio/buffer_pool.rs new file mode 100644 index 0000000..d1097c0 --- /dev/null +++ b/daw-backend/src/audio/buffer_pool.rs @@ -0,0 +1,86 @@ +/// Pool of reusable audio buffers for recursive group rendering +/// +/// This pool allows groups to acquire temporary buffers for submixing +/// child tracks without allocating memory in the audio thread. +pub struct BufferPool { + buffers: Vec>, + available: Vec, + buffer_size: usize, +} + +impl BufferPool { + /// Create a new buffer pool + /// + /// # Arguments + /// * `initial_capacity` - Number of buffers to pre-allocate + /// * `buffer_size` - Size of each buffer in samples + pub fn new(initial_capacity: usize, buffer_size: usize) -> Self { + let mut buffers = Vec::with_capacity(initial_capacity); + let mut available = Vec::with_capacity(initial_capacity); + + // Pre-allocate buffers + for i in 0..initial_capacity { + buffers.push(vec![0.0; buffer_size]); + available.push(i); + } + + Self { + buffers, + available, + buffer_size, + } + } + + /// Acquire a buffer from the pool + /// + /// Returns a zeroed buffer ready for use. If no buffers are available, + /// allocates a new one (though this should be avoided in the audio thread). + pub fn acquire(&mut self) -> Vec { + if let Some(idx) = self.available.pop() { + // Reuse an existing buffer + let mut buf = std::mem::take(&mut self.buffers[idx]); + buf.fill(0.0); + buf + } else { + // No buffers available, allocate a new one + // This should be rare if the pool is sized correctly + vec![0.0; self.buffer_size] + } + } + + /// Release a buffer back to the pool + /// + /// # Arguments + /// * `buffer` - The buffer to return to the pool + pub fn release(&mut self, buffer: Vec) { + // Only add to pool if it's the correct size + if buffer.len() == self.buffer_size { + let idx = self.buffers.len(); + self.buffers.push(buffer); + self.available.push(idx); + } + // Otherwise, drop the buffer (wrong size, shouldn't happen normally) + } + + /// Get the configured buffer size + pub fn buffer_size(&self) -> usize { + self.buffer_size + } + + /// Get the number of available buffers + pub fn available_count(&self) -> usize { + self.available.len() + } + + /// Get the total number of buffers in the pool + pub fn total_count(&self) -> usize { + self.buffers.len() + } +} + +impl Default for BufferPool { + fn default() -> Self { + // Default: 8 buffers of 4096 samples (enough for 85ms at 48kHz stereo) + Self::new(8, 4096) + } +} diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index ca5fc75..62c2cc8 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -1,14 +1,19 @@ +use crate::audio::buffer_pool::BufferPool; use crate::audio::clip::ClipId; +use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent}; use crate::audio::pool::AudioPool; +use crate::audio::project::Project; use crate::audio::track::{Track, TrackId}; use crate::command::{AudioEvent, Command}; +use crate::effects::{Effect, GainEffect, PanEffect, SimpleEQ}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -/// Audio engine for Phase 4: timeline with clips and audio pool +/// Audio engine for Phase 6: hierarchical tracks with groups pub struct Engine { - tracks: Vec, + project: Project, audio_pool: AudioPool, + buffer_pool: BufferPool, playhead: u64, // Playhead position in samples sample_rate: u32, playing: bool, @@ -25,8 +30,11 @@ pub struct Engine { frames_since_last_event: usize, event_interval_frames: usize, - // Mix buffer for combining tracks + // Mix buffer for output mix_buffer: Vec, + + // ID counters + next_midi_clip_id: MidiClipId, } impl Engine { @@ -39,9 +47,13 @@ impl Engine { ) -> Self { let event_interval_frames = (sample_rate as usize * channels as usize) / 10; // Update 10 times per second + // Calculate a reasonable buffer size for the pool (typical audio callback size * channels) + let buffer_size = 512 * channels as usize; + Self { - tracks: Vec::new(), + project: Project::new(), audio_pool: AudioPool::new(), + buffer_pool: BufferPool::new(8, buffer_size), // 8 buffers should handle deep nesting playhead: 0, sample_rate, playing: false, @@ -52,16 +64,55 @@ impl Engine { frames_since_last_event: 0, event_interval_frames, mix_buffer: Vec::new(), + next_midi_clip_id: 0, } } - /// Add a track to the engine + /// Add an audio track to the engine pub fn add_track(&mut self, track: Track) -> TrackId { - let id = track.id; - self.tracks.push(track); + // For backwards compatibility, we'll extract the track data and add it to the project + let name = track.name.clone(); + let id = self.project.add_audio_track(name, None); + + // Copy over the track properties + if let Some(node) = self.project.get_track_mut(id) { + if let crate::audio::track::TrackNode::Audio(audio_track) = node { + audio_track.clips = track.clips; + audio_track.effects = track.effects; + audio_track.volume = track.volume; + audio_track.muted = track.muted; + audio_track.solo = track.solo; + } + } + id } + /// Add an audio track by name + pub fn add_audio_track(&mut self, name: String) -> TrackId { + self.project.add_audio_track(name, None) + } + + /// Add a group track by name + pub fn add_group_track(&mut self, name: String) -> TrackId { + self.project.add_group_track(name, None) + } + + /// Add a MIDI track by name + pub fn add_midi_track(&mut self, name: String) -> TrackId { + self.project.add_midi_track(name, None) + } + + /// Get access to the project + pub fn project(&self) -> &Project { + &self.project + } + + /// Get mutable access to the project + pub fn project_mut(&mut self) -> &mut Project { + &mut self.project + } + /// Get mutable reference to audio pool pub fn audio_pool_mut(&mut self) -> &mut AudioPool { &mut self.audio_pool @@ -95,27 +146,24 @@ impl Engine { self.mix_buffer.resize(output.len(), 0.0); } - // Clear mix buffer - self.mix_buffer.fill(0.0); + // Ensure buffer pool has the correct buffer size + if self.buffer_pool.buffer_size() != output.len() { + // Reallocate buffer pool with correct size if needed + self.buffer_pool = BufferPool::new(8, output.len()); + } // Convert playhead from samples to seconds for timeline-based rendering let playhead_seconds = self.playhead as f64 / (self.sample_rate as f64 * self.channels as f64); - // Check if any track is soloed - let any_solo = self.tracks.iter().any(|t| t.solo); - - // Mix all active tracks using timeline-based rendering - for track in &self.tracks { - if track.is_active(any_solo) { - track.render( - &mut self.mix_buffer, - &self.audio_pool, - playhead_seconds, - self.sample_rate, - self.channels, - ); - } - } + // Render the entire project hierarchy into the mix buffer + self.project.render( + &mut self.mix_buffer, + &self.audio_pool, + &mut self.buffer_pool, + playhead_seconds, + self.sample_rate, + self.channels, + ); // Copy mix to output output.copy_from_slice(&self.mix_buffer); @@ -165,27 +213,175 @@ impl Engine { .store(self.playhead, Ordering::Relaxed); } Command::SetTrackVolume(track_id, volume) => { - if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) { + if let Some(track) = self.project.get_track_mut(track_id) { track.set_volume(volume); } } Command::SetTrackMute(track_id, muted) => { - if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) { + if let Some(track) = self.project.get_track_mut(track_id) { track.set_muted(muted); } } Command::SetTrackSolo(track_id, solo) => { - if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) { + if let Some(track) = self.project.get_track_mut(track_id) { track.set_solo(solo); } } Command::MoveClip(track_id, clip_id, new_start_time) => { - if let Some(track) = self.tracks.iter_mut().find(|t| t.id == track_id) { + if let Some(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(track_id) { if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) { clip.start_time = new_start_time; } } } + Command::AddGainEffect(track_id, gain_db) => { + // Get the track node and handle audio tracks, MIDI tracks, and groups + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Gain") { + effect.set_parameter(0, gain_db); + } else { + track.add_effect(Box::new(GainEffect::with_gain_db(gain_db))); + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Gain") { + effect.set_parameter(0, gain_db); + } else { + track.add_effect(Box::new(GainEffect::with_gain_db(gain_db))); + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(effect) = group.effects.iter_mut().find(|e| e.name() == "Gain") { + effect.set_parameter(0, gain_db); + } else { + group.add_effect(Box::new(GainEffect::with_gain_db(gain_db))); + } + } + None => {} + } + } + Command::AddPanEffect(track_id, pan) => { + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Pan") { + effect.set_parameter(0, pan); + } else { + track.add_effect(Box::new(PanEffect::with_pan(pan))); + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "Pan") { + effect.set_parameter(0, pan); + } else { + track.add_effect(Box::new(PanEffect::with_pan(pan))); + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(effect) = group.effects.iter_mut().find(|e| e.name() == "Pan") { + effect.set_parameter(0, pan); + } else { + group.add_effect(Box::new(PanEffect::with_pan(pan))); + } + } + None => {} + } + } + Command::AddEQEffect(track_id, low_db, mid_db, high_db) => { + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "SimpleEQ") { + effect.set_parameter(0, low_db); + effect.set_parameter(1, mid_db); + effect.set_parameter(2, high_db); + } else { + let mut eq = SimpleEQ::new(); + eq.set_parameter(0, low_db); + eq.set_parameter(1, mid_db); + eq.set_parameter(2, high_db); + track.add_effect(Box::new(eq)); + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(effect) = track.effects.iter_mut().find(|e| e.name() == "SimpleEQ") { + effect.set_parameter(0, low_db); + effect.set_parameter(1, mid_db); + effect.set_parameter(2, high_db); + } else { + let mut eq = SimpleEQ::new(); + eq.set_parameter(0, low_db); + eq.set_parameter(1, mid_db); + eq.set_parameter(2, high_db); + track.add_effect(Box::new(eq)); + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(effect) = group.effects.iter_mut().find(|e| e.name() == "SimpleEQ") { + effect.set_parameter(0, low_db); + effect.set_parameter(1, mid_db); + effect.set_parameter(2, high_db); + } else { + let mut eq = SimpleEQ::new(); + eq.set_parameter(0, low_db); + eq.set_parameter(1, mid_db); + eq.set_parameter(2, high_db); + group.add_effect(Box::new(eq)); + } + } + None => {} + } + } + Command::ClearEffects(track_id) => { + let _ = self.project.clear_effects(track_id); + } + Command::CreateGroup(name) => { + let track_id = self.project.add_group_track(name.clone(), None); + // Notify UI about the new group + let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, true, name)); + } + Command::AddToGroup(track_id, group_id) => { + // Move the track to the new group (Project handles removing from old parent) + self.project.move_to_group(track_id, group_id); + } + Command::RemoveFromGroup(track_id) => { + // Move to root level (None as parent) + self.project.move_to_root(track_id); + } + Command::CreateMidiTrack(name) => { + let track_id = self.project.add_midi_track(name.clone(), None); + // Notify UI about the new MIDI track + let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name)); + } + Command::CreateMidiClip(track_id, start_time, duration) => { + // Create a new MIDI clip with unique ID + let clip_id = self.next_midi_clip_id; + self.next_midi_clip_id += 1; + let clip = MidiClip::new(clip_id, start_time, duration); + let _ = self.project.add_midi_clip(track_id, clip); + } + Command::AddMidiNote(track_id, clip_id, time_offset, note, velocity, duration) => { + // Add a MIDI note event to the specified clip + if let Some(crate::audio::track::TrackNode::Midi(track)) = self.project.get_track_mut(track_id) { + if let Some(clip) = track.clips.iter_mut().find(|c| c.id == clip_id) { + // Convert time to sample timestamp + let timestamp = (time_offset * self.sample_rate as f64) as u64; + let note_on = MidiEvent::note_on(timestamp, 0, note, velocity); + clip.events.push(note_on); + + // Add note off event + let note_off_timestamp = ((time_offset + duration) * self.sample_rate as f64) as u64; + let note_off = MidiEvent::note_off(note_off_timestamp, 0, note, 64); + clip.events.push(note_off); + + // Sort events by timestamp + clip.events.sort_by_key(|e| e.timestamp); + } + } + } + Command::AddLoadedMidiClip(track_id, clip) => { + // Add a pre-loaded MIDI clip to the track + let _ = self.project.add_midi_clip(track_id, clip); + } } } @@ -201,7 +397,7 @@ impl Engine { /// Get number of tracks pub fn track_count(&self) -> usize { - self.tracks.len() + self.project.track_count() } } @@ -256,6 +452,26 @@ impl EngineController { let _ = self.command_tx.push(Command::MoveClip(track_id, clip_id, new_start_time)); } + /// Add or update gain effect on track + pub fn add_gain_effect(&mut self, track_id: TrackId, gain_db: f32) { + let _ = self.command_tx.push(Command::AddGainEffect(track_id, gain_db)); + } + + /// Add or update pan effect on track + pub fn add_pan_effect(&mut self, track_id: TrackId, pan: f32) { + let _ = self.command_tx.push(Command::AddPanEffect(track_id, pan)); + } + + /// Add or update EQ effect on track + pub fn add_eq_effect(&mut self, track_id: TrackId, low_db: f32, mid_db: f32, high_db: f32) { + let _ = self.command_tx.push(Command::AddEQEffect(track_id, low_db, mid_db, high_db)); + } + + /// Clear all effects from a track + pub fn clear_effects(&mut self, track_id: TrackId) { + let _ = self.command_tx.push(Command::ClearEffects(track_id)); + } + /// Get current playhead position in samples pub fn get_playhead_samples(&self) -> u64 { self.playhead.load(Ordering::Relaxed) @@ -266,4 +482,39 @@ impl EngineController { let samples = self.playhead.load(Ordering::Relaxed); samples as f64 / (self.sample_rate as f64 * self.channels as f64) } + + /// Create a new group track + pub fn create_group(&mut self, name: String) { + let _ = self.command_tx.push(Command::CreateGroup(name)); + } + + /// Add a track to a group + pub fn add_to_group(&mut self, track_id: TrackId, group_id: TrackId) { + let _ = self.command_tx.push(Command::AddToGroup(track_id, group_id)); + } + + /// Remove a track from its parent group + pub fn remove_from_group(&mut self, track_id: TrackId) { + let _ = self.command_tx.push(Command::RemoveFromGroup(track_id)); + } + + /// Create a new MIDI track + pub fn create_midi_track(&mut self, name: String) { + let _ = self.command_tx.push(Command::CreateMidiTrack(name)); + } + + /// Create a new MIDI clip on a track + pub fn create_midi_clip(&mut self, track_id: TrackId, start_time: f64, duration: f64) { + let _ = self.command_tx.push(Command::CreateMidiClip(track_id, start_time, duration)); + } + + /// Add a MIDI note to a clip + pub fn add_midi_note(&mut self, track_id: TrackId, clip_id: MidiClipId, time_offset: f64, note: u8, velocity: u8, duration: f64) { + let _ = self.command_tx.push(Command::AddMidiNote(track_id, clip_id, time_offset, note, velocity, duration)); + } + + /// Add a pre-loaded MIDI clip to a track + pub fn add_loaded_midi_clip(&mut self, track_id: TrackId, clip: MidiClip) { + let _ = self.command_tx.push(Command::AddLoadedMidiClip(track_id, clip)); + } } diff --git a/daw-backend/src/audio/midi.rs b/daw-backend/src/audio/midi.rs new file mode 100644 index 0000000..0147440 --- /dev/null +++ b/daw-backend/src/audio/midi.rs @@ -0,0 +1,143 @@ +/// MIDI event representing a single MIDI message +#[derive(Debug, Clone, Copy)] +pub struct MidiEvent { + /// Sample position within the clip + pub timestamp: u64, + /// MIDI status byte (includes channel) + pub status: u8, + /// First data byte (note number, CC number, etc.) + pub data1: u8, + /// Second data byte (velocity, CC value, etc.) + pub data2: u8, +} + +impl MidiEvent { + /// Create a new MIDI event + pub fn new(timestamp: u64, status: u8, data1: u8, data2: u8) -> Self { + Self { + timestamp, + status, + data1, + data2, + } + } + + /// Create a note on event + pub fn note_on(timestamp: u64, channel: u8, note: u8, velocity: u8) -> Self { + Self { + timestamp, + status: 0x90 | (channel & 0x0F), + data1: note, + data2: velocity, + } + } + + /// Create a note off event + pub fn note_off(timestamp: u64, channel: u8, note: u8, velocity: u8) -> Self { + Self { + timestamp, + status: 0x80 | (channel & 0x0F), + data1: note, + data2: velocity, + } + } + + /// Check if this is a note on event (with non-zero velocity) + pub fn is_note_on(&self) -> bool { + (self.status & 0xF0) == 0x90 && self.data2 > 0 + } + + /// Check if this is a note off event (or note on with zero velocity) + pub fn is_note_off(&self) -> bool { + (self.status & 0xF0) == 0x80 || ((self.status & 0xF0) == 0x90 && self.data2 == 0) + } + + /// Get the MIDI channel (0-15) + pub fn channel(&self) -> u8 { + self.status & 0x0F + } + + /// Get the message type (upper 4 bits of status) + pub fn message_type(&self) -> u8 { + self.status & 0xF0 + } +} + +/// MIDI clip ID type +pub type MidiClipId = u32; + +/// MIDI clip containing a sequence of MIDI events +#[derive(Debug, Clone)] +pub struct MidiClip { + pub id: MidiClipId, + pub events: Vec, + pub start_time: f64, // Position on timeline in seconds + pub duration: f64, // Clip duration in seconds + pub loop_enabled: bool, +} + +impl MidiClip { + /// Create a new MIDI clip + pub fn new(id: MidiClipId, start_time: f64, duration: f64) -> Self { + Self { + id, + events: Vec::new(), + start_time, + duration, + loop_enabled: false, + } + } + + /// Add a MIDI event to the clip + pub fn add_event(&mut self, event: MidiEvent) { + self.events.push(event); + // Keep events sorted by timestamp + self.events.sort_by_key(|e| e.timestamp); + } + + /// Get the end time of the clip + pub fn end_time(&self) -> f64 { + self.start_time + self.duration + } + + /// Get events that should be triggered in a given time range + /// + /// Returns events along with their absolute timestamps in samples + pub fn get_events_in_range( + &self, + range_start_seconds: f64, + range_end_seconds: f64, + sample_rate: u32, + ) -> Vec<(u64, MidiEvent)> { + let mut result = Vec::new(); + + // Check if clip overlaps with the range + if range_start_seconds >= self.end_time() || range_end_seconds <= self.start_time { + return result; + } + + // Calculate the intersection + let play_start = range_start_seconds.max(self.start_time); + let play_end = range_end_seconds.min(self.end_time()); + + // Convert to samples + let range_start_samples = (range_start_seconds * sample_rate as f64) as u64; + + // Position within the clip + let clip_position_seconds = play_start - self.start_time; + let clip_position_samples = (clip_position_seconds * sample_rate as f64) as u64; + let clip_end_samples = ((play_end - self.start_time) * sample_rate as f64) as u64; + + // Find events in this range + // Note: Using <= for the end boundary to include events exactly at the clip end + for event in &self.events { + if event.timestamp >= clip_position_samples && event.timestamp <= clip_end_samples { + // Calculate absolute timestamp in the output buffer + let absolute_timestamp = range_start_samples + (event.timestamp - clip_position_samples); + result.push((absolute_timestamp, *event)); + } + } + + result + } +} diff --git a/daw-backend/src/audio/mod.rs b/daw-backend/src/audio/mod.rs index 08e4f06..34fe7c9 100644 --- a/daw-backend/src/audio/mod.rs +++ b/daw-backend/src/audio/mod.rs @@ -1,9 +1,15 @@ +pub mod buffer_pool; pub mod clip; pub mod engine; +pub mod midi; pub mod pool; +pub mod project; pub mod track; +pub use buffer_pool::BufferPool; pub use clip::{Clip, ClipId}; pub use engine::{Engine, EngineController}; +pub use midi::{MidiClip, MidiClipId, MidiEvent}; pub use pool::{AudioFile as PoolAudioFile, AudioPool}; -pub use track::{Track, TrackId}; +pub use project::Project; +pub use track::{AudioTrack, GroupTrack, MidiTrack, Track, TrackId, TrackNode}; diff --git a/daw-backend/src/audio/project.rs b/daw-backend/src/audio/project.rs new file mode 100644 index 0000000..c5ff301 --- /dev/null +++ b/daw-backend/src/audio/project.rs @@ -0,0 +1,412 @@ +use super::buffer_pool::BufferPool; +use super::clip::Clip; +use super::midi::MidiClip; +use super::pool::AudioPool; +use super::track::{AudioTrack, GroupTrack, MidiTrack, TrackId, TrackNode}; +use crate::effects::Effect; +use std::collections::HashMap; + +/// Project manages the hierarchical track structure +/// +/// Tracks are stored in a flat HashMap but can be organized into groups, +/// forming a tree structure. Groups render their children recursively. +pub struct Project { + tracks: HashMap, + next_track_id: TrackId, + root_tracks: Vec, // Top-level tracks (not in any group) +} + +impl Project { + /// Create a new empty project + pub fn new() -> Self { + Self { + tracks: HashMap::new(), + next_track_id: 0, + root_tracks: Vec::new(), + } + } + + /// Generate a new unique track ID + fn next_id(&mut self) -> TrackId { + let id = self.next_track_id; + self.next_track_id += 1; + id + } + + /// Add an audio track to the project + /// + /// # Arguments + /// * `name` - Track name + /// * `parent_id` - Optional parent group ID + /// + /// # Returns + /// The new track's ID + pub fn add_audio_track(&mut self, name: String, parent_id: Option) -> TrackId { + let id = self.next_id(); + let track = AudioTrack::new(id, name); + self.tracks.insert(id, TrackNode::Audio(track)); + + if let Some(parent) = parent_id { + // Add to parent group + if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&parent) { + group.add_child(id); + } + } else { + // Add to root level + self.root_tracks.push(id); + } + + id + } + + /// Add a group track to the project + /// + /// # Arguments + /// * `name` - Group name + /// * `parent_id` - Optional parent group ID + /// + /// # Returns + /// The new group's ID + pub fn add_group_track(&mut self, name: String, parent_id: Option) -> TrackId { + let id = self.next_id(); + let group = GroupTrack::new(id, name); + self.tracks.insert(id, TrackNode::Group(group)); + + if let Some(parent) = parent_id { + // Add to parent group + if let Some(TrackNode::Group(parent_group)) = self.tracks.get_mut(&parent) { + parent_group.add_child(id); + } + } else { + // Add to root level + self.root_tracks.push(id); + } + + id + } + + /// Add a MIDI track to the project + /// + /// # Arguments + /// * `name` - Track name + /// * `parent_id` - Optional parent group ID + /// + /// # Returns + /// The new track's ID + pub fn add_midi_track(&mut self, name: String, parent_id: Option) -> TrackId { + let id = self.next_id(); + let track = MidiTrack::new(id, name); + self.tracks.insert(id, TrackNode::Midi(track)); + + if let Some(parent) = parent_id { + // Add to parent group + if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&parent) { + group.add_child(id); + } + } else { + // Add to root level + self.root_tracks.push(id); + } + + id + } + + /// Remove a track from the project + /// + /// If the track is a group, all children are moved to the parent (or root) + pub fn remove_track(&mut self, track_id: TrackId) { + if let Some(node) = self.tracks.remove(&track_id) { + // If it's a group, handle its children + if let TrackNode::Group(group) = node { + // Find the parent of this group + let parent_id = self.find_parent(track_id); + + // Move children to parent or root + for child_id in group.children { + if let Some(parent) = parent_id { + if let Some(TrackNode::Group(parent_group)) = self.tracks.get_mut(&parent) { + parent_group.add_child(child_id); + } + } else { + self.root_tracks.push(child_id); + } + } + } + + // Remove from parent or root + if let Some(parent_id) = self.find_parent(track_id) { + if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&parent_id) { + parent.remove_child(track_id); + } + } else { + self.root_tracks.retain(|&id| id != track_id); + } + } + } + + /// Find the parent group of a track + fn find_parent(&self, track_id: TrackId) -> Option { + for (id, node) in &self.tracks { + if let TrackNode::Group(group) = node { + if group.children.contains(&track_id) { + return Some(*id); + } + } + } + None + } + + /// Move a track to a different group + pub fn move_to_group(&mut self, track_id: TrackId, new_parent_id: TrackId) { + // First remove from current parent + if let Some(old_parent_id) = self.find_parent(track_id) { + if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&old_parent_id) { + parent.remove_child(track_id); + } + } else { + // Remove from root + self.root_tracks.retain(|&id| id != track_id); + } + + // Add to new parent + if let Some(TrackNode::Group(new_parent)) = self.tracks.get_mut(&new_parent_id) { + new_parent.add_child(track_id); + } + } + + /// Move a track to the root level (remove from any group) + pub fn move_to_root(&mut self, track_id: TrackId) { + // Remove from current parent if any + if let Some(parent_id) = self.find_parent(track_id) { + if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&parent_id) { + parent.remove_child(track_id); + } + // Add to root if not already there + if !self.root_tracks.contains(&track_id) { + self.root_tracks.push(track_id); + } + } + } + + /// Get a reference to a track node + pub fn get_track(&self, track_id: TrackId) -> Option<&TrackNode> { + self.tracks.get(&track_id) + } + + /// Get a mutable reference to a track node + pub fn get_track_mut(&mut self, track_id: TrackId) -> Option<&mut TrackNode> { + self.tracks.get_mut(&track_id) + } + + /// Get all root-level track IDs + pub fn root_tracks(&self) -> &[TrackId] { + &self.root_tracks + } + + /// Get the number of tracks in the project + pub fn track_count(&self) -> usize { + self.tracks.len() + } + + /// Check if any track is soloed + pub fn any_solo(&self) -> bool { + self.tracks.values().any(|node| node.is_solo()) + } + + /// Add a clip to an audio track + pub fn add_clip(&mut self, track_id: TrackId, clip: Clip) -> Result<(), &'static str> { + if let Some(TrackNode::Audio(track)) = self.tracks.get_mut(&track_id) { + track.add_clip(clip); + Ok(()) + } else { + Err("Track not found or is not an audio track") + } + } + + /// Add a MIDI clip to a MIDI track + pub fn add_midi_clip(&mut self, track_id: TrackId, clip: MidiClip) -> Result<(), &'static str> { + if let Some(TrackNode::Midi(track)) = self.tracks.get_mut(&track_id) { + track.add_clip(clip); + Ok(()) + } else { + Err("Track not found or is not a MIDI track") + } + } + + /// Add an effect to a track (audio, MIDI, or group) + pub fn add_effect(&mut self, track_id: TrackId, effect: Box) -> Result<(), &'static str> { + match self.tracks.get_mut(&track_id) { + Some(TrackNode::Audio(track)) => { + track.add_effect(effect); + Ok(()) + } + Some(TrackNode::Midi(track)) => { + track.add_effect(effect); + Ok(()) + } + Some(TrackNode::Group(group)) => { + group.add_effect(effect); + Ok(()) + } + None => Err("Track not found"), + } + } + + /// Clear effects from a track + pub fn clear_effects(&mut self, track_id: TrackId) -> Result<(), &'static str> { + match self.tracks.get_mut(&track_id) { + Some(TrackNode::Audio(track)) => { + track.clear_effects(); + Ok(()) + } + Some(TrackNode::Midi(track)) => { + track.clear_effects(); + Ok(()) + } + Some(TrackNode::Group(group)) => { + group.clear_effects(); + Ok(()) + } + None => Err("Track not found"), + } + } + + /// Render all root tracks into the output buffer + pub fn render( + &mut self, + output: &mut [f32], + pool: &AudioPool, + buffer_pool: &mut BufferPool, + playhead_seconds: f64, + sample_rate: u32, + channels: u32, + ) { + output.fill(0.0); + + let any_solo = self.any_solo(); + + // Render each root track + for &track_id in &self.root_tracks.clone() { + self.render_track( + track_id, + output, + pool, + buffer_pool, + playhead_seconds, + sample_rate, + channels, + any_solo, + false, // root tracks are not inside a soloed parent + ); + } + } + + /// Recursively render a track (audio or group) into the output buffer + fn render_track( + &mut self, + track_id: TrackId, + output: &mut [f32], + pool: &AudioPool, + buffer_pool: &mut BufferPool, + playhead_seconds: f64, + sample_rate: u32, + channels: u32, + any_solo: bool, + parent_is_soloed: bool, + ) { + // Check if track should be rendered based on mute/solo + let should_render = match self.tracks.get(&track_id) { + Some(TrackNode::Audio(track)) => { + // If parent is soloed, only check mute state + // Otherwise, check normal solo logic + if parent_is_soloed { + !track.muted + } else { + track.is_active(any_solo) + } + } + Some(TrackNode::Midi(track)) => { + // Same logic for MIDI tracks + if parent_is_soloed { + !track.muted + } else { + track.is_active(any_solo) + } + } + Some(TrackNode::Group(group)) => { + // Same logic for groups + if parent_is_soloed { + !group.muted + } else { + group.is_active(any_solo) + } + } + None => return, + }; + + if !should_render { + return; + } + + // Handle audio track vs MIDI track vs group track + match self.tracks.get_mut(&track_id) { + Some(TrackNode::Audio(track)) => { + // Render audio track directly into output + track.render(output, pool, playhead_seconds, sample_rate, channels); + } + Some(TrackNode::Midi(track)) => { + // Render MIDI track directly into output + track.render(output, playhead_seconds, sample_rate, channels); + } + Some(TrackNode::Group(group)) => { + // Get children IDs and check if this group is soloed + let children: Vec = group.children.clone(); + let this_group_is_soloed = group.solo; + + // Acquire a temporary buffer for the group mix + let mut group_buffer = buffer_pool.acquire(); + group_buffer.resize(output.len(), 0.0); + group_buffer.fill(0.0); + + // Recursively render all children into the group buffer + // If this group is soloed (or parent was soloed), children inherit that state + let children_parent_soloed = parent_is_soloed || this_group_is_soloed; + for &child_id in &children { + self.render_track( + child_id, + &mut group_buffer, + pool, + buffer_pool, + playhead_seconds, + sample_rate, + channels, + any_solo, + children_parent_soloed, + ); + } + + // Apply group effects + if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&track_id) { + for effect in &mut group.effects { + effect.process(&mut group_buffer, channels as usize, sample_rate); + } + + // Apply group volume and mix into output + for (out_sample, group_sample) in output.iter_mut().zip(group_buffer.iter()) { + *out_sample += group_sample * group.volume; + } + } + + // Release buffer back to pool + buffer_pool.release(group_buffer); + } + None => {} + } + } +} + +impl Default for Project { + fn default() -> Self { + Self::new() + } +} diff --git a/daw-backend/src/audio/track.rs b/daw-backend/src/audio/track.rs index f0384de..eb6072c 100644 --- a/daw-backend/src/audio/track.rs +++ b/daw-backend/src/audio/track.rs @@ -1,32 +1,300 @@ use super::clip::Clip; +use super::midi::MidiClip; use super::pool::AudioPool; +use crate::effects::{Effect, SimpleSynth}; /// Track ID type pub type TrackId = u32; -/// Audio track for Phase 4 with clips -pub struct Track { +/// Type alias for backwards compatibility +pub type Track = AudioTrack; + +/// Node in the track hierarchy - can be an audio track, MIDI track, or a group +pub enum TrackNode { + Audio(AudioTrack), + Midi(MidiTrack), + Group(GroupTrack), +} + +impl TrackNode { + /// Get the track ID + pub fn id(&self) -> TrackId { + match self { + TrackNode::Audio(track) => track.id, + TrackNode::Midi(track) => track.id, + TrackNode::Group(group) => group.id, + } + } + + /// Get the track name + pub fn name(&self) -> &str { + match self { + TrackNode::Audio(track) => &track.name, + TrackNode::Midi(track) => &track.name, + TrackNode::Group(group) => &group.name, + } + } + + /// Get muted state + pub fn is_muted(&self) -> bool { + match self { + TrackNode::Audio(track) => track.muted, + TrackNode::Midi(track) => track.muted, + TrackNode::Group(group) => group.muted, + } + } + + /// Get solo state + pub fn is_solo(&self) -> bool { + match self { + TrackNode::Audio(track) => track.solo, + TrackNode::Midi(track) => track.solo, + TrackNode::Group(group) => group.solo, + } + } + + /// Set volume + pub fn set_volume(&mut self, volume: f32) { + match self { + TrackNode::Audio(track) => track.set_volume(volume), + TrackNode::Midi(track) => track.set_volume(volume), + TrackNode::Group(group) => group.set_volume(volume), + } + } + + /// Set muted state + pub fn set_muted(&mut self, muted: bool) { + match self { + TrackNode::Audio(track) => track.set_muted(muted), + TrackNode::Midi(track) => track.set_muted(muted), + TrackNode::Group(group) => group.set_muted(muted), + } + } + + /// Set solo state + pub fn set_solo(&mut self, solo: bool) { + match self { + TrackNode::Audio(track) => track.set_solo(solo), + TrackNode::Midi(track) => track.set_solo(solo), + TrackNode::Group(group) => group.set_solo(solo), + } + } +} + +/// Group track that contains other tracks (audio or groups) +pub struct GroupTrack { pub id: TrackId, pub name: String, - pub clips: Vec, + pub children: Vec, + pub effects: Vec>, pub volume: f32, pub muted: bool, pub solo: bool, } -impl Track { - /// Create a new track with default settings +impl GroupTrack { + /// Create a new group track + pub fn new(id: TrackId, name: String) -> Self { + Self { + id, + name, + children: Vec::new(), + effects: Vec::new(), + volume: 1.0, + muted: false, + solo: false, + } + } + + /// Add a child track to this group + pub fn add_child(&mut self, track_id: TrackId) { + if !self.children.contains(&track_id) { + self.children.push(track_id); + } + } + + /// Remove a child track from this group + pub fn remove_child(&mut self, track_id: TrackId) { + self.children.retain(|&id| id != track_id); + } + + /// Add an effect to the group's effect chain + pub fn add_effect(&mut self, effect: Box) { + self.effects.push(effect); + } + + /// Clear all effects from the group + pub fn clear_effects(&mut self) { + self.effects.clear(); + } + + /// Set group volume + pub fn set_volume(&mut self, volume: f32) { + self.volume = volume.max(0.0); + } + + /// Set mute state + pub fn set_muted(&mut self, muted: bool) { + self.muted = muted; + } + + /// Set solo state + pub fn set_solo(&mut self, solo: bool) { + self.solo = solo; + } + + /// Check if this group should be audible given the solo state + pub fn is_active(&self, any_solo: bool) -> bool { + !self.muted && (!any_solo || self.solo) + } +} + +/// MIDI track with MIDI clips and a virtual instrument +pub struct MidiTrack { + pub id: TrackId, + pub name: String, + pub clips: Vec, + pub instrument: SimpleSynth, + pub effects: Vec>, + pub volume: f32, + pub muted: bool, + pub solo: bool, +} + +impl MidiTrack { + /// Create a new MIDI track with default settings pub fn new(id: TrackId, name: String) -> Self { Self { id, name, clips: Vec::new(), + instrument: SimpleSynth::new(), + effects: Vec::new(), volume: 1.0, muted: false, solo: false, } } + /// Add an effect to the track's effect chain + pub fn add_effect(&mut self, effect: Box) { + self.effects.push(effect); + } + + /// Clear all effects from the track + pub fn clear_effects(&mut self) { + self.effects.clear(); + } + + /// Add a MIDI clip to this track + pub fn add_clip(&mut self, clip: MidiClip) { + self.clips.push(clip); + } + + /// Set track volume + pub fn set_volume(&mut self, volume: f32) { + self.volume = volume.max(0.0); + } + + /// Set mute state + pub fn set_muted(&mut self, muted: bool) { + self.muted = muted; + } + + /// Set solo state + pub fn set_solo(&mut self, solo: bool) { + self.solo = solo; + } + + /// Check if this track should be audible given the solo state + pub fn is_active(&self, any_solo: bool) -> bool { + !self.muted && (!any_solo || self.solo) + } + + /// Render this MIDI track into the output buffer + pub fn render( + &mut self, + output: &mut [f32], + playhead_seconds: f64, + sample_rate: u32, + channels: u32, + ) { + let buffer_duration_seconds = output.len() as f64 / (sample_rate as f64 * channels as f64); + let buffer_end_seconds = playhead_seconds + buffer_duration_seconds; + + // Collect MIDI events from all clips that overlap with current time range + for clip in &self.clips { + let events = clip.get_events_in_range( + playhead_seconds, + buffer_end_seconds, + sample_rate, + ); + + // Queue events in the instrument + for (_timestamp, event) in events { + self.instrument.queue_event(event); + } + } + + // Generate audio from the instrument + self.instrument.process(output, channels as usize, sample_rate); + + // Apply effect chain + for effect in &mut self.effects { + effect.process(output, channels as usize, sample_rate); + } + + // Apply track volume + for sample in output.iter_mut() { + *sample *= self.volume; + } + } +} + +/// Audio track with clips and effect chain +pub struct AudioTrack { + pub id: TrackId, + pub name: String, + pub clips: Vec, + pub effects: Vec>, + pub volume: f32, + pub muted: bool, + pub solo: bool, +} + +impl AudioTrack { + /// Create a new audio track with default settings + pub fn new(id: TrackId, name: String) -> Self { + Self { + id, + name, + clips: Vec::new(), + effects: Vec::new(), + volume: 1.0, + muted: false, + solo: false, + } + } + + /// Add an effect to the track's effect chain + pub fn add_effect(&mut self, effect: Box) { + self.effects.push(effect); + } + + /// Remove an effect from the chain by index + pub fn remove_effect(&mut self, index: usize) -> Option> { + if index < self.effects.len() { + Some(self.effects.remove(index)) + } else { + None + } + } + + /// Clear all effects from the track + pub fn clear_effects(&mut self) { + self.effects.clear(); + } + /// Add a clip to this track pub fn add_clip(&mut self, clip: Clip) { self.clips.push(clip); @@ -55,7 +323,7 @@ impl Track { /// Render this track into the output buffer at a given timeline position /// Returns the number of samples actually rendered pub fn render( - &self, + &mut self, output: &mut [f32], pool: &AudioPool, playhead_seconds: f64, @@ -82,6 +350,16 @@ impl Track { } } + // Apply effect chain + for effect in &mut self.effects { + effect.process(output, channels as usize, sample_rate); + } + + // Apply track volume + for sample in output.iter_mut() { + *sample *= self.volume; + } + rendered } diff --git a/daw-backend/src/command/types.rs b/daw-backend/src/command/types.rs index 43ac7ef..0132b8e 100644 --- a/daw-backend/src/command/types.rs +++ b/daw-backend/src/command/types.rs @@ -1,4 +1,4 @@ -use crate::audio::{ClipId, TrackId}; +use crate::audio::{ClipId, MidiClip, MidiClipId, TrackId}; /// Commands sent from UI/control thread to audio thread #[derive(Debug, Clone)] @@ -24,6 +24,34 @@ pub enum Command { // Clip management commands /// Move a clip to a new timeline position MoveClip(TrackId, ClipId, f64), + + // Effect management commands + /// Add or update gain effect on track (gain in dB) + AddGainEffect(TrackId, f32), + /// Add or update pan effect on track (-1.0 = left, 0.0 = center, 1.0 = right) + AddPanEffect(TrackId, f32), + /// Add or update EQ effect on track (low_db, mid_db, high_db) + AddEQEffect(TrackId, f32, f32, f32), + /// Clear all effects from a track + ClearEffects(TrackId), + + // Group management commands + /// Create a new group track with a name + CreateGroup(String), + /// Add a track to a group (track_id, group_id) + AddToGroup(TrackId, TrackId), + /// Remove a track from its parent group + RemoveFromGroup(TrackId), + + // MIDI commands + /// Create a new MIDI track with a name + CreateMidiTrack(String), + /// Create a new MIDI clip on a track (track_id, start_time, duration) + CreateMidiClip(TrackId, f64, f64), + /// Add a MIDI note to a clip (track_id, clip_id, time_offset, note, velocity, duration) + AddMidiNote(TrackId, MidiClipId, f64, u8, u8, f64), + /// Add a pre-loaded MIDI clip to a track + AddLoadedMidiClip(TrackId, MidiClip), } /// Events sent from audio thread back to UI/control thread @@ -35,4 +63,6 @@ pub enum AudioEvent { PlaybackStopped, /// Audio buffer underrun detected BufferUnderrun, + /// A new track was created (track_id, is_group, name) + TrackCreated(TrackId, bool, String), } diff --git a/daw-backend/src/dsp/biquad.rs b/daw-backend/src/dsp/biquad.rs new file mode 100644 index 0000000..5f0fbee --- /dev/null +++ b/daw-backend/src/dsp/biquad.rs @@ -0,0 +1,175 @@ +use std::f32::consts::PI; + +/// Biquad filter implementation (2-pole IIR filter) +/// +/// Transfer function: H(z) = (b0 + b1*z^-1 + b2*z^-2) / (1 + a1*z^-1 + a2*z^-2) +#[derive(Clone)] +pub struct BiquadFilter { + // Filter coefficients + b0: f32, + b1: f32, + b2: f32, + a1: f32, + a2: f32, + + // State variables (per channel, supporting up to 2 channels) + x1: [f32; 2], + x2: [f32; 2], + y1: [f32; 2], + y2: [f32; 2], +} + +impl BiquadFilter { + /// Create a new biquad filter with unity gain (pass-through) + pub fn new() -> Self { + Self { + b0: 1.0, + b1: 0.0, + b2: 0.0, + a1: 0.0, + a2: 0.0, + x1: [0.0; 2], + x2: [0.0; 2], + y1: [0.0; 2], + y2: [0.0; 2], + } + } + + /// Create a lowpass filter + /// + /// # Arguments + /// * `frequency` - Cutoff frequency in Hz + /// * `q` - Quality factor (resonance), typically 0.707 for Butterworth + /// * `sample_rate` - Sample rate in Hz + pub fn lowpass(frequency: f32, q: f32, sample_rate: f32) -> Self { + let mut filter = Self::new(); + filter.set_lowpass(frequency, q, sample_rate); + filter + } + + /// Create a highpass filter + /// + /// # Arguments + /// * `frequency` - Cutoff frequency in Hz + /// * `q` - Quality factor (resonance), typically 0.707 for Butterworth + /// * `sample_rate` - Sample rate in Hz + pub fn highpass(frequency: f32, q: f32, sample_rate: f32) -> Self { + let mut filter = Self::new(); + filter.set_highpass(frequency, q, sample_rate); + filter + } + + /// Create a peaking EQ filter + /// + /// # Arguments + /// * `frequency` - Center frequency in Hz + /// * `q` - Quality factor (bandwidth) + /// * `gain_db` - Gain in decibels + /// * `sample_rate` - Sample rate in Hz + pub fn peaking(frequency: f32, q: f32, gain_db: f32, sample_rate: f32) -> Self { + let mut filter = Self::new(); + filter.set_peaking(frequency, q, gain_db, sample_rate); + filter + } + + /// Set coefficients for a lowpass filter + pub fn set_lowpass(&mut self, frequency: f32, q: f32, sample_rate: f32) { + let omega = 2.0 * PI * frequency / sample_rate; + let sin_omega = omega.sin(); + let cos_omega = omega.cos(); + let alpha = sin_omega / (2.0 * q); + + let a0 = 1.0 + alpha; + self.b0 = ((1.0 - cos_omega) / 2.0) / a0; + self.b1 = (1.0 - cos_omega) / a0; + self.b2 = ((1.0 - cos_omega) / 2.0) / a0; + self.a1 = (-2.0 * cos_omega) / a0; + self.a2 = (1.0 - alpha) / a0; + } + + /// Set coefficients for a highpass filter + pub fn set_highpass(&mut self, frequency: f32, q: f32, sample_rate: f32) { + let omega = 2.0 * PI * frequency / sample_rate; + let sin_omega = omega.sin(); + let cos_omega = omega.cos(); + let alpha = sin_omega / (2.0 * q); + + let a0 = 1.0 + alpha; + self.b0 = ((1.0 + cos_omega) / 2.0) / a0; + self.b1 = -(1.0 + cos_omega) / a0; + self.b2 = ((1.0 + cos_omega) / 2.0) / a0; + self.a1 = (-2.0 * cos_omega) / a0; + self.a2 = (1.0 - alpha) / a0; + } + + /// Set coefficients for a peaking EQ filter + pub fn set_peaking(&mut self, frequency: f32, q: f32, gain_db: f32, sample_rate: f32) { + let omega = 2.0 * PI * frequency / sample_rate; + let sin_omega = omega.sin(); + let cos_omega = omega.cos(); + let a_gain = 10.0_f32.powf(gain_db / 40.0); + let alpha = sin_omega / (2.0 * q); + + let a0 = 1.0 + alpha / a_gain; + self.b0 = (1.0 + alpha * a_gain) / a0; + self.b1 = (-2.0 * cos_omega) / a0; + self.b2 = (1.0 - alpha * a_gain) / a0; + self.a1 = (-2.0 * cos_omega) / a0; + self.a2 = (1.0 - alpha / a_gain) / a0; + } + + /// Process a single sample + /// + /// # Arguments + /// * `input` - Input sample + /// * `channel` - Channel index (0 or 1) + /// + /// # Returns + /// Filtered output sample + #[inline] + pub fn process_sample(&mut self, input: f32, channel: usize) -> f32 { + let channel = channel.min(1); // Clamp to 0 or 1 + + // Direct Form II Transposed implementation + let output = self.b0 * input + self.x1[channel]; + + self.x1[channel] = self.b1 * input - self.a1 * output + self.x2[channel]; + self.x2[channel] = self.b2 * input - self.a2 * output; + + output + } + + /// Process a buffer of interleaved samples + /// + /// # Arguments + /// * `buffer` - Interleaved audio samples + /// * `channels` - Number of channels + pub fn process_buffer(&mut self, buffer: &mut [f32], channels: usize) { + if channels == 1 { + // Mono + for sample in buffer.iter_mut() { + *sample = self.process_sample(*sample, 0); + } + } else if channels == 2 { + // Stereo + for frame in buffer.chunks_exact_mut(2) { + frame[0] = self.process_sample(frame[0], 0); + frame[1] = self.process_sample(frame[1], 1); + } + } + } + + /// Reset filter state (clear delay lines) + pub fn reset(&mut self) { + self.x1 = [0.0; 2]; + self.x2 = [0.0; 2]; + self.y1 = [0.0; 2]; + self.y2 = [0.0; 2]; + } +} + +impl Default for BiquadFilter { + fn default() -> Self { + Self::new() + } +} diff --git a/daw-backend/src/dsp/mod.rs b/daw-backend/src/dsp/mod.rs new file mode 100644 index 0000000..8c5eae0 --- /dev/null +++ b/daw-backend/src/dsp/mod.rs @@ -0,0 +1,3 @@ +pub mod biquad; + +pub use biquad::BiquadFilter; diff --git a/daw-backend/src/effects/effect_trait.rs b/daw-backend/src/effects/effect_trait.rs new file mode 100644 index 0000000..975ab6b --- /dev/null +++ b/daw-backend/src/effects/effect_trait.rs @@ -0,0 +1,35 @@ +/// Audio effect processor trait +/// +/// All effects must be Send to be usable in the audio thread. +/// Effects should be real-time safe: no allocations, no blocking operations. +pub trait Effect: Send { + /// Process audio buffer in-place + /// + /// # Arguments + /// * `buffer` - Interleaved audio samples to process + /// * `channels` - Number of audio channels (2 for stereo) + /// * `sample_rate` - Sample rate in Hz + fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32); + + /// Set an effect parameter + /// + /// # Arguments + /// * `id` - Parameter identifier + /// * `value` - Parameter value (normalized or specific units depending on parameter) + fn set_parameter(&mut self, id: u32, value: f32); + + /// Get an effect parameter value + /// + /// # Arguments + /// * `id` - Parameter identifier + /// + /// # Returns + /// Current parameter value + fn get_parameter(&self, id: u32) -> f32; + + /// Reset effect state (clear delays, resonances, etc.) + fn reset(&mut self); + + /// Get the effect name + fn name(&self) -> &str; +} diff --git a/daw-backend/src/effects/eq.rs b/daw-backend/src/effects/eq.rs new file mode 100644 index 0000000..074390a --- /dev/null +++ b/daw-backend/src/effects/eq.rs @@ -0,0 +1,148 @@ +use super::Effect; +use crate::dsp::BiquadFilter; + +/// Simple 3-band EQ (low shelf, mid peak, high shelf) +/// +/// Parameters: +/// - 0: Low gain in dB (-12.0 to +12.0) +/// - 1: Mid gain in dB (-12.0 to +12.0) +/// - 2: High gain in dB (-12.0 to +12.0) +/// - 3: Low frequency in Hz (default: 250) +/// - 4: Mid frequency in Hz (default: 1000) +/// - 5: High frequency in Hz (default: 8000) +pub struct SimpleEQ { + low_gain: f32, + mid_gain: f32, + high_gain: f32, + low_freq: f32, + mid_freq: f32, + high_freq: f32, + + low_filter: BiquadFilter, + mid_filter: BiquadFilter, + high_filter: BiquadFilter, + + sample_rate: f32, +} + +impl SimpleEQ { + /// Create a new SimpleEQ with flat response + pub fn new() -> Self { + Self { + low_gain: 0.0, + mid_gain: 0.0, + high_gain: 0.0, + low_freq: 250.0, + mid_freq: 1000.0, + high_freq: 8000.0, + low_filter: BiquadFilter::new(), + mid_filter: BiquadFilter::new(), + high_filter: BiquadFilter::new(), + sample_rate: 48000.0, // Default, will be updated on first process + } + } + + /// Set low band gain in decibels + pub fn set_low_gain(&mut self, gain_db: f32) { + self.low_gain = gain_db.clamp(-12.0, 12.0); + self.update_filters(); + } + + /// Set mid band gain in decibels + pub fn set_mid_gain(&mut self, gain_db: f32) { + self.mid_gain = gain_db.clamp(-12.0, 12.0); + self.update_filters(); + } + + /// Set high band gain in decibels + pub fn set_high_gain(&mut self, gain_db: f32) { + self.high_gain = gain_db.clamp(-12.0, 12.0); + self.update_filters(); + } + + /// Set low band frequency + pub fn set_low_freq(&mut self, freq: f32) { + self.low_freq = freq.clamp(20.0, 500.0); + self.update_filters(); + } + + /// Set mid band frequency + pub fn set_mid_freq(&mut self, freq: f32) { + self.mid_freq = freq.clamp(200.0, 5000.0); + self.update_filters(); + } + + /// Set high band frequency + pub fn set_high_freq(&mut self, freq: f32) { + self.high_freq = freq.clamp(2000.0, 20000.0); + self.update_filters(); + } + + /// Update filter coefficients based on current parameters + fn update_filters(&mut self) { + // Only update if sample rate has been set + if self.sample_rate > 0.0 { + // Use peaking filters for all bands + // Q factor of 1.0 gives a moderate bandwidth + self.low_filter.set_peaking(self.low_freq, 1.0, self.low_gain, self.sample_rate); + self.mid_filter.set_peaking(self.mid_freq, 1.0, self.mid_gain, self.sample_rate); + self.high_filter.set_peaking(self.high_freq, 1.0, self.high_gain, self.sample_rate); + } + } +} + +impl Default for SimpleEQ { + fn default() -> Self { + Self::new() + } +} + +impl Effect for SimpleEQ { + fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32) { + // Update sample rate if it changed + let sr = sample_rate as f32; + if (self.sample_rate - sr).abs() > 0.1 { + self.sample_rate = sr; + self.update_filters(); + } + + // Process through each filter in series + self.low_filter.process_buffer(buffer, channels); + self.mid_filter.process_buffer(buffer, channels); + self.high_filter.process_buffer(buffer, channels); + } + + fn set_parameter(&mut self, id: u32, value: f32) { + match id { + 0 => self.set_low_gain(value), + 1 => self.set_mid_gain(value), + 2 => self.set_high_gain(value), + 3 => self.set_low_freq(value), + 4 => self.set_mid_freq(value), + 5 => self.set_high_freq(value), + _ => {} + } + } + + fn get_parameter(&self, id: u32) -> f32 { + match id { + 0 => self.low_gain, + 1 => self.mid_gain, + 2 => self.high_gain, + 3 => self.low_freq, + 4 => self.mid_freq, + 5 => self.high_freq, + _ => 0.0, + } + } + + fn reset(&mut self) { + self.low_filter.reset(); + self.mid_filter.reset(); + self.high_filter.reset(); + } + + fn name(&self) -> &str { + "SimpleEQ" + } +} diff --git a/daw-backend/src/effects/gain.rs b/daw-backend/src/effects/gain.rs new file mode 100644 index 0000000..6852bef --- /dev/null +++ b/daw-backend/src/effects/gain.rs @@ -0,0 +1,97 @@ +use super::Effect; + +/// Simple gain/volume effect +/// +/// Parameters: +/// - 0: Gain in dB (-60.0 to +12.0) +pub struct GainEffect { + gain_db: f32, + gain_linear: f32, +} + +impl GainEffect { + /// Create a new gain effect with 0 dB (unity) gain + pub fn new() -> Self { + Self { + gain_db: 0.0, + gain_linear: 1.0, + } + } + + /// Create a gain effect with a specific dB value + pub fn with_gain_db(gain_db: f32) -> Self { + let gain_linear = db_to_linear(gain_db); + Self { + gain_db, + gain_linear, + } + } + + /// Set gain in decibels + pub fn set_gain_db(&mut self, gain_db: f32) { + self.gain_db = gain_db.clamp(-60.0, 12.0); + self.gain_linear = db_to_linear(self.gain_db); + } + + /// Get current gain in decibels + pub fn gain_db(&self) -> f32 { + self.gain_db + } +} + +impl Default for GainEffect { + fn default() -> Self { + Self::new() + } +} + +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 { + self.set_gain_db(value); + } + } + + fn get_parameter(&self, id: u32) -> f32 { + if id == 0 { + self.gain_db + } else { + 0.0 + } + } + + fn reset(&mut self) { + // Gain has no state to reset + } + + fn name(&self) -> &str { + "Gain" + } +} + +/// Convert decibels to linear gain +#[inline] +fn db_to_linear(db: f32) -> f32 { + if db <= -60.0 { + 0.0 + } else { + 10.0_f32.powf(db / 20.0) + } +} + +/// Convert linear gain to decibels +#[inline] +#[allow(dead_code)] +fn linear_to_db(linear: f32) -> f32 { + if linear <= 0.0 { + -60.0 + } else { + 20.0 * linear.log10() + } +} diff --git a/daw-backend/src/effects/mod.rs b/daw-backend/src/effects/mod.rs new file mode 100644 index 0000000..413adcb --- /dev/null +++ b/daw-backend/src/effects/mod.rs @@ -0,0 +1,11 @@ +pub mod effect_trait; +pub mod eq; +pub mod gain; +pub mod pan; +pub mod synth; + +pub use effect_trait::Effect; +pub use eq::SimpleEQ; +pub use gain::GainEffect; +pub use pan::PanEffect; +pub use synth::SimpleSynth; diff --git a/daw-backend/src/effects/pan.rs b/daw-backend/src/effects/pan.rs new file mode 100644 index 0000000..9804249 --- /dev/null +++ b/daw-backend/src/effects/pan.rs @@ -0,0 +1,98 @@ +use super::Effect; + +/// Stereo panning effect using constant-power panning law +/// +/// Parameters: +/// - 0: Pan position (-1.0 = full left, 0.0 = center, +1.0 = full right) +pub struct PanEffect { + pan: f32, + left_gain: f32, + right_gain: f32, +} + +impl PanEffect { + /// Create a new pan effect with center panning + pub fn new() -> Self { + let mut effect = Self { + pan: 0.0, + left_gain: 1.0, + right_gain: 1.0, + }; + effect.update_gains(); + effect + } + + /// Create a pan effect with a specific pan position + pub fn with_pan(pan: f32) -> Self { + let mut effect = Self { + pan: pan.clamp(-1.0, 1.0), + left_gain: 1.0, + right_gain: 1.0, + }; + effect.update_gains(); + effect + } + + /// Set pan position (-1.0 = left, 0.0 = center, +1.0 = right) + pub fn set_pan(&mut self, pan: f32) { + self.pan = pan.clamp(-1.0, 1.0); + self.update_gains(); + } + + /// Get current pan position + pub fn pan(&self) -> f32 { + self.pan + } + + /// Update left/right gains using constant-power panning law + fn update_gains(&mut self) { + use std::f32::consts::PI; + + // Constant-power panning: pan from -1 to +1 maps to angle 0 to PI/2 + let angle = (self.pan + 1.0) * 0.5 * PI / 2.0; + + self.left_gain = angle.cos(); + self.right_gain = angle.sin(); + } +} + +impl Default for PanEffect { + fn default() -> Self { + Self::new() + } +} + +impl Effect for PanEffect { + fn process(&mut self, buffer: &mut [f32], channels: usize, _sample_rate: u32) { + if channels == 2 { + // Stereo processing + for frame in buffer.chunks_exact_mut(2) { + frame[0] *= self.left_gain; + frame[1] *= self.right_gain; + } + } + // Mono and other channel counts: no panning applied + } + + fn set_parameter(&mut self, id: u32, value: f32) { + if id == 0 { + self.set_pan(value); + } + } + + fn get_parameter(&self, id: u32) -> f32 { + if id == 0 { + self.pan + } else { + 0.0 + } + } + + fn reset(&mut self) { + // Pan has no state to reset + } + + fn name(&self) -> &str { + "Pan" + } +} diff --git a/daw-backend/src/effects/synth.rs b/daw-backend/src/effects/synth.rs new file mode 100644 index 0000000..6915c8f --- /dev/null +++ b/daw-backend/src/effects/synth.rs @@ -0,0 +1,213 @@ +use super::Effect; +use crate::audio::midi::MidiEvent; +use std::f32::consts::PI; + +/// Maximum number of simultaneous voices +const MAX_VOICES: usize = 16; + +/// A single synthesizer voice +#[derive(Clone)] +struct SynthVoice { + active: bool, + note: u8, + channel: u8, + velocity: u8, + phase: f32, + frequency: f32, + age: u32, // For voice stealing +} + +impl SynthVoice { + fn new() -> Self { + Self { + active: false, + note: 0, + channel: 0, + velocity: 0, + phase: 0.0, + frequency: 0.0, + age: 0, + } + } + + /// Calculate frequency from MIDI note number + fn note_to_frequency(note: u8) -> f32 { + 440.0 * 2.0_f32.powf((note as f32 - 69.0) / 12.0) + } + + /// Start playing a note + fn note_on(&mut self, channel: u8, note: u8, velocity: u8) { + self.active = true; + self.channel = channel; + self.note = note; + self.velocity = velocity; + self.frequency = Self::note_to_frequency(note); + self.phase = 0.0; + self.age = 0; + } + + /// Stop playing + fn note_off(&mut self) { + self.active = false; + } + + /// Generate one sample + fn process_sample(&mut self, sample_rate: f32) -> f32 { + if !self.active { + return 0.0; + } + + // Simple sine wave + let sample = (self.phase * 2.0 * PI).sin() * (self.velocity as f32 / 127.0) * 0.3; + + // Update phase + self.phase += self.frequency / sample_rate; + if self.phase >= 1.0 { + self.phase -= 1.0; + } + + self.age += 1; + sample + } +} + +/// Simple polyphonic synthesizer using sine waves +pub struct SimpleSynth { + voices: Vec, + sample_rate: f32, + pending_events: Vec, +} + +impl SimpleSynth { + /// Create a new SimpleSynth + pub fn new() -> Self { + Self { + voices: vec![SynthVoice::new(); MAX_VOICES], + sample_rate: 44100.0, + pending_events: Vec::new(), + } + } + + /// Find a free voice, or steal the oldest one + fn find_voice_for_note_on(&mut self) -> usize { + // First, look for an inactive voice + for (i, voice) in self.voices.iter().enumerate() { + if !voice.active { + return i; + } + } + + // No free voices, steal the oldest one + self.voices + .iter() + .enumerate() + .max_by_key(|(_, v)| v.age) + .map(|(i, _)| i) + .unwrap_or(0) + } + + /// Find the voice playing a specific note on a specific channel + fn find_voice_for_note_off(&mut self, channel: u8, note: u8) -> Option { + self.voices + .iter() + .position(|v| v.active && v.channel == channel && v.note == note) + } + + /// Handle a MIDI event + pub fn handle_event(&mut self, event: &MidiEvent) { + if event.is_note_on() { + let voice_idx = self.find_voice_for_note_on(); + self.voices[voice_idx].note_on(event.channel(), event.data1, event.data2); + } else if event.is_note_off() { + if let Some(voice_idx) = self.find_voice_for_note_off(event.channel(), event.data1) { + self.voices[voice_idx].note_off(); + } + } + } + + /// Queue a MIDI event to be processed + pub fn queue_event(&mut self, event: MidiEvent) { + self.pending_events.push(event); + } + + /// Process all queued events + fn process_events(&mut self) { + // Collect events first to avoid borrowing issues + let events: Vec = self.pending_events.drain(..).collect(); + for event in events { + self.handle_event(&event); + } + } +} + +impl Effect for SimpleSynth { + fn process(&mut self, buffer: &mut [f32], channels: usize, sample_rate: u32) { + self.sample_rate = sample_rate as f32; + + // Process any queued MIDI events + self.process_events(); + + // Generate audio from all active voices + if channels == 1 { + // Mono + for sample in buffer.iter_mut() { + let mut sum = 0.0; + for voice in &mut self.voices { + sum += voice.process_sample(self.sample_rate); + } + *sample += sum; + } + } else if channels == 2 { + // Stereo (duplicate mono signal) + for frame in buffer.chunks_exact_mut(2) { + let mut sum = 0.0; + for voice in &mut self.voices { + sum += voice.process_sample(self.sample_rate); + } + frame[0] += sum; + frame[1] += sum; + } + } + } + + fn set_parameter(&mut self, id: u32, value: f32) { + // Parameter 0: Note on + // Parameter 1: Note off + // This is a simple interface for testing without proper MIDI routing + match id { + 0 => { + let note = value as u8; + let voice_idx = self.find_voice_for_note_on(); + self.voices[voice_idx].note_on(0, note, 100); + } + 1 => { + let note = value as u8; + if let Some(voice_idx) = self.find_voice_for_note_off(0, note) { + self.voices[voice_idx].note_off(); + } + } + _ => {} + } + } + + fn get_parameter(&self, _id: u32) -> f32 { + 0.0 + } + + fn reset(&mut self) { + for voice in &mut self.voices { + voice.note_off(); + } + self.pending_events.clear(); + } + + fn name(&self) -> &str { + "SimpleSynth" + } +} + +impl Default for SimpleSynth { + fn default() -> Self { + Self::new() + } +} diff --git a/daw-backend/src/io/midi_file.rs b/daw-backend/src/io/midi_file.rs new file mode 100644 index 0000000..2b2512b --- /dev/null +++ b/daw-backend/src/io/midi_file.rs @@ -0,0 +1,164 @@ +use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent}; +use std::fs; +use std::path::Path; + +/// Load a MIDI file and convert it to a MidiClip +pub fn load_midi_file>( + path: P, + clip_id: MidiClipId, + sample_rate: u32, +) -> Result { + // Read the MIDI file + let data = fs::read(path.as_ref()).map_err(|e| format!("Failed to read MIDI file: {}", e))?; + + // Parse with midly + let smf = midly::Smf::parse(&data).map_err(|e| format!("Failed to parse MIDI file: {}", e))?; + + // Convert timing to ticks per second + let ticks_per_beat = match smf.header.timing { + midly::Timing::Metrical(tpb) => tpb.as_int() as f64, + midly::Timing::Timecode(fps, subframe) => { + // For timecode, calculate equivalent ticks per second + (fps.as_f32() * subframe as f32) as f64 + } + }; + + // First pass: collect all events with their tick positions and tempo changes + #[derive(Debug)] + enum RawEvent { + Midi { + tick: u64, + channel: u8, + message: midly::MidiMessage, + }, + Tempo { + tick: u64, + microseconds_per_beat: f64, + }, + } + + let mut raw_events = Vec::new(); + let mut max_time_ticks = 0u64; + + // Collect all events from all tracks with their absolute tick positions + for track in &smf.tracks { + let mut current_tick = 0u64; + + for event in track { + current_tick += event.delta.as_int() as u64; + max_time_ticks = max_time_ticks.max(current_tick); + + match event.kind { + midly::TrackEventKind::Midi { channel, message } => { + raw_events.push(RawEvent::Midi { + tick: current_tick, + channel: channel.as_int(), + message, + }); + } + midly::TrackEventKind::Meta(midly::MetaMessage::Tempo(tempo)) => { + raw_events.push(RawEvent::Tempo { + tick: current_tick, + microseconds_per_beat: tempo.as_int() as f64, + }); + } + _ => { + // Ignore other meta events + } + } + } + } + + // Sort all events by tick position + raw_events.sort_by_key(|e| match e { + RawEvent::Midi { tick, .. } => *tick, + RawEvent::Tempo { tick, .. } => *tick, + }); + + // Second pass: convert ticks to timestamps with proper tempo tracking + let mut events = Vec::new(); + let mut microseconds_per_beat = 500000.0; // Default: 120 BPM + let mut last_tick = 0u64; + let mut accumulated_time = 0.0; // Time in seconds + + for raw_event in raw_events { + match raw_event { + RawEvent::Tempo { + tick, + microseconds_per_beat: new_tempo, + } => { + // Update accumulated time up to this tempo change + let delta_ticks = tick - last_tick; + let delta_time = (delta_ticks as f64 / ticks_per_beat) + * (microseconds_per_beat / 1_000_000.0); + accumulated_time += delta_time; + last_tick = tick; + + // Update tempo for future events + microseconds_per_beat = new_tempo; + } + RawEvent::Midi { + tick, + channel, + message, + } => { + // Calculate time for this event + let delta_ticks = tick - last_tick; + let delta_time = (delta_ticks as f64 / ticks_per_beat) + * (microseconds_per_beat / 1_000_000.0); + accumulated_time += delta_time; + last_tick = tick; + + let timestamp = (accumulated_time * sample_rate as f64) as u64; + + match message { + midly::MidiMessage::NoteOn { key, vel } => { + let velocity = vel.as_int(); + if velocity > 0 { + events.push(MidiEvent::note_on( + timestamp, + channel, + key.as_int(), + velocity, + )); + } else { + events.push(MidiEvent::note_off(timestamp, channel, key.as_int(), 64)); + } + } + midly::MidiMessage::NoteOff { key, vel } => { + events.push(MidiEvent::note_off( + timestamp, + channel, + key.as_int(), + vel.as_int(), + )); + } + midly::MidiMessage::Controller { controller, value } => { + let status = 0xB0 | channel; + events.push(MidiEvent::new( + timestamp, + status, + controller.as_int(), + value.as_int(), + )); + } + _ => { + // Ignore other MIDI messages + } + } + } + } + } + + // Calculate final clip duration + let final_delta_ticks = max_time_ticks - last_tick; + let final_delta_time = + (final_delta_ticks as f64 / ticks_per_beat) * (microseconds_per_beat / 1_000_000.0); + let duration_seconds = accumulated_time + final_delta_time; + + // Create the MIDI clip + let mut clip = MidiClip::new(clip_id, 0.0, duration_seconds); + clip.events = events; + + Ok(clip) +} diff --git a/daw-backend/src/io/mod.rs b/daw-backend/src/io/mod.rs index 940ea0d..a80ee07 100644 --- a/daw-backend/src/io/mod.rs +++ b/daw-backend/src/io/mod.rs @@ -1,3 +1,5 @@ pub mod audio_file; +pub mod midi_file; pub use audio_file::AudioFile; +pub use midi_file::load_midi_file; diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index 335fd4e..8c7e583 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -1,14 +1,20 @@ -// DAW Backend - Phase 4: Clips & Timeline +// DAW Backend - Phase 6: Hierarchical Tracks // -// A DAW backend with timeline-based playback, clips, and audio pool. -// Supports multiple tracks, mixing, per-track volume/mute/solo, and shared audio data. +// A DAW backend with timeline-based playback, clips, audio pool, effects, and hierarchical track groups. +// Supports multiple tracks, mixing, per-track volume/mute/solo, shared audio data, effect chains, and nested groups. // Uses lock-free command queues, cpal for audio I/O, and symphonia for audio file decoding. pub mod audio; pub mod command; +pub mod dsp; +pub mod effects; pub mod io; // Re-export commonly used types -pub use audio::{AudioPool, Clip, ClipId, Engine, EngineController, PoolAudioFile, Track, TrackId}; +pub use audio::{ + AudioPool, AudioTrack, BufferPool, Clip, ClipId, Engine, EngineController, + GroupTrack, MidiClip, MidiClipId, MidiEvent, MidiTrack, PoolAudioFile, Project, Track, TrackId, TrackNode, +}; pub use command::{AudioEvent, Command}; -pub use io::AudioFile; +pub use effects::{Effect, GainEffect, PanEffect, SimpleEQ, SimpleSynth}; +pub use io::{load_midi_file, AudioFile}; diff --git a/daw-backend/src/main.rs b/daw-backend/src/main.rs index cd73da1..17fbf9a 100644 --- a/daw-backend/src/main.rs +++ b/daw-backend/src/main.rs @@ -1,5 +1,5 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use daw_backend::{AudioEvent, AudioFile, Clip, Engine, PoolAudioFile, Track}; +use daw_backend::{load_midi_file, AudioEvent, AudioFile, Clip, Engine, PoolAudioFile, Track, TrackNode}; use std::env; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { return Ok(()); } - println!("DAW Backend - Phase 4: Clips & Timeline\n"); + println!("DAW Backend - Phase 6: Hierarchical Tracks\n"); // Load all audio files let mut audio_files = Vec::new(); @@ -92,7 +92,7 @@ fn main() -> Result<(), Box> { let mut engine = Engine::new(max_sample_rate, max_channels, command_rx, event_tx); // Add all files to the audio pool and create tracks with clips - let mut track_ids = Vec::new(); + let track_ids = Arc::new(Mutex::new(Vec::new())); let mut clip_info = Vec::new(); // Store (track_id, clip_id, name, duration) let mut max_duration = 0.0f64; let mut clip_id_counter = 0u32; @@ -128,7 +128,7 @@ fn main() -> Result<(), Box> { track.add_clip(clip); engine.add_track(track); - track_ids.push(track_id); + track_ids.lock().unwrap().push(track_id); clip_info.push((track_id, clip_id, name.clone(), duration)); println!(" Track {}: {} (clip {} at 0.0s, duration {:.2}s)", i, name, clip_id, duration); @@ -140,6 +140,7 @@ fn main() -> Result<(), Box> { // Wrap engine in Arc for thread-safe access let engine = Arc::new(Mutex::new(engine)); + let engine_for_commands = Arc::clone(&engine); // Build the output stream let stream = match sample_format { @@ -153,11 +154,15 @@ fn main() -> Result<(), Box> { stream.play()?; println!("\nAudio stream started!"); print_help(); - print_status(0.0, max_duration, &track_ids); + { + let ids = track_ids.lock().unwrap(); + print_status(0.0, max_duration, &ids); + } // Spawn event listener thread let event_rx = Arc::new(Mutex::new(event_rx)); let event_rx_clone = Arc::clone(&event_rx); + let track_ids_clone = Arc::clone(&track_ids); let _event_thread = thread::spawn(move || { loop { thread::sleep(Duration::from_millis(50)); @@ -192,6 +197,17 @@ fn main() -> Result<(), Box> { AudioEvent::BufferUnderrun => { eprintln!("\nWarning: Buffer underrun detected"); } + AudioEvent::TrackCreated(track_id, is_group, name) => { + print!("\r\x1b[K"); + if is_group { + println!("Group {} created: '{}' (ID: {})", track_id, name, track_id); + } else { + println!("Track {} created: '{}' (ID: {})", track_id, name, track_id); + } + track_ids_clone.lock().unwrap().push(track_id); + print!("> "); + io::stdout().flush().ok(); + } } } } @@ -238,11 +254,13 @@ fn main() -> Result<(), Box> { let parts: Vec<&str> = input.split_whitespace().collect(); if parts.len() == 3 { if let (Ok(track_id), Ok(volume)) = (parts[1].parse::(), parts[2].parse::()) { - if track_ids.contains(&track_id) { + let ids = track_ids.lock().unwrap(); + if ids.contains(&track_id) { + drop(ids); controller.set_track_volume(track_id, volume); println!("Set track {} volume to {:.2}", track_id, volume); } else { - println!("Invalid track ID. Available tracks: {:?}", track_ids); + println!("Invalid track ID. Available tracks: {:?}", *ids); } } else { println!("Invalid format. Usage: volume "); @@ -253,11 +271,13 @@ fn main() -> Result<(), Box> { } else if input.starts_with("mute ") { // Parse: mute if let Ok(track_id) = input[5..].trim().parse::() { - if track_ids.contains(&track_id) { + let ids = track_ids.lock().unwrap(); + if ids.contains(&track_id) { + drop(ids); controller.set_track_mute(track_id, true); println!("Muted track {}", track_id); } else { - println!("Invalid track ID. Available tracks: {:?}", track_ids); + println!("Invalid track ID. Available tracks: {:?}", *ids); } } else { println!("Usage: mute "); @@ -265,11 +285,13 @@ fn main() -> Result<(), Box> { } else if input.starts_with("unmute ") { // Parse: unmute if let Ok(track_id) = input[7..].trim().parse::() { - if track_ids.contains(&track_id) { + let ids = track_ids.lock().unwrap(); + if ids.contains(&track_id) { + drop(ids); controller.set_track_mute(track_id, false); println!("Unmuted track {}", track_id); } else { - println!("Invalid track ID. Available tracks: {:?}", track_ids); + println!("Invalid track ID. Available tracks: {:?}", *ids); } } else { println!("Usage: unmute "); @@ -277,11 +299,13 @@ fn main() -> Result<(), Box> { } else if input.starts_with("solo ") { // Parse: solo if let Ok(track_id) = input[5..].trim().parse::() { - if track_ids.contains(&track_id) { + let ids = track_ids.lock().unwrap(); + if ids.contains(&track_id) { + drop(ids); controller.set_track_solo(track_id, true); println!("Soloed track {}", track_id); } else { - println!("Invalid track ID. Available tracks: {:?}", track_ids); + println!("Invalid track ID. Available tracks: {:?}", *ids); } } else { println!("Usage: solo "); @@ -289,11 +313,13 @@ fn main() -> Result<(), Box> { } else if input.starts_with("unsolo ") { // Parse: unsolo if let Ok(track_id) = input[7..].trim().parse::() { - if track_ids.contains(&track_id) { + let ids = track_ids.lock().unwrap(); + if ids.contains(&track_id) { + drop(ids); controller.set_track_solo(track_id, false); println!("Unsoloed track {}", track_id); } else { - println!("Invalid track ID. Available tracks: {:?}", track_ids); + println!("Invalid track ID. Available tracks: {:?}", *ids); } } else { println!("Usage: unsolo "); @@ -322,11 +348,249 @@ fn main() -> Result<(), Box> { println!("Usage: move