make default voice polyphonic

This commit is contained in:
Skyler Lehmkuhl 2026-02-16 00:19:15 -05:00
parent 6c4cc62098
commit 9db34daf85
6 changed files with 148 additions and 114 deletions

View File

@ -689,16 +689,24 @@ fn encode_complete_frame_mp3(
frame.set_pts(Some(pts)); frame.set_pts(Some(pts));
// Copy all planar samples to frame // Copy all planar samples to frame
unsafe {
for ch in 0..channels { for ch in 0..channels {
let plane = frame.data_mut(ch); let plane = frame.data_mut(ch);
let src = &planar_samples[ch]; let src = &planar_samples[ch];
std::ptr::copy_nonoverlapping( // Verify buffer size
src.as_ptr() as *const u8, let byte_size = num_frames * std::mem::size_of::<i16>();
plane.as_mut_ptr(), if plane.len() < byte_size {
num_frames * std::mem::size_of::<i16>(), 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)); frame.set_pts(Some(pts));
// Copy all planar samples to frame // Copy all planar samples to frame
unsafe {
for ch in 0..channels { for ch in 0..channels {
let plane = frame.data_mut(ch); let plane = frame.data_mut(ch);
let src = &planar_samples[ch]; let src = &planar_samples[ch];
std::ptr::copy_nonoverlapping( // Verify buffer size
src.as_ptr() as *const u8, let byte_size = num_frames * std::mem::size_of::<f32>();
plane.as_mut_ptr(), if plane.len() < byte_size {
num_frames * std::mem::size_of::<f32>(), 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);
} }
} }

View File

@ -977,7 +977,6 @@ impl AudioGraph {
// If there's a template graph, deserialize and set it // If there's a template graph, deserialize and set it
if let Some(ref template_preset) = serialized_node.template_graph { 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)?; 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.template_graph_mut() = template_graph;
va.rebuild_voices(); va.rebuild_voices();
} }

View File

@ -188,10 +188,10 @@ impl AudioNode for FilterNode {
// Set filter to match current type // Set filter to match current type
match self.filter_type { match self.filter_type {
FilterType::Lowpass => { 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 => { 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);
} }
} }

View File

@ -316,21 +316,22 @@ impl AudioNode for VoiceAllocatorNode {
// Note: playback_time is 0.0 since voice allocator doesn't track time // 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); 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) // Mix into output (accumulate)
for (i, sample) in mix_slice.iter().enumerate() { for (i, sample) in mix_slice.iter().enumerate() {
output[i] += sample; 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) { fn reset(&mut self) {

View File

@ -3,7 +3,7 @@ use super::clip::{AudioClipInstance, AudioClipInstanceId};
use super::midi::{MidiClipInstance, MidiClipInstanceId, MidiEvent}; use super::midi::{MidiClipInstance, MidiClipInstanceId, MidiEvent};
use super::midi_pool::MidiClipPool; use super::midi_pool::MidiClipPool;
use super::node_graph::AudioGraph; 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::node_graph::preset::GraphPreset;
use super::pool::AudioClipPool; use super::pool::AudioClipPool;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -365,20 +365,9 @@ impl MidiTrack {
// Use a large buffer size that can accommodate any callback // Use a large buffer size that can accommodate any callback
let default_buffer_size = 8192; let default_buffer_size = 8192;
// Create default instrument graph with MidiInput and AudioOutput // Start with empty graph — the frontend loads a default instrument preset
let mut instrument_graph = AudioGraph::new(sample_rate, default_buffer_size); // (bass.json) via graph_load_preset which replaces the entire graph
let 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));
Self { Self {
id, id,

View File

@ -1,27 +1,53 @@
{ {
"metadata": { "metadata": {
"name": "Deep Bass", "name": "Deep Bass",
"description": "Thick sub bass with sawtooth oscillator", "description": "Thick sub bass with sawtooth oscillator (polyphonic)",
"author": "Lightningbeam", "author": "Lightningbeam",
"version": 1, "version": 2,
"tags": ["bass", "sub", "synth"] "tags": ["bass", "sub", "synth"]
}, },
"midi_targets": [0], "midi_targets": [0],
"output_node": 7, "output_node": 2,
"nodes": [ "nodes": [
{ {
"id": 0, "id": 0,
"node_type": "MidiInput", "node_type": "MidiInput",
"name": "MIDI In", "name": "MIDI In",
"parameters": {}, "parameters": {},
"position": [100.0, 100.0] "position": [100.0, 150.0]
},
{
"id": 1,
"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, "id": 1,
"node_type": "MidiToCV", "node_type": "MidiToCV",
"name": "MIDI→CV", "name": "MIDI→CV",
"parameters": {}, "parameters": {},
"position": [400.0, 100.0] "position": [100.0, 0.0]
}, },
{ {
"id": 2, "id": 2,
@ -32,7 +58,7 @@
"1": 0.7, "1": 0.7,
"2": 1.0 "2": 1.0
}, },
"position": [700.0, -100.0] "position": [400.0, -100.0]
}, },
{ {
"id": 3, "id": 3,
@ -44,7 +70,7 @@
"2": 0.8, "2": 0.8,
"3": 0.3 "3": 0.3
}, },
"position": [700.0, 200.0] "position": [400.0, 200.0]
}, },
{ {
"id": 4, "id": 4,
@ -53,19 +79,10 @@
"parameters": { "parameters": {
"0": 1.0 "0": 1.0
}, },
"position": [1000.0, 100.0] "position": [700.0, 0.0]
}, },
{ {
"id": 5, "id": 5,
"node_type": "Gain",
"name": "Velocity",
"parameters": {
"0": 1.0
},
"position": [1150.0, 100.0]
},
{
"id": 6,
"node_type": "Filter", "node_type": "Filter",
"name": "LP Filter", "name": "LP Filter",
"parameters": { "parameters": {
@ -73,25 +90,37 @@
"1": 1.5, "1": 1.5,
"2": 0.0 "2": 0.0
}, },
"position": [1300.0, 100.0] "position": [900.0, 0.0]
}, },
{ {
"id": 7, "id": 6,
"node_type": "AudioOutput", "node_type": "TemplateOutput",
"name": "Out", "name": "Template Output",
"parameters": {}, "parameters": {},
"position": [1600.0, 100.0] "position": [1100.0, 0.0]
} }
], ],
"connections": [ "connections": [
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 }, { "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": 0, "to_node": 2, "to_port": 0 },
{ "from_node": 1, "from_port": 1, "to_node": 3, "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": 2, "from_port": 0, "to_node": 4, "to_port": 0 },
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 }, { "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": 4, "from_port": 0, "to_node": 5, "to_port": 0 },
{ "from_node": 5, "from_port": 0, "to_node": 6, "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 } ]
}
},
{
"id": 2,
"node_type": "AudioOutput",
"name": "Out",
"parameters": {},
"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 }
] ]
} }