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 };
132
133 let insert_pos = self.tracks.iter().position(|t| {
135 matches!(t.kind, TrackKind::SendA | TrackKind::SendB | TrackKind::Master)
136 }).unwrap_or(self.tracks.len());
137
138 let color = insert_pos % 8;
139 let mut track = TrackState::new(name, color, true, TrackKind::Instrument, vec![]);
140 track.mixer_id = Some(mixer_id);
141 track.handle = Some(handle);
142 track.instrument_type = Some(instrument);
143 track.synth_params = match instrument {
144 InstrumentType::Synth | InstrumentType::Sampler => {
145 phosphor_dsp::synth::PARAM_DEFAULTS.to_vec()
146 }
147 InstrumentType::DrumRack => {
148 phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec()
149 }
150 InstrumentType::DX7 => {
151 phosphor_dsp::dx7::PARAM_DEFAULTS.to_vec()
152 }
153 InstrumentType::Jupiter8 => {
154 phosphor_dsp::jupiter::PARAM_DEFAULTS.to_vec()
155 }
156 InstrumentType::Odyssey => {
157 phosphor_dsp::odyssey::PARAM_DEFAULTS.to_vec()
158 }
159 InstrumentType::Juno60 => {
160 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
161 }
162 InstrumentType::Rhodes => {
163 phosphor_dsp::rhodes::PARAM_DEFAULTS.to_vec()
164 }
165 };
166 track.sync_to_audio();
168 self.tracks.insert(insert_pos, track);
169
170 self.track_cursor = insert_pos;
172 if self.track_cursor >= self.track_scroll + MAX_VISIBLE_TRACKS {
173 self.track_scroll = self.track_cursor + 1 - MAX_VISIBLE_TRACKS;
174 }
175
176 self.track_selected = true;
178 self.track_element = TrackElement::Label;
179 self.show_current_track_controls();
180 }
181
182
183 pub fn open_clip_view(&mut self, track_idx: usize, clip_idx: usize) {
184 self.clip_view_visible = true;
185 self.clip_view_target = Some((track_idx, clip_idx));
186 self.clip_view.fx_cursor = 0;
187 tracing::debug!(
188 "open_clip_view: track={} clip={} (notes={})",
189 track_idx, clip_idx,
190 self.tracks.get(track_idx).and_then(|t| t.clips.get(clip_idx)).map(|c| c.notes.len()).unwrap_or(0)
191 );
192 }
193
194 pub fn fx_menu_select(&mut self) {
199 if let Some(fx_type) = FxType::ALL.get(self.fx_menu.cursor) {
201 let inst = FxInstance::new(*fx_type);
202 if let Some(t) = self.current_track_mut() {
203 t.fx_chain.push(inst);
204 }
205 }
206 self.fx_menu.open = false;
207 }
208
209
210 pub fn active_fx_chain_len(&self) -> usize {
211 match self.clip_view.fx_panel_tab {
212 FxPanelTab::TrackFx | FxPanelTab::Synth => {
213 self.current_track().map(|t| t.fx_chain.len().max(1)).unwrap_or(1)
214 }
215 }
216 }
217
218 pub fn sync_clip_view_target(&mut self) {
221 if self.track_selected {
222 if let TrackElement::Clip(idx) = self.track_element {
223 let track_idx = self.track_cursor;
224 if let Some(track) = self.tracks.get(track_idx) {
225 if idx < track.clips.len() {
226 self.clip_view_target = Some((track_idx, idx));
227 self.clip_view_visible = true;
228 return;
229 }
230 }
231 }
232 }
233 }
234
235 pub fn dedup_clips(&mut self) -> Vec<(usize, usize)> {
239 let ppq = phosphor_core::transport::Transport::PPQ;
240 let tolerance = ppq;
241 let mut removed = Vec::new();
242
243 for track in &mut self.tracks {
244 if track.clips.len() < 2 { continue; }
245
246 track.clips.sort_by(|a, b| {
247 a.start_tick.cmp(&b.start_tick)
248 .then(b.length_ticks.cmp(&a.length_ticks))
249 });
250
251 let mut i = 0;
252 while i + 1 < track.clips.len() {
253 let starts_close = (track.clips[i].start_tick - track.clips[i + 1].start_tick).abs() <= tolerance;
254 if starts_close {
255 let shorter_len = track.clips[i + 1].length_ticks;
256 let longer_len = track.clips[i].length_ticks;
257 if longer_len > 0 {
258 let scale = shorter_len as f64 / longer_len as f64;
259 let absorbed: Vec<_> = track.clips[i + 1].notes.iter().map(|n| {
260 let mut rescaled = *n;
261 rescaled.start_frac *= scale;
262 rescaled.duration_frac *= scale;
263 rescaled
264 }).collect();
265 track.clips[i].notes.extend(absorbed);
266 }
267 tracing::debug!(
268 "dedup: absorbed clip #{} (len={}) into clip #{} (len={}) on '{}'",
269 track.clips[i + 1].number, shorter_len,
270 track.clips[i].number, longer_len, track.name
271 );
272 if let Some(mid) = track.mixer_id {
274 removed.push((mid, i + 1));
275 }
276 track.clips.remove(i + 1);
277 } else {
278 i += 1;
279 }
280 }
281
282 for (idx, clip) in track.clips.iter_mut().enumerate() {
283 clip.number = idx + 1;
284 }
285 }
286 removed
287 }
288
289 pub fn receive_clip_snapshot(&mut self, snap: phosphor_core::clip::ClipSnapshot, is_recording: bool) -> Option<(usize, usize)> {
298 tracing::debug!(
299 "clip received: track={} events={} notes={} ticks={}..{} recording={}",
300 snap.track_id, snap.event_count, snap.notes.len(),
301 snap.start_tick, snap.start_tick + snap.length_ticks, is_recording,
302 );
303
304 if !is_recording && self.recording_grace == 0 {
310 tracing::debug!("IGNORED: snapshot while not recording (stale from panic/reset)");
311 return None;
312 }
313 if !is_recording && self.recording_grace > 0 {
315 self.recording_grace -= 1;
316 }
317
318 let track_idx = match self.tracks.iter().position(|t| t.mixer_id == Some(snap.track_id)) {
320 Some(idx) => idx,
321 None => return None,
322 };
323
324 let mut absorbed_count = 0usize;
325 {
326 let track = &mut self.tracks[track_idx];
327 let ppq = phosphor_core::transport::Transport::PPQ;
328 let beats = (snap.length_ticks as f64 / ppq as f64).ceil() as u16;
329 let width = beats.max(2);
330 let snap_end = snap.start_tick + snap.length_ticks;
331
332 let mut absorbed_notes = Vec::new();
335 track.clips.retain(|c| {
336 let c_end = c.start_tick + c.length_ticks;
337 let covered = c.start_tick >= snap.start_tick && c_end <= snap_end;
338 if covered {
339 tracing::debug!(
340 " absorbing clip #{}: tick {}..{} (snap covers {}..{})",
341 c.number, c.start_tick, c_end, snap.start_tick, snap_end
342 );
343 let offset = (c.start_tick - snap.start_tick) as f64 / snap.length_ticks as f64;
345 let scale = c.length_ticks as f64 / snap.length_ticks as f64;
346 for mut n in c.notes.clone() {
347 n.start_frac = n.start_frac * scale + offset;
348 n.duration_frac *= scale;
349 absorbed_notes.push(n);
350 }
351 absorbed_count += 1;
352 false
353 } else {
354 true
355 }
356 });
357
358 let mut all_notes = absorbed_notes;
360 all_notes.extend(snap.notes);
361
362 let merge_tolerance = ppq;
364 if let Some(existing) = track.clips.iter_mut().find(|c| {
365 (c.start_tick - snap.start_tick).abs() <= merge_tolerance
366 }) {
367 if snap.length_ticks != existing.length_ticks && existing.length_ticks > 0 {
369 let scale = snap.length_ticks as f64 / existing.length_ticks as f64;
370 let offset = (snap.start_tick - existing.start_tick) as f64 / existing.length_ticks as f64;
371 for n in &mut all_notes {
372 n.start_frac = n.start_frac * scale + offset;
373 n.duration_frac *= scale;
374 }
375 }
376 existing.notes.extend(all_notes);
377 existing.has_content = true;
378 existing.length_ticks = existing.length_ticks.max(snap.length_ticks);
379 existing.width = width.max(existing.width);
380 tracing::debug!(
381 " merged into existing clip: now {} notes, len={}",
382 existing.notes.len(), existing.length_ticks
383 );
384 } else {
385 let clip_number = track.clips.len() + 1;
387 tracing::debug!(
388 " new clip: #{} at tick {} len {} ({} notes, absorbed {})",
389 clip_number, snap.start_tick, snap.length_ticks, all_notes.len(), absorbed_count
390 );
391 track.clips.push(Clip {
392 number: clip_number,
393 width,
394 has_content: true,
395 start_tick: snap.start_tick,
396 length_ticks: snap.length_ticks,
397 notes: all_notes,
398 hidden_notes: Vec::new(),
399 });
400 }
401
402 for (i, c) in track.clips.iter_mut().enumerate() {
404 c.number = i + 1;
405 }
406 }
407
408 if let Some((ti, ci)) = self.clip_view_target {
411 if ti == track_idx {
412 let num_clips = self.tracks[track_idx].clips.len();
413 if num_clips == 0 {
414 self.clip_view_target = None;
415 self.clip_view_visible = false;
416 } else if ci >= num_clips {
417 self.clip_view_target = Some((track_idx, num_clips - 1));
419 tracing::debug!(
420 " clip_view_target fixed: {} -> {}", ci, num_clips - 1
421 );
422 }
423 }
424 }
425
426 if absorbed_count > 0 {
428 Some((snap.track_id, absorbed_count))
429 } else {
430 None
431 }
432 }
433}
434
435pub fn initial_tracks() -> Vec<TrackState> {
439 vec![
440 TrackState::new("snd a", 5, false, TrackKind::SendA, vec![]),
441 TrackState::new("snd b", 6, false, TrackKind::SendB, vec![]),
442 TrackState::new("mstr", 7, false, TrackKind::Master, vec![]),
443 ]
444}