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 pub fn adjust_volume(&mut self, steps: i32) -> Option<f32> {
40 self.current_track_mut().map(|t| t.adjust_volume(steps))
41 }
42
43
44 pub fn digit_input(&mut self, ch: char) {
45 if self.focused_pane == Pane::Tracks && self.track_selected {
46 self.number_buf.push_digit(ch);
47 }
48 }
49
50
51 pub fn tick(&mut self) {
52 if let Some(clip_num) = self.number_buf.check_timeout() {
53 self.jump_to_clip(clip_num);
54 }
55 }
56
57
58 pub fn jump_to_clip(&mut self, clip_number: usize) {
59 if let Some(track) = self.current_track() {
60 tracing::debug!(
61 "jump_to_clip: looking for #{}, track has {} clips: {:?}",
62 clip_number, track.clips.len(),
63 track.clips.iter().map(|c| (c.number, c.start_tick, c.length_ticks)).collect::<Vec<_>>()
64 );
65 if let Some(idx) = track.clips.iter().position(|c| c.number == clip_number) {
66 self.track_element = TrackElement::Clip(idx);
67 self.open_clip_view(self.track_cursor, idx);
68 tracing::debug!("jump_to_clip: selected idx={}", idx);
69 }
70 }
71 }
72
73
74 pub fn activate_element(&mut self) {
75 match self.track_element {
76 TrackElement::Mute => self.toggle_mute(),
77 TrackElement::Solo => self.toggle_solo(),
78 TrackElement::RecordArm => self.toggle_arm(),
79 TrackElement::Fx => {
80 self.fx_menu.open = true;
81 self.fx_menu.cursor = 0;
82 }
83 TrackElement::Volume => {
84 if self.current_track().is_some_and(super::TrackState::is_live) {
90 self.element_locked = true;
91 }
92 }
93 TrackElement::Clip(idx) => {
94 self.element_locked = true;
95 self.open_clip_view(self.track_cursor, idx);
96 self.clip_view.clip_tab = ClipTab::PianoRoll;
97 self.clip_view.focus = ClipViewFocus::PianoRoll;
98 tracing::debug!(
99 "clip locked: track={} clip={} start={} len={}",
100 self.track_cursor, idx,
101 self.tracks.get(self.track_cursor).and_then(|t| t.clips.get(idx)).map(|c| c.start_tick).unwrap_or(-1),
102 self.tracks.get(self.track_cursor).and_then(|t| t.clips.get(idx)).map(|c| c.length_ticks).unwrap_or(-1),
103 );
104 }
105 _ => {}
106 }
107 }
108
109 pub fn add_instrument_track(
117 &mut self,
118 instrument: InstrumentType,
119 mixer_id: usize,
120 handle: std::sync::Arc<phosphor_core::project::TrackHandle>,
121 ) {
122 let name = match instrument {
123 InstrumentType::Synth => "synth",
124 InstrumentType::DrumRack => "drums",
125 InstrumentType::DX7 => "dx7",
126 InstrumentType::Jupiter8 => "jup8",
127 InstrumentType::Odyssey => "odyss",
128 InstrumentType::Juno60 => "juno",
129 InstrumentType::Rhodes => "rhode",
130 InstrumentType::Sampler => "smplr",
131 InstrumentType::LittlePhatty => "phaty",
132 InstrumentType::Prophet6 => "p6",
133 };
134
135 let insert_pos = self.tracks.iter().position(|t| {
137 matches!(t.kind, TrackKind::SendA | TrackKind::SendB | TrackKind::Master)
138 }).unwrap_or(self.tracks.len());
139
140 let color = insert_pos % 8;
141 let mut track = TrackState::new(name, color, true, TrackKind::Instrument, vec![]);
142 track.mixer_id = Some(mixer_id);
143 track.handle = Some(handle);
144 track.instrument_type = Some(instrument);
145 track.synth_params = match instrument {
146 InstrumentType::Synth | InstrumentType::Sampler => {
147 phosphor_dsp::synth::PARAM_DEFAULTS.to_vec()
148 }
149 InstrumentType::DrumRack => {
150 phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec()
151 }
152 InstrumentType::DX7 => {
153 phosphor_dsp::dx7::PARAM_DEFAULTS.to_vec()
154 }
155 InstrumentType::Jupiter8 => {
156 phosphor_dsp::jupiter::PARAM_DEFAULTS.to_vec()
157 }
158 InstrumentType::Odyssey => {
159 phosphor_dsp::odyssey::PARAM_DEFAULTS.to_vec()
160 }
161 InstrumentType::Juno60 => {
162 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
163 }
164 InstrumentType::Rhodes => {
165 phosphor_dsp::rhodes::PARAM_DEFAULTS.to_vec()
166 }
167 InstrumentType::LittlePhatty => {
168 phosphor_dsp::phatty::PARAM_DEFAULTS.to_vec()
169 }
170 InstrumentType::Prophet6 => {
173 phosphor_dsp::prophet6::param_defaults().to_vec()
174 }
175 };
176 track.sync_to_audio();
178 self.tracks.insert(insert_pos, track);
179
180 self.track_cursor = insert_pos;
182 if self.track_cursor >= self.track_scroll + MAX_VISIBLE_TRACKS {
183 self.track_scroll = self.track_cursor + 1 - MAX_VISIBLE_TRACKS;
184 }
185
186 self.track_selected = true;
188 self.track_element = TrackElement::Label;
189 self.show_current_track_controls();
190 }
191
192
193 pub fn open_clip_view(&mut self, track_idx: usize, clip_idx: usize) {
194 self.clip_view_visible = true;
195 self.clip_view_target = Some((track_idx, clip_idx));
196 self.clip_view.fx_cursor = 0;
197 tracing::debug!(
198 "open_clip_view: track={} clip={} (notes={})",
199 track_idx, clip_idx,
200 self.tracks.get(track_idx).and_then(|t| t.clips.get(clip_idx)).map(|c| c.notes.len()).unwrap_or(0)
201 );
202 }
203
204 pub fn fx_menu_select(&mut self) {
209 if let Some(fx_type) = FxType::ALL.get(self.fx_menu.cursor) {
211 let inst = FxInstance::new(*fx_type);
212 if let Some(t) = self.current_track_mut() {
213 t.fx_chain.push(inst);
214 }
215 }
216 self.fx_menu.open = false;
217 }
218
219
220 pub fn active_fx_chain_len(&self) -> usize {
221 match self.clip_view.fx_panel_tab {
222 FxPanelTab::TrackFx | FxPanelTab::Synth => {
223 self.current_track().map(|t| t.fx_chain.len().max(1)).unwrap_or(1)
224 }
225 }
226 }
227
228 pub fn sync_clip_view_target(&mut self) {
231 if self.track_selected {
232 if let TrackElement::Clip(idx) = self.track_element {
233 let track_idx = self.track_cursor;
234 if let Some(track) = self.tracks.get(track_idx) {
235 if idx < track.clips.len() {
236 self.clip_view_target = Some((track_idx, idx));
237 self.clip_view_visible = true;
238 return;
239 }
240 }
241 }
242 }
243 }
244
245 pub fn dedup_clips(&mut self) -> Vec<(usize, usize)> {
249 let ppq = phosphor_core::transport::Transport::PPQ;
250 let tolerance = ppq;
251 let mut removed = Vec::new();
252
253 for track in &mut self.tracks {
254 if track.clips.len() < 2 { continue; }
255
256 track.clips.sort_by(|a, b| {
257 a.start_tick.cmp(&b.start_tick)
258 .then(b.length_ticks.cmp(&a.length_ticks))
259 });
260
261 let mut i = 0;
262 while i + 1 < track.clips.len() {
263 let starts_close = (track.clips[i].start_tick - track.clips[i + 1].start_tick).abs() <= tolerance;
264 if starts_close {
265 let shorter_len = track.clips[i + 1].length_ticks;
266 let longer_len = track.clips[i].length_ticks;
267 if longer_len > 0 {
268 let scale = shorter_len as f64 / longer_len as f64;
269 let absorbed: Vec<_> = track.clips[i + 1].notes.iter().map(|n| {
270 let mut rescaled = *n;
271 rescaled.start_frac *= scale;
272 rescaled.duration_frac *= scale;
273 rescaled
274 }).collect();
275 track.clips[i].notes.extend(absorbed);
276 }
277 tracing::debug!(
278 "dedup: absorbed clip #{} (len={}) into clip #{} (len={}) on '{}'",
279 track.clips[i + 1].number, shorter_len,
280 track.clips[i].number, longer_len, track.name
281 );
282 if let Some(mid) = track.mixer_id {
284 removed.push((mid, i + 1));
285 }
286 track.clips.remove(i + 1);
287 } else {
288 i += 1;
289 }
290 }
291
292 for (idx, clip) in track.clips.iter_mut().enumerate() {
293 clip.number = idx + 1;
294 }
295 }
296 removed
297 }
298
299 pub fn receive_clip_snapshot(&mut self, snap: phosphor_core::clip::ClipSnapshot, is_recording: bool) -> Option<(usize, usize)> {
308 tracing::debug!(
309 "clip received: track={} events={} notes={} ticks={}..{} recording={}",
310 snap.track_id, snap.event_count, snap.notes.len(),
311 snap.start_tick, snap.start_tick + snap.length_ticks, is_recording,
312 );
313
314 if !is_recording && self.recording_grace == 0 {
320 tracing::debug!("IGNORED: snapshot while not recording (stale from panic/reset)");
321 return None;
322 }
323 if !is_recording && self.recording_grace > 0 {
325 self.recording_grace -= 1;
326 }
327
328 let track_idx = match self.tracks.iter().position(|t| t.mixer_id == Some(snap.track_id)) {
330 Some(idx) => idx,
331 None => return None,
332 };
333
334 let mut absorbed_count = 0usize;
335 {
336 let track = &mut self.tracks[track_idx];
337 let ppq = phosphor_core::transport::Transport::PPQ;
338 let beats = (snap.length_ticks as f64 / ppq as f64).ceil() as u16;
339 let width = beats.max(2);
340 let snap_end = snap.start_tick + snap.length_ticks;
341
342 let mut absorbed_notes = Vec::new();
345 track.clips.retain(|c| {
346 let c_end = c.start_tick + c.length_ticks;
347 let covered = c.start_tick >= snap.start_tick && c_end <= snap_end;
348 if covered {
349 tracing::debug!(
350 " absorbing clip #{}: tick {}..{} (snap covers {}..{})",
351 c.number, c.start_tick, c_end, snap.start_tick, snap_end
352 );
353 let offset = (c.start_tick - snap.start_tick) as f64 / snap.length_ticks as f64;
355 let scale = c.length_ticks as f64 / snap.length_ticks as f64;
356 for mut n in c.notes.clone() {
357 n.start_frac = n.start_frac * scale + offset;
358 n.duration_frac *= scale;
359 absorbed_notes.push(n);
360 }
361 absorbed_count += 1;
362 false
363 } else {
364 true
365 }
366 });
367
368 let mut all_notes = absorbed_notes;
370 all_notes.extend(snap.notes);
371
372 let merge_tolerance = ppq;
374 if let Some(existing) = track.clips.iter_mut().find(|c| {
375 (c.start_tick - snap.start_tick).abs() <= merge_tolerance
376 }) {
377 if snap.length_ticks != existing.length_ticks && existing.length_ticks > 0 {
379 let scale = snap.length_ticks as f64 / existing.length_ticks as f64;
380 let offset = (snap.start_tick - existing.start_tick) as f64 / existing.length_ticks as f64;
381 for n in &mut all_notes {
382 n.start_frac = n.start_frac * scale + offset;
383 n.duration_frac *= scale;
384 }
385 }
386 existing.notes.extend(all_notes);
387 existing.has_content = true;
388 existing.length_ticks = existing.length_ticks.max(snap.length_ticks);
389 existing.width = width.max(existing.width);
390 tracing::debug!(
391 " merged into existing clip: now {} notes, len={}",
392 existing.notes.len(), existing.length_ticks
393 );
394 } else {
395 let clip_number = track.clips.len() + 1;
397 tracing::debug!(
398 " new clip: #{} at tick {} len {} ({} notes, absorbed {})",
399 clip_number, snap.start_tick, snap.length_ticks, all_notes.len(), absorbed_count
400 );
401 track.clips.push(Clip {
402 number: clip_number,
403 width,
404 has_content: true,
405 start_tick: snap.start_tick,
406 length_ticks: snap.length_ticks,
407 notes: all_notes,
408 hidden_notes: Vec::new(),
409 });
410 }
411
412 for (i, c) in track.clips.iter_mut().enumerate() {
414 c.number = i + 1;
415 }
416 }
417
418 if let Some((ti, ci)) = self.clip_view_target {
421 if ti == track_idx {
422 let num_clips = self.tracks[track_idx].clips.len();
423 if num_clips == 0 {
424 self.clip_view_target = None;
425 self.clip_view_visible = false;
426 } else if ci >= num_clips {
427 self.clip_view_target = Some((track_idx, num_clips - 1));
429 tracing::debug!(
430 " clip_view_target fixed: {} -> {}", ci, num_clips - 1
431 );
432 }
433 }
434 }
435
436 if absorbed_count > 0 {
438 Some((snap.track_id, absorbed_count))
439 } else {
440 None
441 }
442 }
443}
444
445pub fn initial_tracks() -> Vec<TrackState> {
449 vec![
450 TrackState::new("snd a", 5, false, TrackKind::SendA, vec![]),
451 TrackState::new("snd b", 6, false, TrackKind::SendB, vec![]),
452 TrackState::new("mstr", 7, false, TrackKind::Master, vec![]),
453 ]
454}