1use super::*;
4
5impl NavState {
6
7 pub fn toggle_mute(&mut self) {
8 if let Some(t) = self.current_track_mut() {
9 t.muted = !t.muted;
10 t.sync_to_audio();
11 }
12 }
13
14
15 pub fn toggle_solo(&mut self) {
16 if let Some(t) = self.current_track_mut() {
17 t.soloed = !t.soloed;
18 t.sync_to_audio();
19 }
20 }
21
22
23 pub fn toggle_arm(&mut self) {
24 if let Some(t) = self.current_track_mut() {
25 t.armed = !t.armed;
26 t.sync_to_audio();
27 }
28 }
29
30
31 pub fn digit_input(&mut self, ch: char) {
32 if self.focused_pane == Pane::Tracks && self.track_selected {
33 self.number_buf.push_digit(ch);
34 }
35 }
36
37
38 pub fn tick(&mut self) {
39 if let Some(clip_num) = self.number_buf.check_timeout() {
40 self.jump_to_clip(clip_num);
41 }
42 }
43
44
45 pub fn jump_to_clip(&mut self, clip_number: usize) {
46 if let Some(track) = self.current_track() {
47 tracing::debug!(
48 "jump_to_clip: looking for #{}, track has {} clips: {:?}",
49 clip_number, track.clips.len(),
50 track.clips.iter().map(|c| (c.number, c.start_tick, c.length_ticks)).collect::<Vec<_>>()
51 );
52 if let Some(idx) = track.clips.iter().position(|c| c.number == clip_number) {
53 self.track_element = TrackElement::Clip(idx);
54 self.open_clip_view(self.track_cursor, idx);
55 tracing::debug!("jump_to_clip: selected idx={}", idx);
56 }
57 }
58 }
59
60
61 pub fn activate_element(&mut self) {
62 match self.track_element {
63 TrackElement::Mute => self.toggle_mute(),
64 TrackElement::Solo => self.toggle_solo(),
65 TrackElement::RecordArm => self.toggle_arm(),
66 TrackElement::Fx => {
67 self.fx_menu.open = true;
68 self.fx_menu.cursor = 0;
69 }
70 TrackElement::Volume => { }
71 TrackElement::Clip(idx) => {
72 self.clip_locked = true;
73 self.open_clip_view(self.track_cursor, idx);
74 self.clip_view.clip_tab = ClipTab::PianoRoll;
75 self.clip_view.focus = ClipViewFocus::PianoRoll;
76 tracing::debug!(
77 "clip locked: track={} clip={} start={} len={}",
78 self.track_cursor, idx,
79 self.tracks.get(self.track_cursor).and_then(|t| t.clips.get(idx)).map(|c| c.start_tick).unwrap_or(-1),
80 self.tracks.get(self.track_cursor).and_then(|t| t.clips.get(idx)).map(|c| c.length_ticks).unwrap_or(-1),
81 );
82 }
83 _ => {}
84 }
85 }
86
87 pub fn add_instrument_track(
95 &mut self,
96 instrument: InstrumentType,
97 mixer_id: usize,
98 handle: std::sync::Arc<phosphor_core::project::TrackHandle>,
99 ) {
100 let name = match instrument {
101 InstrumentType::Synth => "synth",
102 InstrumentType::DrumRack => "drums",
103 InstrumentType::DX7 => "dx7",
104 InstrumentType::Jupiter8 => "jup8",
105 InstrumentType::Odyssey => "odyss",
106 InstrumentType::Juno60 => "juno",
107 InstrumentType::Sampler => "smplr",
108 };
109
110 let insert_pos = self.tracks.iter().position(|t| {
112 matches!(t.kind, TrackKind::SendA | TrackKind::SendB | TrackKind::Master)
113 }).unwrap_or(self.tracks.len());
114
115 let color = insert_pos % 8;
116 let mut track = TrackState::new(name, color, true, TrackKind::Instrument, vec![]);
117 track.mixer_id = Some(mixer_id);
118 track.handle = Some(handle);
119 track.instrument_type = Some(instrument);
120 track.synth_params = match instrument {
121 InstrumentType::Synth | InstrumentType::Sampler => {
122 phosphor_dsp::synth::PARAM_DEFAULTS.to_vec()
123 }
124 InstrumentType::DrumRack => {
125 phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec()
126 }
127 InstrumentType::DX7 => {
128 phosphor_dsp::dx7::PARAM_DEFAULTS.to_vec()
129 }
130 InstrumentType::Jupiter8 => {
131 phosphor_dsp::jupiter::PARAM_DEFAULTS.to_vec()
132 }
133 InstrumentType::Odyssey => {
134 phosphor_dsp::odyssey::PARAM_DEFAULTS.to_vec()
135 }
136 InstrumentType::Juno60 => {
137 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
138 }
139 };
140 track.sync_to_audio();
142 self.tracks.insert(insert_pos, track);
143
144 self.track_cursor = insert_pos;
146 if self.track_cursor >= self.track_scroll + MAX_VISIBLE_TRACKS {
147 self.track_scroll = self.track_cursor + 1 - MAX_VISIBLE_TRACKS;
148 }
149
150 self.track_selected = true;
152 self.track_element = TrackElement::Label;
153 self.show_current_track_controls();
154 }
155
156
157 pub fn open_clip_view(&mut self, track_idx: usize, clip_idx: usize) {
158 self.clip_view_visible = true;
159 self.clip_view_target = Some((track_idx, clip_idx));
160 self.clip_view.fx_cursor = 0;
161 tracing::debug!(
162 "open_clip_view: track={} clip={} (notes={})",
163 track_idx, clip_idx,
164 self.tracks.get(track_idx).and_then(|t| t.clips.get(clip_idx)).map(|c| c.notes.len()).unwrap_or(0)
165 );
166 }
167
168 pub fn fx_menu_select(&mut self) {
173 if let Some(fx_type) = FxType::ALL.get(self.fx_menu.cursor) {
175 let inst = FxInstance::new(*fx_type);
176 if let Some(t) = self.current_track_mut() {
177 t.fx_chain.push(inst);
178 }
179 }
180 self.fx_menu.open = false;
181 }
182
183
184 pub fn active_fx_chain_len(&self) -> usize {
185 match self.clip_view.fx_panel_tab {
186 FxPanelTab::TrackFx | FxPanelTab::Synth => {
187 self.current_track().map(|t| t.fx_chain.len().max(1)).unwrap_or(1)
188 }
189 }
190 }
191
192 pub fn sync_clip_view_target(&mut self) {
195 if self.track_selected {
196 if let TrackElement::Clip(idx) = self.track_element {
197 let track_idx = self.track_cursor;
198 if let Some(track) = self.tracks.get(track_idx) {
199 if idx < track.clips.len() {
200 self.clip_view_target = Some((track_idx, idx));
201 self.clip_view_visible = true;
202 return;
203 }
204 }
205 }
206 }
207 }
208
209 pub fn dedup_clips(&mut self) -> Vec<(usize, usize)> {
213 let ppq = phosphor_core::transport::Transport::PPQ;
214 let tolerance = ppq;
215 let mut removed = Vec::new();
216
217 for track in &mut self.tracks {
218 if track.clips.len() < 2 { continue; }
219
220 track.clips.sort_by(|a, b| {
221 a.start_tick.cmp(&b.start_tick)
222 .then(b.length_ticks.cmp(&a.length_ticks))
223 });
224
225 let mut i = 0;
226 while i + 1 < track.clips.len() {
227 let starts_close = (track.clips[i].start_tick - track.clips[i + 1].start_tick).abs() <= tolerance;
228 if starts_close {
229 let shorter_len = track.clips[i + 1].length_ticks;
230 let longer_len = track.clips[i].length_ticks;
231 if longer_len > 0 {
232 let scale = shorter_len as f64 / longer_len as f64;
233 let absorbed: Vec<_> = track.clips[i + 1].notes.iter().map(|n| {
234 let mut rescaled = *n;
235 rescaled.start_frac *= scale;
236 rescaled.duration_frac *= scale;
237 rescaled
238 }).collect();
239 track.clips[i].notes.extend(absorbed);
240 }
241 tracing::debug!(
242 "dedup: absorbed clip #{} (len={}) into clip #{} (len={}) on '{}'",
243 track.clips[i + 1].number, shorter_len,
244 track.clips[i].number, longer_len, track.name
245 );
246 if let Some(mid) = track.mixer_id {
248 removed.push((mid, i + 1));
249 }
250 track.clips.remove(i + 1);
251 } else {
252 i += 1;
253 }
254 }
255
256 for (idx, clip) in track.clips.iter_mut().enumerate() {
257 clip.number = idx + 1;
258 }
259 }
260 removed
261 }
262
263 pub fn receive_clip_snapshot(&mut self, snap: phosphor_core::clip::ClipSnapshot, is_recording: bool) -> Option<(usize, usize)> {
272 tracing::debug!(
273 "clip received: track={} events={} notes={} ticks={}..{} recording={}",
274 snap.track_id, snap.event_count, snap.notes.len(),
275 snap.start_tick, snap.start_tick + snap.length_ticks, is_recording,
276 );
277
278 if !is_recording && self.recording_grace == 0 {
284 tracing::debug!("IGNORED: snapshot while not recording (stale from panic/reset)");
285 return None;
286 }
287 if !is_recording && self.recording_grace > 0 {
289 self.recording_grace -= 1;
290 }
291
292 let track_idx = match self.tracks.iter().position(|t| t.mixer_id == Some(snap.track_id)) {
294 Some(idx) => idx,
295 None => return None,
296 };
297
298 let mut absorbed_count = 0usize;
299 {
300 let track = &mut self.tracks[track_idx];
301 let ppq = phosphor_core::transport::Transport::PPQ;
302 let beats = (snap.length_ticks as f64 / ppq as f64).ceil() as u16;
303 let width = beats.max(2);
304 let snap_end = snap.start_tick + snap.length_ticks;
305
306 let mut absorbed_notes = Vec::new();
309 track.clips.retain(|c| {
310 let c_end = c.start_tick + c.length_ticks;
311 let covered = c.start_tick >= snap.start_tick && c_end <= snap_end;
312 if covered {
313 tracing::debug!(
314 " absorbing clip #{}: tick {}..{} (snap covers {}..{})",
315 c.number, c.start_tick, c_end, snap.start_tick, snap_end
316 );
317 let offset = (c.start_tick - snap.start_tick) as f64 / snap.length_ticks as f64;
319 let scale = c.length_ticks as f64 / snap.length_ticks as f64;
320 for mut n in c.notes.clone() {
321 n.start_frac = n.start_frac * scale + offset;
322 n.duration_frac *= scale;
323 absorbed_notes.push(n);
324 }
325 absorbed_count += 1;
326 false
327 } else {
328 true
329 }
330 });
331
332 let mut all_notes = absorbed_notes;
334 all_notes.extend(snap.notes);
335
336 let merge_tolerance = ppq;
338 if let Some(existing) = track.clips.iter_mut().find(|c| {
339 (c.start_tick - snap.start_tick).abs() <= merge_tolerance
340 }) {
341 if snap.length_ticks != existing.length_ticks && existing.length_ticks > 0 {
343 let scale = snap.length_ticks as f64 / existing.length_ticks as f64;
344 let offset = (snap.start_tick - existing.start_tick) as f64 / existing.length_ticks as f64;
345 for n in &mut all_notes {
346 n.start_frac = n.start_frac * scale + offset;
347 n.duration_frac *= scale;
348 }
349 }
350 existing.notes.extend(all_notes);
351 existing.has_content = true;
352 existing.length_ticks = existing.length_ticks.max(snap.length_ticks);
353 existing.width = width.max(existing.width);
354 tracing::debug!(
355 " merged into existing clip: now {} notes, len={}",
356 existing.notes.len(), existing.length_ticks
357 );
358 } else {
359 let clip_number = track.clips.len() + 1;
361 tracing::debug!(
362 " new clip: #{} at tick {} len {} ({} notes, absorbed {})",
363 clip_number, snap.start_tick, snap.length_ticks, all_notes.len(), absorbed_count
364 );
365 track.clips.push(Clip {
366 number: clip_number,
367 width,
368 has_content: true,
369 start_tick: snap.start_tick,
370 length_ticks: snap.length_ticks,
371 notes: all_notes,
372 hidden_notes: Vec::new(),
373 });
374 }
375
376 for (i, c) in track.clips.iter_mut().enumerate() {
378 c.number = i + 1;
379 }
380 }
381
382 if let Some((ti, ci)) = self.clip_view_target {
385 if ti == track_idx {
386 let num_clips = self.tracks[track_idx].clips.len();
387 if num_clips == 0 {
388 self.clip_view_target = None;
389 self.clip_view_visible = false;
390 } else if ci >= num_clips {
391 self.clip_view_target = Some((track_idx, num_clips - 1));
393 tracing::debug!(
394 " clip_view_target fixed: {} -> {}", ci, num_clips - 1
395 );
396 }
397 }
398 }
399
400 if absorbed_count > 0 {
402 Some((snap.track_id, absorbed_count))
403 } else {
404 None
405 }
406 }
407}
408
409pub fn initial_tracks() -> Vec<TrackState> {
413 vec![
414 TrackState::new("snd a", 5, false, TrackKind::SendA, vec![]),
415 TrackState::new("snd b", 6, false, TrackKind::SendB, vec![]),
416 TrackState::new("mstr", 7, false, TrackKind::Master, vec![]),
417 ]
418}