From 9db34daf850d04f522516bc8cdc137d43b3738f7 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 16 Feb 2026 00:19:15 -0500 Subject: [PATCH] make default voice polyphonic --- daw-backend/src/audio/export.rs | 52 ++++-- daw-backend/src/audio/node_graph/graph.rs | 1 - .../src/audio/node_graph/nodes/filter.rs | 4 +- .../audio/node_graph/nodes/voice_allocator.rs | 19 +- daw-backend/src/audio/track.rs | 19 +- src/assets/instruments/synthesizers/bass.json | 167 ++++++++++-------- 6 files changed, 148 insertions(+), 114 deletions(-) diff --git a/daw-backend/src/audio/export.rs b/daw-backend/src/audio/export.rs index 4fc1bbf..c0e4c64 100644 --- a/daw-backend/src/audio/export.rs +++ b/daw-backend/src/audio/export.rs @@ -689,16 +689,24 @@ fn encode_complete_frame_mp3( frame.set_pts(Some(pts)); // Copy all planar samples to frame - unsafe { - for ch in 0..channels { - let plane = frame.data_mut(ch); - let src = &planar_samples[ch]; + for ch in 0..channels { + let plane = frame.data_mut(ch); + let src = &planar_samples[ch]; - std::ptr::copy_nonoverlapping( - src.as_ptr() as *const u8, - plane.as_mut_ptr(), - num_frames * std::mem::size_of::(), - ); + // Verify buffer size + let byte_size = num_frames * std::mem::size_of::(); + if plane.len() < byte_size { + return Err(format!( + "FFmpeg frame buffer too small: {} bytes, need {} bytes", + plane.len(), byte_size + )); + } + + // Safe byte-level copy + for (i, &sample) in src.iter().enumerate() { + let bytes = sample.to_ne_bytes(); + let offset = i * 2; + plane[offset..offset + 2].copy_from_slice(&bytes); } } @@ -734,16 +742,24 @@ fn encode_complete_frame_aac( frame.set_pts(Some(pts)); // Copy all planar samples to frame - unsafe { - for ch in 0..channels { - let plane = frame.data_mut(ch); - let src = &planar_samples[ch]; + for ch in 0..channels { + let plane = frame.data_mut(ch); + let src = &planar_samples[ch]; - std::ptr::copy_nonoverlapping( - src.as_ptr() as *const u8, - plane.as_mut_ptr(), - num_frames * std::mem::size_of::(), - ); + // Verify buffer size + let byte_size = num_frames * std::mem::size_of::(); + if plane.len() < byte_size { + return Err(format!( + "FFmpeg frame buffer too small: {} bytes, need {} bytes", + plane.len(), byte_size + )); + } + + // Safe byte-level copy + for (i, &sample) in src.iter().enumerate() { + let bytes = sample.to_ne_bytes(); + let offset = i * 4; + plane[offset..offset + 4].copy_from_slice(&bytes); } } diff --git a/daw-backend/src/audio/node_graph/graph.rs b/daw-backend/src/audio/node_graph/graph.rs index 88f6afa..7638f4a 100644 --- a/daw-backend/src/audio/node_graph/graph.rs +++ b/daw-backend/src/audio/node_graph/graph.rs @@ -977,7 +977,6 @@ impl AudioGraph { // If there's a template graph, deserialize and set it if let Some(ref template_preset) = serialized_node.template_graph { let template_graph = Self::from_preset(template_preset, sample_rate, buffer_size, preset_base_path)?; - // Set the template graph (we'll need to add this method to VoiceAllocator) *va.template_graph_mut() = template_graph; va.rebuild_voices(); } diff --git a/daw-backend/src/audio/node_graph/nodes/filter.rs b/daw-backend/src/audio/node_graph/nodes/filter.rs index d9409a5..ca69b12 100644 --- a/daw-backend/src/audio/node_graph/nodes/filter.rs +++ b/daw-backend/src/audio/node_graph/nodes/filter.rs @@ -188,10 +188,10 @@ impl AudioNode for FilterNode { // Set filter to match current type match self.filter_type { FilterType::Lowpass => { - new_filter.set_lowpass(self.sample_rate as f32, self.cutoff, self.resonance); + new_filter.set_lowpass(self.cutoff, self.resonance, self.sample_rate as f32); } FilterType::Highpass => { - new_filter.set_highpass(self.sample_rate as f32, self.cutoff, self.resonance); + new_filter.set_highpass(self.cutoff, self.resonance, self.sample_rate as f32); } } diff --git a/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs b/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs index 3586cc7..1221fab 100644 --- a/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs +++ b/daw-backend/src/audio/node_graph/nodes/voice_allocator.rs @@ -316,21 +316,22 @@ impl AudioNode for VoiceAllocatorNode { // Note: playback_time is 0.0 since voice allocator doesn't track time self.voice_instances[voice_idx].process(mix_slice, &midi_events, 0.0); + // Auto-deactivate releasing voices that have gone silent + if voice_state.releasing { + let peak = mix_slice.iter().fold(0.0f32, |max, &s| max.max(s.abs())); + if peak < 1e-6 { + voice_state.active = false; + voice_state.releasing = false; + continue; // Don't mix silent output + } + } + // Mix into output (accumulate) for (i, sample) in mix_slice.iter().enumerate() { output[i] += sample; } } } - - // Apply normalization to prevent clipping (divide by active voice count) - let active_count = self.voices[..self.voice_count].iter().filter(|v| v.active).count(); - if active_count > 1 { - let scale = 1.0 / (active_count as f32).sqrt(); // Use sqrt for better loudness perception - for sample in output.iter_mut() { - *sample *= scale; - } - } } fn reset(&mut self) { diff --git a/daw-backend/src/audio/track.rs b/daw-backend/src/audio/track.rs index 766d117..6d69f45 100644 --- a/daw-backend/src/audio/track.rs +++ b/daw-backend/src/audio/track.rs @@ -3,7 +3,7 @@ use super::clip::{AudioClipInstance, AudioClipInstanceId}; use super::midi::{MidiClipInstance, MidiClipInstanceId, MidiEvent}; use super::midi_pool::MidiClipPool; use super::node_graph::AudioGraph; -use super::node_graph::nodes::{AudioInputNode, AudioOutputNode, MidiInputNode}; +use super::node_graph::nodes::{AudioInputNode, AudioOutputNode}; use super::node_graph::preset::GraphPreset; use super::pool::AudioClipPool; use serde::{Serialize, Deserialize}; @@ -365,20 +365,9 @@ impl MidiTrack { // Use a large buffer size that can accommodate any callback let default_buffer_size = 8192; - // Create default instrument graph with MidiInput and AudioOutput - let mut instrument_graph = AudioGraph::new(sample_rate, default_buffer_size); - - // Add MidiInput node (entry point for MIDI events) - let midi_input_node = Box::new(MidiInputNode::new("MIDI Input")); - let midi_input_id = instrument_graph.add_node(midi_input_node); - instrument_graph.set_node_position(midi_input_id, 100.0, 150.0); - instrument_graph.set_midi_target(midi_input_id, true); - - // Add AudioOutput node (final audio output) - let audio_output_node = Box::new(AudioOutputNode::new("Audio Output")); - let audio_output_id = instrument_graph.add_node(audio_output_node); - instrument_graph.set_node_position(audio_output_id, 700.0, 150.0); - instrument_graph.set_output_node(Some(audio_output_id)); + // Start with empty graph — the frontend loads a default instrument preset + // (bass.json) via graph_load_preset which replaces the entire graph + let instrument_graph = AudioGraph::new(sample_rate, default_buffer_size); Self { id, diff --git a/src/assets/instruments/synthesizers/bass.json b/src/assets/instruments/synthesizers/bass.json index 1e148cb..1ff4efe 100644 --- a/src/assets/instruments/synthesizers/bass.json +++ b/src/assets/instruments/synthesizers/bass.json @@ -1,97 +1,126 @@ { "metadata": { "name": "Deep Bass", - "description": "Thick sub bass with sawtooth oscillator", + "description": "Thick sub bass with sawtooth oscillator (polyphonic)", "author": "Lightningbeam", - "version": 1, + "version": 2, "tags": ["bass", "sub", "synth"] }, "midi_targets": [0], - "output_node": 7, + "output_node": 2, "nodes": [ { "id": 0, "node_type": "MidiInput", "name": "MIDI In", "parameters": {}, - "position": [100.0, 100.0] + "position": [100.0, 150.0] }, { "id": 1, - "node_type": "MidiToCV", - "name": "MIDI→CV", - "parameters": {}, - "position": [400.0, 100.0] + "node_type": "VoiceAllocator", + "name": "Voice Allocator", + "parameters": { + "0": 8.0 + }, + "position": [400.0, 150.0], + "template_graph": { + "metadata": { + "name": "Voice Template", + "description": "Per-voice synth patch", + "author": "Lightningbeam", + "version": 1, + "tags": [] + }, + "midi_targets": [0], + "output_node": 6, + "nodes": [ + { + "id": 0, + "node_type": "TemplateInput", + "name": "Template Input", + "parameters": {}, + "position": [-200.0, 0.0] + }, + { + "id": 1, + "node_type": "MidiToCV", + "name": "MIDI→CV", + "parameters": {}, + "position": [100.0, 0.0] + }, + { + "id": 2, + "node_type": "Oscillator", + "name": "Osc", + "parameters": { + "0": 110.0, + "1": 0.7, + "2": 1.0 + }, + "position": [400.0, -100.0] + }, + { + "id": 3, + "node_type": "ADSR", + "name": "Amp Env", + "parameters": { + "0": 0.005, + "1": 0.2, + "2": 0.8, + "3": 0.3 + }, + "position": [400.0, 200.0] + }, + { + "id": 4, + "node_type": "Gain", + "name": "VCA", + "parameters": { + "0": 1.0 + }, + "position": [700.0, 0.0] + }, + { + "id": 5, + "node_type": "Filter", + "name": "LP Filter", + "parameters": { + "0": 800.0, + "1": 1.5, + "2": 0.0 + }, + "position": [900.0, 0.0] + }, + { + "id": 6, + "node_type": "TemplateOutput", + "name": "Template Output", + "parameters": {}, + "position": [1100.0, 0.0] + } + ], + "connections": [ + { "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 }, + { "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 }, + { "from_node": 1, "from_port": 1, "to_node": 3, "to_port": 0 }, + { "from_node": 2, "from_port": 0, "to_node": 4, "to_port": 0 }, + { "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 }, + { "from_node": 4, "from_port": 0, "to_node": 5, "to_port": 0 }, + { "from_node": 5, "from_port": 0, "to_node": 6, "to_port": 0 } + ] + } }, { "id": 2, - "node_type": "Oscillator", - "name": "Osc", - "parameters": { - "0": 110.0, - "1": 0.7, - "2": 1.0 - }, - "position": [700.0, -100.0] - }, - { - "id": 3, - "node_type": "ADSR", - "name": "Amp Env", - "parameters": { - "0": 0.005, - "1": 0.2, - "2": 0.8, - "3": 0.3 - }, - "position": [700.0, 200.0] - }, - { - "id": 4, - "node_type": "Gain", - "name": "VCA", - "parameters": { - "0": 1.0 - }, - "position": [1000.0, 100.0] - }, - { - "id": 5, - "node_type": "Gain", - "name": "Velocity", - "parameters": { - "0": 1.0 - }, - "position": [1150.0, 100.0] - }, - { - "id": 6, - "node_type": "Filter", - "name": "LP Filter", - "parameters": { - "0": 800.0, - "1": 1.5, - "2": 0.0 - }, - "position": [1300.0, 100.0] - }, - { - "id": 7, "node_type": "AudioOutput", "name": "Out", "parameters": {}, - "position": [1600.0, 100.0] + "position": [700.0, 150.0] } ], "connections": [ { "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 }, - { "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 }, - { "from_node": 1, "from_port": 1, "to_node": 3, "to_port": 0 }, - { "from_node": 1, "from_port": 2, "to_node": 5, "to_port": 1 }, - { "from_node": 2, "from_port": 0, "to_node": 4, "to_port": 0 }, - { "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 }, - { "from_node": 4, "from_port": 0, "to_node": 5, "to_port": 0 }, - { "from_node": 5, "from_port": 0, "to_node": 6, "to_port": 0 }, - { "from_node": 6, "from_port": 0, "to_node": 7, "to_port": 0 } + { "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 } ] }