diff --git a/daw-backend/Cargo.lock b/daw-backend/Cargo.lock index c62b395..fa7cd5f 100644 --- a/daw-backend/Cargo.lock +++ b/daw-backend/Cargo.lock @@ -234,6 +234,7 @@ dependencies = [ "cpal", "midly", "rtrb", + "serde", "symphonia", ] @@ -638,6 +639,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/daw-backend/Cargo.toml b/daw-backend/Cargo.toml index e4fdf9f..d586e0d 100644 --- a/daw-backend/Cargo.toml +++ b/daw-backend/Cargo.toml @@ -8,6 +8,7 @@ cpal = "0.15" symphonia = { version = "0.5", features = ["all"] } rtrb = "0.3" midly = "0.5" +serde = { version = "1.0", features = ["derive"] } [dev-dependencies] diff --git a/daw-backend/src/audio/automation.rs b/daw-backend/src/audio/automation.rs new file mode 100644 index 0000000..639e75d --- /dev/null +++ b/daw-backend/src/audio/automation.rs @@ -0,0 +1,279 @@ +/// Automation system for parameter modulation over time +use serde::{Deserialize, Serialize}; + +/// Unique identifier for automation lanes +pub type AutomationLaneId = u32; + +/// Unique identifier for parameters that can be automated +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ParameterId { + /// Track volume + TrackVolume, + /// Track pan + TrackPan, + /// Effect parameter (effect_index, param_id) + EffectParameter(usize, u32), + /// Metatrack time stretch + TimeStretch, + /// Metatrack offset + TimeOffset, +} + +/// Type of interpolation curve between automation points +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum CurveType { + /// Linear interpolation (straight line) + Linear, + /// Exponential curve (smooth acceleration) + Exponential, + /// S-curve (ease in/out) + SCurve, + /// Step (no interpolation, jump to next value) + Step, +} + +/// A single automation point +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AutomationPoint { + /// Time in seconds + pub time: f64, + /// Parameter value (normalized 0.0 to 1.0, or actual value depending on parameter) + pub value: f32, + /// Curve type to next point + pub curve: CurveType, +} + +impl AutomationPoint { + /// Create a new automation point + pub fn new(time: f64, value: f32, curve: CurveType) -> Self { + Self { time, value, curve } + } +} + +/// An automation lane for a specific parameter +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutomationLane { + /// Unique identifier for this lane + pub id: AutomationLaneId, + /// Which parameter this lane controls + pub parameter_id: ParameterId, + /// Sorted list of automation points + points: Vec, + /// Whether this lane is enabled + pub enabled: bool, +} + +impl AutomationLane { + /// Create a new automation lane + pub fn new(id: AutomationLaneId, parameter_id: ParameterId) -> Self { + Self { + id, + parameter_id, + points: Vec::new(), + enabled: true, + } + } + + /// Add an automation point, maintaining sorted order + pub fn add_point(&mut self, point: AutomationPoint) { + // Find insertion position to maintain sorted order + let pos = self.points.binary_search_by(|p| { + p.time.partial_cmp(&point.time).unwrap_or(std::cmp::Ordering::Equal) + }); + + match pos { + Ok(idx) => { + // Replace existing point at same time + self.points[idx] = point; + } + Err(idx) => { + // Insert at correct position + self.points.insert(idx, point); + } + } + } + + /// Remove point at specific time + pub fn remove_point_at_time(&mut self, time: f64, tolerance: f64) -> bool { + if let Some(idx) = self.points.iter().position(|p| (p.time - time).abs() < tolerance) { + self.points.remove(idx); + true + } else { + false + } + } + + /// Remove all points + pub fn clear(&mut self) { + self.points.clear(); + } + + /// Get all points + pub fn points(&self) -> &[AutomationPoint] { + &self.points + } + + /// Get value at a specific time with interpolation + pub fn evaluate(&self, time: f64) -> Option { + if !self.enabled || self.points.is_empty() { + return None; + } + + // Before first point + if time <= self.points[0].time { + return Some(self.points[0].value); + } + + // After last point + if time >= self.points[self.points.len() - 1].time { + return Some(self.points[self.points.len() - 1].value); + } + + // Find surrounding points + for i in 0..self.points.len() - 1 { + let p1 = &self.points[i]; + let p2 = &self.points[i + 1]; + + if time >= p1.time && time <= p2.time { + return Some(interpolate(p1, p2, time)); + } + } + + None + } + + /// Get number of points + pub fn point_count(&self) -> usize { + self.points.len() + } +} + +/// Interpolate between two automation points based on curve type +fn interpolate(p1: &AutomationPoint, p2: &AutomationPoint, time: f64) -> f32 { + // Calculate normalized position between points (0.0 to 1.0) + let t = if p2.time == p1.time { + 0.0 + } else { + ((time - p1.time) / (p2.time - p1.time)) as f32 + }; + + // Apply curve + let curved_t = match p1.curve { + CurveType::Linear => t, + CurveType::Exponential => { + // Exponential curve: y = x^2 + t * t + } + CurveType::SCurve => { + // Smooth S-curve using smoothstep + smoothstep(t) + } + CurveType::Step => { + // Step: hold value until next point + return p1.value; + } + }; + + // Linear interpolation with curved t + p1.value + (p2.value - p1.value) * curved_t +} + +/// Smoothstep function for S-curve interpolation +/// Returns a smooth curve from 0 to 1 +#[inline] +fn smoothstep(t: f32) -> f32 { + // Clamp to [0, 1] + let t = t.clamp(0.0, 1.0); + // 3t^2 - 2t^3 + t * t * (3.0 - 2.0 * t) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_points_sorted() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(2.0, 0.5, CurveType::Linear)); + lane.add_point(AutomationPoint::new(1.0, 0.3, CurveType::Linear)); + lane.add_point(AutomationPoint::new(3.0, 0.8, CurveType::Linear)); + + assert_eq!(lane.points().len(), 3); + assert_eq!(lane.points()[0].time, 1.0); + assert_eq!(lane.points()[1].time, 2.0); + assert_eq!(lane.points()[2].time, 3.0); + } + + #[test] + fn test_replace_point_at_same_time() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(1.0, 0.3, CurveType::Linear)); + lane.add_point(AutomationPoint::new(1.0, 0.5, CurveType::Linear)); + + assert_eq!(lane.points().len(), 1); + assert_eq!(lane.points()[0].value, 0.5); + } + + #[test] + fn test_linear_interpolation() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(0.0, 0.0, CurveType::Linear)); + lane.add_point(AutomationPoint::new(1.0, 1.0, CurveType::Linear)); + + assert_eq!(lane.evaluate(0.0), Some(0.0)); + assert_eq!(lane.evaluate(0.5), Some(0.5)); + assert_eq!(lane.evaluate(1.0), Some(1.0)); + } + + #[test] + fn test_step_interpolation() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(0.0, 0.5, CurveType::Step)); + lane.add_point(AutomationPoint::new(1.0, 1.0, CurveType::Step)); + + assert_eq!(lane.evaluate(0.0), Some(0.5)); + assert_eq!(lane.evaluate(0.5), Some(0.5)); + assert_eq!(lane.evaluate(0.99), Some(0.5)); + assert_eq!(lane.evaluate(1.0), Some(1.0)); + } + + #[test] + fn test_evaluate_outside_range() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(1.0, 0.5, CurveType::Linear)); + lane.add_point(AutomationPoint::new(2.0, 1.0, CurveType::Linear)); + + // Before first point + assert_eq!(lane.evaluate(0.0), Some(0.5)); + // After last point + assert_eq!(lane.evaluate(3.0), Some(1.0)); + } + + #[test] + fn test_disabled_lane() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(0.0, 0.5, CurveType::Linear)); + lane.enabled = false; + + assert_eq!(lane.evaluate(0.0), None); + } + + #[test] + fn test_remove_point() { + let mut lane = AutomationLane::new(0, ParameterId::TrackVolume); + + lane.add_point(AutomationPoint::new(1.0, 0.5, CurveType::Linear)); + lane.add_point(AutomationPoint::new(2.0, 0.8, CurveType::Linear)); + + assert!(lane.remove_point_at_time(1.0, 0.001)); + assert_eq!(lane.points().len(), 1); + assert_eq!(lane.points()[0].time, 2.0); + } +} diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index cb2ef75..7095443 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -3,6 +3,7 @@ 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::recording::RecordingState; use crate::audio::track::{Track, TrackId}; use crate::command::{AudioEvent, Command}; use crate::effects::{Effect, GainEffect, PanEffect, SimpleEQ}; @@ -35,6 +36,11 @@ pub struct Engine { // ID counters next_midi_clip_id: MidiClipId, + next_clip_id: ClipId, + + // Recording state + recording_state: Option, + input_rx: Option>, } impl Engine { @@ -65,9 +71,17 @@ impl Engine { event_interval_frames, mix_buffer: Vec::new(), next_midi_clip_id: 0, + next_clip_id: 0, + recording_state: None, + input_rx: None, } } + /// Set the input ringbuffer consumer for recording + pub fn set_input_rx(&mut self, input_rx: rtrb::Consumer) { + self.input_rx = Some(input_rx); + } + /// Add an audio track to the engine pub fn add_track(&mut self, track: Track) -> TrackId { // For backwards compatibility, we'll extract the track data and add it to the project @@ -190,6 +204,49 @@ impl Engine { // Not playing, output silence output.fill(0.0); } + + // Process recording if active (independent of playback state) + if let Some(recording) = &mut self.recording_state { + if let Some(input_rx) = &mut self.input_rx { + // Pull samples from input ringbuffer + let mut samples = Vec::new(); + while let Ok(sample) = input_rx.pop() { + samples.push(sample); + } + + // Add samples to recording + if !samples.is_empty() { + match recording.add_samples(&samples) { + Ok(flushed) => { + if flushed { + // A flush occurred, update clip duration and send progress event + let duration = recording.duration(); + let clip_id = recording.clip_id; + let track_id = recording.track_id; + + // Update clip duration in project + 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.duration = duration; + } + } + + // Send progress event + let _ = self.event_tx.push(AudioEvent::RecordingProgress(clip_id, duration)); + } + } + Err(e) => { + // Recording error occurred + let _ = self.event_tx.push(AudioEvent::RecordingError( + format!("Recording write error: {}", e) + )); + // Stop recording on error + self.recording_state = None; + } + } + } + } + } } /// Handle a command from the UI thread @@ -362,6 +419,42 @@ impl Engine { metatrack.pitch_shift = semitones; } } + Command::CreateAudioTrack(name) => { + let track_id = self.project.add_audio_track(name.clone(), None); + // Notify UI about the new audio track + let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name)); + } + Command::AddAudioFile(path, data, channels, sample_rate) => { + // Create AudioFile and add to pool + let audio_file = crate::audio::pool::AudioFile::new( + std::path::PathBuf::from(path.clone()), + data, + channels, + sample_rate, + ); + let pool_index = self.audio_pool.add_file(audio_file); + // Notify UI about the new audio file + let _ = self.event_tx.push(AudioEvent::AudioFileAdded(pool_index, path)); + } + Command::AddAudioClip(track_id, pool_index, start_time, duration, offset) => { + // Create a new clip with unique ID + let clip_id = self.next_clip_id; + self.next_clip_id += 1; + let clip = crate::audio::clip::Clip::new( + clip_id, + pool_index, + start_time, + duration, + offset, + ); + + // Add clip to track + if let Some(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(track_id) { + track.clips.push(clip); + // Notify UI about the new clip + let _ = self.event_tx.push(AudioEvent::ClipAdded(track_id, clip_id)); + } + } Command::CreateMidiTrack(name) => { let track_id = self.project.add_midi_track(name.clone(), None); // Notify UI about the new MIDI track @@ -402,6 +495,301 @@ impl Engine { let stats = self.buffer_pool.stats(); let _ = self.event_tx.push(AudioEvent::BufferPoolStats(stats)); } + Command::CreateAutomationLane(track_id, parameter_id) => { + // Create a new automation lane on the specified track + let lane_id = match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + Some(track.add_automation_lane(parameter_id)) + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + Some(track.add_automation_lane(parameter_id)) + } + Some(crate::audio::track::TrackNode::Group(group)) => { + Some(group.add_automation_lane(parameter_id)) + } + None => None, + }; + + if let Some(lane_id) = lane_id { + let _ = self.event_tx.push(AudioEvent::AutomationLaneCreated( + track_id, + lane_id, + parameter_id, + )); + } + } + Command::AddAutomationPoint(track_id, lane_id, time, value, curve) => { + // Add an automation point to the specified lane + let point = crate::audio::AutomationPoint::new(time, value, curve); + + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.add_point(point); + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.add_point(point); + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(lane) = group.get_automation_lane_mut(lane_id) { + lane.add_point(point); + } + } + None => {} + } + } + Command::RemoveAutomationPoint(track_id, lane_id, time, tolerance) => { + // Remove automation point at specified time + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.remove_point_at_time(time, tolerance); + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.remove_point_at_time(time, tolerance); + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(lane) = group.get_automation_lane_mut(lane_id) { + lane.remove_point_at_time(time, tolerance); + } + } + None => {} + } + } + Command::ClearAutomationLane(track_id, lane_id) => { + // Clear all points from the lane + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.clear(); + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.clear(); + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(lane) = group.get_automation_lane_mut(lane_id) { + lane.clear(); + } + } + None => {} + } + } + Command::RemoveAutomationLane(track_id, lane_id) => { + // Remove the automation lane entirely + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + track.remove_automation_lane(lane_id); + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + track.remove_automation_lane(lane_id); + } + Some(crate::audio::track::TrackNode::Group(group)) => { + group.remove_automation_lane(lane_id); + } + None => {} + } + } + Command::SetAutomationLaneEnabled(track_id, lane_id, enabled) => { + // Enable/disable the automation lane + match self.project.get_track_mut(track_id) { + Some(crate::audio::track::TrackNode::Audio(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.enabled = enabled; + } + } + Some(crate::audio::track::TrackNode::Midi(track)) => { + if let Some(lane) = track.get_automation_lane_mut(lane_id) { + lane.enabled = enabled; + } + } + Some(crate::audio::track::TrackNode::Group(group)) => { + if let Some(lane) = group.get_automation_lane_mut(lane_id) { + lane.enabled = enabled; + } + } + None => {} + } + } + Command::StartRecording(track_id, start_time) => { + // Start recording on the specified track + self.handle_start_recording(track_id, start_time); + } + Command::StopRecording => { + // Stop the current recording + self.handle_stop_recording(); + } + Command::PauseRecording => { + // Pause the current recording + if let Some(recording) = &mut self.recording_state { + recording.pause(); + } + } + Command::ResumeRecording => { + // Resume the current recording + if let Some(recording) = &mut self.recording_state { + recording.resume(); + } + } + Command::Reset => { + // Reset the entire project to initial state + // Stop playback + self.playing = false; + self.playhead = 0; + self.playhead_atomic.store(0, Ordering::Relaxed); + + // Stop any active recording + self.recording_state = None; + + // Clear all project data + self.project = Project::new(); + + // Clear audio pool + self.audio_pool = AudioPool::new(); + + // Reset buffer pool (recreate with same settings) + let buffer_size = 512 * self.channels as usize; + self.buffer_pool = BufferPool::new(8, buffer_size); + + // Reset ID counters + self.next_midi_clip_id = 0; + self.next_clip_id = 0; + + // Clear mix buffer + self.mix_buffer.clear(); + + // Notify UI that reset is complete + let _ = self.event_tx.push(AudioEvent::ProjectReset); + } + } + } + + /// Handle starting a recording + fn handle_start_recording(&mut self, track_id: TrackId, start_time: f64) { + use crate::io::WavWriter; + use std::env; + + // Check if track exists and is an audio track + if let Some(crate::audio::track::TrackNode::Audio(_)) = self.project.get_track_mut(track_id) { + // Generate a unique temp file path + let temp_dir = env::temp_dir(); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let temp_file_path = temp_dir.join(format!("daw_recording_{}.wav", timestamp)); + + // Create WAV writer + match WavWriter::create(&temp_file_path, self.sample_rate, self.channels) { + Ok(writer) => { + // Create intermediate clip + let clip_id = self.next_clip_id; + self.next_clip_id += 1; + + let clip = crate::audio::clip::Clip::new( + clip_id, + 0, // Temporary pool index, will be updated on finalization + start_time, + 0.0, // Duration starts at 0, will be updated during recording + 0.0, + ); + + // Add clip to track + if let Some(crate::audio::track::TrackNode::Audio(track)) = self.project.get_track_mut(track_id) { + track.clips.push(clip); + } + + // Create recording state + let flush_interval_seconds = 5.0; // Flush every 5 seconds + let recording_state = RecordingState::new( + track_id, + clip_id, + temp_file_path, + writer, + self.sample_rate, + self.channels, + start_time, + flush_interval_seconds, + ); + + self.recording_state = Some(recording_state); + + // Notify UI that recording has started + let _ = self.event_tx.push(AudioEvent::RecordingStarted(track_id, clip_id)); + } + Err(e) => { + // Send error event to UI + let _ = self.event_tx.push(AudioEvent::RecordingError( + format!("Failed to create temp file: {}", e) + )); + } + } + } else { + // Send error event if track not found or not an audio track + let _ = self.event_tx.push(AudioEvent::RecordingError( + format!("Track {} not found or is not an audio track", track_id) + )); + } + } + + /// Handle stopping a recording + fn handle_stop_recording(&mut self) { + if let Some(recording) = self.recording_state.take() { + let clip_id = recording.clip_id; + let track_id = recording.track_id; + + // Finalize the recording and get temp file path + match recording.finalize() { + Ok(temp_file_path) => { + // Load the recorded audio file + match crate::io::AudioFile::load(&temp_file_path) { + Ok(audio_file) => { + // Add to pool + let pool_file = crate::audio::pool::AudioFile::new( + temp_file_path.clone(), + audio_file.data, + audio_file.channels, + audio_file.sample_rate, + ); + let pool_index = self.audio_pool.add_file(pool_file); + + // Update the clip to reference the pool + 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.audio_pool_index = pool_index; + // Duration should already be set during recording progress updates + } + } + + // Delete temp file + let _ = std::fs::remove_file(&temp_file_path); + + // Notify UI that recording has stopped + let _ = self.event_tx.push(AudioEvent::RecordingStopped(clip_id, pool_index)); + } + Err(e) => { + // Send error event + let _ = self.event_tx.push(AudioEvent::RecordingError( + format!("Failed to load recorded audio: {}", e) + )); + } + } + } + Err(e) => { + // Send error event + let _ = self.event_tx.push(AudioEvent::RecordingError( + format!("Failed to finalize recording: {}", e) + )); + } + } } } @@ -429,6 +817,13 @@ pub struct EngineController { channels: u32, } +// Safety: EngineController is safe to Send across threads because: +// - rtrb::Producer is Send by design (lock-free queue for cross-thread communication) +// - Arc is Send + Sync (atomic types are inherently thread-safe) +// - u32 primitives are Send + Sync (Copy types) +// EngineController is only accessed through Mutex in application state, ensuring no concurrent mutable access. +unsafe impl Send for EngineController {} + impl EngineController { /// Start or resume playback pub fn play(&mut self) { @@ -535,6 +930,21 @@ impl EngineController { let _ = self.command_tx.push(Command::SetPitchShift(track_id, semitones)); } + /// Create a new audio track + pub fn create_audio_track(&mut self, name: String) { + let _ = self.command_tx.push(Command::CreateAudioTrack(name)); + } + + /// Add an audio file to the pool (must be called from non-audio thread with pre-loaded data) + pub fn add_audio_file(&mut self, path: String, data: Vec, channels: u32, sample_rate: u32) { + let _ = self.command_tx.push(Command::AddAudioFile(path, data, channels, sample_rate)); + } + + /// Add a clip to an audio track + pub fn add_audio_clip(&mut self, track_id: TrackId, pool_index: usize, start_time: f64, duration: f64, offset: f64) { + let _ = self.command_tx.push(Command::AddAudioClip(track_id, pool_index, start_time, duration, offset)); + } + /// Create a new MIDI track pub fn create_midi_track(&mut self, name: String) { let _ = self.command_tx.push(Command::CreateMidiTrack(name)); @@ -560,4 +970,92 @@ impl EngineController { pub fn request_buffer_pool_stats(&mut self) { let _ = self.command_tx.push(Command::RequestBufferPoolStats); } + + /// Create a new automation lane on a track + /// Returns an event AutomationLaneCreated with the lane ID + pub fn create_automation_lane(&mut self, track_id: TrackId, parameter_id: crate::audio::ParameterId) { + let _ = self.command_tx.push(Command::CreateAutomationLane(track_id, parameter_id)); + } + + /// Add an automation point to a lane + pub fn add_automation_point( + &mut self, + track_id: TrackId, + lane_id: crate::audio::AutomationLaneId, + time: f64, + value: f32, + curve: crate::audio::CurveType, + ) { + let _ = self.command_tx.push(Command::AddAutomationPoint( + track_id, lane_id, time, value, curve, + )); + } + + /// Remove an automation point at a specific time + pub fn remove_automation_point( + &mut self, + track_id: TrackId, + lane_id: crate::audio::AutomationLaneId, + time: f64, + tolerance: f64, + ) { + let _ = self.command_tx.push(Command::RemoveAutomationPoint( + track_id, lane_id, time, tolerance, + )); + } + + /// Clear all automation points from a lane + pub fn clear_automation_lane( + &mut self, + track_id: TrackId, + lane_id: crate::audio::AutomationLaneId, + ) { + let _ = self.command_tx.push(Command::ClearAutomationLane(track_id, lane_id)); + } + + /// Remove an automation lane entirely + pub fn remove_automation_lane( + &mut self, + track_id: TrackId, + lane_id: crate::audio::AutomationLaneId, + ) { + let _ = self.command_tx.push(Command::RemoveAutomationLane(track_id, lane_id)); + } + + /// Enable or disable an automation lane + pub fn set_automation_lane_enabled( + &mut self, + track_id: TrackId, + lane_id: crate::audio::AutomationLaneId, + enabled: bool, + ) { + let _ = self.command_tx.push(Command::SetAutomationLaneEnabled( + track_id, lane_id, enabled, + )); + } + + /// Start recording on a track + pub fn start_recording(&mut self, track_id: TrackId, start_time: f64) { + let _ = self.command_tx.push(Command::StartRecording(track_id, start_time)); + } + + /// Stop the current recording + pub fn stop_recording(&mut self) { + let _ = self.command_tx.push(Command::StopRecording); + } + + /// Pause the current recording + pub fn pause_recording(&mut self) { + let _ = self.command_tx.push(Command::PauseRecording); + } + + /// Resume the current recording + pub fn resume_recording(&mut self) { + let _ = self.command_tx.push(Command::ResumeRecording); + } + + /// Reset the entire project (clear all tracks, audio pool, and state) + pub fn reset(&mut self) { + let _ = self.command_tx.push(Command::Reset); + } } diff --git a/daw-backend/src/audio/mod.rs b/daw-backend/src/audio/mod.rs index 34c2291..ec2e274 100644 --- a/daw-backend/src/audio/mod.rs +++ b/daw-backend/src/audio/mod.rs @@ -1,15 +1,19 @@ +pub mod automation; pub mod buffer_pool; pub mod clip; pub mod engine; pub mod midi; pub mod pool; pub mod project; +pub mod recording; pub mod track; +pub use automation::{AutomationLane, AutomationLaneId, AutomationPoint, CurveType, ParameterId}; 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 project::Project; +pub use recording::RecordingState; pub use track::{AudioTrack, Metatrack, MidiTrack, RenderContext, Track, TrackId, TrackNode}; diff --git a/daw-backend/src/audio/recording.rs b/daw-backend/src/audio/recording.rs new file mode 100644 index 0000000..d11e846 --- /dev/null +++ b/daw-backend/src/audio/recording.rs @@ -0,0 +1,124 @@ +/// Audio recording system for capturing microphone input +use crate::audio::{ClipId, TrackId}; +use crate::io::WavWriter; +use std::path::PathBuf; + +/// State of an active recording session +pub struct RecordingState { + /// Track being recorded to + pub track_id: TrackId, + /// Clip ID for the intermediate clip + pub clip_id: ClipId, + /// Path to temporary WAV file + pub temp_file_path: PathBuf, + /// WAV file writer + pub writer: WavWriter, + /// Sample rate of recording + pub sample_rate: u32, + /// Number of channels + pub channels: u32, + /// Timeline start position in seconds + pub start_time: f64, + /// Total frames written to disk + pub frames_written: usize, + /// Accumulation buffer for next flush + pub buffer: Vec, + /// Number of frames to accumulate before flushing + pub flush_interval_frames: usize, + /// Whether recording is currently paused + pub paused: bool, +} + +impl RecordingState { + /// Create a new recording state + pub fn new( + track_id: TrackId, + clip_id: ClipId, + temp_file_path: PathBuf, + writer: WavWriter, + sample_rate: u32, + channels: u32, + start_time: f64, + flush_interval_seconds: f64, + ) -> Self { + let flush_interval_frames = (sample_rate as f64 * flush_interval_seconds) as usize; + + Self { + track_id, + clip_id, + temp_file_path, + writer, + sample_rate, + channels, + start_time, + frames_written: 0, + buffer: Vec::new(), + flush_interval_frames, + paused: false, + } + } + + /// Add samples to the accumulation buffer + /// Returns true if a flush occurred + pub fn add_samples(&mut self, samples: &[f32]) -> Result { + if self.paused { + return Ok(false); + } + + self.buffer.extend_from_slice(samples); + + // Check if we should flush + let frames_in_buffer = self.buffer.len() / self.channels as usize; + if frames_in_buffer >= self.flush_interval_frames { + self.flush()?; + return Ok(true); + } + + Ok(false) + } + + /// Flush accumulated samples to disk + pub fn flush(&mut self) -> Result<(), std::io::Error> { + if self.buffer.is_empty() { + return Ok(()); + } + + // Write to WAV file + self.writer.write_samples(&self.buffer)?; + + // Update frames written + let frames_flushed = self.buffer.len() / self.channels as usize; + self.frames_written += frames_flushed; + + // Clear buffer + self.buffer.clear(); + + Ok(()) + } + + /// Get current recording duration in seconds + pub fn duration(&self) -> f64 { + self.frames_written as f64 / self.sample_rate as f64 + } + + /// Finalize the recording and return the temp file path + pub fn finalize(mut self) -> Result { + // Flush any remaining samples + self.flush()?; + + // Finalize the WAV file + self.writer.finalize()?; + + Ok(self.temp_file_path) + } + + /// Pause recording + pub fn pause(&mut self) { + self.paused = true; + } + + /// Resume recording + pub fn resume(&mut self) { + self.paused = false; + } +} diff --git a/daw-backend/src/audio/track.rs b/daw-backend/src/audio/track.rs index a1408a6..9749ead 100644 --- a/daw-backend/src/audio/track.rs +++ b/daw-backend/src/audio/track.rs @@ -1,7 +1,9 @@ +use super::automation::{AutomationLane, AutomationLaneId, ParameterId}; use super::clip::Clip; use super::midi::MidiClip; use super::pool::AudioPool; use crate::effects::{Effect, SimpleSynth}; +use std::collections::HashMap; /// Track ID type pub type TrackId = u32; @@ -141,6 +143,9 @@ pub struct Metatrack { pub pitch_shift: f32, /// Time offset in seconds (shift content forward/backward in time) pub offset: f64, + /// Automation lanes for this metatrack + pub automation_lanes: HashMap, + next_automation_id: AutomationLaneId, } impl Metatrack { @@ -157,9 +162,71 @@ impl Metatrack { time_stretch: 1.0, pitch_shift: 0.0, offset: 0.0, + automation_lanes: HashMap::new(), + next_automation_id: 0, } } + /// Add an automation lane to this metatrack + pub fn add_automation_lane(&mut self, parameter_id: ParameterId) -> AutomationLaneId { + let lane_id = self.next_automation_id; + self.next_automation_id += 1; + + let lane = AutomationLane::new(lane_id, parameter_id); + self.automation_lanes.insert(lane_id, lane); + lane_id + } + + /// Get an automation lane by ID + pub fn get_automation_lane(&self, lane_id: AutomationLaneId) -> Option<&AutomationLane> { + self.automation_lanes.get(&lane_id) + } + + /// Get a mutable automation lane by ID + pub fn get_automation_lane_mut(&mut self, lane_id: AutomationLaneId) -> Option<&mut AutomationLane> { + self.automation_lanes.get_mut(&lane_id) + } + + /// Remove an automation lane + pub fn remove_automation_lane(&mut self, lane_id: AutomationLaneId) -> bool { + self.automation_lanes.remove(&lane_id).is_some() + } + + /// Evaluate automation at a specific time and return effective parameters + pub fn evaluate_automation_at_time(&self, time: f64) -> (f32, f32, f64) { + let mut volume = self.volume; + let mut time_stretch = self.time_stretch; + let mut offset = self.offset; + + // Check for automation + for lane in self.automation_lanes.values() { + if !lane.enabled { + continue; + } + + match lane.parameter_id { + ParameterId::TrackVolume => { + if let Some(automated_value) = lane.evaluate(time) { + volume = automated_value; + } + } + ParameterId::TimeStretch => { + if let Some(automated_value) = lane.evaluate(time) { + time_stretch = automated_value; + } + } + ParameterId::TimeOffset => { + if let Some(automated_value) = lane.evaluate(time) { + offset = automated_value as f64; + } + } + _ => {} + } + } + + (volume, time_stretch, offset) + } + /// Add a child track to this group pub fn add_child(&mut self, track_id: TrackId) { if !self.children.contains(&track_id) { @@ -239,6 +306,9 @@ pub struct MidiTrack { pub volume: f32, pub muted: bool, pub solo: bool, + /// Automation lanes for this track + pub automation_lanes: HashMap, + next_automation_id: AutomationLaneId, } impl MidiTrack { @@ -253,9 +323,36 @@ impl MidiTrack { volume: 1.0, muted: false, solo: false, + automation_lanes: HashMap::new(), + next_automation_id: 0, } } + /// Add an automation lane to this track + pub fn add_automation_lane(&mut self, parameter_id: ParameterId) -> AutomationLaneId { + let lane_id = self.next_automation_id; + self.next_automation_id += 1; + + let lane = AutomationLane::new(lane_id, parameter_id); + self.automation_lanes.insert(lane_id, lane); + lane_id + } + + /// Get an automation lane by ID + pub fn get_automation_lane(&self, lane_id: AutomationLaneId) -> Option<&AutomationLane> { + self.automation_lanes.get(&lane_id) + } + + /// Get a mutable automation lane by ID + pub fn get_automation_lane_mut(&mut self, lane_id: AutomationLaneId) -> Option<&mut AutomationLane> { + self.automation_lanes.get_mut(&lane_id) + } + + /// Remove an automation lane + pub fn remove_automation_lane(&mut self, lane_id: AutomationLaneId) -> bool { + self.automation_lanes.remove(&lane_id).is_some() + } + /// Add an effect to the track's effect chain pub fn add_effect(&mut self, effect: Box) { self.effects.push(effect); @@ -324,11 +421,37 @@ impl MidiTrack { effect.process(output, channels as usize, sample_rate); } + // Evaluate and apply automation + let effective_volume = self.evaluate_automation_at_time(playhead_seconds); + // Apply track volume for sample in output.iter_mut() { - *sample *= self.volume; + *sample *= effective_volume; } } + + /// Evaluate automation at a specific time and return the effective volume + fn evaluate_automation_at_time(&self, time: f64) -> f32 { + let mut volume = self.volume; + + // Check for volume automation + for lane in self.automation_lanes.values() { + if !lane.enabled { + continue; + } + + match lane.parameter_id { + ParameterId::TrackVolume => { + if let Some(automated_value) = lane.evaluate(time) { + volume = automated_value; + } + } + _ => {} + } + } + + volume + } } /// Audio track with clips and effect chain @@ -340,6 +463,9 @@ pub struct AudioTrack { pub volume: f32, pub muted: bool, pub solo: bool, + /// Automation lanes for this track + pub automation_lanes: HashMap, + next_automation_id: AutomationLaneId, } impl AudioTrack { @@ -353,9 +479,36 @@ impl AudioTrack { volume: 1.0, muted: false, solo: false, + automation_lanes: HashMap::new(), + next_automation_id: 0, } } + /// Add an automation lane to this track + pub fn add_automation_lane(&mut self, parameter_id: ParameterId) -> AutomationLaneId { + let lane_id = self.next_automation_id; + self.next_automation_id += 1; + + let lane = AutomationLane::new(lane_id, parameter_id); + self.automation_lanes.insert(lane_id, lane); + lane_id + } + + /// Get an automation lane by ID + pub fn get_automation_lane(&self, lane_id: AutomationLaneId) -> Option<&AutomationLane> { + self.automation_lanes.get(&lane_id) + } + + /// Get a mutable automation lane by ID + pub fn get_automation_lane_mut(&mut self, lane_id: AutomationLaneId) -> Option<&mut AutomationLane> { + self.automation_lanes.get_mut(&lane_id) + } + + /// Remove an automation lane + pub fn remove_automation_lane(&mut self, lane_id: AutomationLaneId) -> bool { + self.automation_lanes.remove(&lane_id).is_some() + } + /// Add an effect to the track's effect chain pub fn add_effect(&mut self, effect: Box) { self.effects.push(effect); @@ -435,14 +588,40 @@ impl AudioTrack { effect.process(output, channels as usize, sample_rate); } + // Evaluate and apply automation + let effective_volume = self.evaluate_automation_at_time(playhead_seconds); + // Apply track volume for sample in output.iter_mut() { - *sample *= self.volume; + *sample *= effective_volume; } rendered } + /// Evaluate automation at a specific time and return the effective volume + fn evaluate_automation_at_time(&self, time: f64) -> f32 { + let mut volume = self.volume; + + // Check for volume automation + for lane in self.automation_lanes.values() { + if !lane.enabled { + continue; + } + + match lane.parameter_id { + ParameterId::TrackVolume => { + if let Some(automated_value) = lane.evaluate(time) { + volume = automated_value; + } + } + _ => {} + } + } + + volume + } + /// Render a single clip into the output buffer fn render_clip( &self, diff --git a/daw-backend/src/command/types.rs b/daw-backend/src/command/types.rs index f294558..c71e87f 100644 --- a/daw-backend/src/command/types.rs +++ b/daw-backend/src/command/types.rs @@ -1,4 +1,7 @@ -use crate::audio::{ClipId, MidiClip, MidiClipId, TrackId}; +use crate::audio::{ + AutomationLaneId, ClipId, CurveType, MidiClip, MidiClipId, ParameterId, + TrackId, +}; use crate::audio::buffer_pool::BufferPoolStats; /// Commands sent from UI/control thread to audio thread @@ -54,6 +57,15 @@ pub enum Command { /// Set metatrack pitch shift in semitones (track_id, semitones) - for future use SetPitchShift(TrackId, f32), + // Audio track commands + /// Create a new audio track with a name + CreateAudioTrack(String), + /// Add an audio file to the pool (path, data, channels, sample_rate) + /// Returns the pool index via an AudioEvent + AddAudioFile(String, Vec, u32, u32), + /// Add a clip to an audio track (track_id, pool_index, start_time, duration, offset) + AddAudioClip(TrackId, usize, f64, f64, f64), + // MIDI commands /// Create a new MIDI track with a name CreateMidiTrack(String), @@ -67,6 +79,34 @@ pub enum Command { // Diagnostics commands /// Request buffer pool statistics RequestBufferPoolStats, + + // Automation commands + /// Create a new automation lane on a track (track_id, parameter_id) + CreateAutomationLane(TrackId, ParameterId), + /// Add an automation point to a lane (track_id, lane_id, time, value, curve) + AddAutomationPoint(TrackId, AutomationLaneId, f64, f32, CurveType), + /// Remove an automation point at a specific time (track_id, lane_id, time, tolerance) + RemoveAutomationPoint(TrackId, AutomationLaneId, f64, f64), + /// Clear all automation points from a lane (track_id, lane_id) + ClearAutomationLane(TrackId, AutomationLaneId), + /// Remove an automation lane (track_id, lane_id) + RemoveAutomationLane(TrackId, AutomationLaneId), + /// Enable/disable an automation lane (track_id, lane_id, enabled) + SetAutomationLaneEnabled(TrackId, AutomationLaneId, bool), + + // Recording commands + /// Start recording on a track (track_id, start_time) + StartRecording(TrackId, f64), + /// Stop the current recording + StopRecording, + /// Pause the current recording + PauseRecording, + /// Resume the current recording + ResumeRecording, + + // Project commands + /// Reset the entire project (remove all tracks, clear audio pool, reset state) + Reset, } /// Events sent from audio thread back to UI/control thread @@ -80,6 +120,22 @@ pub enum AudioEvent { BufferUnderrun, /// A new track was created (track_id, is_metatrack, name) TrackCreated(TrackId, bool, String), + /// An audio file was added to the pool (pool_index, path) + AudioFileAdded(usize, String), + /// A clip was added to a track (track_id, clip_id) + ClipAdded(TrackId, ClipId), /// Buffer pool statistics response BufferPoolStats(BufferPoolStats), + /// Automation lane created (track_id, lane_id, parameter_id) + AutomationLaneCreated(TrackId, AutomationLaneId, ParameterId), + /// Recording started (track_id, clip_id) + RecordingStarted(TrackId, ClipId), + /// Recording progress update (clip_id, current_duration) + RecordingProgress(ClipId, f64), + /// Recording stopped (clip_id, pool_index) + RecordingStopped(ClipId, usize), + /// Recording error (error_message) + RecordingError(String), + /// Project has been reset + ProjectReset, } diff --git a/daw-backend/src/io/audio_file.rs b/daw-backend/src/io/audio_file.rs index c473fb4..c704f92 100644 --- a/daw-backend/src/io/audio_file.rs +++ b/daw-backend/src/io/audio_file.rs @@ -7,6 +7,12 @@ use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct WaveformPeak { + pub min: f32, + pub max: f32, +} + pub struct AudioFile { pub data: Vec, pub channels: u32, @@ -121,4 +127,48 @@ impl AudioFile { frames, }) } + + /// Calculate the duration of the audio file in seconds + pub fn duration(&self) -> f64 { + self.frames as f64 / self.sample_rate as f64 + } + + /// Generate a waveform overview with the specified number of peaks + /// This creates a downsampled representation suitable for timeline visualization + pub fn generate_waveform_overview(&self, target_peaks: usize) -> Vec { + if self.frames == 0 || target_peaks == 0 { + return Vec::new(); + } + + let total_frames = self.frames as usize; + let frames_per_peak = (total_frames / target_peaks).max(1); + let actual_peaks = (total_frames + frames_per_peak - 1) / frames_per_peak; + + let mut peaks = Vec::with_capacity(actual_peaks); + + for peak_idx in 0..actual_peaks { + let start_frame = peak_idx * frames_per_peak; + let end_frame = ((peak_idx + 1) * frames_per_peak).min(total_frames); + + let mut min = 0.0f32; + let mut max = 0.0f32; + + // Scan all samples in this window + for frame_idx in start_frame..end_frame { + // For multi-channel audio, combine all channels + for ch in 0..self.channels as usize { + let sample_idx = frame_idx * self.channels as usize + ch; + if sample_idx < self.data.len() { + let sample = self.data[sample_idx]; + min = min.min(sample); + max = max.max(sample); + } + } + } + + peaks.push(WaveformPeak { min, max }); + } + + peaks + } } diff --git a/daw-backend/src/io/mod.rs b/daw-backend/src/io/mod.rs index a80ee07..50e026a 100644 --- a/daw-backend/src/io/mod.rs +++ b/daw-backend/src/io/mod.rs @@ -1,5 +1,7 @@ pub mod audio_file; pub mod midi_file; +pub mod wav_writer; -pub use audio_file::AudioFile; +pub use audio_file::{AudioFile, WaveformPeak}; pub use midi_file::load_midi_file; +pub use wav_writer::WavWriter; diff --git a/daw-backend/src/io/wav_writer.rs b/daw-backend/src/io/wav_writer.rs new file mode 100644 index 0000000..ff6fddb --- /dev/null +++ b/daw-backend/src/io/wav_writer.rs @@ -0,0 +1,113 @@ +/// Incremental WAV file writer for streaming audio to disk +use std::fs::File; +use std::io::{self, Seek, SeekFrom, Write}; +use std::path::Path; + +/// WAV file writer that supports incremental writing +pub struct WavWriter { + file: File, + sample_rate: u32, + channels: u32, + frames_written: usize, +} + +impl WavWriter { + /// Create a new WAV file and write initial header + /// The header is written with placeholder sizes that will be updated on finalization + pub fn create(path: impl AsRef, sample_rate: u32, channels: u32) -> io::Result { + let mut file = File::create(path)?; + + // Write initial WAV header with placeholder sizes + write_wav_header(&mut file, sample_rate, channels, 0)?; + + Ok(Self { + file, + sample_rate, + channels, + frames_written: 0, + }) + } + + /// Append audio samples to the file + /// Expects interleaved f32 samples in range [-1.0, 1.0] + pub fn write_samples(&mut self, samples: &[f32]) -> io::Result<()> { + // Convert f32 samples to 16-bit PCM + let pcm_data: Vec = samples + .iter() + .flat_map(|&sample| { + let clamped = sample.clamp(-1.0, 1.0); + let pcm_value = (clamped * 32767.0) as i16; + pcm_value.to_le_bytes() + }) + .collect(); + + self.file.write_all(&pcm_data)?; + self.frames_written += samples.len() / self.channels as usize; + + Ok(()) + } + + /// Get the current number of frames written + pub fn frames_written(&self) -> usize { + self.frames_written + } + + /// Get the current duration in seconds + pub fn duration(&self) -> f64 { + self.frames_written as f64 / self.sample_rate as f64 + } + + /// Finalize the WAV file by updating the header with correct sizes + pub fn finalize(mut self) -> io::Result<()> { + // Flush any remaining data + self.file.flush()?; + + // Calculate total data size + let data_size = self.frames_written * self.channels as usize * 2; // 2 bytes per sample (16-bit) + let file_size = 36 + data_size; // 36 = size of header before data + + // Seek to RIFF chunk size (offset 4) + self.file.seek(SeekFrom::Start(4))?; + self.file.write_all(&((file_size - 8) as u32).to_le_bytes())?; + + // Seek to data chunk size (offset 40) + self.file.seek(SeekFrom::Start(40))?; + self.file.write_all(&(data_size as u32).to_le_bytes())?; + + self.file.flush()?; + + Ok(()) + } +} + +/// Write WAV header with specified parameters +fn write_wav_header(file: &mut File, sample_rate: u32, channels: u32, frames: usize) -> io::Result<()> { + let bytes_per_sample = 2u16; // 16-bit PCM + let data_size = (frames * channels as usize * bytes_per_sample as usize) as u32; + let file_size = 36 + data_size; + + // RIFF header + file.write_all(b"RIFF")?; + file.write_all(&(file_size - 8).to_le_bytes())?; + file.write_all(b"WAVE")?; + + // fmt chunk + file.write_all(b"fmt ")?; + file.write_all(&16u32.to_le_bytes())?; // fmt chunk size + file.write_all(&1u16.to_le_bytes())?; // PCM format + file.write_all(&(channels as u16).to_le_bytes())?; + file.write_all(&sample_rate.to_le_bytes())?; + + let byte_rate = sample_rate * channels * bytes_per_sample as u32; + file.write_all(&byte_rate.to_le_bytes())?; + + let block_align = channels as u16 * bytes_per_sample; + file.write_all(&block_align.to_le_bytes())?; + file.write_all(&(bytes_per_sample * 8).to_le_bytes())?; // bits per sample + + // data chunk header + file.write_all(b"data")?; + file.write_all(&data_size.to_le_bytes())?; + + Ok(()) +} diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index d13e39b..61e4bdf 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -12,9 +12,71 @@ pub mod io; // Re-export commonly used types pub use audio::{ - AudioPool, AudioTrack, BufferPool, Clip, ClipId, Engine, EngineController, - Metatrack, MidiClip, MidiClipId, MidiEvent, MidiTrack, PoolAudioFile, Project, RenderContext, Track, TrackId, TrackNode, + AudioPool, AudioTrack, AutomationLane, AutomationLaneId, AutomationPoint, BufferPool, Clip, ClipId, CurveType, Engine, EngineController, + Metatrack, MidiClip, MidiClipId, MidiEvent, MidiTrack, ParameterId, PoolAudioFile, Project, RecordingState, RenderContext, Track, TrackId, + TrackNode, }; pub use command::{AudioEvent, Command}; pub use effects::{Effect, GainEffect, PanEffect, SimpleEQ, SimpleSynth}; -pub use io::{load_midi_file, AudioFile}; +pub use io::{load_midi_file, AudioFile, WaveformPeak, WavWriter}; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + +/// Simple audio system that handles cpal initialization internally +pub struct AudioSystem { + pub controller: EngineController, + pub stream: cpal::Stream, + pub event_rx: rtrb::Consumer, + pub sample_rate: u32, + pub channels: u32, +} + +impl AudioSystem { + /// Initialize the audio system with default device + pub fn new() -> Result { + let host = cpal::default_host(); + let device = host + .default_output_device() + .ok_or("No output device available")?; + + let default_config = device.default_output_config().map_err(|e| e.to_string())?; + let sample_rate = default_config.sample_rate().0; + let channels = default_config.channels() as u32; + + // Create queues + let (command_tx, command_rx) = rtrb::RingBuffer::new(256); + let (event_tx, event_rx) = rtrb::RingBuffer::new(256); + + // Create engine + let mut engine = Engine::new(sample_rate, channels, command_rx, event_tx); + let controller = engine.get_controller(command_tx); + + // Build stream + let config: cpal::StreamConfig = default_config.clone().into(); + let mut buffer = vec![0.0f32; 16384]; + + let stream = device + .build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let buf = &mut buffer[..data.len()]; + buf.fill(0.0); + engine.process(buf); + data.copy_from_slice(buf); + }, + |err| eprintln!("Stream error: {}", err), + None, + ) + .map_err(|e| e.to_string())?; + + stream.play().map_err(|e| e.to_string())?; + + Ok(Self { + controller, + stream, + event_rx, + sample_rate, + channels, + }) + } +} diff --git a/daw-backend/src/main.rs b/daw-backend/src/main.rs index 1427c01..e5f6db3 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::{load_midi_file, AudioEvent, AudioFile, Clip, Engine, PoolAudioFile, Track}; +use daw_backend::{load_midi_file, AudioEvent, AudioFile, Clip, CurveType, Engine, ParameterId, PoolAudioFile, Track}; use std::env; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -224,6 +224,56 @@ fn main() -> Result<(), Box> { print!("> "); io::stdout().flush().ok(); } + AudioEvent::AutomationLaneCreated(track_id, lane_id, parameter_id) => { + print!("\r\x1b[K"); + println!("Automation lane {} created on track {} for parameter {:?}", + lane_id, track_id, parameter_id); + print!("> "); + io::stdout().flush().ok(); + } + AudioEvent::AudioFileAdded(pool_index, path) => { + print!("\r\x1b[K"); + println!("Audio file added to pool at index {}: '{}'", pool_index, path); + print!("> "); + io::stdout().flush().ok(); + } + AudioEvent::ClipAdded(track_id, clip_id) => { + print!("\r\x1b[K"); + println!("Clip {} added to track {}", clip_id, track_id); + print!("> "); + io::stdout().flush().ok(); + } + AudioEvent::RecordingStarted(track_id, clip_id) => { + print!("\r\x1b[K"); + println!("Recording started on track {} (clip {})", track_id, clip_id); + print!("> "); + io::stdout().flush().ok(); + } + AudioEvent::RecordingProgress(clip_id, duration) => { + print!("\r\x1b[K"); + print!("Recording clip {}: {:.2}s", clip_id, duration); + io::stdout().flush().ok(); + } + AudioEvent::RecordingStopped(clip_id, pool_index) => { + print!("\r\x1b[K"); + println!("Recording stopped (clip {}, pool index {})", clip_id, pool_index); + print!("> "); + io::stdout().flush().ok(); + } + AudioEvent::RecordingError(error) => { + print!("\r\x1b[K"); + println!("Recording error: {}", error); + print!("> "); + io::stdout().flush().ok(); + } + AudioEvent::ProjectReset => { + print!("\r\x1b[K"); + println!("Project reset - all tracks and audio cleared"); + // Clear the local track list + track_ids_clone.lock().unwrap().clear(); + print!("> "); + io::stdout().flush().ok(); + } } } } @@ -633,6 +683,35 @@ fn main() -> Result<(), Box> { } } else if input == "stats" || input == "buffers" { controller.request_buffer_pool_stats(); + } else if input.starts_with("autovolume ") { + // Parse: autovolume