1use super::*;
4
5impl NavState {
6
7 pub fn adjust_synth_param(&mut self, delta: f32) -> Option<(usize, usize, f32)> {
10 let idx = self.clip_view.synth_param_cursor;
11 if let Some(track) = self.tracks.get_mut(self.track_cursor) {
12 if idx < track.synth_params.len() {
13 let instrument = track.instrument_type?;
24 let new_val = if crate::discrete::is_discrete(instrument, idx) {
25 crate::discrete::step(instrument, idx, track.synth_params[idx], delta > 0.0)
26 } else {
27 (track.synth_params[idx] + delta).clamp(0.0, 1.0)
28 };
29 track.synth_params[idx] = new_val;
30
31 let is_program_selector = idx == 0
36 || (instrument == InstrumentType::Prophet6
37 && idx == phosphor_dsp::prophet6::P_BANK);
38 if is_program_selector {
44 let new_params: Option<Vec<f32>> = match track.instrument_type {
45 Some(InstrumentType::Synth | InstrumentType::Sampler) => {
46 Some(phosphor_dsp::synth::PhosphorSynth::params_for_patch(new_val).to_vec())
47 }
48 Some(InstrumentType::Jupiter8) => {
49 Some(phosphor_dsp::jupiter::Jupiter8Synth::params_for_patch(new_val).to_vec())
50 }
51 Some(InstrumentType::Odyssey) => {
52 Some(phosphor_dsp::odyssey::OdysseySynth::params_for_patch(new_val).to_vec())
53 }
54 Some(InstrumentType::Juno60) => {
55 Some(phosphor_dsp::juno::Juno60Synth::params_for_patch(new_val).to_vec())
56 }
57 Some(InstrumentType::Rhodes) => {
58 Some(phosphor_dsp::rhodes::RhodesPiano::params_for_patch(new_val).to_vec())
59 }
60 Some(InstrumentType::LittlePhatty) => {
61 Some(phosphor_dsp::phatty::LittlePhatty::params_for_patch(new_val).to_vec())
62 }
63 Some(InstrumentType::Prophet6) => Some(
64 phosphor_dsp::prophet6::params_for_program(
65 track.synth_params[phosphor_dsp::prophet6::P_BANK],
66 track.synth_params[phosphor_dsp::prophet6::P_PROGRAM],
67 )
68 .to_vec(),
69 ),
70 _ => None,
71 };
72 if let Some(preset_params) = new_params {
73 for (slot, v) in track.synth_params.iter_mut().zip(preset_params) {
74 *slot = v;
75 }
76 }
77 }
78
79 if let Some(mixer_id) = track.mixer_id {
80 return Some((mixer_id, idx, new_val));
81 }
82 }
83 }
84 None
85 }
86
87
88 pub fn show_current_track_controls(&mut self) {
92 for track in &self.tracks {
94 if let Some(ref h) = track.handle {
95 h.config.midi_active.store(false, std::sync::atomic::Ordering::Relaxed);
96 }
97 }
98
99 if let Some(track) = self.tracks.get(self.track_cursor) {
100 if track.is_live() {
101 if let Some(ref h) = track.handle {
102 h.config.midi_active.store(true, std::sync::atomic::Ordering::Relaxed);
103 }
104 self.clip_view_visible = true;
105
106 let clip_idx = match self.track_element {
108 super::TrackElement::Clip(i) if i < track.clips.len() => i,
109 _ => 0,
110 };
111 self.clip_view_target = Some((self.track_cursor, clip_idx));
112
113 if track.sequencer.is_some() {
117 self.clip_view.clip_tab = ClipTab::Sequencer;
118 self.clip_view.focus = ClipViewFocus::PianoRoll;
119 self.clip_view.sequencer.focus_band(SeqBand::Grid);
120 } else if !track.clips.is_empty() {
121 self.clip_view.clip_tab = ClipTab::PianoRoll;
122 self.clip_view.focus = ClipViewFocus::PianoRoll;
123 self.clip_view.piano_roll.focus = PianoRollFocus::Navigation;
124 self.clip_view.piano_roll.column = 0;
125 } else {
126 self.clip_view.fx_panel_tab = FxPanelTab::Synth;
127 self.clip_view.focus = ClipViewFocus::FxPanel;
128 self.clip_view.synth_param_cursor = 0;
129 }
130 } else {
131 self.clip_view_visible = false;
133 self.clip_view_target = None;
134 }
135 }
136 }
137
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use phosphor_dsp::{drum_rack, dx7, juno, jupiter, rhodes};
144
145 fn dx7_track() -> NavState {
147 let mut nav = NavState::new(super::super::initial_tracks());
148 let mut track = TrackState::new("dx7", 0, true, TrackKind::Instrument, vec![]);
149 track.instrument_type = Some(InstrumentType::DX7);
150 track.synth_params = dx7::PARAM_DEFAULTS.to_vec();
151 nav.tracks.insert(0, track);
152 nav.track_cursor = 0;
153 nav
154 }
155
156 fn selected(nav: &NavState) -> (usize, usize) {
157 let p = &nav.tracks[0].synth_params;
158 (dx7::bank_index(p[dx7::P_BANK]), dx7::patch_index(p[dx7::P_PATCH]))
159 }
160
161 #[test]
162 fn dx7_selectors_move_one_step_per_keypress() {
163 let mut nav = dx7_track();
167 nav.clip_view.synth_param_cursor = dx7::P_PATCH;
168 let (bank, patch) = selected(&nav);
169 for step in 1..=5 {
170 nav.adjust_synth_param(0.05);
171 assert_eq!(selected(&nav), (bank, patch + step), "patch knob step {step}");
172 }
173 for step in (0..5).rev() {
174 nav.adjust_synth_param(-0.05);
175 assert_eq!(selected(&nav), (bank, patch + step), "patch knob back to {step}");
176 }
177
178 nav.clip_view.synth_param_cursor = dx7::P_BANK;
179 for step in 1..dx7::BANK_COUNT {
180 nav.adjust_synth_param(0.05);
181 assert_eq!(selected(&nav), (step, patch), "bank knob step {step}");
182 }
183 for _ in 0..4 {
185 nav.adjust_synth_param(0.05);
186 }
187 assert_eq!(selected(&nav), (dx7::BANK_COUNT - 1, patch));
188 }
189
190 fn juno_track() -> NavState {
192 let mut nav = NavState::new(super::super::initial_tracks());
193 let mut track = TrackState::new("juno", 0, true, TrackKind::Instrument, vec![]);
194 track.instrument_type = Some(InstrumentType::Juno60);
195 track.synth_params = juno::PARAM_DEFAULTS.to_vec();
196 nav.tracks.insert(0, track);
197 nav.track_cursor = 0;
198 nav
199 }
200
201 fn juno_patch(nav: &NavState) -> usize {
202 juno::patch_index(nav.tracks[0].synth_params[juno::P_PATCH])
203 }
204
205 #[test]
206 fn juno_selectors_move_one_step_per_keypress() {
207 let mut nav = juno_track();
213 nav.clip_view.synth_param_cursor = juno::P_PATCH;
214 for step in 1..juno::PATCH_COUNT {
215 nav.adjust_synth_param(0.05);
216 assert_eq!(juno_patch(&nav), step, "patch knob step {step}");
217 }
218 nav.adjust_synth_param(0.05);
219 assert_eq!(juno_patch(&nav), juno::PATCH_COUNT - 1, "patch knob ran off the top");
220 for step in (0..juno::PATCH_COUNT - 1).rev() {
221 nav.adjust_synth_param(-0.05);
222 assert_eq!(juno_patch(&nav), step, "patch knob back to {step}");
223 }
224
225 for _ in 0..juno::PATCH_COUNT {
228 nav.adjust_synth_param(0.05);
229 }
230 let panel = &nav.tracks[0].synth_params;
231 assert_eq!(juno_patch(&nav), juno::PATCH_COUNT - 1);
232 assert!((panel[juno::P_RESO] - 1.0).abs() < 1e-6, "res {}", panel[juno::P_RESO]);
233
234 let mut nav = juno_track();
237 nav.clip_view.synth_param_cursor = juno::P_PWM_MODE;
238 let label = |nav: &NavState| {
239 juno::discrete_label(juno::P_PWM_MODE, nav.tracks[0].synth_params[juno::P_PWM_MODE])
240 };
241 assert_eq!(label(&nav), Some("LFO"));
242 let mut seen = Vec::new();
243 for _ in 0..3 {
244 nav.adjust_synth_param(0.05);
245 seen.push(label(&nav));
246 }
247 assert_eq!(seen, [Some("MAN"), Some("ENV"), Some("ENV")]);
248 }
249
250 fn rhodes_track() -> NavState {
252 let mut nav = NavState::new(super::super::initial_tracks());
253 let mut track = TrackState::new("rhode", 0, true, TrackKind::Instrument, vec![]);
254 track.instrument_type = Some(InstrumentType::Rhodes);
255 track.synth_params = rhodes::PARAM_DEFAULTS.to_vec();
256 nav.tracks.insert(0, track);
257 nav.track_cursor = 0;
258 nav
259 }
260
261 #[test]
262 fn the_rhodes_patch_knob_moves_one_piano_per_keypress() {
263 let mut nav = rhodes_track();
267 nav.clip_view.synth_param_cursor = rhodes::P_PATCH;
268 let patch = |nav: &NavState| {
269 rhodes::patch_index(nav.tracks[0].synth_params[rhodes::P_PATCH])
270 };
271 assert_eq!(rhodes::PATCH_NAMES[patch(&nav)], "MK1 Stage");
272 for step in 1..rhodes::PATCH_COUNT {
273 nav.adjust_synth_param(0.05);
274 assert_eq!(patch(&nav), step, "patch knob step {step}");
275 }
276 nav.adjust_synth_param(0.05);
277 assert_eq!(patch(&nav), rhodes::PATCH_COUNT - 1, "patch knob ran off the top");
278 let panel = &nav.tracks[0].synth_params;
280 let want = rhodes::RhodesPiano::params_for_patch(panel[rhodes::P_PATCH]);
281 for i in 1..rhodes::PARAM_COUNT {
282 assert!(
283 (panel[i] - want[i]).abs() < 1e-6,
284 "{} came back as {} where the patch says {}",
285 rhodes::PARAM_NAMES[i], panel[i], want[i]
286 );
287 }
288 for step in (0..rhodes::PATCH_COUNT - 1).rev() {
289 nav.adjust_synth_param(-0.05);
290 assert_eq!(patch(&nav), step, "patch knob back to {step}");
291 }
292
293 let mut nav = rhodes_track();
295 nav.clip_view.synth_param_cursor = rhodes::P_VOICING;
296 let before = nav.tracks[0].synth_params.clone();
297 nav.adjust_synth_param(0.05);
298 let after = &nav.tracks[0].synth_params;
299 assert!((after[rhodes::P_VOICING] - (before[rhodes::P_VOICING] + 0.05)).abs() < 1e-6);
300 for i in 0..after.len() {
301 if i != rhodes::P_VOICING {
302 assert_eq!(before[i], after[i], "{} moved with the voicing", rhodes::PARAM_NAMES[i]);
303 }
304 }
305 }
306
307 fn jupiter_track() -> NavState {
309 let mut nav = NavState::new(super::super::initial_tracks());
310 let mut track = TrackState::new("jupiter", 0, true, TrackKind::Instrument, vec![]);
311 track.instrument_type = Some(InstrumentType::Jupiter8);
312 track.synth_params = jupiter::PARAM_DEFAULTS.to_vec();
313 nav.tracks.insert(0, track);
314 nav.track_cursor = 0;
315 nav
316 }
317
318 #[test]
319 fn jupiter_selectors_move_one_step_per_keypress() {
320 let mut nav = jupiter_track();
325 nav.clip_view.synth_param_cursor = jupiter::P_PATCH;
326 let patch = |nav: &NavState| {
327 jupiter::patch_index(nav.tracks[0].synth_params[jupiter::P_PATCH])
328 };
329 for step in 1..jupiter::PATCH_COUNT {
330 nav.adjust_synth_param(0.05);
331 assert_eq!(patch(&nav), step, "patch knob step {step}");
332 }
333 nav.adjust_synth_param(0.05);
334 assert_eq!(patch(&nav), jupiter::PATCH_COUNT - 1, "patch knob ran off the top");
335 for step in (0..jupiter::PATCH_COUNT - 1).rev() {
336 nav.adjust_synth_param(-0.05);
337 assert_eq!(patch(&nav), step, "patch knob back to {step}");
338 }
339
340 let mut nav = jupiter_track();
343 nav.clip_view.synth_param_cursor = jupiter::P_VCO2_WAVE;
344 let label = |nav: &NavState| {
345 jupiter::discrete_label(
346 jupiter::P_VCO2_WAVE,
347 nav.tracks[0].synth_params[jupiter::P_VCO2_WAVE],
348 )
349 };
350 assert_eq!(label(&nav), Some("SAW"));
351 let mut seen = Vec::new();
352 for _ in 0..3 {
353 nav.adjust_synth_param(0.05);
354 seen.push(label(&nav));
355 }
356 assert_eq!(seen, [Some("PLS"), Some("NOISE"), Some("NOISE")]);
357 }
358
359 fn prophet6_track() -> NavState {
361 let mut nav = NavState::new(super::super::initial_tracks());
362 let mut track = TrackState::new("p6", 0, true, TrackKind::Instrument, vec![]);
363 track.instrument_type = Some(InstrumentType::Prophet6);
364 track.synth_params = phosphor_dsp::prophet6::param_defaults().to_vec();
365 nav.tracks.insert(0, track);
366 nav.track_cursor = 0;
367 nav
368 }
369
370 #[test]
378 fn both_prophet_six_selectors_load_the_program() {
379 use phosphor_dsp::prophet6;
380
381 let mut nav = prophet6_track();
382 let panel = |nav: &NavState| nav.tracks[0].synth_params.clone();
383 assert_eq!(panel(&nav), prophet6::params_for_program(0.0, 0.0).to_vec());
384
385 nav.clip_view.synth_param_cursor = prophet6::P_PROGRAM;
387 for step in 1..8 {
388 nav.adjust_synth_param(0.05);
389 let expected = prophet6::params_for_program(
390 nav.tracks[0].synth_params[prophet6::P_BANK],
391 nav.tracks[0].synth_params[prophet6::P_PROGRAM],
392 );
393 assert_eq!(panel(&nav), expected.to_vec(), "program knob step {step}");
394 assert_eq!(
395 prophet6::program_index(
396 nav.tracks[0].synth_params[prophet6::P_BANK],
397 nav.tracks[0].synth_params[prophet6::P_PROGRAM],
398 ),
399 step
400 );
401 }
402
403 nav.clip_view.synth_param_cursor = prophet6::P_BANK;
405 for bank in 1..prophet6::BANK_COUNT {
406 nav.adjust_synth_param(0.05);
407 let expected = prophet6::params_for_program(
408 nav.tracks[0].synth_params[prophet6::P_BANK],
409 nav.tracks[0].synth_params[prophet6::P_PROGRAM],
410 );
411 assert_eq!(panel(&nav), expected.to_vec(), "bank knob step {bank}");
412 assert_eq!(
413 prophet6::program_index(
414 nav.tracks[0].synth_params[prophet6::P_BANK],
415 nav.tracks[0].synth_params[prophet6::P_PROGRAM],
416 ),
417 bank * prophet6::PROGRAMS_PER_BANK + 7,
418 "the bank knob lost the program knob's position"
419 );
420 }
421
422 nav.clip_view.synth_param_cursor = prophet6::P_LP_CUTOFF;
424 let before = panel(&nav);
425 nav.adjust_synth_param(0.05);
426 let after = panel(&nav);
427 for (index, (a, b)) in before.iter().zip(&after).enumerate() {
428 if index != prophet6::P_LP_CUTOFF {
429 assert_eq!(a, b, "{} moved with the cutoff", prophet6::PARAM_NAMES[index]);
430 }
431 }
432 assert!(after[prophet6::P_LP_CUTOFF] > before[prophet6::P_LP_CUTOFF]);
433 }
434
435 fn drum_track() -> NavState {
437 let mut nav = NavState::new(super::super::initial_tracks());
438 let mut track = TrackState::new("drums", 0, true, TrackKind::Instrument, vec![]);
439 track.instrument_type = Some(InstrumentType::DrumRack);
440 track.synth_params = drum_rack::PARAM_DEFAULTS.to_vec();
441 nav.tracks.insert(0, track);
442 nav.track_cursor = 0;
443 nav
444 }
445
446 #[test]
447 fn the_drum_kit_selector_moves_one_kit_per_keypress() {
448 let mut nav = drum_track();
454 nav.clip_view.synth_param_cursor = drum_rack::P_KIT;
455 let kit = |nav: &NavState| {
456 drum_rack::discrete_label(drum_rack::P_KIT, nav.tracks[0].synth_params[drum_rack::P_KIT])
457 };
458 let last = *drum_rack::KIT_LABELS.last().unwrap();
459 assert_eq!(kit(&nav), Some("808"));
460 for label in drum_rack::KIT_LABELS.iter().skip(1) {
461 nav.adjust_synth_param(0.05);
462 assert_eq!(kit(&nav), Some(*label));
463 }
464 nav.adjust_synth_param(0.05);
465 assert_eq!(kit(&nav), Some(last), "the kit knob ran off the top");
466 for label in drum_rack::KIT_LABELS.iter().rev().skip(1) {
467 nav.adjust_synth_param(-0.05);
468 assert_eq!(kit(&nav), Some(*label));
469 }
470
471 nav.clip_view.synth_param_cursor = drum_rack::P_BD_DECAY;
474 let before = nav.tracks[0].synth_params.clone();
475 nav.adjust_synth_param(-0.05);
476 let after = &nav.tracks[0].synth_params;
477 assert!((after[drum_rack::P_BD_DECAY] - 0.45).abs() < 1e-6);
478 for i in 0..after.len() {
479 if i != drum_rack::P_BD_DECAY {
480 assert_eq!(before[i], after[i], "{} moved with the kick's decay", drum_rack::PARAM_NAMES[i]);
481 }
482 }
483 }
484
485 #[test]
486 fn the_dx7_bank_knob_is_the_last_parameter() {
487 let nav = dx7_track();
491 assert_eq!(nav.tracks[0].synth_params.len(), dx7::PARAM_COUNT);
492 assert_eq!(dx7::P_BANK, dx7::PARAM_COUNT - 1);
493 assert_eq!(dx7::PARAM_NAMES[dx7::P_GAIN], "gain", "index 0-7 must not move");
494 }
495}