1use anyhow::Result;
14use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent};
15use crossterm::execute;
16use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
17use ratatui::backend::CrosstermBackend;
18use ratatui::layout::{Constraint, Direction, Layout};
19use ratatui::style::{Color, Modifier, Style};
20use ratatui::widgets::{Block, Borders, Paragraph};
21use ratatui::Terminal;
22use std::io;
23use std::path::PathBuf;
24use std::time::{Duration, Instant};
25
26use crate::audio::engine::EngineHandle;
27use crate::audio::preset::PresetKind;
28use crate::audio::track::{Track, TrackParams};
29use crate::math::genetic::{crossover, mutate, Genome};
30use crate::math::harmony::{golden_pentatonic, rand_f32, rand_u32};
31use crate::math::life::Life;
32use crate::math::rhythm;
33use crate::{persistence, recording};
34use std::sync::atomic::Ordering;
35
36const LIFE_ROWS: usize = 8;
37const LIFE_COLS: usize = 22;
38const DEFAULT_EVOLVE_PERIOD: u32 = 8;
40const AUTO_EVOLVE_STRENGTH: f32 = 0.55;
42const STATUS_TTL: Duration = Duration::from_secs(4);
43
44#[derive(Clone, Copy, PartialEq, Eq, Debug)]
45pub enum Focus {
46 Tracks,
47 Params,
48}
49
50pub struct AppState {
51 pub focus: Focus,
52 pub selected_track: usize,
53 pub selected_param: usize,
54 pub should_quit: bool,
55 pub rng_seed: u64,
56
57 pub life: Life,
59 pub last_beat_index: i64,
60 pub last_evolve_beat: i64,
61 pub evolve_period: u32,
62 pub coupling: bool,
63 pub auto_evolve: bool,
64
65 pub status: Option<(Instant, String)>,
67 pub presets_dir: PathBuf,
68 pub recordings_dir: PathBuf,
69}
70
71impl AppState {
72 pub fn new() -> Self {
73 let mut life = Life::random(LIFE_ROWS, LIFE_COLS, 0xBEEF_F00D, 0.22);
74 life.inject_glider(0, 0);
75 life.inject_glider(4, 10);
76 Self {
77 focus: Focus::Tracks,
78 selected_track: 0,
79 selected_param: 0,
80 should_quit: false,
81 rng_seed: 0x00C0_FFEE_DEAD_BEEF,
82 life,
83 last_beat_index: -1,
84 last_evolve_beat: 0,
85 evolve_period: DEFAULT_EVOLVE_PERIOD,
86 coupling: true,
87 auto_evolve: true,
88 status: None,
89 presets_dir: PathBuf::from("presets"),
90 recordings_dir: PathBuf::from("recordings"),
91 }
92 }
93
94 fn set_status(&mut self, text: impl Into<String>) {
95 self.status = Some((Instant::now(), text.into()));
96 }
97
98 fn current_status(&self) -> Option<&str> {
99 match &self.status {
100 Some((at, text)) if at.elapsed() < STATUS_TTL => Some(text),
101 _ => None,
102 }
103 }
104}
105
106impl Default for AppState {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112pub fn run(engine: &EngineHandle) -> Result<()> {
113 enable_raw_mode()?;
114 let mut stdout = io::stdout();
115 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
116 let backend = CrosstermBackend::new(stdout);
117 let mut terminal = Terminal::new(backend)?;
118
119 let res = run_loop(&mut terminal, engine);
120
121 disable_raw_mode()?;
122 execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
123 terminal.show_cursor()?;
124
125 res
126}
127
128fn run_loop<B: ratatui::backend::Backend>(
129 terminal: &mut Terminal<B>,
130 engine: &EngineHandle,
131) -> Result<()> {
132 let mut app = AppState::new();
133 let tick = Duration::from_millis(33);
134 let mut last = Instant::now();
135
136 loop {
137 advance_beat_sync(&mut app, engine);
138 recompute_patterns(engine);
142 terminal.draw(|f| ui(f, engine, &app))?;
143
144 let timeout = tick.saturating_sub(last.elapsed());
145 if event::poll(timeout)? {
146 if let Event::Key(key) = event::read()? {
147 handle_key(key, engine, &mut app);
148 }
149 }
150 if last.elapsed() >= tick {
151 last = Instant::now();
152 }
153 if app.should_quit {
154 return Ok(());
155 }
156 }
157}
158
159fn advance_beat_sync(app: &mut AppState, engine: &EngineHandle) {
162 let t = engine.phase_clock.value();
163 let bpm = engine.global.bpm.value();
164 let cur_beat = (t * bpm / 60.0).floor() as i64;
165
166 if cur_beat <= app.last_beat_index {
167 return;
168 }
169 let steps = (cur_beat - app.last_beat_index).min(4) as usize;
170 for _ in 0..steps {
171 if app.coupling {
172 seed_from_audio(app, engine, cur_beat);
173 }
174 app.life.step();
175 }
176
177 if app.coupling {
181 push_density_to_tracks(app, engine);
182 } else {
183 reset_life_mods(engine);
184 }
185
186 if app.auto_evolve && cur_beat - app.last_evolve_beat >= app.evolve_period as i64 {
187 if let Some((name, before, after)) = evolve_weakest(app, engine) {
188 app.set_status(format!(
189 "evolved {name}: freq {before:.0}→{after:.0} Hz"
190 ));
191 }
192 app.last_evolve_beat = cur_beat;
193 }
194
195 app.last_beat_index = cur_beat;
196}
197
198fn push_density_to_tracks(app: &AppState, engine: &EngineHandle) {
202 let tracks = engine.tracks.lock();
203 for (i, track) in tracks.iter().enumerate() {
204 if i >= app.life.rows {
205 break;
206 }
207 let alive = app.life.row_alive_count(i);
208 let ratio = alive as f32 / app.life.cols as f32;
209 let shaped = ratio.sqrt();
212 track.params.life_mod.set_value(shaped);
213 }
214}
215
216fn reset_life_mods(engine: &EngineHandle) {
218 let tracks = engine.tracks.lock();
219 for t in tracks.iter() {
220 t.params.life_mod.set_value(1.0);
221 }
222}
223
224fn recompute_patterns(engine: &EngineHandle) {
227 let tracks = engine.tracks.lock();
228 for track in tracks.iter() {
229 let hits = track
230 .params
231 .pattern_hits
232 .value()
233 .round()
234 .clamp(0.0, rhythm::STEPS as f32) as u32;
235 let rotation = track
236 .params
237 .pattern_rotation
238 .value()
239 .round()
240 .clamp(0.0, (rhythm::STEPS - 1) as f32) as u32;
241 let bits = rhythm::euclidean_bits(hits, rotation);
242 track.params.pattern_bits.store(bits, Ordering::Relaxed);
243 }
244}
245
246fn seed_from_audio(app: &mut AppState, engine: &EngineHandle, cur_beat: i64) {
249 let col = cur_beat.rem_euclid(app.life.cols as i64) as usize;
250 let tracks = engine.tracks.lock();
251 for (i, track) in tracks.iter().enumerate() {
252 if i >= app.life.rows {
253 break;
254 }
255 let p = &track.params;
256 if p.mute.value() > 0.5 {
257 continue;
258 }
259 let gain = p.gain.value();
260 app.life.set(i, col, true);
263 if gain > 0.45 {
264 app.life.set(i, (col + 1) % app.life.cols, true);
265 }
266 if matches!(track.kind, PresetKind::Heartbeat) {
267 let r0 = i.saturating_sub(1).min(app.life.rows.saturating_sub(3));
269 let c0 = (col + 2) % app.life.cols;
270 for (dr, dc) in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)] {
271 let r = (r0 + dr).min(app.life.rows - 1);
272 let c = (c0 + dc) % app.life.cols;
273 app.life.set(r, c, true);
274 }
275 }
276 }
277}
278
279fn evolve_weakest(app: &mut AppState, engine: &EngineHandle) -> Option<(String, f32, f32)> {
283 let tracks = engine.tracks.lock();
284 let mut weakest: Option<(usize, usize)> = None;
285 for (i, t) in tracks.iter().enumerate() {
286 if i >= app.life.rows {
287 break;
288 }
289 if t.params.mute.value() > 0.5 {
290 continue;
291 }
292 let count = app.life.row_alive_count(i);
293 weakest = match weakest {
294 None => Some((i, count)),
295 Some((_, c)) if count < c => Some((i, count)),
296 s => s,
297 };
298 }
299 let (idx, _) = weakest?;
300 let name = tracks[idx].name.clone();
301 let before = tracks[idx].params.freq.value();
302 let genome = genome_of(&tracks[idx].params);
303 mutate(&genome, &mut app.rng_seed, AUTO_EVOLVE_STRENGTH);
304 let after = tracks[idx].params.freq.value();
305 Some((name, before, after))
306}
307
308fn genome_of(p: &TrackParams) -> Genome<'_> {
309 Genome {
310 freq: &p.freq,
311 cutoff: &p.cutoff,
312 resonance: &p.resonance,
313 reverb_mix: &p.reverb_mix,
314 pulse_depth: &p.pulse_depth,
315 pattern_hits: &p.pattern_hits,
316 pattern_rotation: &p.pattern_rotation,
317 character: &p.character,
318 }
319}
320
321fn ui(f: &mut ratatui::Frame, engine: &EngineHandle, app: &AppState) {
324 let area = f.area();
325
326 let rows = Layout::default()
327 .direction(Direction::Vertical)
328 .constraints([
329 Constraint::Length(3), Constraint::Length(10), Constraint::Length(7), Constraint::Length(12), Constraint::Min(10), Constraint::Length(3), ])
336 .split(area);
337
338 let rec_text = if engine.recorder.is_recording() {
339 format!(" REC ● {:>5.1}s", engine.recorder.elapsed_seconds())
340 } else {
341 "".to_string()
342 };
343 let status_text = app.current_status().map(|s| format!(" · {s}")).unwrap_or_default();
344 let brightness = engine.global.brightness.value();
345 let shelf_db = crate::audio::preset::shelf_gain_db(
346 crate::audio::preset::brightness_to_shelf_gain(brightness as f64),
347 );
348 let lp_cutoff = crate::audio::preset::brightness_to_lp_cutoff(brightness as f64);
349 let header_text = format!(
350 " rust-synth · mstr {:>3.0}% brt {:>3.0}% ({:>+5.1}dB shelf +LP@{:>5.0}Hz) peak L{:>4.2} R{:>4.2} couple {} evolve {} gen {}{}{}",
351 engine.global.master_gain.value() * 100.0,
352 brightness * 100.0,
353 shelf_db,
354 lp_cutoff,
355 engine.peak_l.value(),
356 engine.peak_r.value(),
357 on_off(app.coupling),
358 on_off(app.auto_evolve),
359 app.life.generation,
360 rec_text,
361 status_text,
362 );
363 let header_style = if engine.recorder.is_recording() {
364 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
365 } else {
366 Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
367 };
368 let header = Paragraph::new(header_text)
369 .style(header_style)
370 .block(Block::default().borders(Borders::ALL).title(" rust-synth "));
371 f.render_widget(header, rows[0]);
372
373 let top = Layout::default()
374 .direction(Direction::Horizontal)
375 .constraints([Constraint::Percentage(32), Constraint::Percentage(68)])
376 .split(rows[1]);
377 super::beats::render(f, top[0], engine);
378 super::life::render(f, top[1], engine, app);
379
380 super::pattern::render(f, rows[2], engine, app);
381
382 let mid = Layout::default()
383 .direction(Direction::Horizontal)
384 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
385 .split(rows[3]);
386 super::waveform::render(f, mid[0], engine);
387 super::waveshape::render(f, mid[1], engine, app);
388
389 let body = Layout::default()
390 .direction(Direction::Horizontal)
391 .constraints([
392 Constraint::Percentage(32),
393 Constraint::Percentage(36),
394 Constraint::Percentage(32),
395 ])
396 .split(rows[4]);
397 super::tracks::render(f, body[0], engine, app);
398 super::params::render(f, body[1], engine, app);
399 super::formula::render(f, body[2], engine, app);
400
401 let help = Paragraph::new(match app.focus {
402 Focus::Tracks => " ↑↓trk·Enter→p · a add · d kill · m mute · t/T kind · r rand · e/E mut · x cross · h/H hits · p/P rot · S/s super · w/l save/load · c REC · ,/. bpm · {/} brt · q quit ",
403 Focus::Params => " ↑↓param · ←→adj · Esc←tracks · t/T kind · e/E mut · h/H hits · p/P rot · S/s super · w/l save/load · c REC · ,/. bpm · {/} brt · q quit ",
404 })
405 .block(Block::default().borders(Borders::ALL))
406 .style(Style::default().fg(Color::Gray));
407 f.render_widget(help, rows[5]);
408}
409
410fn on_off(b: bool) -> &'static str {
411 if b { "ON " } else { "off" }
412}
413
414fn short_path(p: &std::path::Path) -> String {
415 p.file_name()
416 .map(|s| s.to_string_lossy().into_owned())
417 .unwrap_or_else(|| p.display().to_string())
418}
419
420fn handle_key(key: KeyEvent, engine: &EngineHandle, app: &mut AppState) {
423 match key.code {
425 KeyCode::Char('q') => {
426 app.should_quit = true;
427 return;
428 }
429 KeyCode::Char(',') => {
430 bpm_nudge(engine, -1.0);
431 return;
432 }
433 KeyCode::Char('.') => {
434 bpm_nudge(engine, 1.0);
435 return;
436 }
437 KeyCode::Char('<') => {
438 bpm_nudge(engine, -5.0);
439 return;
440 }
441 KeyCode::Char('>') => {
442 bpm_nudge(engine, 5.0);
443 return;
444 }
445 KeyCode::Char('[') => {
446 master_nudge(engine, -0.05);
447 return;
448 }
449 KeyCode::Char(']') => {
450 master_nudge(engine, 0.05);
451 return;
452 }
453 KeyCode::Char('{') => {
454 brightness_nudge(engine, -0.05);
455 return;
456 }
457 KeyCode::Char('}') => {
458 brightness_nudge(engine, 0.05);
459 return;
460 }
461 KeyCode::Char('L') => {
462 app.coupling = !app.coupling;
463 return;
464 }
465 KeyCode::Char('O') => {
466 app.auto_evolve = !app.auto_evolve;
467 return;
468 }
469 KeyCode::Char('e') => {
470 mutate_selected(app, engine, 0.3);
471 return;
472 }
473 KeyCode::Char('E') => {
474 mutate_all_active(app, engine, 0.25);
475 return;
476 }
477 KeyCode::Char('x') => {
478 crossover_with_neighbor(app, engine);
479 return;
480 }
481 KeyCode::Char('R') => {
482 app.life = Life::random(LIFE_ROWS, LIFE_COLS, app.rng_seed, 0.22);
484 app.life.inject_glider(0, 4);
485 return;
486 }
487 KeyCode::Char('S') => {
488 let tracks = engine.tracks.lock();
490 if let Some(track) = tracks.get(app.selected_track) {
491 track.params.supermass.set_value(1.0);
492 }
493 return;
494 }
495 KeyCode::Char('s') => {
496 let tracks = engine.tracks.lock();
498 if let Some(track) = tracks.get(app.selected_track) {
499 track.params.supermass.set_value(0.0);
500 }
501 return;
502 }
503 KeyCode::Char('h') => {
504 pattern_hits_nudge(engine, app, -1.0);
505 return;
506 }
507 KeyCode::Char('H') => {
508 pattern_hits_nudge(engine, app, 1.0);
509 return;
510 }
511 KeyCode::Char('p') => {
512 pattern_rot_nudge(engine, app, -1.0);
513 return;
514 }
515 KeyCode::Char('P') => {
516 pattern_rot_nudge(engine, app, 1.0);
517 return;
518 }
519 KeyCode::Char('t') => {
520 cycle_kind(engine, app, true);
521 return;
522 }
523 KeyCode::Char('T') => {
524 cycle_kind(engine, app, false);
525 return;
526 }
527 KeyCode::Char('w') => {
528 match persistence::save(&app.presets_dir, engine) {
529 Ok(path) => app.set_status(format!("saved preset → {}", short_path(&path))),
530 Err(e) => app.set_status(format!("save failed: {e}")),
531 }
532 return;
533 }
534 KeyCode::Char('l') => {
535 match persistence::load_latest(&app.presets_dir, engine) {
536 Ok(Some((path, n))) => {
537 app.set_status(format!("loaded {} ({} slots) ← {}", n, n, short_path(&path)));
538 }
539 Ok(None) => app.set_status("no presets/ folder yet — press w first".to_string()),
540 Err(e) => app.set_status(format!("load failed: {e}")),
541 }
542 return;
543 }
544 KeyCode::Char('c') => {
545 if engine.recorder.is_recording() {
546 match engine.recorder.stop_and_encode(&app.recordings_dir) {
547 Ok(path) => app.set_status(format!(
548 "rec → {} (encoding in bg, up to {}m cap)",
549 short_path(&path),
550 recording::MAX_MINUTES
551 )),
552 Err(e) => app.set_status(format!("stop failed: {e}")),
553 }
554 } else {
555 engine.recorder.start();
556 app.set_status(format!("recording started (cap {}m)", recording::MAX_MINUTES));
557 }
558 return;
559 }
560 _ => {}
561 }
562
563 match app.focus {
564 Focus::Tracks => handle_tracks_key(key, engine, app),
565 Focus::Params => handle_params_key(key, engine, app),
566 }
567}
568
569fn handle_tracks_key(key: KeyEvent, engine: &EngineHandle, app: &mut AppState) {
570 let tracks = engine.tracks.lock();
571 let n = tracks.len();
572 match key.code {
573 KeyCode::Up => {
574 if app.selected_track > 0 {
575 app.selected_track -= 1;
576 }
577 }
578 KeyCode::Down => {
579 if app.selected_track + 1 < n {
580 app.selected_track += 1;
581 }
582 }
583 KeyCode::Enter | KeyCode::Right | KeyCode::Tab => {
584 app.focus = Focus::Params;
585 }
586 KeyCode::Char('m') => toggle_mute(&tracks[app.selected_track]),
587 KeyCode::Char('a') => {
588 drop(tracks);
589 activate_next(engine, app);
590 }
591 KeyCode::Char('d') => {
592 let p = &tracks[app.selected_track].params;
593 p.mute.set_value(1.0);
594 p.gain.set_value(0.3);
595 }
596 KeyCode::Char('r') => {
597 let p = &tracks[app.selected_track].params;
598 randomize_track(p, &mut app.rng_seed);
599 }
600 _ => {}
601 }
602}
603
604fn handle_params_key(key: KeyEvent, engine: &EngineHandle, app: &mut AppState) {
605 let tracks = engine.tracks.lock();
606 let Some(track) = tracks.get(app.selected_track) else {
607 return;
608 };
609 let n_params = 12;
610
611 match key.code {
612 KeyCode::Esc | KeyCode::Tab | KeyCode::BackTab => app.focus = Focus::Tracks,
613 KeyCode::Up => {
614 if app.selected_param > 0 {
615 app.selected_param -= 1;
616 }
617 }
618 KeyCode::Down => {
619 if app.selected_param + 1 < n_params {
620 app.selected_param += 1;
621 }
622 }
623 KeyCode::Left => adjust(track, app, -1.0),
624 KeyCode::Right => adjust(track, app, 1.0),
625 KeyCode::Char('m') => toggle_mute(track),
626 KeyCode::Char('r') => randomize_track(&track.params, &mut app.rng_seed),
627 _ => {}
628 }
629}
630
631fn toggle_mute(track: &Track) {
632 let p = &track.params;
633 let v = if p.mute.value() > 0.5 { 0.0 } else { 1.0 };
634 p.mute.set_value(v);
635}
636
637fn master_nudge(engine: &EngineHandle, delta: f32) {
638 let v = (engine.global.master_gain.value() + delta).clamp(0.0, 1.5);
639 engine.global.master_gain.set_value(v);
640}
641
642fn bpm_nudge(engine: &EngineHandle, delta: f32) {
643 let v = (engine.global.bpm.value() + delta).clamp(20.0, 200.0);
644 engine.global.bpm.set_value(v);
645}
646
647fn brightness_nudge(engine: &EngineHandle, delta: f32) {
648 let v = (engine.global.brightness.value() + delta).clamp(0.0, 1.0);
649 engine.global.brightness.set_value(v);
650}
651
652fn pattern_hits_nudge(engine: &EngineHandle, app: &AppState, delta: f32) {
653 let tracks = engine.tracks.lock();
654 if let Some(track) = tracks.get(app.selected_track) {
655 let v = (track.params.pattern_hits.value() + delta).clamp(0.0, rhythm::STEPS as f32);
656 track.params.pattern_hits.set_value(v);
657 }
658}
659
660fn pattern_rot_nudge(engine: &EngineHandle, app: &AppState, delta: f32) {
661 let tracks = engine.tracks.lock();
662 if let Some(track) = tracks.get(app.selected_track) {
663 let steps = rhythm::STEPS as f32;
664 let v = (track.params.pattern_rotation.value() + delta).rem_euclid(steps);
665 track.params.pattern_rotation.set_value(v);
666 }
667}
668
669fn cycle_kind(engine: &EngineHandle, app: &mut AppState, forward: bool) {
673 let new_kind = {
674 let mut tracks = engine.tracks.lock();
675 let Some(track) = tracks.get_mut(app.selected_track) else {
676 return;
677 };
678 let nk = if forward {
679 track.kind.next()
680 } else {
681 track.kind.prev()
682 };
683 track.kind = nk;
684 nk
685 };
686 engine.rebuild_graph();
687 app.set_status(format!(
688 "kind → {} (slot {})",
689 new_kind.label(),
690 app.selected_track
691 ));
692}
693
694fn adjust(track: &Track, app: &AppState, sign: f32) {
695 let p = &track.params;
696 match app.selected_param {
697 0 => p.gain.set_value((p.gain.value() + 0.05 * sign).clamp(0.0, 1.0)),
698 1 => {
699 let factor = if sign > 0.0 { 1.12 } else { 1.0 / 1.12 };
700 let v = (p.cutoff.value() * factor).clamp(40.0, 12000.0);
701 p.cutoff.set_value(v);
702 }
703 2 => p.resonance.set_value((p.resonance.value() + 0.05 * sign).clamp(0.0, 0.70)),
706 3 => p.detune.set_value((p.detune.value() + 2.0 * sign).clamp(-50.0, 50.0)),
707 4 => {
708 let semitone = 2f32.powf(1.0 / 12.0);
709 let factor = if sign > 0.0 { semitone } else { 1.0 / semitone };
710 let v = (p.freq.value() * factor).clamp(20.0, 880.0);
711 p.freq.set_value(v);
712 }
713 5 => p.reverb_mix.set_value((p.reverb_mix.value() + 0.05 * sign).clamp(0.0, 1.0)),
714 6 => p.supermass.set_value((p.supermass.value() + 0.1 * sign).clamp(0.0, 1.0)),
715 7 => p.pulse_depth.set_value((p.pulse_depth.value() + 0.05 * sign).clamp(0.0, 1.0)),
716 8 => {
718 let factor = if sign > 0.0 { 1.18 } else { 1.0 / 1.18 };
719 let v = (p.lfo_rate.value() * factor).clamp(0.01, 20.0);
720 p.lfo_rate.set_value(v);
721 }
722 9 => p.lfo_depth.set_value((p.lfo_depth.value() + 0.05 * sign).clamp(0.0, 1.0)),
723 10 => {
724 let n = crate::audio::preset::LFO_TARGETS as i32;
726 let cur = p.lfo_target.value().round() as i32;
727 let next = (cur + sign as i32).rem_euclid(n);
728 p.lfo_target.set_value(next as f32);
729 }
730 11 => p.character.set_value((p.character.value() + 0.05 * sign).clamp(0.0, 1.0)),
731 _ => {}
732 }
733}
734
735fn activate_next(engine: &EngineHandle, app: &mut AppState) {
739 let tracks = engine.tracks.lock();
740
741 let root = tracks
742 .iter()
743 .find(|t| t.params.mute.value() < 0.5)
744 .map(|t| t.params.freq.value())
745 .unwrap_or(55.0);
746 let scale = golden_pentatonic(root);
747
748 let n = tracks.len();
751 let sel = app.selected_track;
752 let target = (0..n)
753 .map(|k| (sel + k) % n)
754 .find(|&i| tracks[i].params.mute.value() > 0.5);
755 let Some(idx) = target else {
756 drop(tracks);
757 app.set_status("no dormant slots — press d to kill one first".to_string());
758 return;
759 };
760
761 let track = &tracks[idx];
762 let p = &track.params;
763 let note = scale[rand_u32(&mut app.rng_seed, scale.len() as u32) as usize];
764 p.freq.set_value(note);
765 p.mute.set_value(0.0);
766 p.gain.set_value(0.28 + 0.15 * rand_f32(&mut app.rng_seed).abs());
767 p.cutoff.set_value(600.0 + 2500.0 * rand_f32(&mut app.rng_seed).abs());
768 p.resonance.set_value(0.15 + 0.30 * rand_f32(&mut app.rng_seed).abs());
769 p.reverb_mix.set_value(0.45 + 0.45 * rand_f32(&mut app.rng_seed).abs());
770 if matches!(track.kind, PresetKind::Heartbeat | PresetKind::BassPulse) {
771 p.pulse_depth.set_value(0.0);
772 } else {
773 p.pulse_depth.set_value(0.2 * rand_f32(&mut app.rng_seed).abs());
774 }
775 let kind_label = track.kind.label();
776 drop(tracks);
777
778 app.selected_track = idx;
779 app.set_status(format!(
780 "activated slot {idx}: {kind_label} @ {note:.0} Hz"
781 ));
782}
783
784fn randomize_track(p: &TrackParams, seed: &mut u64) {
785 let root = p.freq.value();
786 let scale = golden_pentatonic(root);
787 let note = scale[(rand_u32(seed, scale.len() as u32)) as usize];
788 p.freq.set_value(note);
789 p.cutoff.set_value(500.0 + 3000.0 * rand_f32(seed).abs());
790 p.resonance.set_value(0.1 + 0.4 * rand_f32(seed).abs());
791 p.reverb_mix.set_value(0.3 + 0.6 * rand_f32(seed).abs());
792 p.pulse_depth.set_value(0.2 * rand_f32(seed).abs());
793 p.character.set_value(rand_f32(seed).abs());
797}
798
799fn mutate_selected(app: &mut AppState, engine: &EngineHandle, strength: f32) {
800 let tracks = engine.tracks.lock();
801 if let Some(track) = tracks.get(app.selected_track) {
802 let genome = genome_of(&track.params);
803 mutate(&genome, &mut app.rng_seed, strength);
804 }
805}
806
807fn mutate_all_active(app: &mut AppState, engine: &EngineHandle, strength: f32) {
808 let tracks = engine.tracks.lock();
809 for t in tracks.iter() {
810 if t.params.mute.value() < 0.5 {
811 let genome = genome_of(&t.params);
812 mutate(&genome, &mut app.rng_seed, strength);
813 }
814 }
815}
816
817fn crossover_with_neighbor(app: &mut AppState, engine: &EngineHandle) {
818 let tracks = engine.tracks.lock();
819 if tracks.len() < 2 {
820 return;
821 }
822 let me = app.selected_track;
823 let other = (me + 1) % tracks.len();
824 let a = genome_of(&tracks[me].params);
825 let b = genome_of(&tracks[other].params);
826 crossover(&a, &b, &mut app.rng_seed);
827}