make default voice polyphonic
This commit is contained in:
parent
6c4cc62098
commit
9db34daf85
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue