diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index 096d8d3..1d42291 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -717,6 +717,9 @@ impl Engine { // Send a live MIDI note on event to the specified track's instrument self.project.send_midi_note_on(track_id, note, velocity); + // Emit event to UI for visual feedback + let _ = self.event_tx.push(AudioEvent::NoteOn(note, velocity)); + // If MIDI recording is active on this track, capture the event if let Some(recording) = &mut self.midi_recording_state { if recording.track_id == track_id { @@ -732,6 +735,9 @@ impl Engine { // Send a live MIDI note off event to the specified track's instrument self.project.send_midi_note_off(track_id, note); + // Emit event to UI for visual feedback + let _ = self.event_tx.push(AudioEvent::NoteOff(note)); + // If MIDI recording is active on this track, capture the event if let Some(recording) = &mut self.midi_recording_state { if recording.track_id == track_id { diff --git a/daw-backend/src/io/midi_input.rs b/daw-backend/src/io/midi_input.rs index 2554ef8..d2498b2 100644 --- a/daw-backend/src/io/midi_input.rs +++ b/daw-backend/src/io/midi_input.rs @@ -2,11 +2,14 @@ use crate::audio::track::TrackId; use crate::command::Command; use midir::{MidiInput, MidiInputConnection}; use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; /// Manages external MIDI input devices and routes MIDI to the currently active track pub struct MidiInputManager { - connections: Vec, + connections: Arc>>, active_track_id: Arc>>, + command_tx: Arc>>, } struct ActiveMidiConnection { @@ -20,11 +23,50 @@ impl MidiInputManager { /// Create a new MIDI input manager and auto-connect to all available devices pub fn new(command_tx: rtrb::Producer) -> Result { let active_track_id = Arc::new(Mutex::new(None)); - let mut connections = Vec::new(); + let connections = Arc::new(Mutex::new(Vec::new())); // Wrap command producer in Arc for sharing across MIDI callbacks let shared_command_tx = Arc::new(Mutex::new(command_tx)); + // Connect to all currently available devices + Self::connect_to_devices(&connections, &shared_command_tx, &active_track_id)?; + + // Create the manager + let manager = Self { + connections: connections.clone(), + active_track_id: active_track_id.clone(), + command_tx: shared_command_tx.clone(), + }; + + // Spawn hot-plug monitoring thread + let hotplug_connections = connections.clone(); + let hotplug_command_tx = shared_command_tx.clone(); + let hotplug_active_id = active_track_id.clone(); + + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(2)); // Check every 2 seconds + + // Try to connect to new devices + if let Err(e) = Self::connect_to_devices( + &hotplug_connections, + &hotplug_command_tx, + &hotplug_active_id, + ) { + eprintln!("MIDI hot-plug scan error: {}", e); + } + } + }); + + Ok(manager) + } + + /// Connect to all available MIDI devices (skips already connected devices) + fn connect_to_devices( + connections: &Arc>>, + command_tx: &Arc>>, + active_track_id: &Arc>>, + ) -> Result<(), String> { // Initialize MIDI input let mut midi_in = MidiInput::new("Lightningbeam") .map_err(|e| format!("Failed to initialize MIDI input: {}", e))?; @@ -32,18 +74,31 @@ impl MidiInputManager { // Get all available MIDI input ports let ports = midi_in.ports(); - println!("MIDI Input: Found {} device(s)", ports.len()); + // Get list of already connected device names + let connected_devices: Vec = { + let conns = connections.lock().unwrap(); + conns.iter().map(|c| c.device_name.clone()).collect() + }; - // We need to recreate MidiInput for each connection since connect() consumes it // Store port info first let mut port_infos = Vec::new(); for port in &ports { if let Ok(port_name) = midi_in.port_name(port) { - port_infos.push((port.clone(), port_name)); + // Skip if already connected + if !connected_devices.contains(&port_name) { + port_infos.push((port.clone(), port_name)); + } } } - // Connect to each available device + // If no new devices, return early + if port_infos.is_empty() { + return Ok(()); + } + + println!("MIDI: Found {} new device(s)", port_infos.len()); + + // Connect to each new device for (port, port_name) in port_infos { println!("MIDI: Connecting to device: {}", port_name); @@ -52,7 +107,7 @@ impl MidiInputManager { .map_err(|e| format!("Failed to recreate MIDI input: {}", e))?; let device_name = port_name.clone(); - let cmd_tx = shared_command_tx.clone(); + let cmd_tx = command_tx.clone(); let active_id = active_track_id.clone(); match midi_in.connect( @@ -64,7 +119,8 @@ impl MidiInputManager { (), ) { Ok(connection) => { - connections.push(ActiveMidiConnection { + let mut conns = connections.lock().unwrap(); + conns.push(ActiveMidiConnection { device_name: port_name.clone(), connection, }); @@ -83,12 +139,10 @@ impl MidiInputManager { } } - println!("MIDI Input: Connected to {} device(s)", connections.len()); + let conn_count = connections.lock().unwrap().len(); + println!("MIDI Input: Total connected devices: {}", conn_count); - Ok(Self { - connections, - active_track_id, - }) + Ok(()) } /// MIDI input callback - parses MIDI messages and sends commands to audio engine @@ -185,6 +239,6 @@ impl MidiInputManager { /// Get the number of connected devices pub fn device_count(&self) -> usize { - self.connections.len() + self.connections.lock().unwrap().len() } } diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index cddd71f..5d09468 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -44,7 +44,12 @@ impl AudioSystem { /// /// # Arguments /// * `event_emitter` - Optional event emitter for pushing events to external systems - pub fn new(event_emitter: Option>) -> Result { + /// * `buffer_size` - Audio buffer size in frames (128, 256, 512, 1024, etc.) + /// Smaller = lower latency but higher CPU usage. Default: 256 + pub fn new( + event_emitter: Option>, + buffer_size: u32, + ) -> Result { let host = cpal::default_host(); // Get output device @@ -87,14 +92,38 @@ impl AudioSystem { } } - // Build output stream - let output_config: cpal::StreamConfig = default_output_config.clone().into(); + // Build output stream with configurable buffer size + let mut output_config: cpal::StreamConfig = default_output_config.clone().into(); + + // Set the requested buffer size + output_config.buffer_size = cpal::BufferSize::Fixed(buffer_size); + let mut output_buffer = vec![0.0f32; 16384]; + // Log audio configuration + println!("Audio Output Configuration:"); + println!(" Sample Rate: {} Hz", output_config.sample_rate.0); + println!(" Channels: {}", output_config.channels); + println!(" Buffer Size: {:?}", output_config.buffer_size); + + // Calculate expected latency + if let cpal::BufferSize::Fixed(size) = output_config.buffer_size { + let latency_ms = (size as f64 / output_config.sample_rate.0 as f64) * 1000.0; + println!(" Expected Latency: {:.2} ms", latency_ms); + } + + let mut first_callback = true; let output_stream = output_device .build_output_stream( &output_config, move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + if first_callback { + let frames = data.len() / output_config.channels as usize; + let latency_ms = (frames as f64 / output_config.sample_rate.0 as f64) * 1000.0; + println!("Audio callback buffer size: {} samples ({} frames, {:.2} ms latency)", + data.len(), frames, latency_ms); + first_callback = false; + } let buf = &mut output_buffer[..data.len()]; buf.fill(0.0); engine.process(buf); diff --git a/daw-backend/src/main.rs b/daw-backend/src/main.rs index 011bd67..92773bc 100644 --- a/daw-backend/src/main.rs +++ b/daw-backend/src/main.rs @@ -46,8 +46,8 @@ fn main() -> Result<(), Box> { let (event_tx, event_rx) = rtrb::RingBuffer::new(256); let emitter = Arc::new(TuiEventEmitter::new(event_tx)); - // Initialize audio system with event emitter - let mut audio_system = AudioSystem::new(Some(emitter))?; + // Initialize audio system with event emitter and default buffer size + let mut audio_system = AudioSystem::new(Some(emitter), 256)?; println!("Audio system initialized:"); println!(" Sample rate: {} Hz", audio_system.sample_rate); diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 51cf695..9578d37 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -33,6 +33,7 @@ pub struct AudioState { controller: Option, sample_rate: u32, channels: u32, + buffer_size: u32, next_track_id: u32, next_pool_index: usize, next_graph_node_id: u32, @@ -46,6 +47,7 @@ impl Default for AudioState { controller: None, sample_rate: 0, channels: 0, + buffer_size: 256, // Default buffer size next_track_id: 0, next_pool_index: 0, next_graph_node_id: 0, @@ -116,9 +118,15 @@ impl EventEmitter for TauriEventEmitter { pub async fn audio_init( state: tauri::State<'_, Arc>>, app_handle: tauri::AppHandle, + buffer_size: Option, ) -> Result { let mut audio_state = state.lock().unwrap(); + // Update buffer size if provided (from config) + if let Some(size) = buffer_size { + audio_state.buffer_size = size; + } + // Check if already initialized - if so, reset DAW state (for hot-reload) if let Some(controller) = &mut audio_state.controller { controller.reset(); @@ -134,12 +142,15 @@ pub async fn audio_init( // Create TauriEventEmitter let emitter = Arc::new(TauriEventEmitter { app_handle }); + // Get buffer size from audio_state (default is 256) + let buffer_size = audio_state.buffer_size; + // AudioSystem handles all cpal initialization internally - let system = AudioSystem::new(Some(emitter))?; + let system = AudioSystem::new(Some(emitter), buffer_size)?; let info = format!( - "Audio initialized: {} Hz, {} ch", - system.sample_rate, system.channels + "Audio initialized: {} Hz, {} ch, {} frame buffer", + system.sample_rate, system.channels, buffer_size ); // Leak the stream to keep it alive for the lifetime of the app @@ -149,6 +160,7 @@ pub async fn audio_init( audio_state.controller = Some(system.controller); audio_state.sample_rate = system.sample_rate; audio_state.channels = system.channels; + audio_state.buffer_size = buffer_size; audio_state.next_track_id = 0; audio_state.next_pool_index = 0; audio_state.next_graph_node_id = 0; diff --git a/src/main.js b/src/main.js index 6c70657..34fe61a 100644 --- a/src/main.js +++ b/src/main.js @@ -751,7 +751,7 @@ window.addEventListener("DOMContentLoaded", () => { (async () => { try { console.log('Initializing audio system...'); - const result = await invoke('audio_init'); + const result = await invoke('audio_init', { bufferSize: config.audioBufferSize }); console.log('Audio system initialized:', result); } catch (error) { if (error === 'Audio already initialized') { @@ -1297,6 +1297,26 @@ async function handleAudioEvent(event) { // Preset loaded - layers are already populated during graph reload console.log('GraphPresetLoaded event received for track:', event.track_id); break; + + case 'NoteOn': + // MIDI note started - update virtual piano visual feedback + if (context.pianoWidget) { + context.pianoWidget.pressedKeys.add(event.note); + if (context.pianoRedraw) { + context.pianoRedraw(); + } + } + break; + + case 'NoteOff': + // MIDI note stopped - update virtual piano visual feedback + if (context.pianoWidget) { + context.pianoWidget.pressedKeys.delete(event.note); + if (context.pianoRedraw) { + context.pianoRedraw(); + } + } + break; } } @@ -10346,6 +10366,18 @@ function showPreferencesDialog() { +
+ + + Requires app restart to take effect +