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

621 lines
22 KiB
Rust

use super::buffer_pool::BufferPool;
use super::clip::{AudioClipInstanceId, Clip};
use super::midi::{MidiClip, MidiClipId, MidiClipInstance, MidiClipInstanceId, MidiEvent};
use super::midi_pool::MidiClipPool;
use super::pool::AudioClipPool;
use super::track::{AudioTrack, Metatrack, MidiTrack, RenderContext, TrackId, TrackNode};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
/// Project manages the hierarchical track structure and clip pools
///
/// Tracks are stored in a flat HashMap but can be organized into groups,
/// forming a tree structure. Groups render their children recursively.
///
/// Clip content is stored in pools (MidiClipPool), while tracks store
/// clip instances that reference the pool content.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
tracks: HashMap<TrackId, TrackNode>,
next_track_id: TrackId,
root_tracks: Vec<TrackId>, // Top-level tracks (not in any group)
sample_rate: u32, // System sample rate
/// Pool for MIDI clip content
pub midi_clip_pool: MidiClipPool,
/// Next MIDI clip instance ID (for generating unique IDs)
next_midi_clip_instance_id: MidiClipInstanceId,
}
impl Project {
/// Create a new empty project
pub fn new(sample_rate: u32) -> Self {
Self {
tracks: HashMap::new(),
next_track_id: 0,
root_tracks: Vec::new(),
sample_rate,
midi_clip_pool: MidiClipPool::new(),
next_midi_clip_instance_id: 1,
}
}
/// Generate a new unique track ID
fn next_id(&mut self) -> TrackId {
let id = self.next_track_id;
self.next_track_id += 1;
id
}
/// Add an audio track to the project
///
/// # Arguments
/// * `name` - Track name
/// * `parent_id` - Optional parent group ID
///
/// # Returns
/// The new track's ID
pub fn add_audio_track(&mut self, name: String, parent_id: Option<TrackId>) -> TrackId {
let id = self.next_id();
let track = AudioTrack::new(id, name, self.sample_rate);
self.tracks.insert(id, TrackNode::Audio(track));
if let Some(parent) = parent_id {
// Add to parent group
if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&parent) {
group.add_child(id);
}
} else {
// Add to root level
self.root_tracks.push(id);
}
id
}
/// Add a group track to the project
///
/// # Arguments
/// * `name` - Group name
/// * `parent_id` - Optional parent group ID
///
/// # Returns
/// The new group's ID
pub fn add_group_track(&mut self, name: String, parent_id: Option<TrackId>) -> TrackId {
let id = self.next_id();
let group = Metatrack::new(id, name);
self.tracks.insert(id, TrackNode::Group(group));
if let Some(parent) = parent_id {
// Add to parent group
if let Some(TrackNode::Group(parent_group)) = self.tracks.get_mut(&parent) {
parent_group.add_child(id);
}
} else {
// Add to root level
self.root_tracks.push(id);
}
id
}
/// Add a MIDI track to the project
///
/// # Arguments
/// * `name` - Track name
/// * `parent_id` - Optional parent group ID
///
/// # Returns
/// The new track's ID
pub fn add_midi_track(&mut self, name: String, parent_id: Option<TrackId>) -> TrackId {
let id = self.next_id();
let track = MidiTrack::new(id, name, self.sample_rate);
self.tracks.insert(id, TrackNode::Midi(track));
if let Some(parent) = parent_id {
// Add to parent group
if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&parent) {
group.add_child(id);
}
} else {
// Add to root level
self.root_tracks.push(id);
}
id
}
/// Remove a track from the project
///
/// If the track is a group, all children are moved to the parent (or root)
pub fn remove_track(&mut self, track_id: TrackId) {
if let Some(node) = self.tracks.remove(&track_id) {
// If it's a group, handle its children
if let TrackNode::Group(group) = node {
// Find the parent of this group
let parent_id = self.find_parent(track_id);
// Move children to parent or root
for child_id in group.children {
if let Some(parent) = parent_id {
if let Some(TrackNode::Group(parent_group)) = self.tracks.get_mut(&parent) {
parent_group.add_child(child_id);
}
} else {
self.root_tracks.push(child_id);
}
}
}
// Remove from parent or root
if let Some(parent_id) = self.find_parent(track_id) {
if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&parent_id) {
parent.remove_child(track_id);
}
} else {
self.root_tracks.retain(|&id| id != track_id);
}
}
}
/// Find the parent group of a track
fn find_parent(&self, track_id: TrackId) -> Option<TrackId> {
for (id, node) in &self.tracks {
if let TrackNode::Group(group) = node {
if group.children.contains(&track_id) {
return Some(*id);
}
}
}
None
}
/// Move a track to a different group
pub fn move_to_group(&mut self, track_id: TrackId, new_parent_id: TrackId) {
// First remove from current parent
if let Some(old_parent_id) = self.find_parent(track_id) {
if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&old_parent_id) {
parent.remove_child(track_id);
}
} else {
// Remove from root
self.root_tracks.retain(|&id| id != track_id);
}
// Add to new parent
if let Some(TrackNode::Group(new_parent)) = self.tracks.get_mut(&new_parent_id) {
new_parent.add_child(track_id);
}
}
/// Move a track to the root level (remove from any group)
pub fn move_to_root(&mut self, track_id: TrackId) {
// Remove from current parent if any
if let Some(parent_id) = self.find_parent(track_id) {
if let Some(TrackNode::Group(parent)) = self.tracks.get_mut(&parent_id) {
parent.remove_child(track_id);
}
// Add to root if not already there
if !self.root_tracks.contains(&track_id) {
self.root_tracks.push(track_id);
}
}
}
/// Get a reference to a track node
pub fn get_track(&self, track_id: TrackId) -> Option<&TrackNode> {
self.tracks.get(&track_id)
}
/// Get a mutable reference to a track node
pub fn get_track_mut(&mut self, track_id: TrackId) -> Option<&mut TrackNode> {
self.tracks.get_mut(&track_id)
}
/// Get oscilloscope data from a node in a track's graph
pub fn get_oscilloscope_data(&self, track_id: TrackId, node_id: u32, sample_count: usize) -> Option<(Vec<f32>, Vec<f32>)> {
if let Some(TrackNode::Midi(track)) = self.tracks.get(&track_id) {
let graph = &track.instrument_graph;
let node_idx = petgraph::stable_graph::NodeIndex::new(node_id as usize);
// Get audio data
let audio = graph.get_oscilloscope_data(node_idx, sample_count)?;
// Get CV data (may be empty if no CV input or not an oscilloscope node)
let cv = graph.get_oscilloscope_cv_data(node_idx, sample_count).unwrap_or_default();
return Some((audio, cv));
}
None
}
/// Get oscilloscope data from a node inside a VoiceAllocator's best voice
pub fn get_voice_oscilloscope_data(&self, track_id: TrackId, va_node_id: u32, inner_node_id: u32, sample_count: usize) -> Option<(Vec<f32>, Vec<f32>)> {
if let Some(TrackNode::Midi(track)) = self.tracks.get(&track_id) {
let graph = &track.instrument_graph;
let va_idx = petgraph::stable_graph::NodeIndex::new(va_node_id as usize);
let node = graph.get_node(va_idx)?;
let va = node.as_any().downcast_ref::<crate::audio::node_graph::nodes::VoiceAllocatorNode>()?;
return va.get_voice_oscilloscope_data(inner_node_id, sample_count);
}
None
}
/// Get all root-level track IDs
pub fn root_tracks(&self) -> &[TrackId] {
&self.root_tracks
}
/// Get the number of tracks in the project
pub fn track_count(&self) -> usize {
self.tracks.len()
}
/// Check if any track is soloed
pub fn any_solo(&self) -> bool {
self.tracks.values().any(|node| node.is_solo())
}
/// Add a clip to an audio track
pub fn add_clip(&mut self, track_id: TrackId, clip: Clip) -> Result<AudioClipInstanceId, &'static str> {
if let Some(TrackNode::Audio(track)) = self.tracks.get_mut(&track_id) {
let instance_id = clip.id;
track.add_clip(clip);
Ok(instance_id)
} else {
Err("Track not found or is not an audio track")
}
}
/// Add a MIDI clip instance to a MIDI track
/// The clip content should already exist in the midi_clip_pool
pub fn add_midi_clip_instance(&mut self, track_id: TrackId, instance: MidiClipInstance) -> Result<(), &'static str> {
if let Some(TrackNode::Midi(track)) = self.tracks.get_mut(&track_id) {
track.add_clip_instance(instance);
Ok(())
} else {
Err("Track not found or is not a MIDI track")
}
}
/// Create a new MIDI clip in the pool and add an instance to a track
/// Returns (clip_id, instance_id) on success
pub fn create_midi_clip_with_instance(
&mut self,
track_id: TrackId,
events: Vec<MidiEvent>,
duration: f64,
name: String,
external_start: f64,
) -> Result<(MidiClipId, MidiClipInstanceId), &'static str> {
// Verify track exists and is a MIDI track
if !matches!(self.tracks.get(&track_id), Some(TrackNode::Midi(_))) {
return Err("Track not found or is not a MIDI track");
}
// Create clip in pool
let clip_id = self.midi_clip_pool.add_clip(events, duration, name);
// Create instance
let instance_id = self.next_midi_clip_instance_id;
self.next_midi_clip_instance_id += 1;
let instance = MidiClipInstance::from_full_clip(instance_id, clip_id, duration, external_start);
// Add instance to track
if let Some(TrackNode::Midi(track)) = self.tracks.get_mut(&track_id) {
track.add_clip_instance(instance);
}
Ok((clip_id, instance_id))
}
/// Generate a new unique MIDI clip instance ID
pub fn next_midi_clip_instance_id(&mut self) -> MidiClipInstanceId {
let id = self.next_midi_clip_instance_id;
self.next_midi_clip_instance_id += 1;
id
}
/// Legacy method for backwards compatibility - creates clip and instance from old MidiClip format
pub fn add_midi_clip(&mut self, track_id: TrackId, clip: MidiClip) -> Result<MidiClipInstanceId, &'static str> {
self.add_midi_clip_at(track_id, clip, 0.0)
}
/// Add a MIDI clip to the pool and create an instance at the given timeline position
pub fn add_midi_clip_at(&mut self, track_id: TrackId, clip: MidiClip, start_time: f64) -> Result<MidiClipInstanceId, &'static str> {
// Add the clip to the pool (it already has events and duration)
let duration = clip.duration;
let clip_id = clip.id;
self.midi_clip_pool.add_existing_clip(clip);
// Create an instance that uses the full clip at the given position
let instance_id = self.next_midi_clip_instance_id();
let instance = MidiClipInstance::from_full_clip(instance_id, clip_id, duration, start_time);
self.add_midi_clip_instance(track_id, instance)?;
Ok(instance_id)
}
/// Remove a MIDI clip instance from a track (for undo/redo support)
pub fn remove_midi_clip(&mut self, track_id: TrackId, instance_id: MidiClipInstanceId) -> Result<(), &'static str> {
if let Some(track) = self.get_track_mut(track_id) {
track.remove_midi_clip_instance(instance_id);
Ok(())
} else {
Err("Track not found")
}
}
/// Remove an audio clip instance from a track (for undo/redo support)
pub fn remove_audio_clip(&mut self, track_id: TrackId, instance_id: AudioClipInstanceId) -> Result<(), &'static str> {
if let Some(track) = self.get_track_mut(track_id) {
track.remove_audio_clip_instance(instance_id);
Ok(())
} else {
Err("Track not found")
}
}
/// Render all root tracks into the output buffer
pub fn render(
&mut self,
output: &mut [f32],
audio_pool: &AudioClipPool,
buffer_pool: &mut BufferPool,
playhead_seconds: f64,
sample_rate: u32,
channels: u32,
) {
output.fill(0.0);
let any_solo = self.any_solo();
// Create initial render context
let ctx = RenderContext::new(
playhead_seconds,
sample_rate,
channels,
output.len(),
);
// Render each root track (index-based to avoid clone)
for i in 0..self.root_tracks.len() {
let track_id = self.root_tracks[i];
self.render_track(
track_id,
output,
audio_pool,
buffer_pool,
ctx,
any_solo,
false, // root tracks are not inside a soloed parent
);
}
}
/// Recursively render a track (audio or group) into the output buffer
fn render_track(
&mut self,
track_id: TrackId,
output: &mut [f32],
audio_pool: &AudioClipPool,
buffer_pool: &mut BufferPool,
ctx: RenderContext,
any_solo: bool,
parent_is_soloed: bool,
) {
// Check if track should be rendered based on mute/solo
let should_render = match self.tracks.get(&track_id) {
Some(TrackNode::Audio(track)) => {
// If parent is soloed, only check mute state
// Otherwise, check normal solo logic
if parent_is_soloed {
!track.muted
} else {
track.is_active(any_solo)
}
}
Some(TrackNode::Midi(track)) => {
// Same logic for MIDI tracks
if parent_is_soloed {
!track.muted
} else {
track.is_active(any_solo)
}
}
Some(TrackNode::Group(group)) => {
// Same logic for groups
if parent_is_soloed {
!group.muted
} else {
group.is_active(any_solo)
}
}
None => return,
};
if !should_render {
return;
}
// Handle audio track vs MIDI track vs group track
match self.tracks.get_mut(&track_id) {
Some(TrackNode::Audio(track)) => {
// Render audio track directly into output
track.render(output, audio_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
}
Some(TrackNode::Midi(track)) => {
// Render MIDI track directly into output
// Access midi_clip_pool from self - safe because we only need immutable access
track.render(output, &self.midi_clip_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
}
Some(TrackNode::Group(group)) => {
// Read group properties and transform context (index-based child iteration to avoid clone)
let num_children = group.children.len();
let this_group_is_soloed = group.solo;
let child_ctx = group.transform_context(ctx);
// Acquire a temporary buffer for the group mix
let mut group_buffer = buffer_pool.acquire();
group_buffer.resize(output.len(), 0.0);
group_buffer.fill(0.0);
// Recursively render all children into the group buffer
// If this group is soloed (or parent was soloed), children inherit that state
let children_parent_soloed = parent_is_soloed || this_group_is_soloed;
for i in 0..num_children {
let child_id = match self.tracks.get(&track_id) {
Some(TrackNode::Group(g)) => g.children[i],
_ => break,
};
self.render_track(
child_id,
&mut group_buffer,
audio_pool,
buffer_pool,
child_ctx,
any_solo,
children_parent_soloed,
);
}
// Apply group volume and mix into output
if let Some(TrackNode::Group(group)) = self.tracks.get_mut(&track_id) {
for (out_sample, group_sample) in output.iter_mut().zip(group_buffer.iter()) {
*out_sample += group_sample * group.volume;
}
}
// Release buffer back to pool
buffer_pool.release(group_buffer);
}
None => {}
}
}
/// Reset all per-clip read-ahead target frames before a new render cycle.
pub fn reset_read_ahead_targets(&self) {
for track in self.tracks.values() {
if let TrackNode::Audio(audio_track) = track {
for clip in &audio_track.clips {
if let Some(ra) = clip.read_ahead.as_deref() {
ra.reset_target_frame();
}
}
}
}
}
/// Stop all notes on all MIDI tracks
pub fn stop_all_notes(&mut self) {
for track in self.tracks.values_mut() {
if let TrackNode::Midi(midi_track) = track {
midi_track.stop_all_notes();
}
}
}
/// Set export (blocking) mode on all clip read-ahead buffers.
/// When enabled, `render_from_file` blocks until the disk reader
/// has filled the needed frames instead of returning silence.
pub fn set_export_mode(&self, export: bool) {
for track in self.tracks.values() {
if let TrackNode::Audio(t) = track {
for clip in &t.clips {
if let Some(ref ra) = clip.read_ahead {
ra.set_export_mode(export);
}
}
}
}
}
/// Reset all node graphs (clears effect buffers on seek)
pub fn reset_all_graphs(&mut self) {
for track in self.tracks.values_mut() {
match track {
TrackNode::Audio(t) => t.effects_graph.reset(),
TrackNode::Midi(t) => t.instrument_graph.reset(),
TrackNode::Group(_) => {}
}
}
}
/// Process live MIDI input from all MIDI tracks (called even when not playing)
pub fn process_live_midi(&mut self, output: &mut [f32], sample_rate: u32, channels: u32) {
// Process all MIDI tracks to handle queued live input events
for track in self.tracks.values_mut() {
if let TrackNode::Midi(midi_track) = track {
// Process only queued live events, not clips
midi_track.process_live_input(output, sample_rate, channels);
}
}
}
/// Send a live MIDI note on event to a track's instrument
/// Note: With node-based instruments, MIDI events are handled during the process() call
pub fn send_midi_note_on(&mut self, track_id: TrackId, note: u8, velocity: u8) {
// Queue the MIDI note-on event to the track's live MIDI queue
if let Some(TrackNode::Midi(track)) = self.tracks.get_mut(&track_id) {
let event = MidiEvent::note_on(0.0, 0, note, velocity);
track.queue_live_midi(event);
}
}
/// Send a live MIDI note off event to a track's instrument
pub fn send_midi_note_off(&mut self, track_id: TrackId, note: u8) {
// Queue the MIDI note-off event to the track's live MIDI queue
if let Some(TrackNode::Midi(track)) = self.tracks.get_mut(&track_id) {
let event = MidiEvent::note_off(0.0, 0, note, 0);
track.queue_live_midi(event);
}
}
/// Prepare all tracks for serialization by saving their audio graphs as presets
pub fn prepare_for_save(&mut self) {
for track in self.tracks.values_mut() {
match track {
TrackNode::Audio(audio_track) => {
audio_track.prepare_for_save();
}
TrackNode::Midi(midi_track) => {
midi_track.prepare_for_save();
}
TrackNode::Group(_) => {
// Groups don't have audio graphs
}
}
}
}
/// Rebuild all audio graphs from presets after deserialization
///
/// This should be called after deserializing a Project to reconstruct
/// the AudioGraph instances from their stored presets.
///
/// # Arguments
/// * `buffer_size` - Buffer size for audio processing (typically 8192)
pub fn rebuild_audio_graphs(&mut self, buffer_size: usize) -> Result<(), String> {
for track in self.tracks.values_mut() {
match track {
TrackNode::Audio(audio_track) => {
audio_track.rebuild_audio_graph(self.sample_rate, buffer_size)?;
}
TrackNode::Midi(midi_track) => {
midi_track.rebuild_audio_graph(self.sample_rate, buffer_size)?;
}
TrackNode::Group(_) => {
// Groups don't have audio graphs
}
}
}
Ok(())
}
}
impl Default for Project {
fn default() -> Self {
Self::new(48000) // Use 48kHz as default, will be overridden when created with actual sample rate
}
}