Lightningbeam/daw-backend/src/audio/track.rs

1305 lines
49 KiB
Rust

use super::automation::{AutomationLane, AutomationLaneId, ParameterId};
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};
use super::node_graph::preset::GraphPreset;
use super::pool::AudioClipPool;
use crate::tempo_map::TempoMap;
use crate::time::{Beats, Seconds};
use serde::{Serialize, Deserialize};
use std::collections::{HashMap, HashSet};
/// Track ID type
pub type TrackId = u32;
/// Default function for creating empty AudioGraph during deserialization
fn default_audio_graph() -> AudioGraph {
AudioGraph::new(48000, 8192)
}
/// Type alias for backwards compatibility
pub type Track = AudioTrack;
/// Rendering context that carries timing information through the track hierarchy
///
/// This allows metatracks to transform time for their children (time stretch, offset, etc.)
#[derive(Clone, Copy)]
pub struct RenderContext<'a> {
/// Current playhead position in seconds (in transformed time)
pub playhead_seconds: Seconds,
/// Tempo map for beat ↔ second conversion
pub tempo_map: &'a TempoMap,
/// Audio sample rate
pub sample_rate: u32,
/// Number of channels
pub channels: u32,
/// Size of the buffer being rendered (in interleaved samples)
pub buffer_size: usize,
/// Accumulated time stretch factor (1.0 = normal, 0.5 = half speed, 2.0 = double speed)
pub time_stretch: f32,
/// When true: skip clip event collection; only render instrument state and live MIDI queue.
/// Used after pause/stop to route note-off tails through the normal group hierarchy
/// without re-triggering notes from clips at the paused position.
pub live_only: bool,
}
impl<'a> RenderContext<'a> {
pub fn new(
playhead_seconds: Seconds,
tempo_map: &'a TempoMap,
sample_rate: u32,
channels: u32,
buffer_size: usize,
) -> Self {
Self {
playhead_seconds,
tempo_map,
sample_rate,
channels,
buffer_size,
time_stretch: 1.0,
live_only: false,
}
}
pub fn buffer_duration(&self) -> Seconds {
Seconds(self.buffer_size as f64 / (self.sample_rate as f64 * self.channels as f64))
}
pub fn buffer_end(&self) -> Seconds {
self.playhead_seconds + self.buffer_duration()
}
pub fn playhead_beats(&self) -> Beats {
self.tempo_map.seconds_to_beats(self.playhead_seconds)
}
pub fn buffer_end_beats(&self) -> Beats {
self.tempo_map.seconds_to_beats(self.buffer_end())
}
}
/// Node in the track hierarchy - can be an audio track, MIDI track, or a metatrack
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TrackNode {
Audio(AudioTrack),
Midi(MidiTrack),
Group(Metatrack),
}
impl TrackNode {
/// Get the track ID
pub fn id(&self) -> TrackId {
match self {
TrackNode::Audio(track) => track.id,
TrackNode::Midi(track) => track.id,
TrackNode::Group(group) => group.id,
}
}
/// Get the track name
pub fn name(&self) -> &str {
match self {
TrackNode::Audio(track) => &track.name,
TrackNode::Midi(track) => &track.name,
TrackNode::Group(group) => &group.name,
}
}
/// Get muted state
pub fn is_muted(&self) -> bool {
match self {
TrackNode::Audio(track) => track.muted,
TrackNode::Midi(track) => track.muted,
TrackNode::Group(group) => group.muted,
}
}
/// Get solo state
pub fn is_solo(&self) -> bool {
match self {
TrackNode::Audio(track) => track.solo,
TrackNode::Midi(track) => track.solo,
TrackNode::Group(group) => group.solo,
}
}
/// Set volume
pub fn set_volume(&mut self, volume: f32) {
match self {
TrackNode::Audio(track) => track.set_volume(volume),
TrackNode::Midi(track) => track.set_volume(volume),
TrackNode::Group(group) => group.set_volume(volume),
}
}
/// Set muted state
pub fn set_muted(&mut self, muted: bool) {
match self {
TrackNode::Audio(track) => track.set_muted(muted),
TrackNode::Midi(track) => track.set_muted(muted),
TrackNode::Group(group) => group.set_muted(muted),
}
}
/// Set solo state
pub fn set_solo(&mut self, solo: bool) {
match self {
TrackNode::Audio(track) => track.set_solo(solo),
TrackNode::Midi(track) => track.set_solo(solo),
TrackNode::Group(group) => group.set_solo(solo),
}
}
/// Remove a MIDI clip instance (only works on MIDI tracks)
pub fn remove_midi_clip_instance(&mut self, instance_id: MidiClipInstanceId) {
if let TrackNode::Midi(track) = self {
track.remove_midi_clip_instance(instance_id);
}
}
/// Remove an audio clip instance (only works on audio tracks)
pub fn remove_audio_clip_instance(&mut self, instance_id: AudioClipInstanceId) {
if let TrackNode::Audio(track) = self {
track.remove_audio_clip_instance(instance_id);
}
}
}
/// Metatrack that contains other tracks with time transformation capabilities
#[derive(Debug, Serialize, Deserialize)]
pub struct Metatrack {
pub id: TrackId,
pub name: String,
pub children: Vec<TrackId>,
pub volume: f32,
pub muted: bool,
pub solo: bool,
/// Time stretch factor (0.5 = half speed, 1.0 = normal, 2.0 = double speed)
pub time_stretch: f32,
/// Pitch shift in semitones (for future implementation)
pub pitch_shift: f32,
/// Time offset (shift content forward/backward in time)
pub offset: Seconds,
/// Trim start: offset into the metatrack's internal content
/// Children will see time starting from this point
pub trim_start: Seconds,
/// Trim end: offset into the metatrack's internal content
/// None means no end trim (play until content ends)
pub trim_end: Option<Seconds>,
/// Automation lanes for this metatrack
pub automation_lanes: HashMap<AutomationLaneId, AutomationLane>,
next_automation_id: AutomationLaneId,
/// Audio node graph for effects processing (input → output)
#[serde(skip, default = "default_audio_graph")]
pub audio_graph: AudioGraph,
/// Saved graph preset for serialization
audio_graph_preset: Option<GraphPreset>,
/// True while the mixing graph is still the auto-generated default (no user edits).
/// Used to auto-connect new subtracks and to prompt before loading a preset.
#[serde(default)]
pub graph_is_default: bool,
}
impl Clone for Metatrack {
fn clone(&self) -> Self {
Self {
id: self.id,
name: self.name.clone(),
children: self.children.clone(),
volume: self.volume,
muted: self.muted,
solo: self.solo,
time_stretch: self.time_stretch,
pitch_shift: self.pitch_shift,
offset: self.offset,
trim_start: self.trim_start,
trim_end: self.trim_end,
automation_lanes: self.automation_lanes.clone(),
next_automation_id: self.next_automation_id,
audio_graph: default_audio_graph(), // Create fresh graph, not cloned
audio_graph_preset: self.audio_graph_preset.clone(),
graph_is_default: self.graph_is_default,
}
}
}
impl Metatrack {
/// Create a new metatrack. The mixing graph is set up later via `set_subtrack_graph`
/// once the child track list is known.
pub fn new(id: TrackId, name: String, sample_rate: u32) -> Self {
let default_buffer_size = 8192;
let audio_graph = Self::create_empty_graph(sample_rate, default_buffer_size);
Self {
id,
name,
children: Vec::new(),
volume: 1.0,
muted: false,
solo: false,
time_stretch: 1.0,
pitch_shift: 0.0,
offset: Seconds::ZERO,
trim_start: Seconds::ZERO,
trim_end: None,
automation_lanes: HashMap::new(),
next_automation_id: 0,
audio_graph,
audio_graph_preset: None,
graph_is_default: true,
}
}
/// Minimal graph used before subtracks are known (just an AudioOutput node).
fn create_empty_graph(sample_rate: u32, buffer_size: usize) -> AudioGraph {
let mut graph = AudioGraph::new(sample_rate, buffer_size);
let output_node = Box::new(AudioOutputNode::new("Audio Output"));
let output_id = graph.add_node(output_node);
graph.set_node_position(output_id, 500.0, 150.0);
graph.set_output_node(Some(output_id));
graph
}
/// Build the default subtrack mixing graph: SubtrackInputs → Mixer → Gain → AudioOutput,
/// with an AutomationInput ("Volume", range 0..2) feeding the Gain's CV port.
///
/// Existing Volume keyframes are preserved across rebuilds so that adding/removing
/// a child track doesn't reset the automation.
///
/// `subtracks` is an ordered list of (backend TrackId, display name) for each child.
/// Replaces the current graph and marks `graph_is_default = true`.
pub fn set_subtrack_graph(
&mut self,
subtracks: Vec<(TrackId, String)>,
sample_rate: u32,
buffer_size: usize,
) {
use super::node_graph::nodes::{SubtrackInputsNode, MixerNode, GainNode, AutomationInputNode};
use super::node_graph::nodes::AutomationKeyframe;
use crate::time::Beats;
// Preserve existing Volume keyframes before rebuilding.
let existing_volume_kfs = self.get_volume_automation_keyframes();
let n = subtracks.len();
let mut graph = AudioGraph::new(sample_rate, buffer_size);
// SubtrackInputs node (N outputs, one per child)
let mut inputs_node = SubtrackInputsNode::new("Subtrack Inputs", subtracks);
let subtracks_copy = inputs_node.subtracks().to_vec();
inputs_node.update_subtracks(subtracks_copy, buffer_size);
let inputs_id = graph.add_node(Box::new(inputs_node));
graph.set_node_position(inputs_id, 100.0, 150.0);
// Mixer node
let mixer_node = Box::new(MixerNode::new("Mixer"));
let mixer_id = graph.add_node(mixer_node);
graph.set_node_position(mixer_id, 330.0, 150.0);
// Gain node — group volume control
let gain_id = graph.add_node(Box::new(GainNode::new("Volume")));
graph.set_node_position(gain_id, 520.0, 150.0);
// AutomationInput — drives the Gain's CV port
let mut auto_node = AutomationInputNode::new("Volume CV");
auto_node.set_display_name("Volume".to_string());
auto_node.value_min = 0.0;
auto_node.value_max = 2.0;
auto_node.clear_keyframes();
if existing_volume_kfs.is_empty() {
auto_node.add_keyframe(AutomationKeyframe::new(Beats::ZERO, 1.0));
} else {
for kf in existing_volume_kfs {
auto_node.add_keyframe(kf);
}
}
let auto_id = graph.add_node(Box::new(auto_node));
graph.set_node_position(auto_id, 520.0, 320.0);
// AudioOutput node
let output_node = Box::new(AudioOutputNode::new("Audio Output"));
let output_id = graph.add_node(output_node);
graph.set_node_position(output_id, 720.0, 150.0);
// Connect SubtrackInputs[i] → Mixer[i] for each subtrack
for i in 0..n {
let _ = graph.connect(inputs_id, i, mixer_id, i);
}
let _ = graph.connect(mixer_id, 0, gain_id, 0); // Mixer → Gain audio
let _ = graph.connect(auto_id, 0, gain_id, 1); // AutomationInput → Gain CV
let _ = graph.connect(gain_id, 0, output_id, 0); // Gain → Audio Out
graph.set_output_node(Some(output_id));
self.audio_graph = graph;
self.audio_graph_preset = None;
self.graph_is_default = true;
}
/// Extract Volume AutomationInput keyframes from the current graph (if any),
/// so they can be preserved across `set_subtrack_graph` rebuilds.
fn get_volume_automation_keyframes(&self) -> Vec<super::node_graph::nodes::AutomationKeyframe> {
use super::node_graph::nodes::AutomationInputNode;
for idx in self.audio_graph.node_indices() {
if let Some(node) = self.audio_graph.get_graph_node(idx) {
if node.node.node_type() == "AutomationInput" {
if let Some(auto_node) = node.node.as_any().downcast_ref::<AutomationInputNode>() {
return auto_node.keyframes().to_vec();
}
}
}
}
Vec::new()
}
/// Add a new subtrack port to the existing graph.
///
/// If `graph_is_default`: also connects the new port to a new Mixer input.
/// If the user has modified the graph: just adds the port (unconnected).
pub fn add_subtrack_to_graph(&mut self, track_id: TrackId, name: String, buffer_size: usize) {
use super::node_graph::nodes::SubtrackInputsNode;
// Find SubtrackInputs node index
let si_idx = self.audio_graph.node_indices()
.find(|&idx| self.audio_graph.get_graph_node(idx)
.map(|n| n.node.node_type() == "SubtrackInputs")
.unwrap_or(false));
let si_idx = match si_idx {
Some(idx) => idx,
None => return, // No subtrack graph set up yet
};
// Get current subtrack count (= new port index after adding)
let new_slot = {
let gn = self.audio_graph.get_graph_node_mut(si_idx).unwrap();
let si = gn.node.as_any_mut().downcast_mut::<SubtrackInputsNode>().unwrap();
let mut subtracks = si.subtracks().to_vec();
subtracks.push((track_id, name));
let n = subtracks.len();
si.update_subtracks(subtracks, buffer_size);
// Rebuild output buffers for the new port count
n - 1 // index of the newly added slot
};
// Reallocate GraphNode output buffers to match new port count
self.audio_graph.reallocate_node_output_buffers(si_idx, buffer_size);
if self.graph_is_default {
// Find the Mixer node and connect the new subtrack port to a new Mixer input
let mixer_idx = self.audio_graph.node_indices()
.find(|&idx| self.audio_graph.get_graph_node(idx)
.map(|n| n.node.node_type() == "Mixer")
.unwrap_or(false));
if let Some(mixer_idx) = mixer_idx {
// n_incoming after connecting = new_slot + 1; auto-grow handled by connect()
let _ = self.audio_graph.connect(si_idx, new_slot, mixer_idx, new_slot);
}
}
}
/// Remove a subtrack from the graph (by TrackId).
///
/// Always disconnects any connections from the removed port and removes the port.
/// If `graph_is_default`: also reshuffles Mixer connections to stay compact.
pub fn remove_subtrack_from_graph(&mut self, track_id: TrackId, buffer_size: usize) {
use super::node_graph::nodes::SubtrackInputsNode;
let si_idx = self.audio_graph.node_indices()
.find(|&idx| self.audio_graph.get_graph_node(idx)
.map(|n| n.node.node_type() == "SubtrackInputs")
.unwrap_or(false));
let si_idx = match si_idx {
Some(idx) => idx,
None => return,
};
// Find the slot index for this track
let slot = {
let gn = self.audio_graph.get_graph_node(si_idx).unwrap();
let si = gn.node.as_any().downcast_ref::<SubtrackInputsNode>().unwrap();
si.subtrack_index_for(track_id)
};
let slot = match slot {
Some(s) => s,
None => return,
};
// Remove all connections from this output port
self.audio_graph.disconnect_output_port(si_idx, slot);
// Update the SubtrackInputsNode's subtrack list
{
let gn = self.audio_graph.get_graph_node_mut(si_idx).unwrap();
let si = gn.node.as_any_mut().downcast_mut::<SubtrackInputsNode>().unwrap();
let mut subtracks = si.subtracks().to_vec();
subtracks.remove(slot);
si.update_subtracks(subtracks, buffer_size);
}
self.audio_graph.reallocate_node_output_buffers(si_idx, buffer_size);
if self.graph_is_default {
// Rebuild default Mixer connections (they've shifted after removal)
let mixer_idx = self.audio_graph.node_indices()
.find(|&idx| self.audio_graph.get_graph_node(idx)
.map(|n| n.node.node_type() == "Mixer")
.unwrap_or(false));
if let Some(mixer_idx) = mixer_idx {
// Clear all connections TO mixer
self.audio_graph.disconnect_all_inputs(mixer_idx);
// Get new subtrack count
let n = {
let gn = self.audio_graph.get_graph_node(si_idx).unwrap();
gn.node.as_any().downcast_ref::<SubtrackInputsNode>().unwrap().num_subtracks()
};
// Resize mixer and reconnect
{
let gn = self.audio_graph.get_graph_node_mut(mixer_idx).unwrap();
let mixer = gn.node.as_any_mut().downcast_mut::<super::node_graph::nodes::MixerNode>().unwrap();
mixer.resize(n + 1);
}
for i in 0..n {
let _ = self.audio_graph.connect(si_idx, i, mixer_idx, i);
}
}
}
}
/// Return the current ordered subtrack list from SubtrackInputsNode, or empty vec if none.
pub fn current_subtracks(&self) -> Vec<(TrackId, String)> {
use super::node_graph::nodes::SubtrackInputsNode;
for idx in self.audio_graph.node_indices().collect::<Vec<_>>() {
if let Some(gn) = self.audio_graph.get_graph_node(idx) {
if let Some(si) = gn.node.as_any().downcast_ref::<SubtrackInputsNode>() {
return si.subtracks().to_vec();
}
}
}
Vec::new()
}
/// Prepare for serialization by saving the audio graph as a preset
pub fn prepare_for_save(&mut self) {
self.audio_graph_preset = Some(self.audio_graph.to_preset("Metatrack Graph"));
}
/// Rebuild the audio graph from preset after deserialization.
///
/// After loading, the caller must call `update_subtrack_ids` to re-associate
/// backend TrackIds with the SubtrackInputsNode's port slots.
pub fn rebuild_audio_graph(&mut self, sample_rate: u32, buffer_size: usize) -> Result<(), String> {
if let Some(preset) = &self.audio_graph_preset {
if !preset.nodes.is_empty() && preset.output_node.is_some() {
self.audio_graph = AudioGraph::from_preset(preset, sample_rate, buffer_size, None, None)?;
// graph_is_default remains as serialized (false for user-modified graphs)
} else {
self.audio_graph = Self::create_empty_graph(sample_rate, buffer_size);
self.graph_is_default = true;
}
} else {
self.audio_graph = Self::create_empty_graph(sample_rate, buffer_size);
self.graph_is_default = true;
}
Ok(())
}
/// Re-associate backend TrackIds with the SubtrackInputsNode's port slots after reload.
///
/// The preset stores placeholder TrackId=0 entries; this call fills in the real IDs.
pub fn update_subtrack_ids(&mut self, subtracks: Vec<(TrackId, String)>, buffer_size: usize) {
use super::node_graph::nodes::SubtrackInputsNode;
for idx in self.audio_graph.node_indices().collect::<Vec<_>>() {
if let Some(gn) = self.audio_graph.get_graph_node_mut(idx) {
if let Some(si) = gn.node.as_any_mut().downcast_mut::<SubtrackInputsNode>() {
si.update_subtracks(subtracks, buffer_size);
return;
}
}
}
}
/// 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: Beats) -> (f32, f32, Seconds) {
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 = Seconds(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) {
self.children.push(track_id);
}
}
/// Remove a child track from this group
pub fn remove_child(&mut self, track_id: TrackId) {
self.children.retain(|&id| id != track_id);
}
/// Set group volume
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.max(0.0);
}
/// Set mute state
pub fn set_muted(&mut self, muted: bool) {
self.muted = muted;
}
/// Set solo state
pub fn set_solo(&mut self, solo: bool) {
self.solo = solo;
}
/// Check if this group should be audible given the solo state
pub fn is_active(&self, any_solo: bool) -> bool {
!self.muted && (!any_solo || self.solo)
}
/// Check whether this metatrack should produce audio at the given parent time.
/// Returns false if the playhead is outside the trim window.
pub fn is_active_at_time(&self, parent_playhead: Seconds) -> bool {
let local_time = (parent_playhead - self.offset) * self.time_stretch as f64;
if local_time < self.trim_start {
return false;
}
if let Some(end) = self.trim_end {
if local_time >= end {
return false;
}
}
true
}
/// Transform a render context for this metatrack's children
///
/// Applies time stretching, offset, and trim transformations.
/// Time stretch affects how fast content plays: 0.5 = half speed, 2.0 = double speed
/// Offset shifts content forward/backward in time
/// Trim start offsets into the internal content
pub fn transform_context<'a>(&self, ctx: RenderContext<'a>) -> RenderContext<'a> {
let mut transformed = ctx;
// Apply transformations in order:
// 1. First, subtract offset (positive offset = content appears later)
// At parent time 0.0s with offset=2.0s, child sees -2.0s (before content starts)
// At parent time 2.0s with offset=2.0s, child sees 0.0s (content starts)
let adjusted_playhead = transformed.playhead_seconds - self.offset;
// 2. Then apply time stretch (< 1.0 = slower/half speed, > 1.0 = faster/double speed)
// With stretch=0.5, when parent time is 2.0s, child reads from 1.0s (plays slower, pitches down)
// With stretch=2.0, when parent time is 2.0s, child reads from 4.0s (plays faster, pitches up)
// Note: This creates pitch shift as well - true time stretching would require resampling
let stretched = adjusted_playhead * self.time_stretch as f64;
// 3. Add trim_start so children see time starting from the trim point
// If trim_start=2.0s, children start seeing time 2.0s when parent reaches offset
transformed.playhead_seconds = stretched + self.trim_start;
// Accumulate time stretch for nested metatracks
transformed.time_stretch *= self.time_stretch;
transformed
}
}
/// MIDI track with MIDI clip instances and a node-based instrument
#[derive(Debug, Serialize, Deserialize)]
pub struct MidiTrack {
pub id: TrackId,
pub name: String,
/// Clip instances placed on this track (reference clips in the MidiClipPool)
pub clip_instances: Vec<MidiClipInstance>,
/// Serialized instrument graph (used for save/load)
#[serde(default, skip_serializing_if = "Option::is_none")]
instrument_graph_preset: Option<GraphPreset>,
/// Runtime instrument graph (rebuilt from preset on load)
#[serde(skip, default = "default_audio_graph")]
pub instrument_graph: AudioGraph,
pub volume: f32,
pub muted: bool,
pub solo: bool,
/// Automation lanes for this track
pub automation_lanes: HashMap<AutomationLaneId, AutomationLane>,
next_automation_id: AutomationLaneId,
/// Queue for live MIDI input (virtual keyboard, MIDI controllers)
#[serde(skip)]
live_midi_queue: Vec<MidiEvent>,
/// Clip instances that were active (overlapping playhead) in the previous render buffer.
/// Used to detect when the playhead exits a clip, so we can send all-notes-off.
#[serde(skip)]
prev_active_instances: HashSet<MidiClipInstanceId>,
/// Peak level of last render() call (for VU metering)
#[serde(skip, default)]
pub peak_level: f32,
/// True while the instrument graph is still the auto-generated default (no user edits).
/// Used to prompt before loading a preset.
#[serde(default)]
pub graph_is_default: bool,
}
impl Clone for MidiTrack {
fn clone(&self) -> Self {
Self {
id: self.id,
name: self.name.clone(),
clip_instances: self.clip_instances.clone(),
instrument_graph_preset: self.instrument_graph_preset.clone(),
instrument_graph: default_audio_graph(), // Create fresh graph, not cloned
volume: self.volume,
muted: self.muted,
solo: self.solo,
automation_lanes: self.automation_lanes.clone(),
next_automation_id: self.next_automation_id,
live_midi_queue: Vec::new(), // Don't clone live MIDI queue
prev_active_instances: HashSet::new(),
peak_level: 0.0,
graph_is_default: self.graph_is_default,
}
}
}
impl MidiTrack {
/// Create a new MIDI track with default settings
pub fn new(id: TrackId, name: String, sample_rate: u32) -> Self {
// Use a large buffer size that can accommodate any callback
let default_buffer_size = 8192;
// 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,
name,
clip_instances: Vec::new(),
instrument_graph_preset: None,
instrument_graph,
volume: 1.0,
muted: false,
solo: false,
automation_lanes: HashMap::new(),
next_automation_id: 0,
live_midi_queue: Vec::new(),
prev_active_instances: HashSet::new(),
peak_level: 0.0,
graph_is_default: true,
}
}
/// Prepare for serialization by saving the instrument graph as a preset
pub fn prepare_for_save(&mut self) {
self.instrument_graph_preset = Some(self.instrument_graph.to_preset("Instrument Graph"));
}
/// Rebuild the instrument graph from preset after deserialization
pub fn rebuild_audio_graph(&mut self, sample_rate: u32, buffer_size: usize) -> Result<(), String> {
if let Some(preset) = &self.instrument_graph_preset {
self.instrument_graph = AudioGraph::from_preset(preset, sample_rate, buffer_size, None, None)?;
} else {
// No preset - create default graph
self.instrument_graph = AudioGraph::new(sample_rate, buffer_size);
}
Ok(())
}
/// 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 a MIDI clip instance to this track
pub fn add_clip_instance(&mut self, instance: MidiClipInstance) {
self.clip_instances.push(instance);
}
/// Remove a MIDI clip instance from this track by instance ID (for undo/redo support)
pub fn remove_midi_clip_instance(&mut self, instance_id: MidiClipInstanceId) {
self.clip_instances.retain(|instance| instance.id != instance_id);
}
/// Set track volume
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.max(0.0);
}
/// Set mute state
pub fn set_muted(&mut self, muted: bool) {
self.muted = muted;
}
/// Set solo state
pub fn set_solo(&mut self, solo: bool) {
self.solo = solo;
}
/// Check if this track should be audible given the solo state
pub fn is_active(&self, any_solo: bool) -> bool {
!self.muted && (!any_solo || self.solo)
}
/// Stop all currently playing notes on this track's instrument
/// Note: With node-based instruments, stopping is handled by ceasing MIDI input
pub fn stop_all_notes(&mut self) {
// Send note-off for all 128 possible MIDI notes to silence the instrument
let mut note_offs = Vec::new();
for note in 0..128 {
note_offs.push(MidiEvent::note_off(Beats::ZERO, 0, note, 0));
}
// Create a silent buffer to process the note-offs
let buffer_size = 512 * 2; // stereo
let mut silent_buffer = vec![0.0f32; buffer_size];
self.instrument_graph.process(&mut silent_buffer, &note_offs, Beats::ZERO);
}
/// Queue a live MIDI event (from virtual keyboard or MIDI controller)
pub fn queue_live_midi(&mut self, event: MidiEvent) {
self.live_midi_queue.push(event);
}
/// Clear the live MIDI queue
pub fn clear_live_midi_queue(&mut self) {
self.live_midi_queue.clear();
}
/// Render this MIDI track into the output buffer.
///
/// When `ctx.live_only` is true, clip event collection is skipped and only the live MIDI
/// queue is processed. This lets note-off tails (and live keyboard input) route through
/// the normal group hierarchy without re-triggering notes from clips at the paused position.
pub fn render(
&mut self,
output: &mut [f32],
midi_pool: &MidiClipPool,
ctx: RenderContext,
) {
let mut midi_events = Vec::new();
if !ctx.live_only {
let playhead_beats = ctx.playhead_beats();
let buffer_end_beats = ctx.buffer_end_beats();
// Collect MIDI events from all clip instances that overlap with current beat range
let mut currently_active = HashSet::new();
for instance in &self.clip_instances {
if instance.overlaps_range(playhead_beats, buffer_end_beats) {
currently_active.insert(instance.id);
}
if let Some(clip) = midi_pool.get_clip(instance.clip_id) {
let events = instance.get_events_in_range(clip, playhead_beats, buffer_end_beats);
midi_events.extend(events);
}
}
// Send all-notes-off for clip instances that just became inactive
for prev_id in &self.prev_active_instances {
if !currently_active.contains(prev_id) {
for note in 0..128u8 {
midi_events.push(MidiEvent::note_off(playhead_beats, 0, note, 0));
}
break;
}
}
self.prev_active_instances = currently_active;
}
// Add live MIDI events (from virtual keyboard or MIDI controllers)
midi_events.extend(self.live_midi_queue.drain(..));
// Generate audio using instrument graph
self.instrument_graph.process(output, &midi_events, ctx.playhead_beats());
// Evaluate and apply automation (skip automation in live_only mode — no playhead to evaluate at)
let effective_volume = if ctx.live_only { self.volume } else { self.evaluate_automation_at_time(ctx.playhead_beats()) };
// Apply track volume
for sample in output.iter_mut() {
*sample *= effective_volume;
}
}
/// Evaluate automation at a specific time and return the effective volume
fn evaluate_automation_at_time(&self, time: Beats) -> 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 audio clip instances
#[derive(Debug, Serialize, Deserialize)]
pub struct AudioTrack {
pub id: TrackId,
pub name: String,
/// Audio clip instances (reference content in the AudioClipPool)
pub clips: Vec<AudioClipInstance>,
pub volume: f32,
pub muted: bool,
pub solo: bool,
/// Automation lanes for this track
pub automation_lanes: HashMap<AutomationLaneId, AutomationLane>,
next_automation_id: AutomationLaneId,
/// Serialized effects graph (used for save/load)
#[serde(default, skip_serializing_if = "Option::is_none")]
effects_graph_preset: Option<GraphPreset>,
/// Runtime effects processing graph (rebuilt from preset on load)
#[serde(skip, default = "default_audio_graph")]
pub effects_graph: AudioGraph,
/// Pre-allocated buffer for clip rendering (avoids heap allocation per callback)
#[serde(skip, default)]
clip_render_buffer: Vec<f32>,
/// Peak level of last render() call (for VU metering)
#[serde(skip, default)]
pub peak_level: f32,
/// True while the effects graph is still the auto-generated default (no user edits).
/// Used to prompt before loading a preset.
#[serde(default)]
pub graph_is_default: bool,
}
impl Clone for AudioTrack {
fn clone(&self) -> Self {
Self {
id: self.id,
name: self.name.clone(),
clips: self.clips.clone(),
volume: self.volume,
muted: self.muted,
solo: self.solo,
automation_lanes: self.automation_lanes.clone(),
next_automation_id: self.next_automation_id,
effects_graph_preset: self.effects_graph_preset.clone(),
effects_graph: default_audio_graph(), // Create fresh graph, not cloned
clip_render_buffer: Vec::new(),
peak_level: 0.0,
graph_is_default: self.graph_is_default,
}
}
}
impl AudioTrack {
/// Create a new audio track with default settings
pub fn new(id: TrackId, name: String, sample_rate: u32) -> Self {
// Use a large buffer size that can accommodate any callback
let default_buffer_size = 8192;
// Create the effects graph with default AudioInput -> AudioOutput chain
let mut effects_graph = AudioGraph::new(sample_rate, default_buffer_size);
// Add AudioInput node
let input_node = Box::new(AudioInputNode::new("Audio Input"));
let input_id = effects_graph.add_node(input_node);
// Set position for AudioInput (left side, similar to instrument preset spacing)
effects_graph.set_node_position(input_id, 100.0, 150.0);
// Add AudioOutput node
let output_node = Box::new(AudioOutputNode::new("Audio Output"));
let output_id = effects_graph.add_node(output_node);
// Set position for AudioOutput (right side, spaced apart)
effects_graph.set_node_position(output_id, 500.0, 150.0);
// Connect AudioInput -> AudioOutput
let _ = effects_graph.connect(input_id, 0, output_id, 0);
// Set the AudioOutput node as the graph's output
effects_graph.set_output_node(Some(output_id));
Self {
id,
name,
clips: Vec::new(),
volume: 1.0,
muted: false,
solo: false,
automation_lanes: HashMap::new(),
next_automation_id: 0,
effects_graph_preset: None,
effects_graph,
clip_render_buffer: Vec::new(),
peak_level: 0.0,
graph_is_default: true,
}
}
/// Prepare for serialization by saving the effects graph as a preset
pub fn prepare_for_save(&mut self) {
self.effects_graph_preset = Some(self.effects_graph.to_preset("Effects Graph"));
}
/// Rebuild the effects graph from preset after deserialization
pub fn rebuild_audio_graph(&mut self, sample_rate: u32, buffer_size: usize) -> Result<(), String> {
if let Some(preset) = &self.effects_graph_preset {
// Check if preset is empty or missing required nodes
let has_nodes = !preset.nodes.is_empty();
let has_output = preset.output_node.is_some();
if has_nodes && has_output {
// Valid preset - rebuild from it
self.effects_graph = AudioGraph::from_preset(preset, sample_rate, buffer_size, None, None)?;
} else {
// Empty or invalid preset - create default graph
self.effects_graph = Self::create_default_graph(sample_rate, buffer_size);
}
} else {
// No preset - create default graph
self.effects_graph = Self::create_default_graph(sample_rate, buffer_size);
}
Ok(())
}
/// Create a default effects graph with AudioInput -> AudioOutput
fn create_default_graph(sample_rate: u32, buffer_size: usize) -> AudioGraph {
let mut effects_graph = AudioGraph::new(sample_rate, buffer_size);
// Add AudioInput node
let input_node = Box::new(AudioInputNode::new("Audio Input"));
let input_id = effects_graph.add_node(input_node);
effects_graph.set_node_position(input_id, 100.0, 150.0);
// Add AudioOutput node
let output_node = Box::new(AudioOutputNode::new("Audio Output"));
let output_id = effects_graph.add_node(output_node);
effects_graph.set_node_position(output_id, 500.0, 150.0);
// Connect AudioInput -> AudioOutput
let _ = effects_graph.connect(input_id, 0, output_id, 0);
// Set the AudioOutput node as the graph's output
effects_graph.set_output_node(Some(output_id));
effects_graph
}
/// 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 audio clip instance to this track
pub fn add_clip(&mut self, clip: AudioClipInstance) {
self.clips.push(clip);
}
/// Remove an audio clip instance from this track by instance ID (for undo/redo support)
pub fn remove_audio_clip_instance(&mut self, instance_id: AudioClipInstanceId) {
self.clips.retain(|instance| instance.id != instance_id);
}
/// Set track volume (0.0 = silence, 1.0 = unity gain, >1.0 = amplification)
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.max(0.0);
}
/// Set mute state
pub fn set_muted(&mut self, muted: bool) {
self.muted = muted;
}
/// Set solo state
pub fn set_solo(&mut self, solo: bool) {
self.solo = solo;
}
/// Check if this track should be audible given the solo state of all tracks
pub fn is_active(&self, any_solo: bool) -> bool {
!self.muted && (!any_solo || self.solo)
}
/// Render this track into the output buffer at a given timeline position
/// Returns the number of samples actually rendered
pub fn render(
&mut self,
output: &mut [f32],
pool: &AudioClipPool,
ctx: RenderContext<'_>,
) -> usize {
let buffer_end = ctx.buffer_end();
// Split borrow: take clip_render_buffer out to avoid borrow conflict with &self methods
let mut clip_buffer = std::mem::take(&mut self.clip_render_buffer);
clip_buffer.resize(output.len(), 0.0);
clip_buffer.fill(0.0);
let mut rendered = 0;
// Render all active clip instances into the buffer
for clip in &self.clips {
if clip.external_start_secs(ctx.tempo_map) < buffer_end && clip.external_end_secs(ctx.tempo_map) > ctx.playhead_seconds {
rendered += self.render_clip(clip, &mut clip_buffer, pool, ctx);
}
}
// Find and inject audio into the AudioInputNode
let node_indices: Vec<_> = self.effects_graph.node_indices().collect();
for node_idx in node_indices {
if let Some(graph_node) = self.effects_graph.get_graph_node_mut(node_idx) {
if graph_node.node.node_type() == "AudioInput" {
if let Some(input_node) = graph_node.node.as_any_mut().downcast_mut::<AudioInputNode>() {
input_node.inject_audio(&clip_buffer);
break;
}
}
}
}
// Process through the effects graph (this will write to output buffer)
self.effects_graph.process(output, &[], ctx.playhead_beats());
// Put the buffer back for reuse next callback
self.clip_render_buffer = clip_buffer;
// Evaluate and apply automation
let effective_volume = self.evaluate_automation_at_time(ctx.playhead_beats());
// Apply track volume
for sample in output.iter_mut() {
*sample *= effective_volume;
}
rendered
}
/// Evaluate automation at a specific time and return the effective volume
fn evaluate_automation_at_time(&self, time: Beats) -> 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 audio clip instance into the output buffer
/// Handles looping when external_duration > internal_duration
fn render_clip(
&self,
clip: &AudioClipInstance,
output: &mut [f32],
pool: &AudioClipPool,
ctx: RenderContext<'_>,
) -> usize {
let playhead = ctx.playhead_seconds;
let buffer_end = ctx.buffer_end();
let tempo_map = ctx.tempo_map;
let sample_rate = ctx.sample_rate;
let channels = ctx.channels;
// Determine the time range we need to render (intersection of buffer and clip external bounds)
let render_start = playhead.max(clip.external_start_secs(tempo_map));
let render_end = buffer_end.min(clip.external_end_secs(tempo_map));
if render_start >= render_end {
return 0;
}
let internal_duration = clip.internal_duration();
if internal_duration <= Seconds::ZERO {
return 0;
}
let combined_gain = clip.gain;
let mut total_rendered = 0;
let samples_per_second = sample_rate as f64 * channels as f64;
let output_start_offset = ((render_start - playhead).0 * samples_per_second + 0.5) as usize;
let output_end_offset = ((render_end - playhead).0 * samples_per_second + 0.5) as usize;
if output_end_offset > output.len() || output_start_offset > output.len() {
return 0;
}
if !clip.is_looping(tempo_map) {
let content_start = clip.get_content_position(render_start, tempo_map).unwrap_or(clip.internal_start);
let output_len = output.len();
let output_slice = &mut output[output_start_offset..output_end_offset.min(output_len)];
total_rendered = pool.render_from_file(
clip.audio_pool_index,
output_slice,
content_start,
combined_gain,
sample_rate,
channels,
clip.read_ahead.as_deref(),
);
} else {
// Looping case: handle wrap-around at loop boundaries
let mut timeline_pos = render_start;
let mut output_offset = output_start_offset;
while timeline_pos < render_end && output_offset < output.len() {
let relative_pos = timeline_pos - clip.external_start_secs(tempo_map);
let loop_offset = relative_pos.0 % internal_duration.0;
let content_pos = clip.internal_start + Seconds(loop_offset);
let time_to_loop_end = Seconds(internal_duration.0 - loop_offset);
let time_to_render_end = render_end - timeline_pos;
let chunk_duration = time_to_loop_end.min(time_to_render_end);
let chunk_samples = (chunk_duration.0 * samples_per_second) as usize;
let chunk_samples = chunk_samples.min(output.len() - output_offset);
if chunk_samples == 0 {
break;
}
let output_slice = &mut output[output_offset..output_offset + chunk_samples];
let rendered = pool.render_from_file(
clip.audio_pool_index,
output_slice,
content_pos,
combined_gain,
sample_rate,
channels,
clip.read_ahead.as_deref(),
);
total_rendered += rendered;
output_offset += chunk_samples;
timeline_pos = timeline_pos + chunk_duration;
}
}
total_rendered
}
}