use super::*;
impl App {
fn capture_clip_state(&self, track_idx: usize, clip_idx: usize) -> Option<crate::state::undo::UndoAction> {
let track = self.nav.tracks.get(track_idx)?;
let clip = track.clips.get(clip_idx)?;
Some(crate::state::undo::UndoAction::ModifyClip {
track_idx,
clip_idx,
prev_start: clip.start_tick,
prev_length: clip.length_ticks,
prev_notes: clip.notes.clone(),
prev_hidden: clip.hidden_notes.clone(),
})
}
pub(crate) fn move_clip(&mut self, clip_idx: usize, direction: i64) {
use crate::debug_log as dbg;
let ppq = phosphor_core::transport::Transport::PPQ;
let beat_ticks = ppq;
let track_idx = self.nav.track_cursor;
if let Some(track) = self.nav.tracks.get(track_idx) {
if let Some(clip) = track.clips.get(clip_idx) {
let old_start = clip.start_tick;
let clip_len = clip.length_ticks;
let mut new_start = (old_start + direction * beat_ticks).max(0);
if direction < 0 {
let prev_end = track.clips.iter()
.filter(|c| c.start_tick < old_start)
.map(|c| c.start_tick + c.length_ticks)
.max();
if let Some(pe) = prev_end {
new_start = new_start.max(pe);
}
} else {
let next_start = track.clips.iter()
.filter(|c| c.start_tick > old_start)
.map(|c| c.start_tick)
.min();
if let Some(ns) = next_start {
new_start = new_start.min(ns - clip_len).max(0);
}
}
if new_start == old_start { return; }
if let Some(undo) = self.capture_clip_state(track_idx, clip_idx) {
self.nav.undo_stack.push(undo);
}
let track = self.nav.tracks.get_mut(track_idx).unwrap();
let clip = track.clips.get_mut(clip_idx).unwrap();
clip.start_tick = new_start;
dbg::system(&format!("clip move: {} → {} ticks", old_start, new_start));
self.sync_clip_to_audio(track_idx, clip_idx);
self.status_message = Some((
format!("clip moved to beat {}", new_start / ppq + 1),
std::time::Instant::now(),
));
}
}
}
pub(crate) fn move_clip_right_edge(&mut self, clip_idx: usize, direction: i64) {
use crate::debug_log as dbg;
let ppq = phosphor_core::transport::Transport::PPQ;
let beat_ticks = ppq;
let track_idx = self.nav.track_cursor;
if let Some(track) = self.nav.tracks.get(track_idx) {
if let Some(clip) = track.clips.get(clip_idx) {
let old_len = clip.length_ticks;
let clip_start = clip.start_tick;
let mut new_len = (old_len + direction * beat_ticks).max(ppq);
let next_start = track.clips.iter()
.filter(|c| c.start_tick > clip_start)
.map(|c| c.start_tick)
.min();
if let Some(ns) = next_start {
new_len = new_len.min(ns - clip_start).max(ppq);
}
if new_len == old_len { return; }
if let Some(undo) = self.capture_clip_state(track_idx, clip_idx) {
self.nav.undo_stack.push(undo);
}
let track = self.nav.tracks.get_mut(track_idx).unwrap();
let clip = track.clips.get_mut(clip_idx).unwrap();
let mut all_ticks: Vec<(i64, i64, u8, u8)> = clip.notes.drain(..)
.map(|n| {
let start_tick = (n.start_frac * old_len as f64) as i64;
let dur_tick = (n.duration_frac * old_len as f64) as i64;
(start_tick, dur_tick, n.note, n.velocity)
})
.collect();
all_ticks.extend(clip.hidden_notes.drain(..));
let mut visible = Vec::new();
let mut hidden = Vec::new();
for (st, dur, note, vel) in all_ticks {
if st < new_len {
let clamped_dur = dur.min(new_len - st);
visible.push(phosphor_core::clip::NoteSnapshot {
note,
velocity: vel,
start_frac: st as f64 / new_len as f64,
duration_frac: clamped_dur as f64 / new_len as f64,
});
} else {
hidden.push((st, dur, note, vel));
}
}
clip.notes = visible;
clip.hidden_notes = hidden;
clip.length_ticks = new_len;
let beats = (new_len as f64 / ppq as f64).ceil() as u16;
clip.width = beats.max(2);
dbg::system(&format!(
"clip right edge: len {} → {}, {} visible, {} hidden",
old_len, new_len, clip.notes.len(), clip.hidden_notes.len()
));
self.sync_clip_to_audio(track_idx, clip_idx);
self.status_message = Some((
format!("clip length: {} beats", new_len / ppq),
std::time::Instant::now(),
));
}
}
}
pub(crate) fn move_clip_left_edge(&mut self, clip_idx: usize, direction: i64) {
use crate::debug_log as dbg;
let ppq = phosphor_core::transport::Transport::PPQ;
let beat_ticks = ppq;
let track_idx = self.nav.track_cursor;
if let Some(track) = self.nav.tracks.get(track_idx) {
if let Some(clip) = track.clips.get(clip_idx) {
let old_start = clip.start_tick;
let old_len = clip.length_ticks;
let end_tick = old_start + old_len;
let mut new_start = (old_start + direction * beat_ticks).max(0);
if new_start >= end_tick - ppq { return; }
let prev_end = track.clips.iter()
.filter(|c| c.start_tick < old_start)
.map(|c| c.start_tick + c.length_ticks)
.max();
if let Some(pe) = prev_end {
new_start = new_start.max(pe);
}
if new_start == old_start { return; }
let new_len = end_tick - new_start;
if let Some(undo) = self.capture_clip_state(track_idx, clip_idx) {
self.nav.undo_stack.push(undo);
}
let track = self.nav.tracks.get_mut(track_idx).unwrap();
let clip = track.clips.get_mut(clip_idx).unwrap();
let mut all_ticks: Vec<(i64, i64, u8, u8)> = clip.notes.drain(..)
.map(|n| {
let abs_start = old_start + (n.start_frac * old_len as f64) as i64;
let dur_tick = (n.duration_frac * old_len as f64) as i64;
(abs_start, dur_tick, n.note, n.velocity)
})
.collect();
for (st, dur, note, vel) in clip.hidden_notes.drain(..) {
all_ticks.push((old_start + st, dur, note, vel));
}
let mut visible = Vec::new();
let mut hidden = Vec::new();
for (abs_st, dur, note, vel) in all_ticks {
let rel = abs_st - new_start;
if rel >= 0 && rel < new_len {
visible.push(phosphor_core::clip::NoteSnapshot {
note, velocity: vel,
start_frac: rel as f64 / new_len as f64,
duration_frac: dur.min(new_len - rel) as f64 / new_len as f64,
});
} else {
hidden.push((abs_st - new_start, dur, note, vel));
}
}
clip.notes = visible;
clip.hidden_notes = hidden;
clip.start_tick = new_start;
clip.length_ticks = new_len;
let beats = (new_len as f64 / ppq as f64).ceil() as u16;
clip.width = beats.max(2);
dbg::system(&format!(
"clip left edge: start {} → {}, len {}, {} visible, {} hidden",
old_start, new_start, new_len, clip.notes.len(), clip.hidden_notes.len()
));
self.sync_clip_to_audio(track_idx, clip_idx);
self.status_message = Some((
format!("clip start: beat {}", new_start / ppq + 1),
std::time::Instant::now(),
));
}
}
}
pub(crate) fn yank_clip(&mut self, clip_idx: usize) {
let track_idx = self.nav.track_cursor;
if let Some(track) = self.nav.tracks.get(track_idx) {
if let Some(clip) = track.clips.get(clip_idx) {
self.yanked_clip = Some(clip.clone());
self.yanked_clip_start = clip.start_tick;
self.status_message = Some((
format!("clip {} yanked", clip_idx + 1),
std::time::Instant::now(),
));
}
}
}
fn paste_clip_at(&mut self, start_tick: i64) {
use crate::debug_log as dbg;
let yanked = match self.yanked_clip.clone() {
Some(c) => c,
None => {
self.status_message = Some(("no clip to paste".into(), std::time::Instant::now()));
return;
}
};
let track_idx = self.nav.track_cursor;
if let Some(track) = self.nav.tracks.get_mut(track_idx) {
let mut new_clip = yanked;
new_clip.start_tick = start_tick;
new_clip.number = track.clips.len() + 1;
if let Some(mixer_id) = track.mixer_id {
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::CreateClip {
track_id: mixer_id,
start_tick: new_clip.start_tick,
length_ticks: new_clip.length_ticks,
});
let events = phosphor_core::clip::NoteSnapshot::to_clip_events(
&new_clip.notes, new_clip.length_ticks,
);
let new_idx = track.clips.len();
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::UpdateClip {
track_id: mixer_id,
clip_index: new_idx,
events,
});
}
let new_idx = track.clips.len();
track.clips.push(new_clip);
dbg::system(&format!("pasted clip to track {} at tick {}", track_idx, start_tick));
self.nav.undo_stack.push(crate::state::undo::UndoAction::AddClip {
track_idx, clip_idx: new_idx,
});
self.nav.track_element = crate::state::TrackElement::Clip(new_idx);
self.nav.open_clip_view(track_idx, new_idx);
}
}
pub(crate) fn paste_clip_after(&mut self, clip_idx: usize) {
let track_idx = self.nav.track_cursor;
let after_tick = self.nav.tracks.get(track_idx)
.and_then(|t| t.clips.get(clip_idx))
.map(|c| c.start_tick + c.length_ticks)
.unwrap_or(0);
self.paste_clip_at(after_tick);
self.status_message = Some((
format!("clip pasted at beat {}", after_tick / phosphor_core::transport::Transport::PPQ + 1),
std::time::Instant::now(),
));
}
pub(crate) fn paste_clip_to_track(&mut self) {
let start_tick = self.yanked_clip_start;
self.paste_clip_at(start_tick);
self.status_message = Some(("clip pasted to track".into(), std::time::Instant::now()));
}
pub(crate) fn duplicate_clip(&mut self, clip_idx: usize) {
self.yank_clip(clip_idx);
self.paste_clip_after(clip_idx);
self.status_message = Some(("clip duplicated".into(), std::time::Instant::now()));
}
pub(crate) fn sync_clip_to_audio(&self, track_idx: usize, clip_idx: usize) {
use crate::debug_log as dbg;
if let Some(track) = self.nav.tracks.get(track_idx) {
if let (Some(mixer_id), Some(clip)) = (track.mixer_id, track.clips.get(clip_idx)) {
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::UpdateClipPosition {
track_id: mixer_id,
clip_index: clip_idx,
start_tick: clip.start_tick,
length_ticks: clip.length_ticks,
});
let events = phosphor_core::clip::NoteSnapshot::to_clip_events(
&clip.notes, clip.length_ticks,
);
let event_count = events.len();
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::UpdateClip {
track_id: mixer_id,
clip_index: clip_idx,
events,
});
dbg::system(&format!(
"sync clip audio: track={} clip={} start={} len={} events={}",
mixer_id, clip_idx, clip.start_tick, clip.length_ticks, event_count
));
}
}
}
pub(crate) fn sync_dedup_to_audio(&mut self) {
use crate::debug_log as dbg;
let removed = self.nav.dedup_clips();
for &(mixer_id, clip_index) in removed.iter().rev() {
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::RemoveClip {
track_id: mixer_id,
clip_index,
});
dbg::system(&format!("dedup audio: removed clip {} on mixer {}", clip_index, mixer_id));
}
let affected_mixers: Vec<usize> = removed.iter().map(|&(mid, _)| mid).collect();
for track in &self.nav.tracks {
if let Some(mid) = track.mixer_id {
if !affected_mixers.contains(&mid) { continue; }
for (ci, clip) in track.clips.iter().enumerate() {
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::UpdateClipPosition {
track_id: mid,
clip_index: ci,
start_tick: clip.start_tick,
length_ticks: clip.length_ticks,
});
let events = phosphor_core::clip::NoteSnapshot::to_clip_events(
&clip.notes, clip.length_ticks,
);
let _ = self.engine.shared.mixer_command_tx.send(MixerCommand::UpdateClip {
track_id: mid,
clip_index: ci,
events,
});
}
dbg::system(&format!("dedup audio: resynced {} clips on mixer {}", track.clips.len(), mid));
}
}
}
}