Add nested audio tracks
This commit is contained in:
parent
70855963cb
commit
469849a0d6
|
|
@ -658,8 +658,8 @@ impl Engine {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
Command::CreateMetatrack(name) => {
|
||||
let track_id = self.project.add_group_track(name.clone(), None);
|
||||
Command::CreateMetatrack(name, parent_id) => {
|
||||
let track_id = self.project.add_group_track(name.clone(), parent_id);
|
||||
// Notify UI about the new metatrack
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, true, name));
|
||||
}
|
||||
|
|
@ -686,8 +686,18 @@ impl Engine {
|
|||
metatrack.pitch_shift = semitones;
|
||||
}
|
||||
}
|
||||
Command::CreateAudioTrack(name) => {
|
||||
let track_id = self.project.add_audio_track(name.clone(), None);
|
||||
Command::SetTrimStart(track_id, trim_start) => {
|
||||
if let Some(crate::audio::track::TrackNode::Group(metatrack)) = self.project.get_track_mut(track_id) {
|
||||
metatrack.trim_start = trim_start.max(0.0);
|
||||
}
|
||||
}
|
||||
Command::SetTrimEnd(track_id, trim_end) => {
|
||||
if let Some(crate::audio::track::TrackNode::Group(metatrack)) = self.project.get_track_mut(track_id) {
|
||||
metatrack.trim_end = trim_end.map(|t| t.max(0.0));
|
||||
}
|
||||
}
|
||||
Command::CreateAudioTrack(name, parent_id) => {
|
||||
let track_id = self.project.add_audio_track(name.clone(), parent_id);
|
||||
// Notify UI about the new audio track
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name));
|
||||
}
|
||||
|
|
@ -793,8 +803,8 @@ impl Engine {
|
|||
eprintln!("[Engine] ERROR: Track {} not found or is not an audio track", track_id);
|
||||
}
|
||||
}
|
||||
Command::CreateMidiTrack(name) => {
|
||||
let track_id = self.project.add_midi_track(name.clone(), None);
|
||||
Command::CreateMidiTrack(name, parent_id) => {
|
||||
let track_id = self.project.add_midi_track(name.clone(), parent_id);
|
||||
// Notify UI about the new MIDI track
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name));
|
||||
}
|
||||
|
|
@ -2348,20 +2358,24 @@ impl Engine {
|
|||
|
||||
QueryResponse::TrackGraphLoaded(result)
|
||||
}
|
||||
Query::CreateAudioTrackSync(name) => {
|
||||
let track_id = self.project.add_audio_track(name.clone(), None);
|
||||
eprintln!("[Engine] Created audio track '{}' with ID {}", name, track_id);
|
||||
// Notify UI about the new audio track
|
||||
Query::CreateAudioTrackSync(name, parent_id) => {
|
||||
let track_id = self.project.add_audio_track(name.clone(), parent_id);
|
||||
eprintln!("[Engine] Created audio track '{}' with ID {} (parent: {:?})", name, track_id, parent_id);
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name));
|
||||
QueryResponse::TrackCreated(Ok(track_id))
|
||||
}
|
||||
Query::CreateMidiTrackSync(name) => {
|
||||
let track_id = self.project.add_midi_track(name.clone(), None);
|
||||
eprintln!("[Engine] Created MIDI track '{}' with ID {}", name, track_id);
|
||||
// Notify UI about the new MIDI track
|
||||
Query::CreateMidiTrackSync(name, parent_id) => {
|
||||
let track_id = self.project.add_midi_track(name.clone(), parent_id);
|
||||
eprintln!("[Engine] Created MIDI track '{}' with ID {} (parent: {:?})", name, track_id, parent_id);
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, false, name));
|
||||
QueryResponse::TrackCreated(Ok(track_id))
|
||||
}
|
||||
Query::CreateMetatrackSync(name, parent_id) => {
|
||||
let track_id = self.project.add_group_track(name.clone(), parent_id);
|
||||
eprintln!("[Engine] Created metatrack '{}' with ID {} (parent: {:?})", name, track_id, parent_id);
|
||||
let _ = self.event_tx.push(AudioEvent::TrackCreated(track_id, true, name));
|
||||
QueryResponse::TrackCreated(Ok(track_id))
|
||||
}
|
||||
Query::GetPoolWaveform(pool_index, target_peaks) => {
|
||||
match self.audio_pool.generate_waveform(pool_index, target_peaks) {
|
||||
Some(waveform) => QueryResponse::PoolWaveform(Ok(waveform)),
|
||||
|
|
@ -2930,7 +2944,7 @@ impl EngineController {
|
|||
|
||||
/// Create a new metatrack
|
||||
pub fn create_metatrack(&mut self, name: String) {
|
||||
let _ = self.command_tx.push(Command::CreateMetatrack(name));
|
||||
let _ = self.command_tx.push(Command::CreateMetatrack(name, None));
|
||||
}
|
||||
|
||||
/// Add a track to a metatrack
|
||||
|
|
@ -2960,9 +2974,19 @@ impl EngineController {
|
|||
let _ = self.command_tx.push(Command::SetPitchShift(track_id, semitones));
|
||||
}
|
||||
|
||||
/// Set metatrack trim start in seconds
|
||||
pub fn set_trim_start(&mut self, track_id: TrackId, trim_start: f64) {
|
||||
let _ = self.command_tx.push(Command::SetTrimStart(track_id, trim_start));
|
||||
}
|
||||
|
||||
/// Set metatrack trim end in seconds (None = no end trim)
|
||||
pub fn set_trim_end(&mut self, track_id: TrackId, trim_end: Option<f64>) {
|
||||
let _ = self.command_tx.push(Command::SetTrimEnd(track_id, trim_end));
|
||||
}
|
||||
|
||||
/// Create a new audio track
|
||||
pub fn create_audio_track(&mut self, name: String) {
|
||||
let _ = self.command_tx.push(Command::CreateAudioTrack(name));
|
||||
let _ = self.command_tx.push(Command::CreateAudioTrack(name, None));
|
||||
}
|
||||
|
||||
/// Add an audio file to the pool (must be called from non-audio thread with pre-loaded data)
|
||||
|
|
@ -3012,7 +3036,7 @@ impl EngineController {
|
|||
|
||||
/// Create a new MIDI track
|
||||
pub fn create_midi_track(&mut self, name: String) {
|
||||
let _ = self.command_tx.push(Command::CreateMidiTrack(name));
|
||||
let _ = self.command_tx.push(Command::CreateMidiTrack(name, None));
|
||||
}
|
||||
|
||||
/// Add a MIDI clip to the pool without placing it on any track
|
||||
|
|
@ -3022,8 +3046,8 @@ impl EngineController {
|
|||
}
|
||||
|
||||
/// Create a new audio track synchronously (waits for creation to complete)
|
||||
pub fn create_audio_track_sync(&mut self, name: String) -> Result<TrackId, String> {
|
||||
if let Err(_) = self.query_tx.push(Query::CreateAudioTrackSync(name)) {
|
||||
pub fn create_audio_track_sync(&mut self, name: String, parent: Option<TrackId>) -> Result<TrackId, String> {
|
||||
if let Err(_) = self.query_tx.push(Query::CreateAudioTrackSync(name, parent)) {
|
||||
return Err("Failed to send track creation query".to_string());
|
||||
}
|
||||
|
||||
|
|
@ -3042,8 +3066,8 @@ impl EngineController {
|
|||
}
|
||||
|
||||
/// Create a new MIDI track synchronously (waits for creation to complete)
|
||||
pub fn create_midi_track_sync(&mut self, name: String) -> Result<TrackId, String> {
|
||||
if let Err(_) = self.query_tx.push(Query::CreateMidiTrackSync(name)) {
|
||||
pub fn create_midi_track_sync(&mut self, name: String, parent: Option<TrackId>) -> Result<TrackId, String> {
|
||||
if let Err(_) = self.query_tx.push(Query::CreateMidiTrackSync(name, parent)) {
|
||||
return Err("Failed to send track creation query".to_string());
|
||||
}
|
||||
|
||||
|
|
@ -3061,6 +3085,25 @@ impl EngineController {
|
|||
Err("Track creation timeout".to_string())
|
||||
}
|
||||
|
||||
/// Create a new metatrack/group synchronously (waits for creation to complete)
|
||||
pub fn create_group_track_sync(&mut self, name: String, parent: Option<TrackId>) -> Result<TrackId, String> {
|
||||
if let Err(_) = self.query_tx.push(Query::CreateMetatrackSync(name, parent)) {
|
||||
return Err("Failed to send metatrack creation query".to_string());
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(2);
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
if let Ok(QueryResponse::TrackCreated(result)) = self.query_response_rx.pop() {
|
||||
return result;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
|
||||
Err("Metatrack creation timeout".to_string())
|
||||
}
|
||||
|
||||
/// Create a new MIDI clip on a track
|
||||
pub fn create_midi_clip(&mut self, track_id: TrackId, start_time: f64, duration: f64) -> MidiClipId {
|
||||
// Peek at the next clip ID that will be used
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ impl Project {
|
|||
/// 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);
|
||||
let group = Metatrack::new(id, name, self.sample_rate);
|
||||
self.tracks.insert(id, TrackNode::Group(group));
|
||||
|
||||
if let Some(parent) = parent_id {
|
||||
|
|
@ -450,6 +450,11 @@ impl Project {
|
|||
track.render(output, &self.midi_clip_pool, ctx.playhead_seconds, ctx.sample_rate, ctx.channels);
|
||||
}
|
||||
Some(TrackNode::Group(group)) => {
|
||||
// Skip rendering if playhead is outside the metatrack's trim window
|
||||
if !group.is_active_at_time(ctx.playhead_seconds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
@ -479,14 +484,37 @@ impl Project {
|
|||
);
|
||||
}
|
||||
|
||||
// Apply group volume and mix into output
|
||||
// Route children's mix through metatrack's audio graph
|
||||
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;
|
||||
// Inject children's mix into audio graph's input node
|
||||
let node_indices: Vec<_> = group.audio_graph.node_indices().collect();
|
||||
for node_idx in node_indices {
|
||||
if let Some(graph_node) = group.audio_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::<super::node_graph::nodes::AudioInputNode>()
|
||||
{
|
||||
input_node.inject_audio(&group_buffer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process through the audio graph into a fresh buffer
|
||||
let mut graph_output = buffer_pool.acquire();
|
||||
graph_output.resize(output.len(), 0.0);
|
||||
graph_output.fill(0.0);
|
||||
group.audio_graph.process(&mut graph_output, &[], ctx.playhead_seconds);
|
||||
|
||||
// Apply group volume and mix into output
|
||||
for (out_sample, graph_sample) in output.iter_mut().zip(graph_output.iter()) {
|
||||
*out_sample += graph_sample * group.volume;
|
||||
}
|
||||
buffer_pool.release(graph_output);
|
||||
}
|
||||
|
||||
// Release buffer back to pool
|
||||
// Release children mix buffer back to pool
|
||||
buffer_pool.release(group_buffer);
|
||||
}
|
||||
None => {}
|
||||
|
|
@ -581,8 +609,8 @@ impl Project {
|
|||
TrackNode::Midi(midi_track) => {
|
||||
midi_track.prepare_for_save();
|
||||
}
|
||||
TrackNode::Group(_) => {
|
||||
// Groups don't have audio graphs
|
||||
TrackNode::Group(group) => {
|
||||
group.prepare_for_save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -604,8 +632,8 @@ impl Project {
|
|||
TrackNode::Midi(midi_track) => {
|
||||
midi_track.rebuild_audio_graph(self.sample_rate, buffer_size)?;
|
||||
}
|
||||
TrackNode::Group(_) => {
|
||||
// Groups don't have audio graphs
|
||||
TrackNode::Group(group) => {
|
||||
group.rebuild_audio_graph(self.sample_rate, buffer_size)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ impl TrackNode {
|
|||
}
|
||||
|
||||
/// Metatrack that contains other tracks with time transformation capabilities
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Metatrack {
|
||||
pub id: TrackId,
|
||||
pub name: String,
|
||||
|
|
@ -167,14 +167,50 @@ pub struct Metatrack {
|
|||
pub pitch_shift: f32,
|
||||
/// Time offset in seconds (shift content forward/backward in time)
|
||||
pub offset: f64,
|
||||
/// Trim start: offset into the metatrack's internal content (seconds)
|
||||
/// Children will see time starting from this point
|
||||
pub trim_start: f64,
|
||||
/// Trim end: offset into the metatrack's internal content (seconds)
|
||||
/// None means no end trim (play until content ends)
|
||||
pub trim_end: Option<f64>,
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metatrack {
|
||||
/// Create a new metatrack
|
||||
pub fn new(id: TrackId, name: String) -> Self {
|
||||
/// Create a new metatrack with an audio graph (input → output)
|
||||
pub fn new(id: TrackId, name: String, sample_rate: u32) -> Self {
|
||||
let default_buffer_size = 8192;
|
||||
let audio_graph = Self::create_default_graph(sample_rate, default_buffer_size);
|
||||
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
|
|
@ -185,11 +221,52 @@ impl Metatrack {
|
|||
time_stretch: 1.0,
|
||||
pitch_shift: 0.0,
|
||||
offset: 0.0,
|
||||
trim_start: 0.0,
|
||||
trim_end: None,
|
||||
automation_lanes: HashMap::new(),
|
||||
next_automation_id: 0,
|
||||
audio_graph,
|
||||
audio_graph_preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a default audio graph with AudioInput -> AudioOutput
|
||||
fn create_default_graph(sample_rate: u32, buffer_size: usize) -> AudioGraph {
|
||||
let mut graph = AudioGraph::new(sample_rate, buffer_size);
|
||||
|
||||
let input_node = Box::new(AudioInputNode::new("Audio Input"));
|
||||
let input_id = graph.add_node(input_node);
|
||||
graph.set_node_position(input_id, 100.0, 150.0);
|
||||
|
||||
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);
|
||||
|
||||
let _ = graph.connect(input_id, 0, output_id, 0);
|
||||
graph.set_output_node(Some(output_id));
|
||||
|
||||
graph
|
||||
}
|
||||
|
||||
/// 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
|
||||
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)?;
|
||||
} else {
|
||||
self.audio_graph = Self::create_default_graph(sample_rate, buffer_size);
|
||||
}
|
||||
} else {
|
||||
self.audio_graph = Self::create_default_graph(sample_rate, buffer_size);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
|
@ -282,11 +359,27 @@ impl Metatrack {
|
|||
!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: f64) -> 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 and offset transformations.
|
||||
/// 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(&self, ctx: RenderContext) -> RenderContext {
|
||||
let mut transformed = ctx;
|
||||
|
||||
|
|
@ -300,7 +393,11 @@ impl Metatrack {
|
|||
// 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
|
||||
transformed.playhead_seconds = adjusted_playhead * self.time_stretch as f64;
|
||||
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.0, children start seeing time 2.0 when parent reaches offset
|
||||
transformed.playhead_seconds = stretched + self.trim_start;
|
||||
|
||||
// Accumulate time stretch for nested metatracks
|
||||
transformed.time_stretch *= self.time_stretch;
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ pub enum Command {
|
|||
ExtendClip(TrackId, ClipId, f64),
|
||||
|
||||
// Metatrack management commands
|
||||
/// Create a new metatrack with a name
|
||||
CreateMetatrack(String),
|
||||
/// Create a new metatrack with a name and optional parent group
|
||||
CreateMetatrack(String, Option<TrackId>),
|
||||
/// Add a track to a metatrack (track_id, metatrack_id)
|
||||
AddToMetatrack(TrackId, TrackId),
|
||||
/// Remove a track from its parent metatrack
|
||||
|
|
@ -54,10 +54,16 @@ pub enum Command {
|
|||
SetOffset(TrackId, f64),
|
||||
/// Set metatrack pitch shift in semitones (track_id, semitones) - for future use
|
||||
SetPitchShift(TrackId, f32),
|
||||
/// Set metatrack trim start in seconds (track_id, trim_start)
|
||||
/// Children won't hear content before this point
|
||||
SetTrimStart(TrackId, f64),
|
||||
/// Set metatrack trim end in seconds (track_id, trim_end)
|
||||
/// None means no end trim
|
||||
SetTrimEnd(TrackId, Option<f64>),
|
||||
|
||||
// Audio track commands
|
||||
/// Create a new audio track with a name
|
||||
CreateAudioTrack(String),
|
||||
/// Create a new audio track with a name and optional parent group
|
||||
CreateAudioTrack(String, Option<TrackId>),
|
||||
/// Add an audio file to the pool (path, data, channels, sample_rate)
|
||||
/// Returns the pool index via an AudioEvent
|
||||
AddAudioFile(String, Vec<f32>, u32, u32),
|
||||
|
|
@ -65,8 +71,8 @@ pub enum Command {
|
|||
AddAudioClip(TrackId, usize, f64, f64, f64),
|
||||
|
||||
// MIDI commands
|
||||
/// Create a new MIDI track with a name
|
||||
CreateMidiTrack(String),
|
||||
/// Create a new MIDI track with a name and optional parent group
|
||||
CreateMidiTrack(String, Option<TrackId>),
|
||||
/// Add a MIDI clip to the pool without placing it on a track
|
||||
AddMidiClipToPool(MidiClip),
|
||||
/// Create a new MIDI clip on a track (track_id, start_time, duration)
|
||||
|
|
@ -361,10 +367,12 @@ pub enum Query {
|
|||
SerializeTrackGraph(TrackId, std::path::PathBuf),
|
||||
/// Load a track's effects/instrument graph (track_id, preset_json, project_path)
|
||||
LoadTrackGraph(TrackId, String, std::path::PathBuf),
|
||||
/// Create a new audio track (name) - returns track ID synchronously
|
||||
CreateAudioTrackSync(String),
|
||||
/// Create a new MIDI track (name) - returns track ID synchronously
|
||||
CreateMidiTrackSync(String),
|
||||
/// Create a new audio track (name, parent) - returns track ID synchronously
|
||||
CreateAudioTrackSync(String, Option<TrackId>),
|
||||
/// Create a new MIDI track (name, parent) - returns track ID synchronously
|
||||
CreateMidiTrackSync(String, Option<TrackId>),
|
||||
/// Create a new metatrack/group (name, parent) - returns track ID synchronously
|
||||
CreateMetatrackSync(String, Option<TrackId>),
|
||||
/// Get waveform data from audio pool (pool_index, target_peaks)
|
||||
GetPoolWaveform(usize, usize),
|
||||
/// Get file info from audio pool (pool_index) - returns (duration, sample_rate, channels)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ pub struct BackendContext<'a> {
|
|||
/// Mapping from document clip instance UUIDs to backend clip instance IDs
|
||||
pub clip_instance_to_backend_map: &'a mut HashMap<Uuid, BackendClipInstanceId>,
|
||||
|
||||
/// Mapping from movie clip UUIDs to backend metatrack (group track) TrackIds
|
||||
pub clip_to_metatrack_map: &'a HashMap<Uuid, daw_backend::TrackId>,
|
||||
|
||||
// Future: pub video_controller: Option<&'a mut VideoController>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ pub struct AddLayerAction {
|
|||
/// The layer to add
|
||||
layer: AnyLayer,
|
||||
|
||||
/// If Some, add to this VectorClip's layers instead of root
|
||||
target_clip_id: Option<Uuid>,
|
||||
|
||||
/// ID of the created layer (set after execution)
|
||||
created_layer_id: Option<Uuid>,
|
||||
}
|
||||
|
|
@ -26,6 +29,7 @@ impl AddLayerAction {
|
|||
let layer = VectorLayer::new(name);
|
||||
Self {
|
||||
layer: AnyLayer::Vector(layer),
|
||||
target_clip_id: None,
|
||||
created_layer_id: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -38,10 +42,17 @@ impl AddLayerAction {
|
|||
pub fn new(layer: AnyLayer) -> Self {
|
||||
Self {
|
||||
layer,
|
||||
target_clip_id: None,
|
||||
created_layer_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the target clip for this action (add layer inside a movie clip)
|
||||
pub fn with_target_clip(mut self, clip_id: Option<Uuid>) -> Self {
|
||||
self.target_clip_id = clip_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the ID of the created layer (after execution)
|
||||
pub fn created_layer_id(&self) -> Option<Uuid> {
|
||||
self.created_layer_id
|
||||
|
|
@ -50,8 +61,19 @@ impl AddLayerAction {
|
|||
|
||||
impl Action for AddLayerAction {
|
||||
fn execute(&mut self, document: &mut Document) -> Result<(), String> {
|
||||
// Add layer to the document's root
|
||||
let layer_id = document.root_mut().add_child(self.layer.clone());
|
||||
let layer_id = if let Some(clip_id) = self.target_clip_id {
|
||||
// Add layer inside a vector clip (movie clip)
|
||||
let clip = document.vector_clips.get_mut(&clip_id)
|
||||
.ok_or_else(|| format!("Target clip {} not found", clip_id))?;
|
||||
let id = self.layer.id();
|
||||
clip.layers.add_root(self.layer.clone());
|
||||
// Register in layer_to_clip_map for O(1) lookup
|
||||
document.layer_to_clip_map.insert(id, clip_id);
|
||||
id
|
||||
} else {
|
||||
// Add layer to the document's root
|
||||
document.root_mut().add_child(self.layer.clone())
|
||||
};
|
||||
|
||||
// Store the ID for rollback
|
||||
self.created_layer_id = Some(layer_id);
|
||||
|
|
@ -62,7 +84,15 @@ impl Action for AddLayerAction {
|
|||
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||
// Remove the created layer if it exists
|
||||
if let Some(layer_id) = self.created_layer_id {
|
||||
document.root_mut().remove_child(&layer_id);
|
||||
if let Some(clip_id) = self.target_clip_id {
|
||||
// Remove from vector clip
|
||||
if let Some(clip) = document.vector_clips.get_mut(&clip_id) {
|
||||
clip.layers.roots.retain(|node| node.data.id() != layer_id);
|
||||
}
|
||||
document.layer_to_clip_map.remove(&layer_id);
|
||||
} else {
|
||||
document.root_mut().remove_child(&layer_id);
|
||||
}
|
||||
|
||||
// Clear the stored ID
|
||||
self.created_layer_id = None;
|
||||
|
|
|
|||
|
|
@ -190,6 +190,21 @@ impl Action for MoveClipInstancesAction {
|
|||
let layer = document.get_layer(layer_id)
|
||||
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
||||
|
||||
// Handle vector layers: update metatrack offset for movie clips with audio
|
||||
if let AnyLayer::Vector(vl) = layer {
|
||||
for (instance_id, _old_start, new_start) in moves {
|
||||
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
|
||||
// Check if this clip has a metatrack
|
||||
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) {
|
||||
controller.set_offset(metatrack_id, *new_start);
|
||||
controller.set_trim_start(metatrack_id, instance.trim_start);
|
||||
controller.set_trim_end(metatrack_id, instance.trim_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process audio layers
|
||||
if !matches!(layer, AnyLayer::Audio(_)) {
|
||||
continue;
|
||||
|
|
@ -260,6 +275,20 @@ impl Action for MoveClipInstancesAction {
|
|||
let layer = document.get_layer(layer_id)
|
||||
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
||||
|
||||
// Handle vector layers: restore metatrack offset for movie clips with audio
|
||||
if let AnyLayer::Vector(vl) = layer {
|
||||
for (instance_id, old_start, _new_start) in moves {
|
||||
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
|
||||
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) {
|
||||
controller.set_offset(metatrack_id, *old_start);
|
||||
controller.set_trim_start(metatrack_id, instance.trim_start);
|
||||
controller.set_trim_end(metatrack_id, instance.trim_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process audio layers
|
||||
if !matches!(layer, AnyLayer::Audio(_)) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ impl SetLayerPropertiesAction {
|
|||
impl Action for SetLayerPropertiesAction {
|
||||
fn execute(&mut self, document: &mut Document) -> Result<(), String> {
|
||||
for (i, &layer_id) in self.layer_ids.iter().enumerate() {
|
||||
// Find the layer in the document
|
||||
if let Some(layer) = document.root_mut().get_child_mut(&layer_id) {
|
||||
// Find the layer in the document (searches root + inside movie clips)
|
||||
if let Some(layer) = document.get_layer_mut(&layer_id) {
|
||||
// Store old value if not already stored
|
||||
if self.old_values[i].is_none() {
|
||||
self.old_values[i] = Some(match &self.property {
|
||||
|
|
@ -106,8 +106,8 @@ impl Action for SetLayerPropertiesAction {
|
|||
|
||||
fn rollback(&mut self, document: &mut Document) -> Result<(), String> {
|
||||
for (i, &layer_id) in self.layer_ids.iter().enumerate() {
|
||||
// Find the layer in the document
|
||||
if let Some(layer) = document.root_mut().get_child_mut(&layer_id) {
|
||||
// Find the layer in the document (searches root + inside movie clips)
|
||||
if let Some(layer) = document.get_layer_mut(&layer_id) {
|
||||
// Restore old value if we have one
|
||||
if let Some(old_value) = &self.old_values[i] {
|
||||
match old_value {
|
||||
|
|
|
|||
|
|
@ -355,6 +355,21 @@ impl Action for TrimClipInstancesAction {
|
|||
let layer = document.get_layer(layer_id)
|
||||
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
||||
|
||||
// Handle vector layers: update metatrack trim for movie clips with audio
|
||||
if let AnyLayer::Vector(vl) = layer {
|
||||
for (instance_id, _trim_type, _old, _new) in trims {
|
||||
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
|
||||
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) {
|
||||
// Instance already has new values after execute()
|
||||
controller.set_offset(metatrack_id, instance.timeline_start);
|
||||
controller.set_trim_start(metatrack_id, instance.trim_start);
|
||||
controller.set_trim_end(metatrack_id, instance.trim_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process audio layers
|
||||
if !matches!(layer, AnyLayer::Audio(_)) {
|
||||
continue;
|
||||
|
|
@ -430,6 +445,21 @@ impl Action for TrimClipInstancesAction {
|
|||
let layer = document.get_layer(layer_id)
|
||||
.ok_or_else(|| format!("Layer {} not found", layer_id))?;
|
||||
|
||||
// Handle vector layers: restore metatrack trim for movie clips with audio
|
||||
if let AnyLayer::Vector(vl) = layer {
|
||||
for (instance_id, _trim_type, _old, _new) in trims {
|
||||
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
|
||||
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) {
|
||||
// Instance already has old values after rollback()
|
||||
controller.set_offset(metatrack_id, instance.timeline_start);
|
||||
controller.set_trim_start(metatrack_id, instance.trim_start);
|
||||
controller.set_trim_end(metatrack_id, instance.trim_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process audio layers
|
||||
if !matches!(layer, AnyLayer::Audio(_)) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -90,14 +90,49 @@ impl VectorClip {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calculate the duration of this clip based on its internal keyframe content.
|
||||
/// Returns the time of the last keyframe across all layers, plus one frame.
|
||||
/// Falls back to the stored `duration` field if no keyframes exist.
|
||||
/// Calculate the duration of this clip based on its internal content.
|
||||
///
|
||||
/// Considers:
|
||||
/// - Vector layer keyframes (last keyframe time + one frame)
|
||||
/// - Audio/video/effect layer clip instances (timeline_start + effective duration)
|
||||
///
|
||||
/// The `clip_duration_fn` resolves referenced clip durations for non-vector layers.
|
||||
/// Falls back to the stored `duration` field if no content exists.
|
||||
pub fn content_duration(&self, framerate: f64) -> f64 {
|
||||
self.content_duration_with(framerate, |_| None)
|
||||
}
|
||||
|
||||
/// Like `content_duration`, but with a closure that resolves clip durations
|
||||
/// for audio/video/effect clip instances inside this movie clip.
|
||||
pub fn content_duration_with(&self, framerate: f64, clip_duration_fn: impl Fn(&Uuid) -> Option<f64>) -> f64 {
|
||||
let frame_duration = 1.0 / framerate;
|
||||
let mut last_time: Option<f64> = None;
|
||||
|
||||
for layer_node in self.layers.iter() {
|
||||
// Check clip instances on ALL layer types (vector, audio, video, effect)
|
||||
let clip_instances: &[ClipInstance] = match &layer_node.data {
|
||||
AnyLayer::Vector(vl) => &vl.clip_instances,
|
||||
AnyLayer::Audio(al) => &al.clip_instances,
|
||||
AnyLayer::Video(vl) => &vl.clip_instances,
|
||||
AnyLayer::Effect(el) => &el.clip_instances,
|
||||
};
|
||||
for ci in clip_instances {
|
||||
let end = if let Some(td) = ci.timeline_duration {
|
||||
ci.timeline_start + td
|
||||
} else if let Some(te) = ci.trim_end {
|
||||
ci.timeline_start + (te - ci.trim_start).max(0.0)
|
||||
} else if let Some(clip_dur) = clip_duration_fn(&ci.clip_id) {
|
||||
ci.timeline_start + (clip_dur - ci.trim_start).max(0.0)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
last_time = Some(match last_time {
|
||||
Some(t) => t.max(end),
|
||||
None => end,
|
||||
});
|
||||
}
|
||||
|
||||
// Also check vector layer keyframes
|
||||
if let AnyLayer::Vector(vector_layer) = &layer_node.data {
|
||||
if let Some(last_kf) = vector_layer.keyframes.last() {
|
||||
last_time = Some(match last_time {
|
||||
|
|
|
|||
|
|
@ -462,6 +462,15 @@ impl Document {
|
|||
self.context_layers_mut(clip_id).into_iter().find(|l| &l.id() == layer_id)
|
||||
}
|
||||
|
||||
/// Get all layers across the entire document (root + inside all vector clips).
|
||||
pub fn all_layers(&self) -> Vec<&AnyLayer> {
|
||||
let mut layers: Vec<&AnyLayer> = self.root.children.iter().collect();
|
||||
for clip in self.vector_clips.values() {
|
||||
layers.extend(clip.layers.root_data());
|
||||
}
|
||||
layers
|
||||
}
|
||||
|
||||
// === CLIP LIBRARY METHODS ===
|
||||
|
||||
/// Add a vector clip to the library
|
||||
|
|
@ -645,7 +654,21 @@ impl Document {
|
|||
if clip.is_group {
|
||||
Some(clip.duration)
|
||||
} else {
|
||||
Some(clip.content_duration(self.framerate))
|
||||
Some(clip.content_duration_with(self.framerate, |id| {
|
||||
// Resolve nested clip durations (audio, video, other vector clips)
|
||||
if let Some(vc) = self.vector_clips.get(id) {
|
||||
// Avoid deep recursion — use stored duration for nested vector clips
|
||||
Some(vc.content_duration(self.framerate))
|
||||
} else if let Some(ac) = self.audio_clips.get(id) {
|
||||
Some(ac.duration)
|
||||
} else if let Some(vc) = self.video_clips.get(id) {
|
||||
Some(vc.duration)
|
||||
} else if self.effect_definitions.contains_key(id) {
|
||||
Some(crate::effect::EFFECT_DURATION)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else if let Some(clip) = self.video_clips.get(clip_id) {
|
||||
Some(clip.duration)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ pub struct SerializedAudioBackend {
|
|||
/// Preserves the connection between UI layers and audio engine tracks across save/load
|
||||
#[serde(default)]
|
||||
pub layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>,
|
||||
|
||||
/// Mapping from movie clip UUIDs to backend metatrack (group track) TrackIds
|
||||
#[serde(default)]
|
||||
pub clip_to_metatrack_map: std::collections::HashMap<uuid::Uuid, u32>,
|
||||
}
|
||||
|
||||
/// Settings for saving a project
|
||||
|
|
@ -96,6 +100,9 @@ pub struct LoadedProject {
|
|||
/// Mapping from UI layer UUIDs to backend TrackIds (empty for old files)
|
||||
pub layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>,
|
||||
|
||||
/// Mapping from movie clip UUIDs to backend metatrack TrackIds (empty for old files)
|
||||
pub clip_to_metatrack_map: std::collections::HashMap<uuid::Uuid, u32>,
|
||||
|
||||
/// Loaded audio pool entries
|
||||
pub audio_pool_entries: Vec<AudioPoolEntry>,
|
||||
|
||||
|
|
@ -147,6 +154,7 @@ pub fn save_beam(
|
|||
audio_project: &mut AudioProject,
|
||||
audio_pool_entries: Vec<AudioPoolEntry>,
|
||||
layer_to_track_map: &std::collections::HashMap<uuid::Uuid, u32>,
|
||||
clip_to_metatrack_map: &std::collections::HashMap<uuid::Uuid, u32>,
|
||||
_settings: &SaveSettings,
|
||||
) -> Result<(), String> {
|
||||
let fn_start = std::time::Instant::now();
|
||||
|
|
@ -375,6 +383,7 @@ pub fn save_beam(
|
|||
project: audio_project.clone(),
|
||||
audio_pool_entries: modified_entries,
|
||||
layer_to_track_map: layer_to_track_map.clone(),
|
||||
clip_to_metatrack_map: clip_to_metatrack_map.clone(),
|
||||
},
|
||||
};
|
||||
eprintln!("📊 [SAVE_BEAM] Step 5: Build BeamProject structure took {:.2}ms", step5_start.elapsed().as_secs_f64() * 1000.0);
|
||||
|
|
@ -462,6 +471,7 @@ pub fn load_beam(path: &Path) -> Result<LoadedProject, String> {
|
|||
let mut audio_project = beam_project.audio_backend.project;
|
||||
let audio_pool_entries = beam_project.audio_backend.audio_pool_entries;
|
||||
let layer_to_track_map = beam_project.audio_backend.layer_to_track_map;
|
||||
let clip_to_metatrack_map = beam_project.audio_backend.clip_to_metatrack_map;
|
||||
eprintln!("📊 [LOAD_BEAM] Step 5: Extract document and audio state took {:.2}ms", step5_start.elapsed().as_secs_f64() * 1000.0);
|
||||
|
||||
// 6. Rebuild AudioGraphs from presets
|
||||
|
|
@ -607,6 +617,7 @@ pub fn load_beam(path: &Path) -> Result<LoadedProject, String> {
|
|||
document,
|
||||
audio_project,
|
||||
layer_to_track_map,
|
||||
clip_to_metatrack_map,
|
||||
audio_pool_entries: restored_entries,
|
||||
missing_files,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -498,7 +498,7 @@ fn render_clip_instance(
|
|||
}
|
||||
0.0
|
||||
} else {
|
||||
let clip_dur = vector_clip.content_duration(document.framerate);
|
||||
let clip_dur = document.get_clip_duration(&vector_clip.id).unwrap_or(vector_clip.duration);
|
||||
let Some(t) = clip_instance.remap_time(time, clip_dur) else {
|
||||
return; // Clip instance not active at this time
|
||||
};
|
||||
|
|
|
|||
|
|
@ -384,6 +384,7 @@ enum FileCommand {
|
|||
path: std::path::PathBuf,
|
||||
document: lightningbeam_core::document::Document,
|
||||
layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>,
|
||||
clip_to_metatrack_map: std::collections::HashMap<uuid::Uuid, u32>,
|
||||
progress_tx: std::sync::mpsc::Sender<FileProgress>,
|
||||
},
|
||||
Load {
|
||||
|
|
@ -458,8 +459,8 @@ impl FileOperationsWorker {
|
|||
fn run(self) {
|
||||
while let Ok(command) = self.command_rx.recv() {
|
||||
match command {
|
||||
FileCommand::Save { path, document, layer_to_track_map, progress_tx } => {
|
||||
self.handle_save(path, document, &layer_to_track_map, progress_tx);
|
||||
FileCommand::Save { path, document, layer_to_track_map, clip_to_metatrack_map, progress_tx } => {
|
||||
self.handle_save(path, document, &layer_to_track_map, &clip_to_metatrack_map, progress_tx);
|
||||
}
|
||||
FileCommand::Load { path, progress_tx } => {
|
||||
self.handle_load(path, progress_tx);
|
||||
|
|
@ -474,6 +475,7 @@ impl FileOperationsWorker {
|
|||
path: std::path::PathBuf,
|
||||
document: lightningbeam_core::document::Document,
|
||||
layer_to_track_map: &std::collections::HashMap<uuid::Uuid, u32>,
|
||||
clip_to_metatrack_map: &std::collections::HashMap<uuid::Uuid, u32>,
|
||||
progress_tx: std::sync::mpsc::Sender<FileProgress>,
|
||||
) {
|
||||
use lightningbeam_core::file_io::{save_beam, SaveSettings};
|
||||
|
|
@ -516,7 +518,7 @@ impl FileOperationsWorker {
|
|||
let step3_start = std::time::Instant::now();
|
||||
|
||||
let settings = SaveSettings::default();
|
||||
match save_beam(&path, &document, &mut audio_project, audio_pool_entries, layer_to_track_map, &settings) {
|
||||
match save_beam(&path, &document, &mut audio_project, audio_pool_entries, layer_to_track_map, clip_to_metatrack_map, &settings) {
|
||||
Ok(()) => {
|
||||
eprintln!("📊 [SAVE] Step 3: save_beam() took {:.2}ms", step3_start.elapsed().as_secs_f64() * 1000.0);
|
||||
eprintln!("📊 [SAVE] ✅ Total save time: {:.2}ms", save_start.elapsed().as_secs_f64() * 1000.0);
|
||||
|
|
@ -704,6 +706,8 @@ struct EditorApp {
|
|||
// Track ID mapping (Document layer UUIDs <-> daw-backend TrackIds)
|
||||
layer_to_track_map: HashMap<Uuid, daw_backend::TrackId>,
|
||||
track_to_layer_map: HashMap<daw_backend::TrackId, Uuid>,
|
||||
// Movie clip ID -> backend metatrack (group track) mapping
|
||||
clip_to_metatrack_map: HashMap<Uuid, daw_backend::TrackId>,
|
||||
/// Generation counter - incremented on project load to force UI components to reload
|
||||
project_generation: u64,
|
||||
// Clip instance ID mapping (Document clip instance UUIDs <-> backend clip instance IDs)
|
||||
|
|
@ -936,6 +940,7 @@ impl EditorApp {
|
|||
)),
|
||||
layer_to_track_map: HashMap::new(),
|
||||
track_to_layer_map: HashMap::new(),
|
||||
clip_to_metatrack_map: HashMap::new(),
|
||||
project_generation: 0,
|
||||
clip_instance_to_backend_map: HashMap::new(),
|
||||
playback_time: 0.0, // Start at beginning
|
||||
|
|
@ -1309,63 +1314,79 @@ impl EditorApp {
|
|||
fn sync_audio_layers_to_backend(&mut self) {
|
||||
use lightningbeam_core::layer::{AnyLayer, AudioLayerType};
|
||||
|
||||
// Iterate through all layers in the document
|
||||
// Collect audio layers from root and inside vector clips
|
||||
// Each entry: (layer_id, layer_name, audio_type, parent_clip_id)
|
||||
let mut audio_layers_to_sync: Vec<(uuid::Uuid, String, AudioLayerType, Option<uuid::Uuid>)> = Vec::new();
|
||||
|
||||
// Root layers
|
||||
for layer in &self.action_executor.document().root.children {
|
||||
// Only process Audio layers
|
||||
if let AnyLayer::Audio(audio_layer) = layer {
|
||||
let layer_id = audio_layer.layer.id;
|
||||
let layer_name = &audio_layer.layer.name;
|
||||
|
||||
// Skip if already mapped (shouldn't happen, but be defensive)
|
||||
if self.layer_to_track_map.contains_key(&layer_id) {
|
||||
continue;
|
||||
if !self.layer_to_track_map.contains_key(&layer_id) {
|
||||
audio_layers_to_sync.push((
|
||||
layer_id,
|
||||
audio_layer.layer.name.clone(),
|
||||
audio_layer.audio_layer_type,
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle both MIDI and Sampled audio tracks
|
||||
match audio_layer.audio_layer_type {
|
||||
AudioLayerType::Midi => {
|
||||
// Create daw-backend MIDI track
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_midi_track_sync(layer_name.clone()) {
|
||||
Ok(track_id) => {
|
||||
// Store bidirectional mapping
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
// Layers inside vector clips
|
||||
for (&clip_id, clip) in &self.action_executor.document().vector_clips {
|
||||
for layer in clip.layers.root_data() {
|
||||
if let AnyLayer::Audio(audio_layer) = layer {
|
||||
let layer_id = audio_layer.layer.id;
|
||||
if !self.layer_to_track_map.contains_key(&layer_id) {
|
||||
audio_layers_to_sync.push((
|
||||
layer_id,
|
||||
audio_layer.layer.name.clone(),
|
||||
audio_layer.audio_layer_type,
|
||||
Some(clip_id),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load default instrument
|
||||
if let Err(e) = default_instrument::load_default_instrument(&mut *controller, track_id) {
|
||||
eprintln!("⚠️ Failed to load default instrument for {}: {}", layer_name, e);
|
||||
} else {
|
||||
println!("✅ Synced MIDI layer '{}' to backend (TrackId: {})", layer_name, track_id);
|
||||
}
|
||||
// Now create backend tracks for each
|
||||
for (layer_id, layer_name, audio_type, parent_clip_id) in audio_layers_to_sync {
|
||||
// If inside a clip, ensure a metatrack exists
|
||||
let parent_track = parent_clip_id.and_then(|cid| self.ensure_metatrack_for_clip(cid));
|
||||
|
||||
// TODO: Sync any existing clips on this layer to the backend
|
||||
// This will be implemented when we add clip synchronization
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Failed to create daw-backend track for MIDI layer '{}': {}", layer_name, e);
|
||||
match audio_type {
|
||||
AudioLayerType::Midi => {
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_midi_track_sync(layer_name.clone(), parent_track) {
|
||||
Ok(track_id) => {
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
|
||||
if let Err(e) = default_instrument::load_default_instrument(&mut *controller, track_id) {
|
||||
eprintln!("⚠️ Failed to load default instrument for {}: {}", layer_name, e);
|
||||
} else {
|
||||
println!("✅ Synced MIDI layer '{}' to backend (TrackId: {}, parent: {:?})", layer_name, track_id, parent_track);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Failed to create daw-backend track for MIDI layer '{}': {}", layer_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
AudioLayerType::Sampled => {
|
||||
// Create daw-backend Audio track
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_audio_track_sync(layer_name.clone()) {
|
||||
Ok(track_id) => {
|
||||
// Store bidirectional mapping
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
println!("✅ Synced Audio layer '{}' to backend (TrackId: {})", layer_name, track_id);
|
||||
|
||||
// TODO: Sync any existing clips on this layer to the backend
|
||||
// This will be implemented when we add clip synchronization
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Failed to create daw-backend audio track for '{}': {}", layer_name, e);
|
||||
}
|
||||
}
|
||||
AudioLayerType::Sampled => {
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_audio_track_sync(layer_name.clone(), parent_track) {
|
||||
Ok(track_id) => {
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
println!("✅ Synced Audio layer '{}' to backend (TrackId: {}, parent: {:?})", layer_name, track_id, parent_track);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Failed to create daw-backend audio track for '{}': {}", layer_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1374,6 +1395,36 @@ impl EditorApp {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ensure a backend metatrack (group track) exists for a movie clip.
|
||||
/// Returns the metatrack's TrackId, creating one if needed.
|
||||
fn ensure_metatrack_for_clip(&mut self, clip_id: Uuid) -> Option<daw_backend::TrackId> {
|
||||
// Return existing metatrack if already mapped
|
||||
if let Some(&track_id) = self.clip_to_metatrack_map.get(&clip_id) {
|
||||
return Some(track_id);
|
||||
}
|
||||
|
||||
// Create a new metatrack in the backend
|
||||
let clip_name = self.action_executor.document().vector_clips
|
||||
.get(&clip_id)
|
||||
.map(|c| c.name.clone())
|
||||
.unwrap_or_else(|| format!("Clip {}", clip_id));
|
||||
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_group_track_sync(format!("[{}]", clip_name), None) {
|
||||
Ok(track_id) => {
|
||||
self.clip_to_metatrack_map.insert(clip_id, track_id);
|
||||
println!("✅ Created metatrack for clip '{}' (TrackId: {})", clip_name, track_id);
|
||||
return Some(track_id);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Failed to create metatrack for clip '{}': {}", clip_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Split clip instances at the current playhead position
|
||||
///
|
||||
/// Only splits clips on the active layer, plus any clips linked to them
|
||||
|
|
@ -1487,6 +1538,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
|
||||
if let Err(e) = self.action_executor.execute_with_backend(Box::new(action), &mut backend_context) {
|
||||
|
|
@ -1659,6 +1711,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.action_executor
|
||||
|
|
@ -1780,6 +1833,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.action_executor
|
||||
|
|
@ -1894,6 +1948,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
if let Err(e) = self.action_executor.execute_with_backend(Box::new(action), &mut backend_context) {
|
||||
eprintln!("Duplicate clip failed: {}", e);
|
||||
|
|
@ -1973,6 +2028,7 @@ impl EditorApp {
|
|||
// TODO: Add ResetProject command to EngineController
|
||||
self.layer_to_track_map.clear();
|
||||
self.track_to_layer_map.clear();
|
||||
self.clip_to_metatrack_map.clear();
|
||||
|
||||
// Clear file path
|
||||
self.current_file_path = None;
|
||||
|
|
@ -2176,6 +2232,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
match self.action_executor.undo_with_backend(&mut backend_context) {
|
||||
Ok(true) => {
|
||||
|
|
@ -2211,6 +2268,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
match self.action_executor.redo_with_backend(&mut backend_context) {
|
||||
Ok(true) => {
|
||||
|
|
@ -2327,58 +2385,71 @@ impl EditorApp {
|
|||
// Layer menu
|
||||
MenuAction::AddLayer => {
|
||||
// Create a new vector layer with a default name
|
||||
let layer_count = self.action_executor.document().root.children.len();
|
||||
let editing_clip_id = self.editing_context.current_clip_id();
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
let layer_count = context_layers.len();
|
||||
let layer_name = format!("Layer {}", layer_count + 1);
|
||||
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new_vector(layer_name);
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new_vector(layer_name)
|
||||
.with_target_clip(editing_clip_id);
|
||||
let _ = self.action_executor.execute(Box::new(action));
|
||||
|
||||
// Select the newly created layer (last child in the document)
|
||||
if let Some(last_layer) = self.action_executor.document().root.children.last() {
|
||||
// Select the newly created layer (last in context)
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
if let Some(last_layer) = context_layers.last() {
|
||||
self.active_layer_id = Some(last_layer.id());
|
||||
}
|
||||
}
|
||||
MenuAction::AddVideoLayer => {
|
||||
println!("Menu: Add Video Layer");
|
||||
// Create a new video layer with a default name
|
||||
let layer_number = self.action_executor.document().root.children.len() + 1;
|
||||
let editing_clip_id = self.editing_context.current_clip_id();
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
let layer_number = context_layers.len() + 1;
|
||||
let layer_name = format!("Video {}", layer_number);
|
||||
let new_layer = lightningbeam_core::layer::AnyLayer::Video(
|
||||
lightningbeam_core::layer::VideoLayer::new(&layer_name)
|
||||
);
|
||||
|
||||
// Add the layer to the document
|
||||
self.action_executor.document_mut().root.add_child(new_layer.clone());
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new(new_layer)
|
||||
.with_target_clip(editing_clip_id);
|
||||
let _ = self.action_executor.execute(Box::new(action));
|
||||
|
||||
// Set it as the active layer
|
||||
if let Some(last_layer) = self.action_executor.document().root.children.last() {
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
if let Some(last_layer) = context_layers.last() {
|
||||
self.active_layer_id = Some(last_layer.id());
|
||||
}
|
||||
}
|
||||
MenuAction::AddAudioTrack => {
|
||||
// Create a new sampled audio layer with a default name
|
||||
let layer_count = self.action_executor.document().root.children.len();
|
||||
let editing_clip_id = self.editing_context.current_clip_id();
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
let layer_count = context_layers.len();
|
||||
let layer_name = format!("Audio Track {}", layer_count + 1);
|
||||
|
||||
// Create audio layer in document
|
||||
let audio_layer = AudioLayer::new_sampled(layer_name.clone());
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new(AnyLayer::Audio(audio_layer));
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new(AnyLayer::Audio(audio_layer))
|
||||
.with_target_clip(editing_clip_id);
|
||||
let _ = self.action_executor.execute(Box::new(action));
|
||||
|
||||
// Get the newly created layer ID
|
||||
if let Some(last_layer) = self.action_executor.document().root.children.last() {
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
if let Some(last_layer) = context_layers.last() {
|
||||
let layer_id = last_layer.id();
|
||||
self.active_layer_id = Some(layer_id);
|
||||
|
||||
// If inside a clip, ensure a metatrack exists for it
|
||||
let parent_track = editing_clip_id.and_then(|cid| self.ensure_metatrack_for_clip(cid));
|
||||
|
||||
// Create corresponding daw-backend audio track
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_audio_track_sync(layer_name.clone()) {
|
||||
match controller.create_audio_track_sync(layer_name.clone(), parent_track) {
|
||||
Ok(track_id) => {
|
||||
// Store bidirectional mapping
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
println!("✅ Created {} (backend TrackId: {})", layer_name, track_id);
|
||||
println!("✅ Created {} (backend TrackId: {}, parent: {:?})", layer_name, track_id, parent_track);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Failed to create daw-backend audio track for {}: {}", layer_name, e);
|
||||
|
|
@ -2390,23 +2461,30 @@ impl EditorApp {
|
|||
}
|
||||
MenuAction::AddMidiTrack => {
|
||||
// Create a new MIDI audio layer with a default name
|
||||
let layer_count = self.action_executor.document().root.children.len();
|
||||
let editing_clip_id = self.editing_context.current_clip_id();
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
let layer_count = context_layers.len();
|
||||
let layer_name = format!("MIDI Track {}", layer_count + 1);
|
||||
|
||||
// Create MIDI layer in document
|
||||
let midi_layer = AudioLayer::new_midi(layer_name.clone());
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new(AnyLayer::Audio(midi_layer));
|
||||
let action = lightningbeam_core::actions::AddLayerAction::new(AnyLayer::Audio(midi_layer))
|
||||
.with_target_clip(editing_clip_id);
|
||||
let _ = self.action_executor.execute(Box::new(action));
|
||||
|
||||
// Get the newly created layer ID
|
||||
if let Some(last_layer) = self.action_executor.document().root.children.last() {
|
||||
let context_layers = self.action_executor.document().context_layers(editing_clip_id.as_ref());
|
||||
if let Some(last_layer) = context_layers.last() {
|
||||
let layer_id = last_layer.id();
|
||||
self.active_layer_id = Some(layer_id);
|
||||
|
||||
// If inside a clip, ensure a metatrack exists for it
|
||||
let parent_track = editing_clip_id.and_then(|cid| self.ensure_metatrack_for_clip(cid));
|
||||
|
||||
// Create corresponding daw-backend MIDI track
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match controller.create_midi_track_sync(layer_name.clone()) {
|
||||
match controller.create_midi_track_sync(layer_name.clone(), parent_track) {
|
||||
Ok(track_id) => {
|
||||
// Store bidirectional mapping
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
|
|
@ -2627,6 +2705,7 @@ impl EditorApp {
|
|||
path: path.clone(),
|
||||
document,
|
||||
layer_to_track_map: self.layer_to_track_map.clone(),
|
||||
clip_to_metatrack_map: self.clip_to_metatrack_map.clone(),
|
||||
progress_tx,
|
||||
};
|
||||
|
||||
|
|
@ -2776,6 +2855,15 @@ impl EditorApp {
|
|||
eprintln!("📊 [APPLY] Step 5: No saved track mappings (old file format)");
|
||||
}
|
||||
|
||||
// Restore clip-to-metatrack mappings
|
||||
if !loaded_project.clip_to_metatrack_map.is_empty() {
|
||||
for (&clip_id, &track_id) in &loaded_project.clip_to_metatrack_map {
|
||||
self.clip_to_metatrack_map.insert(clip_id, track_id);
|
||||
}
|
||||
eprintln!("📊 [APPLY] Step 5b: Restored {} clip-to-metatrack mappings",
|
||||
loaded_project.clip_to_metatrack_map.len());
|
||||
}
|
||||
|
||||
// Sync any audio layers that don't have a mapping yet (new layers, or old file format)
|
||||
let step6_start = std::time::Instant::now();
|
||||
self.sync_audio_layers_to_backend();
|
||||
|
|
@ -3317,12 +3405,16 @@ impl EditorApp {
|
|||
// Update active layer to the new layer
|
||||
self.active_layer_id = target_layer_id;
|
||||
|
||||
// If inside a clip, ensure a metatrack exists for it
|
||||
let editing_clip_id = self.editing_context.current_clip_id();
|
||||
let parent_track = editing_clip_id.and_then(|cid| self.ensure_metatrack_for_clip(cid));
|
||||
|
||||
// Create a backend audio/MIDI track and add the mapping
|
||||
if let Some(ref controller_arc) = self.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
match asset_info.clip_type {
|
||||
panes::DragClipType::AudioSampled => {
|
||||
match controller.create_audio_track_sync(layer_name.clone()) {
|
||||
match controller.create_audio_track_sync(layer_name.clone(), parent_track) {
|
||||
Ok(track_id) => {
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
|
|
@ -3331,7 +3423,7 @@ impl EditorApp {
|
|||
}
|
||||
}
|
||||
panes::DragClipType::AudioMidi => {
|
||||
match controller.create_midi_track_sync(layer_name.clone()) {
|
||||
match controller.create_midi_track_sync(layer_name.clone(), parent_track) {
|
||||
Ok(track_id) => {
|
||||
self.layer_to_track_map.insert(layer_id, track_id);
|
||||
self.track_to_layer_map.insert(track_id, layer_id);
|
||||
|
|
@ -3432,6 +3524,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
|
||||
if let Err(e) = self.action_executor.execute_with_backend(Box::new(action), &mut backend_context) {
|
||||
|
|
@ -3477,6 +3570,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
|
||||
if let Err(e) = self.action_executor.execute_with_backend(Box::new(audio_action), &mut backend_context) {
|
||||
|
|
@ -3505,8 +3599,9 @@ impl EditorApp {
|
|||
let document = self.action_executor.document();
|
||||
let mut video_instance_info: Option<(uuid::Uuid, uuid::Uuid, f64)> = None; // (layer_id, instance_id, timeline_start)
|
||||
|
||||
// Search all layers for a video clip instance with matching clip_id
|
||||
for layer in &document.root.children {
|
||||
// Search all layers (root + inside movie clips) for a video clip instance with matching clip_id
|
||||
let all_layers = document.all_layers();
|
||||
for layer in &all_layers {
|
||||
if let AnyLayer::Video(video_layer) = layer {
|
||||
for instance in &video_layer.clip_instances {
|
||||
if instance.clip_id == video_clip_id {
|
||||
|
|
@ -3559,6 +3654,7 @@ impl EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
|
||||
if let Err(e) = self.action_executor.execute_with_backend(Box::new(audio_action), &mut backend_context) {
|
||||
|
|
@ -3923,10 +4019,8 @@ impl eframe::App for EditorApp {
|
|||
let clip_instance = ClipInstance::new(doc_clip_id)
|
||||
.with_timeline_start(self.recording_start_time);
|
||||
|
||||
// Add instance to layer
|
||||
if let Some(layer) = self.action_executor.document_mut().root.children.iter_mut()
|
||||
.find(|l| l.id() == layer_id)
|
||||
{
|
||||
// Add instance to layer (works for root and inside movie clips)
|
||||
if let Some(layer) = self.action_executor.document_mut().get_layer_mut(&layer_id) {
|
||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||
audio_layer.clip_instances.push(clip_instance);
|
||||
println!("✅ Created recording clip instance on layer {}", layer_id);
|
||||
|
|
@ -3948,8 +4042,7 @@ impl eframe::App for EditorApp {
|
|||
// First, find the clip_id from the layer (read-only borrow)
|
||||
let clip_id = {
|
||||
let document = self.action_executor.document();
|
||||
document.root.children.iter()
|
||||
.find(|l| l.id() == layer_id)
|
||||
document.get_layer(&layer_id)
|
||||
.and_then(|layer| {
|
||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||
audio_layer.clip_instances.last().map(|i| i.clip_id)
|
||||
|
|
@ -4014,8 +4107,7 @@ impl eframe::App for EditorApp {
|
|||
// First, find the clip instance and clip id
|
||||
let (clip_id, instance_id, timeline_start, trim_start) = {
|
||||
let document = self.action_executor.document();
|
||||
document.root.children.iter()
|
||||
.find(|l| l.id() == layer_id)
|
||||
document.get_layer(&layer_id)
|
||||
.and_then(|layer| {
|
||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||
audio_layer.clip_instances.last().map(|instance| {
|
||||
|
|
@ -4093,8 +4185,7 @@ impl eframe::App for EditorApp {
|
|||
if let Some(layer_id) = self.recording_layer_id {
|
||||
let doc_clip_id = {
|
||||
let document = self.action_executor.document();
|
||||
document.root.children.iter()
|
||||
.find(|l| l.id() == layer_id)
|
||||
document.get_layer(&layer_id)
|
||||
.and_then(|layer| {
|
||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||
audio_layer.clip_instances.last().map(|i| i.clip_id)
|
||||
|
|
@ -4153,8 +4244,7 @@ impl eframe::App for EditorApp {
|
|||
if let Some(layer_id) = self.recording_layer_id {
|
||||
let doc_clip_id = {
|
||||
let document = self.action_executor.document();
|
||||
document.root.children.iter()
|
||||
.find(|l| l.id() == layer_id)
|
||||
document.get_layer(&layer_id)
|
||||
.and_then(|layer| {
|
||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||
audio_layer.clip_instances.last().map(|i| i.clip_id)
|
||||
|
|
@ -4512,6 +4602,7 @@ impl eframe::App for EditorApp {
|
|||
{
|
||||
let time = self.playback_time;
|
||||
let document = self.action_executor.document_mut();
|
||||
// Bake animation transforms for root layers
|
||||
for layer in document.root.children.iter_mut() {
|
||||
if let lightningbeam_core::layer::AnyLayer::Vector(vl) = layer {
|
||||
for ci in &mut vl.clip_instances {
|
||||
|
|
@ -4523,6 +4614,20 @@ impl eframe::App for EditorApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Bake animation transforms for layers inside movie clips
|
||||
for clip in document.vector_clips.values_mut() {
|
||||
for layer_node in clip.layers.roots.iter_mut() {
|
||||
if let lightningbeam_core::layer::AnyLayer::Vector(vl) = &mut layer_node.data {
|
||||
for ci in &mut vl.clip_instances {
|
||||
let (t, opacity) = vl.layer.animation_data.eval_clip_instance_transform(
|
||||
ci.id, time, &ci.transform, ci.opacity,
|
||||
);
|
||||
ci.transform = t;
|
||||
ci.opacity = opacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create render context
|
||||
|
|
@ -4636,6 +4741,7 @@ impl eframe::App for EditorApp {
|
|||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: &self.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut self.clip_instance_to_backend_map,
|
||||
clip_to_metatrack_map: &self.clip_to_metatrack_map,
|
||||
};
|
||||
|
||||
// Execute action with backend synchronization
|
||||
|
|
|
|||
|
|
@ -1310,8 +1310,8 @@ impl AssetLibraryPane {
|
|||
|
||||
/// Check if an asset is currently in use (has clip instances on layers)
|
||||
fn is_asset_in_use(document: &Document, asset_id: Uuid, category: AssetCategory) -> bool {
|
||||
// Check all layers for clip instances referencing this asset
|
||||
for layer in &document.root.children {
|
||||
// Check all layers (root + inside movie clips) for clip instances referencing this asset
|
||||
for layer in document.all_layers() {
|
||||
match layer {
|
||||
lightningbeam_core::layer::AnyLayer::Vector(vl) => {
|
||||
if category == AssetCategory::Vector {
|
||||
|
|
|
|||
|
|
@ -636,10 +636,12 @@ impl NodeGraphPane {
|
|||
let mut controller = audio_controller.lock().unwrap();
|
||||
// Node graph actions don't use clip instances, so we use an empty map
|
||||
let mut empty_clip_map = std::collections::HashMap::new();
|
||||
let empty_metatrack_map = std::collections::HashMap::new();
|
||||
let mut backend_context = lightningbeam_core::action::BackendContext {
|
||||
audio_controller: Some(&mut *controller),
|
||||
layer_to_track_map: shared.layer_to_track_map,
|
||||
clip_instance_to_backend_map: &mut empty_clip_map,
|
||||
clip_to_metatrack_map: &empty_metatrack_map,
|
||||
};
|
||||
|
||||
if let Err(e) = shared.action_executor.execute_with_backend(action, &mut backend_context) {
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ fn effective_clip_duration(
|
|||
let end = vl.group_visibility_end(&clip_instance.id, clip_instance.timeline_start, frame_duration);
|
||||
Some((end - clip_instance.timeline_start).max(0.0))
|
||||
} else {
|
||||
// Movie clips: duration based on internal keyframe content
|
||||
Some(vc.content_duration(document.framerate))
|
||||
// Movie clips: duration based on all internal content (keyframes + clip instances)
|
||||
document.get_clip_duration(&clip_instance.clip_id)
|
||||
}
|
||||
}
|
||||
AnyLayer::Audio(_) => document.get_audio_clip(&clip_instance.clip_id).map(|c| c.duration),
|
||||
|
|
@ -315,9 +315,7 @@ impl TimelinePane {
|
|||
let clip_instance = ClipInstance::new(doc_clip_id)
|
||||
.with_timeline_start(start_time);
|
||||
|
||||
if let Some(layer) = shared.action_executor.document_mut().root.children.iter_mut()
|
||||
.find(|l| l.id() == active_layer_id)
|
||||
{
|
||||
if let Some(layer) = shared.action_executor.document_mut().get_layer_mut(&active_layer_id) {
|
||||
if let lightningbeam_core::layer::AnyLayer::Audio(audio_layer) = layer {
|
||||
audio_layer.clip_instances.push(clip_instance);
|
||||
}
|
||||
|
|
@ -3313,8 +3311,15 @@ impl PaneRenderer for TimelinePane {
|
|||
let new_layer = super::create_layer_for_clip_type(dragging.clip_type, &layer_name);
|
||||
let new_layer_id = new_layer.id();
|
||||
|
||||
// Add the layer
|
||||
shared.action_executor.document_mut().root.add_child(new_layer);
|
||||
// Add the layer to the current editing context
|
||||
if let Some(clip_id) = shared.editing_clip_id {
|
||||
if let Some(clip) = shared.action_executor.document_mut().vector_clips.get_mut(&clip_id) {
|
||||
clip.layers.add_root(new_layer);
|
||||
}
|
||||
shared.action_executor.document_mut().layer_to_clip_map.insert(new_layer_id, clip_id);
|
||||
} else {
|
||||
shared.action_executor.document_mut().root.add_child(new_layer);
|
||||
}
|
||||
|
||||
// Now add the clip to the new layer
|
||||
if dragging.clip_type == DragClipType::Effect {
|
||||
|
|
|
|||
Loading…
Reference in New Issue