use super::interaction::ChordDrill;
use super::song_ids::song_id_index;
use super::*;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
const SAMPLE_RATE: f32 = 48_000.0;
fn live_session(controls: FluidControls, automation: AutomationState) -> LiveSession {
let mut song = SongState::from_controls(controls);
song.automation = automation;
LiveSession::new(LiveSessionSnapshot::from_song(&song))
}
fn assert_within(actual: f32, expected: f32, tolerance: f32, name: &str) {
assert!(
(actual - expected).abs() <= tolerance,
"{name}: expected {expected} within {tolerance}, got {actual}"
);
}
fn assert_close(actual: f32, expected: f32) {
assert_close_named(actual, expected, "value");
}
fn assert_close_named(actual: f32, expected: f32, name: &str) {
assert_within(actual, expected, f32::EPSILON, name);
}
fn assert_near(actual: f32, expected: f32) {
assert_within(actual, expected, 1e-5, "value");
}
fn pad_chord(progression: usize, step: usize, tune: f32) -> [f32; 4] {
pad_chord_midi(progression, step).map(|note| midi_to_hz(note) * tune_ratio(tune))
}
fn timing(sample: u64, bpm: f32) -> TimingContext {
let sample_rate = f64::from(SAMPLE_RATE);
let bpm = f64::from(bpm);
let beat = sample as f64 * bpm / (60.0 * sample_rate);
TimingContext::new(sample_rate, bpm, beat)
}
fn engine_for(controls: FluidControls, automation: AutomationState) -> FluidEngine {
FluidEngine::new(
SAMPLE_RATE,
live_session(controls, automation),
no_morph(),
Arc::new(FluidTelemetry::default()),
)
}
fn pad_engine(controls: &PadControls) -> PadEngine {
PadEngine::new(
SAMPLE_RATE,
controls,
0.0,
Arc::new(FluidTelemetry::default()),
)
}
fn advance_chords(pad: &mut PadEngine, controls: &PadControls, chords: u64) {
for chord in 1..=chords {
let sample = chord * SAMPLE_RATE as u64 * 2;
let _ = pad.next(controls, 0.0, timing(sample, 120.0));
}
}
const QUANTIZED_TOLERANCE: f32 = 1e-4;
fn assert_quantized(actual: f32, expected: f32) {
assert_quantized_named(actual, expected, "value");
}
fn assert_quantized_named(actual: f32, expected: f32, name: &str) {
let tolerance = QUANTIZED_TOLERANCE * expected.abs().max(1.0);
assert_within(actual, expected, tolerance, name);
}
fn buffer_text(buffer: &Buffer) -> String {
buffer
.content
.iter()
.map(ratatui::buffer::Cell::symbol)
.collect::<String>()
}
fn progression_drill() -> ChordDrill {
ChordDrill::Progression { return_to: 4 }
}
fn slot_drill(slot: usize) -> ChordDrill {
ChordDrill::Slot { slot, return_to: 4 }
}
struct RenderTest<'a> {
size: (u16, u16),
tab: Tab,
cursor: usize,
submenu: usize,
beat: f64,
fluid: &'a RippleField,
automation: &'a AutomationState,
controls: &'a FluidControls,
footer: Option<&'a str>,
drill: ChordDrill,
active_chord: u64,
mute: &'a MuteState,
}
fn render_to_buffer(test: RenderTest<'_>) -> Buffer {
let RenderTest {
size: (width, height),
tab,
cursor,
submenu,
beat,
fluid,
automation,
controls,
footer,
drill,
active_chord,
mute,
} = test;
let mut song = SongState::from_controls(controls.clone());
song.automation = automation.clone();
song.muted = *mute;
let session = LiveSessionSnapshot::from_song(&song);
let navigation = match tab {
Tab::Chords => interaction::Navigation::Chords {
selected: cursor,
drill,
},
Tab::Master => interaction::Navigation::Master { selected: cursor },
Tab::Lead => interaction::Navigation::Lead {
selected: cursor,
drill: interaction::LeadDrill::None,
},
page => interaction::Navigation::Standard {
page: match page {
Tab::Perc => interaction::StandardPage::Perc,
Tab::Bass => interaction::StandardPage::Bass,
Tab::Kick => interaction::StandardPage::Kick,
Tab::Tonal => interaction::StandardPage::Tonal,
Tab::Clap => interaction::StandardPage::Clap,
Tab::Arp => interaction::StandardPage::Arp,
Tab::Chords | Tab::Lead | Tab::Master => unreachable!("handled above"),
},
selected: cursor,
},
};
let mode = match automation.active_kind() {
Some(ModKind::Lfo) => {
interaction::InteractionMode::Automation(interaction::AutomationMode::Lfo {
depth: interaction::LfoDepth::Editor,
selected: submenu,
})
}
Some(ModKind::Envelope) => {
interaction::InteractionMode::Automation(interaction::AutomationMode::Envelope {
selected: submenu,
})
}
None => interaction::InteractionMode::Browsing,
};
let interaction = interaction::InteractionModel {
navigation,
mode,
..interaction::InteractionModel::default()
};
let flipped = FlippedUnits::new();
let view = UiViewModel::project(ViewProjection {
interaction: &interaction,
session: &session,
telemetry: TelemetryView { beat, active_chord },
presentation: ViewPresentation {
fluid,
flipped: &flipped,
cursor_visible: false,
notices: ViewNotices {
effect: footer.map(str::to_string),
..ViewNotices::default()
},
gesture_now_seconds: 0.0,
gesture_holds_available: true,
},
});
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| render(frame, &view)).unwrap();
terminal.backend().buffer().clone()
}
#[test]
fn midi_to_hz_matches_known_notes() {
assert_close(midi_to_hz(69), 440.0); assert_close(midi_to_hz(45), 110.0); assert_close(midi_to_hz(60), 440.0 * 2f32.powf((60.0 - 69.0) / 12.0)); }
#[test]
fn pad_chord_converts_progression_a_first_chord() {
let chord = pad_chord(0, 0, 0.0);
assert_close(chord[0], 110.0); assert_close(chord[1], 440.0 * 2f32.powf((50.0 - 69.0) / 12.0)); assert_close(chord[2], 440.0 * 2f32.powf((55.0 - 69.0) / 12.0)); assert_close(chord[3], 440.0 * 2f32.powf((60.0 - 69.0) / 12.0)); }
#[test]
fn pad_chord_applies_master_tune_offset() {
let flat = pad_chord(0, 0, 0.0);
let up_octave = pad_chord(0, 0, 12.0);
let down_octave = pad_chord(0, 0, -12.0);
for i in 0..4 {
assert_close(up_octave[i], flat[i] * 2.0);
assert_close(down_octave[i], flat[i] * 0.5);
}
}
#[test]
fn pad_chord_converts_progression_d_last_chord() {
let chord = pad_chord(3, 7, 0.0);
assert_close(chord[0], 440.0 * 2f32.powf((43.0 - 69.0) / 12.0)); assert_close(chord[1], 440.0 * 2f32.powf((50.0 - 69.0) / 12.0)); assert_close(chord[2], 440.0 * 2f32.powf((55.0 - 69.0) / 12.0)); assert_close(chord[3], 440.0 * 2f32.powf((64.0 - 69.0) / 12.0)); }
#[test]
fn pad_chord_wraps_progression_and_step_index() {
let wrapped_progression = pad_chord(8, 0, 0.0);
let base_progression = pad_chord(0, 0, 0.0);
assert_eq!(wrapped_progression, base_progression);
let wrapped_step = pad_chord(0, 8, 0.0);
let base_step = pad_chord(0, 0, 0.0);
assert_eq!(wrapped_step, base_step);
}
#[test]
fn new_progressions_hold_a_common_tone_between_consecutive_steps() {
for (progression_index, progression) in PROGRESSIONS.iter().enumerate().skip(4) {
for step in 0..8 {
let current = progression[step];
let next = progression[(step + 1) % 8];
let shares_a_tone = current.iter().any(|note| next.contains(note));
assert!(
shares_a_tone,
"progression {progression_index} step {step} -> {} shares no common tone \
(an 8s release needs at least one held tone so chords don't clash)",
(step + 1) % 8
);
}
}
}
#[test]
fn tonal_phrase_a_keeps_existing_zero_randomness_melody() {
assert_eq!(tonal_phrase(0), &[45, 50, 55, 48, 52, 57, 50, 55]);
}
#[test]
fn tonal_note_applies_master_tune_offset() {
let flat = note_hz(45, 0.0);
assert_close(note_hz(45, 12.0), flat * 2.0);
assert_close(note_hz(45, -12.0), flat * 0.5);
}
#[test]
fn piano_harmonics_interpolate_with_note_pitch() {
let profile = piano_profile(3);
let low = piano_harmonic_amplitudes(profile, 36);
let mid = piano_harmonic_amplitudes(profile, 48);
let high = piano_harmonic_amplitudes(profile, 60);
assert!(low[6] > high[6]);
assert!(mid[1] > low[1]);
}
#[test]
fn piano_harmonic_decay_gets_faster_with_pitch() {
let profile = piano_profile(1);
let low = piano_harmonic_decay_rates(profile, 36, note_hz(36, 0.0));
let high = piano_harmonic_decay_rates(profile, 60, note_hz(60, 0.0));
assert!(high[15] > low[15]);
}
#[test]
fn attack_decay_gain_ramps_attack_then_decays() {
assert_close(attack_decay_gain(0.0, 0.5, 0.5, 1.0), 0.0);
assert_close(attack_decay_gain(0.25, 0.5, 0.5, 1.0), 0.5);
assert_close(attack_decay_gain(0.5, 0.5, 0.5, 1.0), 1.0);
assert_close(attack_decay_gain(0.75, 0.5, 0.5, 1.0), 0.5);
assert_close(attack_decay_gain(1.0, 0.5, 0.5, 1.0), 0.0);
}
#[test]
fn attack_decay_gain_is_pure_decay_with_zero_attack() {
let decay = 2.0;
for &t in &[0.0f32, 0.1, 0.25, 0.6, 0.9, 1.0] {
let elapsed = t * decay;
let gain = attack_decay_gain(elapsed, 0.0, decay, 2.0);
assert_near(gain, (1.0 - t).powf(2.0));
}
}
#[test]
fn attack_decay_gain_peaks_at_end_of_attack() {
assert_close(attack_decay_gain(0.1, 0.2, 1.0, 1.0), 0.5);
assert_close(attack_decay_gain(0.2, 0.2, 1.0, 1.0), 1.0);
assert!(attack_decay_gain(0.3, 0.2, 1.0, 1.0) < 1.0);
}
#[test]
fn attack_decay_gain_reaches_zero_at_end_of_life() {
assert_close(attack_decay_gain(1.0, 0.2, 0.8, 1.0), 0.0);
assert_close(attack_decay_gain(0.05, 0.1, 0.0, 1.0), 0.0);
}
fn piano_test_note(attack_time: f32, decay_time: f32) -> TonalNote {
TonalNote {
midi: 60,
hz: 440.0,
pan: 0.0,
sample_rate: SAMPLE_RATE,
attack_time,
decay_time,
}
}
#[test]
fn tonal_attack_control_changes_piano_voice_onset() {
let profile = piano_profile(1);
let mut fast = PianoTonalVoice::new(profile, piano_test_note(0.0, 2.0));
let mut slow = PianoTonalVoice::new(profile, piano_test_note(0.05, 2.0));
let fast_energy: f32 = (0..10).map(|_| fast.next().0.abs()).sum();
let slow_energy: f32 = (0..10).map(|_| slow.next().0.abs()).sum();
assert!(
fast_energy > slow_energy,
"a longer tonal.attack should produce a quieter onset: fast={fast_energy}, slow={slow_energy}"
);
}
#[test]
fn tonal_decay_control_sets_ring_length() {
let profile = piano_profile(1);
let mut long_decay = PianoTonalVoice::new(profile, piano_test_note(0.0, 2.0));
let mut short_decay = PianoTonalVoice::new(profile, piano_test_note(0.0, 0.1));
let quarter_second = (SAMPLE_RATE * 0.25) as u64;
for _ in 0..quarter_second {
long_decay.next();
short_decay.next();
}
let (long_l, _) = long_decay.next();
let (short_l, _) = short_decay.next();
assert!(
short_decay.is_done(),
"a short-decay note must have ended by 0.25s"
);
assert!(
long_l.abs() > short_l.abs(),
"a longer tonal.decay should still be ringing when the short note is silent: \
long={long_l}, short={short_l}"
);
}
#[test]
fn tonal_engine_triggers_all_non_sine_type_variants() {
let controls = TonalControls {
level: 1.0,
randomness: 0.0,
..TonalControls::default()
};
for synth_type in 1..=9 {
let controls = TonalControls {
synth_type: synth_type as f32,
..controls.clone()
};
let mut tonal = TonalEngine::new(SAMPLE_RATE);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.0),
);
assert!(matches!(tonal.voices.first(), Some(TonalVoice::Piano(_))));
}
}
#[test]
fn tonal_type_labels_cover_exploration_variants() {
assert_eq!(type_label(0.0, TONAL_SYNTH_TYPES), "Sine");
assert_eq!(type_label(1.0, TONAL_SYNTH_TYPES), "Rhodes");
assert_eq!(type_label(2.0, TONAL_SYNTH_TYPES), "Wurli");
assert_eq!(type_label(3.0, TONAL_SYNTH_TYPES), "Felt");
assert_eq!(type_label(4.0, TONAL_SYNTH_TYPES), "Marimba");
assert_eq!(type_label(5.0, TONAL_SYNTH_TYPES), "Kalimba");
assert_eq!(type_label(6.0, TONAL_SYNTH_TYPES), "Pluck");
assert_eq!(type_label(7.0, TONAL_SYNTH_TYPES), "Dulcet");
assert_eq!(type_label(8.0, TONAL_SYNTH_TYPES), "Cloud Keys");
assert_eq!(type_label(9.0, TONAL_SYNTH_TYPES), "Haze");
}
#[test]
fn tonal_low_cut_reduces_sub_energy_without_thinning_low_notes() {
fn filtered_sine_rms(hz: f32) -> f32 {
let mut low_cut = TonalLowCut::new(SAMPLE_RATE, TONAL_LOW_CUT_HZ);
let total = SAMPLE_RATE as u64 * 2;
let warmup = SAMPLE_RATE as u64 / 2;
let settled: Vec<f32> = (0..total)
.map(|sample| {
let phase = TAU * hz * sample as f32 / SAMPLE_RATE;
low_cut.process(phase.sin())
})
.skip(warmup as usize)
.collect();
crate::synth::fm::rms(&settled)
}
let sub = filtered_sine_rms(TONAL_LOW_CUT_HZ * 0.5);
let low_a = filtered_sine_rms(110.0);
assert!(sub < 0.4, "sub rms should be reduced, got {sub}");
assert!(
low_a > 0.55,
"lowest tonal fundamental should stay present, got {low_a}"
);
assert!(
low_a > sub * 1.5,
"low note should survive more than sub energy: sub {sub}, low_a {low_a}"
);
}
#[test]
fn tonal_cycle_crops_phrase_without_stretching_rate() {
assert_eq!(tonal_loop_len(4.0, 0.5), 8);
assert_eq!(tonal_loop_len(16.0, 0.5), 32);
assert_eq!(tonal_loop_len(4.0, 1.0), 4);
assert_eq!(tonal_cycle_step(3.75, 4.0, 0.5), 7);
assert_eq!(tonal_cycle_step(3.75, 4.0, 1.0), 3);
assert_eq!(tonal_cycle_step(4.0, 4.0, 0.5), 0);
}
#[test]
fn tonal_offset_moves_the_phrase_window_without_delaying_triggers() {
let controls = TonalControls {
rate_beats: 1.0,
step_interval_beats: 2.0,
offset_beats: 1.0,
randomness: 0.0,
..TonalControls::default()
};
let mut tonal = TonalEngine::new(SAMPLE_RATE);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.0),
);
assert_eq!(tonal.step_index, 1);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 1.0),
);
assert_eq!(tonal.step_index, 2);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 2.0),
);
assert_eq!(tonal.step_index, 1);
}
#[test]
fn tonal_level_ducks_notes_that_are_already_sounding() {
let mut controls = TonalControls {
level: 0.8,
rate_beats: 0.25,
decay: 4.0,
randomness: 0.0,
..TonalControls::default()
};
let mut tonal = TonalEngine::new(SAMPLE_RATE);
let mut beat = 0.0;
let mut sounding = 0.0f32;
for step in 0..4_000 {
beat += 4.0 / f64::from(SAMPLE_RATE);
let (l, r) = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, beat),
);
if step > 2_000 {
sounding = sounding.max((l * l + r * r).sqrt());
}
}
assert!(
sounding > 0.001,
"no tonal notes sounding to duck: {sounding}"
);
controls.level = 0.0;
let mut after = 0.0f32;
for _ in 0..2_000 {
beat += 4.0 / f64::from(SAMPLE_RATE);
let (l, r) = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, beat),
);
after = after.max((l * l + r * r).sqrt());
}
assert!(
after <= sounding * 0.01,
"notes kept ringing at {after} after Level reached 0 (was {sounding})"
);
}
#[test]
fn tonal_rate_controls_trigger_spacing_independent_of_cycle() {
let controls = TonalControls {
rate_beats: 1.0,
step_interval_beats: 4.0,
randomness: 0.0,
..TonalControls::default()
};
let mut tonal = TonalEngine::new(SAMPLE_RATE);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.0),
);
assert_eq!(tonal.step_index, 0);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
assert_eq!(tonal.step_index, 0);
let _ = tonal.next(
&controls,
0.0,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 1.0),
);
assert_eq!(tonal.step_index, 1);
}
#[test]
fn tonal_evolve_rate_maps_to_actual_notes_per_cycle() {
assert_eq!(tonal_evolve_note_count(0.0, 8), 0);
assert_eq!(tonal_evolve_note_count(0.01, 8), 1);
assert_eq!(tonal_evolve_note_count(0.50, 8), 2);
assert_eq!(tonal_evolve_note_count(1.0, 8), 4);
}
#[test]
fn tonal_engine_evolves_notes_proportional_to_rate() {
for (rate, expected_changed) in [(0.01, 1), (1.0, 4)] {
let mut tonal = TonalEngine::new(SAMPLE_RATE);
tonal.rng = StdRng::seed_from_u64(5);
let before = tonal.evolved_phrase.clone();
tonal.evolve_phrase(rate);
let changed = before
.iter()
.zip(&tonal.evolved_phrase)
.filter(|(before, after)| before != after)
.count();
assert_eq!(
changed, expected_changed,
"evolve rate {rate}: unexpected changed-note count"
);
}
}
#[test]
fn tonal_sequence_snapshot_resumes_the_next_evolution() {
let snapshot = TonalSequenceState {
phrase: 2,
notes: tonal_phrase(2).to_vec(),
evolution_seed: 42,
evolution_count: 7,
};
let session = live_session(FluidControls::default(), AutomationState::default());
session.update(|live| live.tonal_sequence = snapshot.clone());
let mut resumed = TonalEngine::new_with_session_state(SAMPLE_RATE, Some(session.clone()));
let uninterrupted_session = live_session(FluidControls::default(), AutomationState::default());
uninterrupted_session.update(|live| live.tonal_sequence = snapshot.clone());
let mut uninterrupted =
TonalEngine::new_with_session_state(SAMPLE_RATE, Some(uninterrupted_session));
resumed.evolve_phrase(1.0);
uninterrupted.evolve_phrase(1.0);
assert_eq!(resumed.evolved_phrase, uninterrupted.evolved_phrase);
assert_eq!(session.load().tonal_sequence.evolution_count, 8);
}
#[test]
fn pad_defaults_use_progression_a_and_sixteen_beat_chords() {
let controls = PadControls::default();
assert_close(controls.chord_bars, 4.0);
assert_close(controls.progression, 0.0);
assert_close(controls.voice_type, 0.0);
}
#[test]
fn fresh_start_chooses_only_a_builtin_progression() {
let mut rng = StdRng::seed_from_u64(42);
let choices = (0..64)
.map(|_| randomized_start_song(&mut rng).controls.pad.progression as usize)
.collect::<Vec<_>>();
assert!(choices.iter().all(|&choice| choice < PROGRESSIONS.len()));
}
#[test]
fn fresh_start_varies_the_progression_between_launches() {
let mut rng = StdRng::seed_from_u64(42);
let first = randomized_start_song(&mut rng).controls.pad.progression;
let varied = (0..16).any(|_| randomized_start_song(&mut rng).controls.pad.progression != first);
assert!(varied);
}
#[test]
fn chords_tab_shows_type_row_with_letter_display() {
let controls = FluidControls::default();
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[3].id, "pad.type");
assert_eq!(rows[3].label, "Type");
for (voice_type, display) in [
(0.0, "Warm"),
(1.0, "Dark"),
(2.0, "Glass"),
(3.0, "Choir"),
(4.0, "Hollow"),
(5.0, "Tape"),
] {
let mut controls = FluidControls::default();
controls.pad.voice_type = voice_type;
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[3].display, display, "pad.type {voice_type}");
}
}
#[test]
fn tab_previous_wraps_back_one_tab() {
assert_eq!(Tab::Master.previous(), Tab::Lead);
assert_eq!(Tab::Lead.previous(), Tab::Arp);
assert_eq!(Tab::Kick.previous(), Tab::Bass);
assert_eq!(Tab::Bass.previous(), Tab::Perc);
}
#[test]
fn render_fluid_draws_without_terminal_backend() {
let controls = FluidControls::default();
let fluid = RippleField::new();
let automation = AutomationState::default();
render_to_buffer(RenderTest {
size: (100, 32),
tab: Tab::Master,
cursor: 0,
submenu: 0,
beat: 0.0,
fluid: &fluid,
automation: &automation,
controls: &controls,
footer: None,
drill: ChordDrill::None,
active_chord: 0,
mute: &[false; TAB_COUNT],
});
}
#[test]
fn automation_open_or_create_uses_safe_lfo_defaults() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("master.level");
let route = automation.open_or_create(address);
assert_close(route.cycle_beats, 2.0);
assert_close(route.depth_ratio, 0.0);
assert_eq!(route.shape, LfoShape::Sine);
assert_close(route.phase_offset_beats, 0.0);
assert_eq!(automation.active_address(), Some(address));
}
#[test]
fn lfo_field_adjust_steps_and_clamps() {
let mut route = LfoRoute::default();
route.adjust_field_at(LfoField::Amount, 1.0, 0.0);
assert_close(route.depth_ratio, 0.01);
route.set_field_at(LfoField::Amount, 0.0, 0.0);
route.adjust_field_at(LfoField::Amount, -1.0, 0.0);
assert_close(route.depth_ratio, 0.0);
route.adjust_field_at(LfoField::Interval, 1.0, 0.0);
assert_close(route.cycle_beats, 2.25);
route.set_field_at(LfoField::Interval, 4.0, 0.0);
for expected in [8.0, 12.0, 16.0, 32.0, 64.0, 64.0] {
route.adjust_field_at(LfoField::Interval, 1.0, 0.0);
assert_close(route.cycle_beats, expected);
}
for _ in 0..150 {
route.adjust_field_at(LfoField::Interval, -1.0, 0.0);
}
assert_close(route.cycle_beats, 0.125);
route.adjust_field_at(LfoField::Offset, -1.0, 0.0);
assert_close(route.phase_offset_beats, 0.0);
route.adjust_field_at(LfoField::Offset, 1.0, 0.0);
assert_close(route.phase_offset_beats, 0.125);
for _ in 0..100 {
route.adjust_field_at(LfoField::Offset, 1.0, 0.0);
}
assert_close(route.phase_offset_beats, 4.0);
}
#[test]
fn lfo_submenu_arrow_navigation_clamps_until_explicitly_closed() {
assert_eq!(clamp_lfo_selection(1, -1, 4), 1);
assert_eq!(clamp_lfo_selection(2, -1, 4), 1);
assert_eq!(clamp_lfo_selection(3, 1, 4), 4);
assert_eq!(clamp_lfo_selection(4, 1, 4), 4);
}
#[test]
fn startup_fade_reaches_full_gain_in_two_seconds() {
let halfway = SAMPLE_RATE as u64;
let complete = SAMPLE_RATE as u64 * 2;
assert_near(startup_fade(0, SAMPLE_RATE), 0.0);
assert_near(startup_fade(halfway, SAMPLE_RATE), 0.5);
assert_near(startup_fade(complete, SAMPLE_RATE), 1.0);
assert_near(startup_fade(complete * 2, SAMPLE_RATE), 1.0);
}
#[test]
fn lfo_field_set_keeps_exact_rate_and_snaps_offset_to_grid() {
let mut route = LfoRoute::default();
route.set_field_at(LfoField::Interval, 3.1, 0.0);
assert_close(route.cycle_beats, 3.1);
route.set_field_at(LfoField::Interval, 0.17, 0.0);
assert_close(route.cycle_beats, 0.17);
route.set_field_at(LfoField::Interval, 100.0, 0.0);
assert_close(route.cycle_beats, 64.0);
route.set_field_at(LfoField::Amount, 130.0, 0.0);
assert_close(route.depth_ratio, 1.0);
route.set_field_at(LfoField::Amount, 40.0, 0.0);
assert_close(route.depth_ratio, 0.4);
route.set_field_at(LfoField::Offset, 1.3, 0.0);
assert_close(route.phase_offset_beats, 1.25);
route.set_field_at(LfoField::Offset, 9.0, 0.0);
assert_close(route.phase_offset_beats, 4.0);
}
#[test]
fn envelope_times_sweep_their_full_range_in_one_taper_sweep() {
let mut route = EnvelopeRoute::default();
route.set_field_raw(EnvField::Attack, 0.0);
for _ in 0..(TAPER_STEPS_PER_SWEEP as usize) {
route.adjust_field(EnvField::Attack, 1.0);
}
assert_near(route.attack_beats, MAX_ENV_ATTACK_BEATS);
for _ in 0..(TAPER_STEPS_PER_SWEEP as usize) {
route.adjust_field(EnvField::Attack, -1.0);
}
assert_near(route.attack_beats, 0.0);
}
#[test]
fn lfo_depth_means_the_same_musical_amount_wherever_the_base_sits() {
let controls = FluidControls::default();
let spec = spec_by_id("bass.slot1.time").unwrap().contextual(&controls);
let route = LfoRoute {
depth_ratio: 0.25,
..LfoRoute::default()
};
let octaves_up = |base: f32| {
let peak = spec
.taper
.value_at(
spec.taper.ratio(base, spec.min, spec.max) + 0.25,
spec.min,
spec.max,
)
.clamp(spec.min, spec.max);
(peak / base).log2()
};
let low = octaves_up(200.0);
let mid = octaves_up(800.0);
assert!(
(low - mid).abs() < 0.01,
"same depth must move the same interval: {low} vs {mid} octaves"
);
let engine_peak = (0..64)
.map(|i| modulated_control_value(&spec, &route, 800.0, i as f64 / 8.0))
.fold(f32::MIN, f32::max);
assert!(
(engine_peak / 800.0).log2() > 1.0,
"a quarter-depth LFO should open well over an octave, got {engine_peak} Hz"
);
}
#[test]
fn envelope_time_bars_give_ordinary_settings_visible_throw() {
let mut route = EnvelopeRoute::default();
route.set_field_raw(EnvField::Decay, 4.0);
assert!(
route.field_value(EnvField::Decay) == 4.0
&& EnvField::Decay.scale().ratio(4.0) > 0.15
&& EnvField::Decay.scale().ratio(4.0) < 0.5,
"4 beats sits at {} of the decay bar",
EnvField::Decay.scale().ratio(4.0)
);
}
#[test]
fn lfo_rate_bar_divides_the_full_throw_across_arrow_rungs() {
let mut route = LfoRoute::default();
let denominator = (LFO_RATE_ARROW_STEPS.len() - 1) as f32;
for (index, rate) in LFO_RATE_ARROW_STEPS.iter().copied().enumerate() {
route.cycle_beats = rate;
assert_near(
LfoField::Interval.scale().ratio(route.cycle_beats),
index as f32 / denominator,
);
}
route.cycle_beats = 6.0;
assert_near(
LfoField::Interval.scale().ratio(route.cycle_beats),
(16.0 + 0.5) / denominator,
);
}
#[test]
fn ordered_step_ratio_spans_the_bar_and_interpolates_typed_values() {
let steps = [0.125, 0.25, 0.5, 4.0, 8.0, 16.0, 64.0];
assert_near(ordered_step_ratio(0.125, &steps), 0.0);
assert_near(ordered_step_ratio(4.0, &steps), 0.5);
assert_near(ordered_step_ratio(6.0, &steps), 7.0 / 12.0);
assert_near(ordered_step_ratio(64.0, &steps), 1.0);
}
#[test]
fn beat_grid_bars_give_every_arrow_rung_equal_visual_space() {
let controls = FluidControls::default();
let offset = spec_by_id("kick.offset_beats").unwrap();
let offset_ratios = [0.0, 0.125, 0.25, 0.5].map(|value| offset.ratio(value, &controls));
assert_near(
offset_ratios[1] - offset_ratios[0],
offset_ratios[2] - offset_ratios[1],
);
assert_near(
offset_ratios[2] - offset_ratios[1],
offset_ratios[3] - offset_ratios[2],
);
let interval = spec_by_id("kick.interval_beats").unwrap();
let interval_ratios = [0.125, 0.25, 0.5, 0.75].map(|value| interval.ratio(value, &controls));
assert_near(
interval_ratios[1] - interval_ratios[0],
interval_ratios[2] - interval_ratios[1],
);
assert_near(
interval_ratios[2] - interval_ratios[1],
interval_ratios[3] - interval_ratios[2],
);
}
#[test]
fn every_registered_slider_visual_mapping_spans_its_full_range() {
let controls = FluidControls::default();
for spec in all_specs() {
let spec = spec.contextual(&controls);
assert_near(spec.ratio(spec.min, &controls), 0.0);
assert_near(spec.ratio(spec.max, &controls), 1.0);
}
}
#[test]
fn discrete_fields_clamp_at_their_ends_instead_of_wrapping() {
let mut route = LfoRoute::default();
route.adjust_field_at(LfoField::Shape, -1.0, 0.0);
assert_eq!(
route.shape,
LfoShape::Sine,
"shape must not wrap below sine"
);
for _ in 0..20 {
route.adjust_field_at(LfoField::Shape, 1.0, 0.0);
}
assert_eq!(
route.shape,
LfoShape::Steps,
"shape must stop at the last entry"
);
route.set_field_at(LfoField::Shape, 99.0, 0.0);
assert_eq!(
route.shape,
LfoShape::Steps,
"numeric entry clamps, not wraps"
);
let mut env = EnvelopeRoute::default();
for _ in 0..20 {
env.adjust_field(EnvField::Trigger, 1.0);
}
assert_eq!(env.trigger, EnvTrigger::Once, "trigger must stop at once");
env.adjust_field(EnvField::Trigger, 1.0);
assert_eq!(env.trigger, EnvTrigger::Once);
for _ in 0..20 {
env.adjust_field(EnvField::Trigger, -1.0);
}
assert_eq!(env.trigger, EnvTrigger::EveryBeats(1.0));
}
#[test]
fn lfo_field_reset_uses_slider_minimums() {
let mut route = LfoRoute {
cycle_beats: 4.0,
depth_ratio: 0.75,
phase_offset_beats: 2.0,
..LfoRoute::default()
};
route.reset_field_at(LfoField::Amount, 1.0);
assert_close(route.depth_ratio, 0.0);
route.reset_field_at(LfoField::Interval, 1.0);
assert_close(route.cycle_beats, MIN_LFO_CYCLE_BEATS);
route.reset_field_at(LfoField::Offset, 1.0);
assert_close(route.phase_offset_beats, 0.0);
}
#[test]
fn lfo_rate_edit_keeps_offset_and_hands_off_where_waveforms_cross() {
let mut route = LfoRoute {
cycle_beats: 2.0,
phase_offset_beats: 0.0,
..LfoRoute::default()
};
let beat = 4.0;
let old_route = route;
route.adjust_field_at(LfoField::Interval, 1.0, beat);
assert_close(route.cycle_beats, 2.25);
assert_close(route.phase_offset_beats, 0.0);
let pickup = route.pickup.expect("rate edit should schedule a pickup");
assert!(pickup.at_beat > beat);
let before_pickup = pickup.at_beat - 1e-6;
let after_pickup = pickup.at_beat + 1e-6;
assert_near(
route.wave_at(before_pickup),
old_route.wave_at(before_pickup),
);
assert!(
(route.wave_at(before_pickup) - route.wave_at(after_pickup)).abs() < 1e-4,
"handoff must be value-continuous"
);
let globally_anchored = LfoRoute {
cycle_beats: 2.25,
..LfoRoute::default()
};
assert_near(
route.wave_at(after_pickup),
globally_anchored.wave_at(after_pickup),
);
}
#[test]
fn close_editor_deletes_zero_depth_route() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("master.level");
automation.open_or_create(address).depth_ratio = 0.0;
automation.close_editor();
assert!(automation.route(address).is_none());
assert!(!automation.is_editor_open());
automation.open_or_create(address);
automation.close_editor();
assert!(automation.route(address).is_none());
}
#[test]
fn x_removes_the_open_route_or_clears_the_whole_control() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.open_or_create(address).depth_ratio = 0.4;
automation.remove_open_route();
assert!(!automation.is_editor_open());
assert!(automation.route(address).is_none());
automation.open_or_create(address).depth_ratio = 0.4;
automation.close_editor();
automation.clear_control(address);
assert!(automation.route(address).is_none());
}
#[test]
fn engine_publishes_beat_telemetry() {
let controls = FluidControls::default();
let bpm = f64::from(controls.master.bpm);
let mut engine = engine_for(controls, AutomationState::default());
for _ in 0..512 {
engine.next_stereo();
}
let expected = 256.0 * bpm / (60.0 * f64::from(SAMPLE_RATE));
let beat = engine.telemetry.beat();
assert!(beat > 0.0);
assert!(
(beat - expected).abs() / expected < 0.01,
"expected ~{expected}, got {beat}"
);
}
#[test]
fn master_mute_gates_the_final_output_with_level_automation_active() {
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new("master.level"),
LfoRoute {
depth_ratio: 1.0,
..LfoRoute::default()
},
);
let session = live_session(FluidControls::default(), automation);
let mut effects = EffectExecutor::new(
session.clone(),
AutoControls::new(no_morph(), decode_auto_states(), DEFAULT_AUTO_BARS),
);
let mut engine = FluidEngine::new(
SAMPLE_RATE,
session.clone(),
no_morph(),
Arc::new(FluidTelemetry::default()),
);
engine.reseed(42);
let warmup = (SAMPLE_RATE * 2.1) as usize;
let audible = (0..warmup)
.map(|_| engine.next_stereo())
.any(|sample| sample != (0.0, 0.0));
assert!(audible, "the unmuted fixture must produce audio");
effects.toggle_mute(Tab::Master);
let settle = (SAMPLE_RATE * LEVEL_RAMP_MS * 0.001) as usize + 256;
for _ in 0..settle {
engine.next_stereo();
}
assert!((0..512).all(|_| engine.next_stereo() == (0.0, 0.0)));
}
const GOLDEN_RENDER_SAMPLES: usize = 48_000;
#[cfg(debug_assertions)]
const GOLDEN_RENDER_CHECKSUM: u64 = 0x2615_bcf4_552e_c37c;
#[cfg(debug_assertions)]
fn fold_sample_bits(hash: u64, bits: u32) -> u64 {
(hash ^ u64::from(bits)).wrapping_mul(0x100000001b3)
}
#[test]
fn a_seeded_render_repeats_itself_exactly() {
let render = || {
let mut engine = engine_for(golden_render_controls(), AutomationState::default());
engine.reseed(42);
(0..GOLDEN_RENDER_SAMPLES)
.map(|_| {
let (l, r) = engine.next_stereo();
(l.to_bits(), r.to_bits())
})
.collect::<Vec<_>>()
};
assert_eq!(
render(),
render(),
"the same seed rendered differently twice"
);
}
fn golden_render_controls() -> FluidControls {
FluidControls {
master: MasterControls {
bpm: 140.0,
..MasterControls::default()
},
tonal: TonalControls {
level: 0.5,
synth_type: 2.0,
rate_beats: 0.25,
..TonalControls::default()
},
arp: ArpControls {
gain: 0.4,
..ArpControls::default()
},
..FluidControls::default()
}
}
#[cfg(debug_assertions)]
#[test]
fn golden_render_is_byte_identical_for_a_seed() {
let mut engine = engine_for(golden_render_controls(), AutomationState::default());
engine.reseed(42);
let mut hash = 0xcbf2_9ce4_8422_2325u64; for _ in 0..GOLDEN_RENDER_SAMPLES {
let (l, r) = engine.next_stereo();
hash = fold_sample_bits(hash, l.to_bits());
hash = fold_sample_bits(hash, r.to_bits());
}
assert_eq!(
hash, GOLDEN_RENDER_CHECKSUM,
"seeded render output changed — this either broke reproducibility \
unintentionally, or is an expected result of a deliberate DSP change. \
If the latter, re-bless GOLDEN_RENDER_CHECKSUM (checksum was {hash:#x})"
);
}
#[test]
fn lfo_phase_at_uses_cycle_and_offset() {
let route = LfoRoute {
cycle_beats: 2.0,
phase_offset_beats: 0.5,
..LfoRoute::default()
};
assert!((route.phase_at(1.0) - 0.75).abs() < 1e-9);
assert!((route.phase_at(2.0) - 0.25).abs() < 1e-9);
}
#[test]
fn zero_offset_lfos_share_the_global_rate_grid_across_targets() {
let mut automation = AutomationState::default();
let master = ControlAddress::new("master.level");
let pad = ControlAddress::new("pad.level");
automation.set_route(
master,
LfoRoute {
cycle_beats: 16.0,
..LfoRoute::default()
},
);
automation.set_route(
pad,
LfoRoute {
cycle_beats: 16.0,
depth_ratio: 0.75,
..LfoRoute::default()
},
);
let master_route = automation.route(master).unwrap();
let pad_route = automation.route(pad).unwrap();
for (beat, expected_phase) in [
(0.0, 0.0),
(4.0, 0.25),
(8.0, 0.5),
(12.0, 0.75),
(16.0, 0.0),
(32.0, 0.0),
] {
assert!((master_route.phase_at(beat) - expected_phase).abs() < 1e-9);
assert_eq!(master_route.phase_at(beat), pad_route.phase_at(beat));
}
let thirty_two = LfoRoute {
cycle_beats: 32.0,
..LfoRoute::default()
};
for beat in [0.0, 32.0, 64.0] {
assert!(thirty_two.phase_at(beat).abs() < 1e-9);
}
}
#[test]
fn render_fluid_draws_lfo_submenu_and_animated_lane() {
let controls = FluidControls::default();
let fluid = RippleField::new();
let items = tab_controls(Tab::Master, &controls);
let mut automation = AutomationState::default();
automation.open_or_create(ControlAddress::new(items[0].id));
let draw_at = |beat: f64| {
render_to_buffer(RenderTest {
size: (120, 40),
tab: Tab::Master,
cursor: 0,
submenu: 1,
beat,
fluid: &fluid,
automation: &automation,
controls: &controls,
footer: None,
drill: ChordDrill::None,
active_chord: 0,
mute: &[false; TAB_COUNT],
})
};
let at_start = draw_at(0.0);
let text = buffer_text(&at_start);
assert!(text.contains("amount"));
assert!(text.contains("rate"));
assert!(text.contains("offset"));
assert!(text.contains("0%"));
let at_half_cycle = draw_at(1.0);
assert_ne!(at_start, at_half_cycle);
}
#[test]
fn lfo_lane_is_phase_locked() {
let route = LfoRoute::default();
let start = lfo_lane_line(&route, 0.0, 24, true);
let same_phase = lfo_lane_line(&route, 2.0, 24, true);
let opposite_phase = lfo_lane_line(&route, 1.0, 24, true);
let styles =
|line: &ratatui::text::Line<'_>| line.spans.iter().map(|s| s.style).collect::<Vec<_>>();
assert_eq!(styles(&start), styles(&same_phase));
assert_ne!(styles(&start), styles(&opposite_phase));
}
#[test]
fn envelope_lane_keeps_its_green_palette_for_negative_amounts() {
let positive = EnvelopeRoute {
amount: 0.5,
..EnvelopeRoute::default()
};
let negative = EnvelopeRoute {
amount: -0.5,
..EnvelopeRoute::default()
};
let styles = |route: &EnvelopeRoute| {
env_lane_line(route, env_ctx(1.0), 24, true)
.spans
.into_iter()
.map(|span| span.style)
.collect::<Vec<_>>()
};
assert_eq!(styles(&negative), styles(&positive));
}
#[test]
fn automation_applies_bounded_lfo_offset_and_clamps_to_spec_range() {
let mut controls = FluidControls::default();
controls.master.level = 0.9;
let mut automation = AutomationState::default();
let route = automation.open_or_create(ControlAddress::new("master.level"));
route.depth_ratio = 0.5;
apply_automation(
&mut controls,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
assert_close(controls.master.level, 1.0);
}
#[test]
fn automation_uses_beat_cycle_phase_for_opposite_lfo_offsets() {
let mut automation = AutomationState::default();
let route = automation.open_or_create(ControlAddress::new("master.level"));
route.cycle_beats = 2.0;
route.depth_ratio = 0.25;
let mut positive = FluidControls::default();
positive.master.level = 0.5;
apply_automation(
&mut positive,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
let mut negative = FluidControls::default();
negative.master.level = 0.5;
apply_automation(
&mut negative,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 1.5),
);
assert_near(positive.master.level, 0.75);
assert_near(negative.master.level, 0.25);
}
#[test]
fn automation_preserves_base_controls_and_modulates_only_effective_clone() {
let mut base = FluidControls::default();
base.master.level = 0.5;
let mut effective = base.clone();
let mut automation = AutomationState::default();
let route = automation.open_or_create(ControlAddress::new("master.level"));
route.depth_ratio = 0.25;
apply_automation(
&mut effective,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
assert_near(effective.master.level, 0.75);
assert_close(base.master.level, 0.5);
}
#[test]
fn defaults_match_current_mix() {
let controls = FluidControls::default();
assert_close(controls.master.bpm, 82.0);
assert_eq!(controls.modules.master[0].kind().unwrap().id, "drive");
assert_close(controls.modules.master[0].amount, 0.05);
assert_eq!(controls.modules.master[1].kind().unwrap().id, "compression");
assert_close(controls.modules.master[1].time, -8.0);
assert_close(controls.perc.decay_ms, 200.0);
assert_eq!(controls.modules.perc[0].kind().unwrap().id, "filter");
assert_close(controls.modules.perc[0].time, 8_000.0);
assert_close(controls.perc.interval_beats, 0.25);
assert_close(controls.perc.offset_beats, 0.0);
assert_close(controls.kick.start_freq, 160.0);
assert_close(controls.kick.pitch_decay_ms, 55.0);
assert_close(controls.kick.amp_decay_ms, 150.0);
assert_eq!(controls.modules.kick[0].kind().unwrap().id, "filter");
assert_close(controls.modules.kick[0].time, 8_000.0);
assert_eq!(controls.modules.kick[1].kind().unwrap().id, "drive");
assert_close(controls.tonal.phrase, 0.0);
assert_close(controls.tonal.synth_type, 0.0);
assert_close(controls.tonal.rate_beats, 0.5);
assert_close(controls.tonal.step_interval_beats, 16.0);
assert_close(controls.tonal.decay, 1.2);
assert_close(controls.tonal.randomness, 0.5);
assert_close(controls.tonal.evolve_rate, 0.0);
assert_eq!(controls.modules.pad[0].kind().unwrap().id, "room");
assert_eq!(controls.modules.bass[0].kind().unwrap().id, "filter");
assert_eq!(controls.modules.bass[1].kind().unwrap().id, "drive");
assert_close(controls.modules.pad[0].amount, 0.4);
assert_eq!(controls.modules.tonal[0].kind().unwrap().id, "room");
assert_close(controls.modules.tonal[0].amount, 0.1);
assert!(controls.modules.clap[0].is_empty());
}
#[test]
fn apply_reset_moves_selected_control_to_floor() {
let mut controls = FluidControls::default();
controls.modules.master[0].amount = 0.8;
spec_by_id("master.slot1.amount")
.unwrap()
.apply_reset(&mut controls);
assert_close(controls.modules.master[0].amount, 0.0);
controls.master.bpm = 120.0;
apply_reset(Tab::Master, 8, &mut controls);
assert_close(controls.master.bpm, 30.0);
controls.master.tone = 0.5;
spec_by_id("master.tone")
.unwrap()
.apply_reset(&mut controls);
assert_close(controls.master.tone, -1.0);
controls.pad.chord_bars = 16.0;
apply_reset(Tab::Chords, 4, &mut controls);
assert_close(controls.pad.chord_bars, 1.0);
}
#[test]
fn apply_value_accepts_percent_style_unit_controls() {
let mut controls = FluidControls::default();
apply_value(Tab::Master, 9, 42.0, &mut controls);
assert_close(controls.master.level, 0.42);
apply_value(Tab::Master, 9, 1.0, &mut controls);
assert_close(controls.master.level, 0.01);
}
#[test]
fn apply_value_snaps_direct_numeric_entry_to_control_grid() {
let mut controls = FluidControls::default();
apply_value(Tab::Kick, 4, 1.13, &mut controls);
assert_close(controls.kick.interval_beats, 1.25);
apply_value(Tab::Kick, 4, 0.16, &mut controls);
assert_close(controls.kick.interval_beats, 0.125);
apply_value(Tab::Chords, 4, 12.0, &mut controls);
assert_close(controls.pad.chord_bars, 4.0);
apply_value(Tab::Clap, 5, 3.6, &mut controls);
assert_close(controls.clap.slap_count, 4.0);
}
#[test]
fn default_template_preloads_shared_effect_modules() {
let mut controls = FluidControls::default();
resolve_module_chain(&mut controls);
assert_eq!(controls.modules.kick[0].kind().unwrap().id, "filter");
assert_close(controls.modules.kick[0].amount, 1.0);
assert_eq!(controls.modules.kick[1].kind().unwrap().id, "drive");
assert_close(controls.modules.kick[1].amount, 0.2);
assert_eq!(controls.modules.bass[0].kind().unwrap().id, "filter");
assert_close(controls.modules.bass[0].amount, 1.0);
assert_close(controls.modules.bass[1].amount, 0.15);
assert_close(controls.modules.pad[0].amount, 0.4);
assert_close(controls.modules.tonal[0].amount, 0.1);
assert!(controls.modules.clap[0].is_empty());
assert_close(controls.perc.swing, 0.0);
assert_close(controls.tonal.swing, 0.0);
assert_close(controls.arp.swing, 0.0);
}
#[test]
fn a_slots_amount_drives_the_voice_it_belongs_to() {
let mut controls = FluidControls::default();
controls.modules.kick[1] = preset_slot("swing", 0.4);
controls.modules.perc[0] = preset_slot("swing", 0.4);
controls.modules.bass[1] = preset_slot("swing", 0.4);
controls.modules.tonal[1] = preset_slot("swing", 0.4);
controls.modules.clap[1] = preset_slot("swing", 0.4);
controls.modules.arp[1] = preset_slot("swing", 0.4);
resolve_module_chain(&mut controls);
assert_close(controls.kick.swing, 0.4);
assert_close(controls.perc.swing, 0.4);
assert_close(controls.bass.swing, 0.4);
assert_close(controls.tonal.swing, 0.4);
assert_close(controls.clap.swing, 0.4);
assert_close(controls.arp.swing, 0.4);
controls.modules.kick[1] = ModuleSlot::default();
resolve_module_chain(&mut controls);
assert_close(controls.kick.swing, 0.0);
}
#[test]
fn a_loaded_slot_row_is_labelled_with_its_module() {
let mut controls = FluidControls::default();
let row = tab_controls(Tab::Kick, &controls)
.into_iter()
.find(|item| item.id == "kick.slot1.time")
.expect("kick slot 1 ships pre-loaded with Filter");
assert_eq!(row.label, "Filter");
let row = tab_controls(Tab::Kick, &controls)
.into_iter()
.find(|item| item.id == "kick.slot2.amount")
.expect("kick slot 2 ships pre-loaded with Drive");
assert_eq!(row.label, "Drive");
controls.modules.tonal[0] = preset_slot("swing", 0.0);
let row = tab_controls(Tab::Tonal, &controls)
.into_iter()
.find(|item| item.id == "tonal.slot1.amount")
.expect("an explicitly added zero-amount Swing remains visible");
assert_eq!(row.label, "Swing");
}
#[test]
fn the_folded_slider_ids_are_gone_from_the_registry() {
for id in [
"perc.swing",
"tonal.swing",
"arp.swing",
"bass.drive",
"kick.drive",
"clap.room",
"pad.reverb_mix",
"tonal.reverb_mix",
"arp.reverb_mix",
"master.drive",
"master.comp_amount",
"master.comp_release_ms",
"master.comp_threshold",
"master.comp_ratio",
"master.comp_makeup",
"perc.filter",
"bass.cutoff",
"kick.filter",
] {
assert!(spec_by_id(id).is_none(), "{id} should be retired");
}
}
#[test]
fn empty_module_slots_never_render() {
let controls = FluidControls::default();
for tab in Tab::all() {
for item in tab_controls(tab, &controls) {
let is_template_slot = item.id.contains(".slot1.")
|| (matches!(tab, Tab::Bass | Tab::Kick | Tab::Master)
&& item.id.contains(".slot2."));
assert!(
!item.id.contains(".slot") || is_template_slot,
"{tab:?} shows empty slot row {}",
item.id
);
}
}
}
#[test]
fn an_occupied_slot_shows_only_the_params_its_family_uses() {
let alcohol = module_kind_value("alcohol");
let sidechain = module_kind_value("sidechain");
let mut controls = FluidControls::default();
controls.modules.bass[1] = ModuleSlot::default();
controls.modules.bass[0].kind = alcohol;
let ids: Vec<&str> = tab_controls(Tab::Bass, &controls)
.iter()
.map(|item| item.id)
.filter(|id| id.contains(".slot"))
.collect();
assert_eq!(ids, ["bass.slot1.amount"]);
controls.modules.bass[0].kind = sidechain;
let ids: Vec<&str> = tab_controls(Tab::Bass, &controls)
.iter()
.map(|item| item.id)
.filter(|id| id.contains(".slot"))
.collect();
assert_eq!(ids, ["bass.slot1.amount", "bass.slot1.time"]);
}
#[test]
fn effect_families_project_complete_coherent_detail_rows() {
let mut controls = FluidControls::default();
controls.modules.clap[1] = preset_slot("delay", 0.5);
controls.modules.clap[2] = preset_slot("room", 0.5);
controls.modules.clap[3] = preset_slot("compression", 0.5);
controls.modules.clap[4] = preset_slot("filter", 0.5);
let labels = |slot| {
module_detail_controls(Tab::Clap, slot, &controls)
.into_iter()
.map(|item| item.label)
.collect::<Vec<_>>()
};
assert_eq!(
labels(1),
["Amount", "Left Time", "Right Time", "Feedback", "Vintage"]
);
assert_eq!(labels(2), ["Amount", "Size", "Damping"]);
assert_eq!(
labels(3),
["Amount", "Threshold", "Ratio", "Release", "Makeup"]
);
assert_eq!(labels(4), ["Amount", "Cutoff", "Resonance", "Type"]);
}
#[test]
fn module_context_owns_edit_reset_and_automation_units() {
let mut controls = FluidControls::default();
controls.modules.clap[1] = preset_slot("delay", 0.5);
controls.modules.clap[2] = preset_slot("room", 0.5);
controls.modules.clap[3] = preset_slot("compression", 0.5);
let left = spec_by_id("clap.slot2.time").unwrap();
let right = spec_by_id("clap.slot2.right_time").unwrap();
switch_delay_clock(&mut controls.modules.clap[1], true, 120.0);
left.apply_reset(&mut controls);
right.apply_reset(&mut controls);
assert_eq!(controls.modules.clap[1].time, DELAY_SYNC_MIN_BEATS);
assert_eq!(controls.modules.clap[1].right_time, DELAY_FREE_MIN_MS);
spec_by_id("clap.slot3.time")
.unwrap()
.apply_value(55.0, &mut controls);
assert_close(controls.modules.clap[2].time, 0.55);
spec_by_id("clap.slot4.time")
.unwrap()
.apply_value(-17.0, &mut controls);
assert_eq!(controls.modules.clap[3].time, -17.0);
let mut automation = AutomationState::default();
let route = automation.open_or_create(ControlAddress::new("clap.slot4.time"));
route.depth_ratio = 0.5;
apply_automation(
&mut controls,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
assert!(controls.modules.clap[3].time >= -40.0);
assert!(controls.modules.clap[3].time <= 0.0);
assert_ne!(controls.modules.clap[3].time, -17.0);
}
#[test]
fn master_compression_uses_the_shared_module_detail_shape() {
let controls = FluidControls::default();
let root_labels: Vec<_> = tab_controls(Tab::Master, &controls)
.into_iter()
.map(|item| item.label)
.collect();
assert!(root_labels.iter().any(|label| label == "Compression"));
assert!(!root_labels.iter().any(|label| label == "Comp Release"));
let labels: Vec<_> = module_detail_controls(Tab::Master, 1, &controls)
.into_iter()
.map(|item| item.label)
.collect();
assert_eq!(
labels,
["Amount", "Threshold", "Ratio", "Release", "Makeup"]
);
}
#[test]
fn module_slots_round_trip_through_a_song_code() {
let mut controls = FluidControls::default();
controls.modules.kick[2] = preset_slot("delay", 0.5);
controls.modules.kick[2].time = 0.5;
controls.modules.kick[2].right_time = 750.0;
controls.modules.kick[2].right_clock = DelayClock::Free.value();
controls.modules.kick[2].feedback = 0.35;
controls.modules.kick[2].vintage = 0.4;
controls.modules.arp[7].kind = 1.0;
let state = SongState::from_controls(controls.clone());
let code = encode_song_code(&state).expect("encode");
let decoded = decode_song_code(&code).expect("round trip");
assert_eq!(
decoded.controls.modules.kick[2].kind,
controls.modules.kick[2].kind
);
assert_eq!(decoded.controls.modules.kick[2].amount, 0.5);
assert_eq!(decoded.controls.modules.kick[2].time, 0.5);
assert_eq!(decoded.controls.modules.kick[2].right_time, 750.0);
assert_eq!(decoded.controls.modules.kick[2].clock, 0.0);
assert_eq!(decoded.controls.modules.kick[2].right_clock, 1.0);
assert_eq!(decoded.controls.modules.kick[2].feedback, 0.35);
assert_near(decoded.controls.modules.kick[2].vintage, 0.4);
assert_eq!(decoded.controls.modules.arp[7].kind, 1.0);
assert_eq!(decoded.controls.modules.pad[0].kind().unwrap().id, "room");
}
#[test]
fn empty_module_slots_cost_no_song_code_bytes() {
let bare = encode_song_code(&SongState::from_controls(FluidControls::default()))
.expect("encode defaults");
let mut controls = FluidControls::default();
controls.bass.level = 0.42;
let with_edit = encode_song_code(&SongState::from_controls(controls)).expect("encode one edit");
assert!(
with_edit.len() < bare.len() + 16,
"empty slots leaked into the code: {} -> {}",
bare.len(),
with_edit.len()
);
}
#[test]
fn tab_controls_classify_each_slider_kind() {
use ControlKind::{Continuous, Discrete, Gain, Timing};
let controls = FluidControls::default();
let cases = [
(
Tab::Master,
vec![
Gain, Gain, Gain, Gain, Gain, Gain, Gain, Gain, Timing, Gain, Continuous, Discrete,
Gain, Gain,
],
),
(Tab::Perc, vec![Gain, Timing, Timing, Timing, Continuous]),
(Tab::Chords, {
let mut kinds = vec![
Gain, Timing, Timing, Discrete, Timing, Discrete, Discrete, Gain, Gain, Gain,
];
kinds.extend(vec![Discrete; 40]);
kinds.push(Gain); kinds
}),
(
Tab::Bass,
vec![
Gain, Timing, Timing, Discrete, Timing, Timing, Discrete, Discrete, Continuous,
Gain,
],
),
(
Tab::Kick,
vec![
Gain, Timing, Timing, Discrete, Timing, Timing, Continuous, Gain, Continuous, Gain,
],
),
(
Tab::Tonal,
vec![
Gain, Timing, Timing, Discrete, Discrete, Discrete, Timing, Timing, Timing, Gain,
Continuous, Gain,
],
),
(
Tab::Clap,
vec![Gain, Gain, Timing, Timing, Timing, Discrete, Timing, Gain],
),
(
Tab::Arp,
vec![
Gain, Timing, Timing, Discrete, Timing, Timing, Discrete, Discrete, Gain,
],
),
(
Tab::Lead,
vec![
Gain, Discrete, Timing, Timing, Timing, Discrete, Discrete, Discrete, Timing,
Timing, Discrete, Gain,
],
),
];
for (tab, expected) in cases {
let actual: Vec<_> = tab_controls(tab, &controls)
.into_iter()
.map(|item| item.kind)
.collect();
assert_eq!(actual, expected, "unexpected kind map for {}", tab.name());
}
}
#[test]
fn control_registry_specs_are_internally_consistent() {
for (i, tab) in Tab::all().into_iter().enumerate() {
assert_eq!(
tab as usize, i,
"TAB_META row {i} out of discriminant order"
);
if let Some(id) = tab.level_id() {
assert!(
spec_by_id(id).is_some(),
"{}: level_id {id} names no registry control",
tab.name()
);
}
for spec in tab_specs(tab) {
let ctx = format!("{} / {}", tab.name(), spec.label);
assert!(!spec.id.is_empty(), "{ctx}: empty stable id");
assert!(!spec.label.is_empty(), "{ctx}: empty label");
assert!(spec.min < spec.max, "{ctx}: min must be below max");
assert!(
spec.reset >= spec.min && spec.reset <= spec.max,
"{ctx}: reset outside [min, max]"
);
if spec.taper == Taper::Log2 {
assert!(spec.min > 0.0, "{ctx}: log taper needs positive min");
}
if let Taper::Exp(n) = spec.taper {
assert!(n > 0.0, "{ctx}: exp taper needs a positive exponent");
assert!(spec.min >= 0.0, "{ctx}: exp taper needs a non-negative min");
}
if let Step::Linear(step) = spec.step {
assert!(step > 0.0, "{ctx}: step must be positive");
}
let mut c = FluidControls::default();
(spec.set)(&mut c, spec.max);
assert!(
((spec.get)(&c) - spec.max).abs() < 1e-6,
"{ctx}: get/set roundtrip failed at max"
);
(spec.set)(&mut c, spec.reset);
assert!(
((spec.get)(&c) - spec.reset).abs() < 1e-6,
"{ctx}: get/set roundtrip failed at reset"
);
}
}
}
#[test]
fn song_code_round_trips_quantized_snapshot_values() {
let mut controls = FluidControls::default();
controls.master.bpm = 123.4;
controls.pad.chord_bars = 12.0;
controls.clap.slap_count = 6.6;
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
assert_close(decoded.controls.master.bpm, 123.0);
assert_close(decoded.controls.pad.chord_bars, 16.0);
assert_close(decoded.controls.clap.slap_count, 7.0);
}
#[test]
fn song_code_round_trips_muted_layers() {
let mut song = SongState::from_controls(FluidControls::default());
song.muted[Tab::Tonal as usize] = true;
song.muted[Tab::Master as usize] = true;
let code = encode_song_code(&song).unwrap();
let decoded = decode_song_code(&code).unwrap();
assert_eq!(decoded.muted, song.muted);
}
#[test]
fn song_code_stays_short_with_every_stateful_module_filled() {
let mut controls = FluidControls::default();
controls.modules.pad[0] = preset_slot("delay", 0.8);
controls.modules.pad[1] = preset_slot("room", 0.8);
controls.modules.bass[0] = preset_slot("compression", 0.8);
let code = encode_song_code(&SongState::from_controls(controls)).unwrap();
assert!(
code.len() < 2_000,
"song code grew to {} characters",
code.len()
);
}
#[test]
fn song_code_round_trips_tonal_sequence_state() {
let sequence = TonalSequenceState {
phrase: 4,
notes: vec![52, 55, 60, 64, 67, 64, 60, 55],
evolution_seed: 123,
evolution_count: 9,
};
let song = SongState {
tonal_sequence: Some(sequence.clone()),
..SongState::from_controls(FluidControls::default())
};
let decoded = song::decode_song_code(&song::encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.tonal_sequence, Some(sequence));
}
#[test]
fn pads_root_projects_effect_modules_outside_the_chord_drill() {
let mut controls = FluidControls::default();
controls.modules.pad[0] = preset_slot("delay", 0.4);
let root = chords_tab_controls(&controls, ChordDrill::None);
let progression = chords_tab_controls(&controls, ChordDrill::Progression { return_to: 4 });
assert!(root.iter().any(|item| item.id == "pad.slot1.amount"));
assert!(!progression.iter().any(|item| item.id == "pad.slot1.amount"));
}
#[test]
fn song_code_round_trips_a_custom_progression() {
let mut controls = FluidControls::default();
controls.pad.progression = CUSTOM_PROGRESSION_INDEX as f32;
controls.pad.chord_count = 3.0;
controls.pad.chord_slots[0].degree = 2.0;
controls.pad.chord_slots[0].accidental = -1.0;
controls.pad.chord_slots[0].extension = 2.0;
controls.pad.chord_slots[0].inversion = 1.0;
controls.pad.chord_slots[2].degree = -3.0;
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
assert_close(
decoded.controls.pad.progression,
CUSTOM_PROGRESSION_INDEX as f32,
);
assert_close(decoded.controls.pad.chord_count, 3.0);
assert_close(decoded.controls.pad.chord_slots[0].degree, 2.0);
assert_close(decoded.controls.pad.chord_slots[0].accidental, -1.0);
assert_close(decoded.controls.pad.chord_slots[0].extension, 2.0);
assert_close(decoded.controls.pad.chord_slots[0].inversion, 1.0);
assert_close(decoded.controls.pad.chord_slots[2].degree, -3.0);
}
#[test]
fn song_code_predating_custom_progression_decodes_with_defaults() {
let controls = FluidControls::default();
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
let encoded = code.strip_prefix("n1_").unwrap();
let bytes = URL_SAFE_NO_PAD.decode(encoded).unwrap();
assert!(
!bytes
.windows(b"pad.chord1_degree".len())
.any(|window| window == b"pad.chord1_degree")
);
let decoded = song::decode_song_code(&code).unwrap();
assert_close(decoded.controls.pad.progression, 0.0);
assert_close(decoded.controls.pad.chord_count, 8.0);
assert_close(decoded.controls.pad.chord_slots[0].degree, 0.0);
}
#[test]
fn full_engine_renders_a_custom_progression_from_song_code_without_panicking() {
let mut controls = FluidControls::default();
controls.pad.progression = CUSTOM_PROGRESSION_INDEX as f32;
controls.pad.chord_count = 5.0;
controls.pad.chord_bars = 1.0;
for (slot, degree) in [2.0, -2.0, 4.0, -4.0, 6.0].into_iter().enumerate() {
controls.pad.chord_slots[slot].degree = degree;
controls.pad.chord_slots[slot].accidental = if slot % 2 == 0 { 1.0 } else { -1.0 };
controls.pad.chord_slots[slot].extension = (slot % 4) as f32;
controls.pad.chord_slots[slot].inversion = (slot % 3) as f32;
}
controls.bass.level = 0.5;
controls.arp.gain = 0.5;
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
assert_close(
decoded.controls.pad.progression,
CUSTOM_PROGRESSION_INDEX as f32,
);
let mut engine = engine_for(decoded.controls, decoded.automation);
for _ in 0..(SAMPLE_RATE as usize * 4) {
let (l, r) = engine.next_stereo();
assert!(
l.is_finite() && r.is_finite(),
"engine produced non-finite output"
);
}
}
fn round_trip(set: impl Fn(&mut FluidControls)) -> FluidControls {
let mut controls = FluidControls::default();
set(&mut controls);
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
song::decode_song_code(&code).unwrap().controls
}
#[test]
fn song_code_round_trips_control_values() {
let decoded = round_trip(|c| c.bass.voice_type = 2.0);
assert_close_named(decoded.bass.voice_type, 2.0, "bass.type");
let decoded = round_trip(|c| c.pad.voice_type = 5.0);
assert_close_named(decoded.pad.voice_type, 5.0, "pad.type");
let decoded = round_trip(|c| c.modules.bass[0].time = 500.0);
assert_quantized_named(decoded.modules.bass[0].time, 500.0, "bass filter cutoff");
let decoded = round_trip(|c| c.tonal.octave = -1.0);
assert_close_named(decoded.tonal.octave, -1.0, "tonal.octave");
let decoded = round_trip(|c| c.modules.arp[0].amount = 0.9);
assert_close_named(decoded.modules.arp[0].amount, 0.9, "arp reverb amount");
let decoded = round_trip(|c| c.arp.offset_beats = 1.5);
assert_close_named(decoded.arp.offset_beats, 1.5, "arp.offset_beats");
let decoded = round_trip(|c| {
c.tonal.attack = 0.2;
c.tonal.decay = 1.5;
});
assert_quantized_named(decoded.tonal.attack, 0.2, "tonal.attack");
assert_quantized_named(decoded.tonal.decay, 1.5, "tonal.decay");
}
#[test]
fn song_code_decodes_missing_controls_as_defaults() {
let decoded = round_trip(|_| {});
let default = FluidControls::default();
assert_close_named(
decoded.bass.voice_type,
default.bass.voice_type,
"bass.type",
);
assert_close_named(decoded.pad.voice_type, default.pad.voice_type, "pad.type");
assert_close_named(
decoded.modules.bass[0].time,
default.modules.bass[0].time,
"bass filter cutoff",
);
assert_close_named(decoded.tonal.octave, default.tonal.octave, "tonal.octave");
assert_close_named(
decoded.modules.arp[0].amount,
default.modules.arp[0].amount,
"arp reverb amount",
);
assert_close_named(
decoded.arp.offset_beats,
default.arp.offset_beats,
"arp.offset_beats",
);
assert_close_named(decoded.tonal.attack, default.tonal.attack, "tonal.attack");
assert_close_named(decoded.tonal.decay, default.tonal.decay, "tonal.decay");
assert_close_named(decoded.pad.level, default.pad.level, "pad.level");
}
#[test]
fn song_code_decodes_snapshot_only_payload_with_empty_automation() {
let mut controls = FluidControls::default();
controls.master.bpm = 120.0;
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
assert_eq!(decoded.automation.routes().count(), 0);
}
#[test]
fn song_code_round_trips_lfo_automation_record() {
let mut controls = FluidControls::default();
controls.master.level = 0.6;
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new("master.level"),
LfoRoute {
cycle_beats: 4.0,
depth_ratio: 0.4,
shape: LfoShape::Sine,
phase_offset_beats: 0.25,
..LfoRoute::default()
},
);
let song = SongState {
automation,
..SongState::from_controls(controls)
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let route = decoded
.automation
.route(ControlAddress::new("master.level"))
.unwrap();
assert_close(decoded.controls.master.level, 0.6);
assert_close(route.cycle_beats, 4.0);
assert_close(route.depth_ratio, 0.4);
assert_eq!(route.shape, LfoShape::Sine);
assert_close(route.phase_offset_beats, 0.25);
}
#[test]
fn song_code_skips_unknown_records() {
let mut controls = FluidControls::default();
controls.master.tune = 5.0;
let code = song::code_from_records(
song::CONTAINER_VERSION,
&[
(song::SNAPSHOT_RECORD, &song::snapshot_payload(&controls)),
(99, &[1, 2, 3, 4]),
],
);
let decoded = song::decode_song_code(&code).unwrap();
assert_close(decoded.controls.master.tune, 5.0);
}
#[test]
fn song_code_v2_skips_unknown_song_id_indexes_without_losing_alignment() {
let mut controls = FluidControls::default();
controls.master.tune = 5.0;
let mut payload = Vec::new();
payload.extend_from_slice(&2u16.to_le_bytes());
payload.extend_from_slice(&u16::MAX.to_le_bytes()); payload.push(song::VALUE_TAG_FLOAT);
payload.extend_from_slice(&0.75f32.to_le_bytes());
payload.extend_from_slice(&song_id_index("master.tune").unwrap().to_le_bytes());
payload.push(song::VALUE_TAG_INT);
payload.extend_from_slice(&7i16.to_le_bytes());
let code = song::code_from_records(
song::CONTAINER_VERSION,
&[
(song::SNAPSHOT_RECORD, &song::snapshot_payload(&controls)),
(song::SNAPSHOT_RECORD, &payload),
],
);
let decoded = song::decode_song_code(&code).unwrap();
assert_close(decoded.controls.master.tune, 7.0);
assert_close(
decoded.controls.master.level,
FluidControls::default().master.level,
);
}
#[test]
fn song_code_is_a_direct_cargo_run_argument() {
let code = song::encode_song_code(&SongState::default()).unwrap();
assert!(code.starts_with("n1_") && !code.contains(char::is_whitespace));
}
#[test]
fn control_kind_smoothing_policy_is_explicit() {
assert!(ControlKind::Gain.smooths_audio());
assert!(!ControlKind::Continuous.smooths_audio());
assert!(!ControlKind::Timing.smooths_audio());
assert!(!ControlKind::Discrete.smooths_audio());
}
#[test]
fn gain_smoother_reaches_target_over_ramp() {
let mut smoother = GainSmoother::new(0.0);
smoother.set_target(1.0, 10);
assert_near(smoother.next(), 0.028);
for _ in 0..4 {
smoother.next();
}
assert_near(smoother.ramp.current, 0.5);
for _ in 0..4 {
smoother.next();
}
assert_close(smoother.next(), 1.0);
assert_close(smoother.next(), 1.0);
}
#[test]
fn gain_smoothers_ramp_live_gain_controls_without_timing_changes() {
let mut controls = FluidControls::default();
controls.pad.level = 0.0;
controls.modules.pad[0].amount = 0.0;
controls.kick.click = 0.0;
controls.modules.kick[0].amount = 0.0;
controls.tonal.randomness = 0.0;
controls.clap.filter = 0.5;
controls.clap.body = 0.0;
controls.master.level = 0.0;
controls.modules.master[0].amount = 0.0;
controls.modules.bass[1].amount = 0.0;
let mut smoothers = GainSmoothers::new(&controls);
controls.pad.level = 1.0;
controls.modules.pad[0].amount = 1.0;
controls.kick.click = 0.2;
controls.tonal.randomness = 1.0;
controls.clap.filter = 1.0;
controls.clap.body = 1.0;
controls.master.level = 0.5;
controls.modules.master[0].amount = 1.0;
controls.master.bpm = 123.0;
controls.modules.bass[1].amount = 1.0;
controls.modules.kick[0].amount = 1.0;
smoothers.set_targets(&controls, 100.0);
let next = smoothers.next_controls(&controls);
assert_close(next.master.bpm, 123.0);
assert!(next.pad.level > 0.0 && next.pad.level < 1.0);
assert!(next.modules.pad[0].amount > 0.0 && next.modules.pad[0].amount < 1.0);
assert!(next.kick.click > 0.0 && next.kick.click < 0.2);
assert!(next.tonal.randomness > 0.0 && next.tonal.randomness < 1.0);
assert!(next.clap.filter > 0.5 && next.clap.filter < 1.0);
assert!(next.clap.body > 0.0 && next.clap.body < 1.0);
assert!(next.master.level > 0.0 && next.master.level < 0.5);
assert!(next.modules.master[0].amount > 0.0 && next.modules.master[0].amount < 1.0);
assert!(next.modules.bass[1].amount > 0.0 && next.modules.bass[1].amount < 1.0);
assert!(next.modules.kick[0].amount > 0.0 && next.modules.kick[0].amount < 1.0);
}
#[test]
fn gain_smoothers_cover_every_unique_gain_spec() {
let controls = FluidControls::default();
let smoothers = GainSmoothers::new(&controls);
let expected: std::collections::BTreeSet<_> = all_specs()
.filter(|spec| spec.kind == ControlKind::Gain)
.map(|spec| spec.id)
.collect();
let actual: std::collections::BTreeSet<_> = smoothers
.smoothers
.iter()
.map(|smoother| smoother.spec.id)
.collect();
assert_eq!(actual, expected);
}
#[test]
fn chords_tab_shows_progression_row_with_letter_display() {
let mut controls = FluidControls::default();
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[5].label, "Chord Count");
assert_eq!(rows[5].display, "8");
assert_eq!(rows[6].label, "Progression");
for (progression, display) in [
(0.0, "A"),
(2.0, "C"),
(CUSTOM_PROGRESSION_INDEX as f32, "Custom"),
] {
controls.pad.progression = progression;
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[6].display, display, "pad.progression {progression}");
}
}
#[test]
fn tonal_tab_separates_rate_from_cycle() {
let rows = tab_controls(Tab::Tonal, &FluidControls::default());
assert_eq!(rows[3].id, "tonal.synth_type");
assert_eq!(rows[3].label, "Type");
assert_eq!(rows[3].display, "Sine");
assert_eq!(rows[4].id, "tonal.octave");
assert_eq!(rows[4].label, "Octave");
assert_eq!(rows[5].id, "tonal.phrase");
assert_eq!(rows[5].label, "Phrase");
assert_eq!(rows[6].id, "tonal.rate_beats");
assert_eq!(rows[6].label, "Rate");
assert_eq!(rows[6].display, "0.50 beats");
assert_eq!(rows[7].id, "tonal.step_interval_beats");
assert_eq!(rows[7].label, "Cycle");
assert_eq!(rows[7].display, "16.00 beats");
}
#[test]
fn chords_progression_adjusts_and_clamps() {
let mut controls = FluidControls::default();
apply_delta(Tab::Chords, 6, 1.0, &mut controls);
assert_close(controls.pad.progression, 1.0);
controls.pad.progression = CUSTOM_PROGRESSION_INDEX as f32;
apply_delta(Tab::Chords, 6, 1.0, &mut controls);
assert_close(controls.pad.progression, CUSTOM_PROGRESSION_INDEX as f32);
controls.pad.progression = 0.0;
apply_delta(Tab::Chords, 6, -1.0, &mut controls);
assert_close(controls.pad.progression, 0.0);
controls.pad.progression = 2.0;
apply_reset(Tab::Chords, 6, &mut controls);
assert_close(controls.pad.progression, 0.0);
}
#[test]
fn chords_tab_controls_none_shows_only_base_params() {
let controls = FluidControls::default();
let rows = chords_tab_controls(&controls, ChordDrill::None);
assert_eq!(rows.len(), 11);
assert_eq!(rows[0].id, "pad.level");
assert_eq!(rows[6].id, "pad.progression");
assert_eq!(rows[2].id, "pad.release_time");
assert!(rows.iter().all(|r| !r.label.contains("Root")));
}
#[test]
fn chords_tab_controls_progression_lists_active_slot_roots() {
let mut controls = FluidControls::default();
controls.pad.chord_count = 3.0;
let rows = chords_tab_controls(&controls, progression_drill());
assert_eq!(
rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>(),
vec!["Chord 1 Root", "Chord 2 Root", "Chord 3 Root"]
);
controls.pad.chord_count = 8.0;
let rows = chords_tab_controls(&controls, progression_drill());
assert_eq!(rows.len(), 8);
assert_eq!(rows[7].label, "Chord 8 Root");
}
#[test]
fn chords_tab_controls_slot_shows_accidental_quality_extension_inversion() {
let controls = FluidControls::default();
let rows = chords_tab_controls(&controls, slot_drill(2));
assert_eq!(
rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>(),
vec![
"Chord 3 Accidental",
"Chord 3 Quality",
"Chord 3 Extension",
"Chord 3 Inversion"
]
);
}
#[test]
fn chords_flat_index_maps_visible_rows_to_chords_controls_indices() {
assert_eq!(chords_flat_index(ChordDrill::None, 4), 4);
assert_eq!(chords_flat_index(progression_drill(), 0), 10);
assert_eq!(chords_flat_index(progression_drill(), 2), 20);
assert_eq!(chords_flat_index(slot_drill(2), 0), 21);
let controls = FluidControls::default();
let expected = tab_controls(Tab::Chords, &controls)[21].id;
assert_eq!(expected, "pad.chord3_accidental");
}
#[test]
fn render_fluid_shows_chords_drill_breadcrumb_and_footer() {
let controls = FluidControls::default();
let fluid = RippleField::new();
let automation = AutomationState::default();
let buffer = render_to_buffer(RenderTest {
size: (120, 40),
tab: Tab::Chords,
cursor: 0,
submenu: 0,
beat: 0.0,
fluid: &fluid,
automation: &automation,
controls: &controls,
footer: None,
drill: slot_drill(1),
active_chord: 0,
mute: &[false; TAB_COUNT],
});
let text = buffer_text(&buffer);
assert!(text.contains("Pads › Chord 2"));
assert!(text.contains("BROWSE · Chord 2 Shift+R randomize set Esc: back"));
}
fn render_progression(controls: &FluidControls, active_chord: u64) -> String {
let fluid = RippleField::new();
let automation = AutomationState::default();
buffer_text(&render_to_buffer(RenderTest {
size: (120, 40),
tab: Tab::Chords,
cursor: 0,
submenu: 0,
beat: 0.0,
fluid: &fluid,
automation: &automation,
controls,
footer: None,
drill: progression_drill(),
active_chord,
mute: &[false; TAB_COUNT],
}))
}
#[test]
fn render_marks_single_active_chord_in_progression() {
let mut controls = FluidControls::default();
controls.pad.chord_count = 4.0;
let text = render_progression(&controls, 2);
assert_eq!(text.matches('♪').count(), 1);
}
#[test]
fn render_active_chord_index_wraps_by_chord_count() {
let mut controls = FluidControls::default();
controls.pad.chord_count = 4.0;
let text = render_progression(&controls, 6);
assert_eq!(text.matches('♪').count(), 1);
}
#[test]
fn render_slot_breadcrumb_marks_live_chord() {
let mut controls = FluidControls::default();
controls.pad.chord_count = 4.0;
let fluid = RippleField::new();
let automation = AutomationState::default();
let draw = |active_chord: u64| {
buffer_text(&render_to_buffer(RenderTest {
size: (120, 40),
tab: Tab::Chords,
cursor: 0,
submenu: 0,
beat: 0.0,
fluid: &fluid,
automation: &automation,
controls: &controls,
footer: None,
drill: slot_drill(2),
active_chord,
mute: &[false; TAB_COUNT],
}))
};
assert!(draw(2).contains("Chord 3 ♪"));
assert!(!draw(0).contains("Chord 3 ♪"));
}
#[test]
fn bass_rhythms_have_expected_hit_counts() {
assert_eq!(BASS_RHYTHMS[0].iter().filter(|&&b| b).count(), 4);
assert!(BASS_RHYTHMS[0][0]);
assert!(BASS_RHYTHMS[1].iter().filter(|&&b| b).count() > 4);
assert_eq!(BASS_RHYTHMS[2].iter().filter(|&&b| b).count(), 8);
}
#[test]
fn bass_root_note_follows_authored_bass_line() {
let pad = PadControls::default();
assert_eq!(bass_root_note(0, 0, &pad), 45);
assert_eq!(bass_root_note(0, 3, &pad), 43);
assert_eq!(bass_root_note(2, 3, &pad), 43);
}
#[test]
fn bass_root_note_follows_custom_chord_slot_root_when_selected() {
let mut pad = PadControls::default();
pad.chord_slots[3].degree = -1.0;
pad.chord_slots[3].accidental = 1.0;
let root = bass_root_note(CUSTOM_PROGRESSION_INDEX, 3, &pad);
assert_eq!(root, pad_chord_root_note(&pad.chord_slots[3]));
}
#[test]
fn bass_defaults_are_silent_quarter_note_a() {
let controls = BassControls::default();
assert_close(controls.level, 0.0);
assert_close(controls.voice_type, 0.0);
assert_close(controls.rhythm, 0.0);
assert_close(controls.octave, -1.0);
assert_close(controls.interval_beats, 4.0);
}
#[test]
fn bass_tab_shows_type_and_rhythm_rows_with_letter_display() {
let mut controls = FluidControls::default();
let rows = tab_controls(Tab::Bass, &controls);
assert_eq!(rows[3].id, "bass.type");
assert_eq!(rows[3].label, "Type");
assert_eq!(rows[6].label, "Rhythm");
for (voice_type, display) in [(0.0, "Sub"), (1.0, "Saw"), (2.0, "Pluck")] {
controls.bass.voice_type = voice_type;
let rows = tab_controls(Tab::Bass, &controls);
assert_eq!(rows[3].display, display, "bass.type {voice_type}");
}
for (rhythm, display) in [(0.0, "A"), (3.0, "D")] {
controls.bass.rhythm = rhythm;
let rows = tab_controls(Tab::Bass, &controls);
assert_eq!(rows[6].display, display, "bass.rhythm {rhythm}");
}
}
#[test]
fn bass_controls_adjust_and_clamp() {
let mut controls = FluidControls::default();
apply_delta(Tab::Bass, 6, 1.0, &mut controls);
assert_close(controls.bass.rhythm, 1.0);
controls.bass.rhythm = 3.0;
apply_delta(Tab::Bass, 6, 1.0, &mut controls);
assert_close(controls.bass.rhythm, 3.0);
controls.bass.octave = -1.0;
apply_delta(Tab::Bass, 7, -1.0, &mut controls);
apply_delta(Tab::Bass, 7, -1.0, &mut controls);
assert_close(controls.bass.octave, -3.0);
apply_reset(Tab::Bass, 0, &mut controls);
assert_close(controls.bass.level, 0.0);
controls.bass.decay_time = 0.4;
apply_delta(Tab::Bass, 2, 1.0, &mut controls);
assert!(controls.bass.decay_time > 0.4);
apply_reset(Tab::Bass, 2, &mut controls);
assert_close(controls.bass.decay_time, 0.005);
}
#[test]
fn bass_engine_follows_pad_chord_root_across_advances() {
let sample_rate = 48_000.0;
let mut bass = BassEngine::new(sample_rate);
let pad = PadControls {
chord_bars: 1.0 / 4.0, ..PadControls::default()
};
let bass_controls = BassControls {
interval_beats: 1.0,
rhythm: 0.0,
..BassControls::default()
};
let mut clock = TempoClock::new(sample_rate, 120.0);
for _ in 0..(sample_rate as usize) {
let timing = clock.tick(120.0);
bass.next(&bass_controls, &pad, 0.0, timing);
}
assert_ne!(bass.progression.step_index, 0);
assert!(bass.rhythm_step < BASS_RHYTHMS[0].len());
}
#[test]
fn bass_engine_is_monophonic_and_hard_cuts_on_retrigger() {
let sample_rate = 48_000.0;
let mut bass = BassEngine::new(sample_rate);
let pad = PadControls::default();
let bass_controls = BassControls {
interval_beats: 1.0,
rhythm: 0.0, decay_time: 5.0, attack_time: 0.001,
..BassControls::default()
};
let mut clock = TempoClock::new(sample_rate, 120.0);
for _ in 0..(sample_rate * 1.2) as usize {
let timing = clock.tick(120.0);
bass.next(&bass_controls, &pad, 0.0, timing);
}
assert!(bass.voice.is_some());
assert!(bass.fading_voice.is_none());
}
#[test]
fn bass_voice_decays_to_silence_without_sustaining() {
let sample_rate = 48_000.0;
let mut voice = BassVoice::new(0, 110.0, 0.005, 0.05, sample_rate);
for _ in 0..(sample_rate * 0.5) as usize {
voice.next();
}
assert!(voice.next().abs() < 0.001);
}
#[test]
fn bass_type_zero_matches_legacy_sub_voice_exactly() {
let sample_rate = 48_000.0;
let mut dispatched = BassVoice::new(0, 110.0, 0.01, 0.05, sample_rate);
let mut legacy = SubBassVoice::new(110.0, 0.01, 0.05, sample_rate);
for _ in 0..(sample_rate * 0.3) as usize {
assert_eq!(dispatched.next(), legacy.next());
}
}
type SoundVariant<'a> = (&'a str, Box<dyn FnMut() -> (f32, f32)>);
fn assert_types_differ_but_balanced(label: &str, samples: usize, mut types: Vec<SoundVariant>) {
let mut level_samples: Vec<Vec<f32>> = (0..types.len())
.map(|_| Vec::with_capacity(samples))
.collect();
let mut diff_samples: Vec<Vec<f32>> = (0..types.len())
.map(|_| Vec::with_capacity(samples))
.collect();
for _ in 0..samples {
for (i, (_, step)) in types.iter_mut().enumerate() {
let (level, diff) = step();
level_samples[i].push(level);
diff_samples[i].push(diff);
}
}
let rms: Vec<f32> = level_samples
.iter()
.map(|levels| crate::synth::fm::rms(levels))
.collect();
for (i, &r) in rms.iter().enumerate() {
assert!(r > 0.0, "{label}: {} produced silence", types[i].0);
}
for i in 1..types.len() {
let any_diff = diff_samples[0]
.iter()
.zip(&diff_samples[i])
.any(|(a, b)| (a - b).abs() > 1e-6);
assert!(
any_diff,
"{label}: {} does not differ from {}",
types[i].0, types[0].0
);
}
let max = rms.iter().cloned().fold(f32::MIN, f32::max);
let min = rms.iter().cloned().fold(f32::MAX, f32::min);
let levels = types
.iter()
.zip(&rms)
.map(|((name, _), r)| format!("{name}={r}"))
.collect::<Vec<_>>()
.join(", ");
assert!(max / min < 2.0, "{label} types not level-matched: {levels}");
}
#[test]
fn bass_types_produce_differing_but_comparably_balanced_audio() {
let sample_rate = 48_000.0;
let samples = (sample_rate * 0.4) as usize;
let types: Vec<SoundVariant> = [(0usize, "sub"), (1, "saw"), (2, "pluck")]
.into_iter()
.map(|(voice_type, name)| {
let mut voice = BassVoice::new(voice_type, 110.0, 0.01, 0.3, sample_rate);
let step: Box<dyn FnMut() -> (f32, f32)> = Box::new(move || {
let s = voice.next();
(s, s)
});
(name, step)
})
.collect();
assert_types_differ_but_balanced("bass", samples, types);
}
#[test]
fn pad_type_zero_keeps_the_legacy_warm_signal_path() {
let warm = PadTone::new(0, 220.0, 0.2, 0.15, 0.5, 1.0, 48_000.0);
assert!(matches!(warm.stage, PadStage::None));
assert_eq!(warm.output_gain, 1.0);
}
#[test]
fn pad_types_produce_differing_but_comparably_balanced_audio() {
let sample_rate = 48_000.0;
let samples = (sample_rate * 0.4) as usize;
let types: Vec<SoundVariant> = [
(0usize, "warm"),
(1, "dark"),
(2, "glass"),
(3, "choir"),
(4, "hollow"),
(5, "tape"),
]
.into_iter()
.map(|(character, name)| {
let mut tone = PadTone::new(character, 220.0, 0.0, 0.15, 0.05, 1.0, sample_rate);
let step: Box<dyn FnMut() -> (f32, f32)> = Box::new(move || {
let (l, r) = tone.next_stereo(0.8, 0.5, 0.5);
(((l * l + r * r) / 2.0).sqrt(), l)
});
(name, step)
})
.collect();
assert_types_differ_but_balanced("pad", samples, types);
}
#[test]
fn kick_type_zero_matches_legacy_sub_voice_exactly() {
let sample_rate = 48_000.0;
let controls = KickControls {
level: 0.6,
..Default::default()
};
let mut dispatched = KickVoice::new(0, &controls, sample_rate, &mut StdRng::seed_from_u64(7));
let mut legacy = LowpassKickVoice::new(
&KICK_SUB,
&controls,
sample_rate,
&mut StdRng::seed_from_u64(7),
);
let mut click_rng_a = StdRng::seed_from_u64(99);
let mut click_rng_b = StdRng::seed_from_u64(99);
for _ in 0..(sample_rate * 0.3) as usize {
assert_eq!(
dispatched.next(&mut click_rng_a),
legacy.next(&mut click_rng_b)
);
}
}
#[test]
fn bass_interval_crops_phrase_instead_of_stretching_it() {
let hits_within = |rhythm: usize, loop_len: usize| -> Vec<usize> {
(0..loop_len)
.filter(|&s| s < BASS_RHYTHMS[rhythm].len() && BASS_RHYTHMS[rhythm][s])
.collect()
};
assert_eq!(hits_within(0, 16), vec![0, 4, 8, 12]);
assert_eq!(hits_within(0, 4), vec![0]);
assert_eq!(hits_within(0, 8), vec![0, 4]);
let full = hits_within(1, 16);
let cropped = hits_within(1, 8);
assert!(cropped.len() < full.len());
assert!(cropped.iter().all(|s| full.contains(s)));
}
#[test]
fn chords_shows_the_preloaded_shared_reverb_module() {
let controls = FluidControls::default();
let rows = tab_controls(Tab::Chords, &controls);
assert!(rows.iter().any(|row| row.label == "Reverb"));
}
#[test]
fn chords_release_row_present_with_lowered_attack_floor() {
let controls = FluidControls::default();
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[1].label, "Attack");
assert_close(rows[1].min, 0.05);
assert_eq!(rows[2].label, "Release");
assert_close(rows[2].value, 8.0);
assert_close(rows[2].min, 0.05);
assert_close(rows[2].max, 20.0);
}
#[test]
fn chords_attack_and_release_adjust_and_clamp_low() {
let mut controls = FluidControls::default();
controls.pad.attack_time = 0.1;
apply_delta(Tab::Chords, 1, -1.0, &mut controls);
assert!(controls.pad.attack_time < 0.1 && controls.pad.attack_time >= 0.05);
apply_reset(Tab::Chords, 1, &mut controls);
assert_close(controls.pad.attack_time, 0.05);
apply_delta(Tab::Chords, 1, -1.0, &mut controls);
assert_close(controls.pad.attack_time, 0.05);
controls.pad.release_time = 0.1;
apply_delta(Tab::Chords, 2, -1.0, &mut controls);
assert!(controls.pad.release_time < 0.1 && controls.pad.release_time >= 0.05);
apply_reset(Tab::Chords, 2, &mut controls);
assert_close(controls.pad.release_time, 0.05);
apply_delta(Tab::Chords, 2, -1.0, &mut controls);
assert_close(controls.pad.release_time, 0.05);
}
#[test]
fn kick_interval_floor_is_eighth_beat() {
let mut controls = FluidControls::default();
controls.kick.interval_beats = 1.0;
apply_reset(Tab::Kick, 4, &mut controls);
assert_close(controls.kick.interval_beats, 0.125);
controls.kick.interval_beats = 0.125;
apply_delta(Tab::Kick, 4, -1.0, &mut controls);
assert_close(controls.kick.interval_beats, 0.125);
}
#[test]
fn perc_continuous_mode_pushes_no_hits() {
let controls = PercControls {
level: 1.0,
interval_beats: 4.25,
..Default::default()
};
let mut engine = PercEngine::new(SAMPLE_RATE);
engine.rng = StdRng::seed_from_u64(7);
let bpm = 82.0;
for sample in 0..(SAMPLE_RATE as u64 * 2) {
let t = timing(sample, bpm);
engine.next(&controls, t);
}
assert!(engine.hits.is_empty());
}
#[test]
fn perc_continuous_mode_has_no_periodic_rms_dips() {
let controls = PercControls {
level: 1.0,
interval_beats: 4.25,
..Default::default()
};
let mut engine = PercEngine::new(SAMPLE_RATE);
engine.rng = StdRng::seed_from_u64(7);
let bpm = 82.0;
let window_samples = (SAMPLE_RATE * 0.01) as usize;
let total_samples = SAMPLE_RATE as usize * 2;
let mut window_rms = Vec::new();
let mut window = Vec::with_capacity(window_samples);
for sample in 0..total_samples as u64 {
let t = timing(sample, bpm);
let out = engine.next(&controls, t);
window.push(out);
if window.len() == window_samples {
window_rms.push(crate::synth::fm::rms(&window));
window.clear();
}
}
let settle_windows = (SAMPLE_RATE * 0.25) as usize / window_samples;
let rms_tail = &window_rms[settle_windows..];
let min_rms = rms_tail.iter().cloned().fold(f32::INFINITY, f32::min);
let max_rms = rms_tail.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
assert!(
min_rms > 0.0,
"continuous mode produced silence in a window"
);
assert!(
max_rms / min_rms < 2.0,
"windowed RMS varies too much ({min_rms}..{max_rms}), suggests periodic triggering survived"
);
}
#[test]
fn perc_tab_controls_include_interval_and_offset() {
let controls = FluidControls::default();
let rows = tab_controls(Tab::Perc, &controls);
assert_eq!(rows.len(), 5);
assert_eq!(rows[2].label, "Interval");
assert_close(rows[2].min, 0.125);
assert_close(rows[2].max, 4.25);
assert_eq!(rows[3].label, "Offset");
assert_close(rows[3].min, 0.0);
assert_close(rows[3].max, 4.0);
}
#[test]
fn perc_interval_displays_continuous_at_top() {
let mut controls = FluidControls::default();
controls.perc.interval_beats = 4.25;
let rows = tab_controls(Tab::Perc, &controls);
assert_eq!(rows[2].display, "Continuous");
}
#[test]
fn perc_interval_and_offset_adjust_and_clamp() {
let mut controls = FluidControls::default();
apply_delta(Tab::Perc, 2, 1.0, &mut controls);
assert_close(controls.perc.interval_beats, 0.5);
controls.perc.interval_beats = 0.25;
apply_delta(Tab::Perc, 2, -1.0, &mut controls);
assert_close(controls.perc.interval_beats, 0.125);
apply_delta(Tab::Perc, 2, 1.0, &mut controls);
assert_close(controls.perc.interval_beats, 0.25);
controls.perc.interval_beats = 4.25;
apply_delta(Tab::Perc, 2, 1.0, &mut controls);
assert_close(controls.perc.interval_beats, 4.25);
apply_delta(Tab::Perc, 3, 1.0, &mut controls);
assert_close(controls.perc.offset_beats, 0.125);
controls.perc.offset_beats = 4.0;
apply_delta(Tab::Perc, 3, 1.0, &mut controls);
assert_close(controls.perc.offset_beats, 4.0);
apply_reset(Tab::Perc, 2, &mut controls);
assert_close(controls.perc.interval_beats, 0.125);
apply_reset(Tab::Perc, 3, &mut controls);
assert_close(controls.perc.offset_beats, 0.0);
}
#[test]
fn offset_grid_keeps_true_zero_reachable_below_the_floor() {
let mut controls = FluidControls::default();
apply_value(Tab::Perc, 3, 0.03, &mut controls);
assert_close(controls.perc.offset_beats, 0.0);
apply_value(Tab::Perc, 3, 0.09, &mut controls);
assert_close(controls.perc.offset_beats, 0.125);
apply_value(Tab::Perc, 3, 0.3, &mut controls);
assert_close(controls.perc.offset_beats, 0.25);
controls.perc.offset_beats = 0.125;
apply_delta(Tab::Perc, 3, -1.0, &mut controls);
assert_close(controls.perc.offset_beats, 0.0);
apply_delta(Tab::Perc, 3, -1.0, &mut controls);
assert_close(controls.perc.offset_beats, 0.0);
}
#[test]
fn pad_engine_caps_released_layers() {
let controls = PadControls {
chord_bars: 1.0,
attack_time: 1.0,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
for chord in 1..12 {
let sample = chord * SAMPLE_RATE as u64 * 2;
let _ = pad.next(&controls, 0.0, timing(sample, 120.0));
assert!(pad.layers.len() <= MAX_PAD_LAYERS);
}
}
#[test]
fn pad_engine_step_index_wraps_at_eight() {
let controls = PadControls {
chord_bars: 1.0,
attack_time: 1.0,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
advance_chords(&mut pad, &controls, 9);
let final_index = pad.telemetry.chord_index.load(Ordering::Relaxed);
assert!(
final_index < 8,
"step_index must wrap into 0..8, got {final_index}"
);
}
#[test]
fn pad_engine_progression_switch_waits_for_the_next_loop_boundary() {
let mut controls = PadControls {
chord_bars: 64.0, attack_time: 0.001,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
for sample in 0..10 {
let _ = pad.next(&controls, 0.0, timing(sample, 120.0));
}
let layers_before = pad.layers.len();
controls.progression = 1.0;
let _ = pad.next(&controls, 0.0, timing(10, 120.0));
assert_eq!(pad.layers.len(), layers_before);
}
#[test]
fn pad_voices_its_opening_chord_at_the_master_tune() {
let controls = PadControls {
chord_bars: 64.0,
level: 1.0,
..PadControls::default()
};
let render = |tune: f32| {
let mut pad = PadEngine::new(
SAMPLE_RATE,
&controls,
tune,
Arc::new(FluidTelemetry::default()),
);
(0..4_000)
.map(|sample| {
let (l, _) = pad.next(&controls, tune, timing(sample, 120.0));
l
})
.collect::<Vec<_>>()
};
let concert = render(0.0);
let transposed = render(12.0);
let difference = concert
.iter()
.zip(&transposed)
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
assert!(
difference > 1e-4,
"the opening chord ignored master.tune (max difference {difference})"
);
}
#[test]
fn pad_engine_type_change_revoices_the_current_chord_immediately() {
let mut controls = PadControls {
chord_bars: 64.0,
attack_time: 0.001,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
for sample in 0..10 {
let _ = pad.next(&controls, 0.0, timing(sample, 120.0));
}
let layers_before = pad.layers.len();
controls.voice_type = 3.0;
let _ = pad.next(&controls, 0.0, timing(10, 120.0));
assert_eq!(
pad.layers.len(),
layers_before,
"a Pad Type change must not voice a new layer"
);
assert!(
pad.layers
.iter()
.flat_map(|layer| &layer.tones)
.all(|tone| matches!(tone.stage, PadStage::Choir { .. })),
"changing Pad Type must revoice the sounding chord without waiting for its trigger"
);
}
#[test]
fn pad_chord_notes_with_slot_builds_notes_from_root_extension_and_inversion() {
let slot = ChordSlotControls {
degree: 1.0,
accidental: -1.0,
quality: 0.0,
extension: 2.0,
inversion: 1.0,
};
let notes = pad_chord_notes_with_slot(&slot);
assert_eq!(notes, pad_chord_notes_with_slot(&slot));
assert_eq!(notes.len(), 4);
assert!(notes.windows(2).all(|pair| pair[0] < pair[1]));
}
#[test]
fn chord_slot_quality_overrides_the_third_for_modal_interchange() {
let tonic = ChordSlotControls::default();
assert_eq!(pad_chord_notes_with_slot(&tonic)[1], 45 + 3);
assert!(pad_chord_slot_is_minor(&tonic));
let tonic_major = ChordSlotControls {
quality: 1.0,
..ChordSlotControls::default()
};
let notes = pad_chord_notes_with_slot(&tonic_major);
assert_eq!(notes[0], 45);
assert_eq!(notes[1], 45 + 4);
assert_eq!(notes[2], 45 + 7);
assert!(!pad_chord_slot_is_minor(&tonic_major));
let third_degree = ChordSlotControls {
degree: 2.0,
..ChordSlotControls::default()
};
assert!(!pad_chord_slot_is_minor(&third_degree));
let third_degree_minor = ChordSlotControls {
degree: 2.0,
quality: -1.0,
..ChordSlotControls::default()
};
let root = pad_chord_notes_with_slot(&third_degree_minor)[0];
assert_eq!(pad_chord_notes_with_slot(&third_degree_minor)[1], root + 3);
assert!(pad_chord_slot_is_minor(&third_degree_minor));
let mut controls = FluidControls::default();
let display = spec_by_id("pad.chord1_quality")
.expect("quality spec")
.display;
assert_eq!(display(&controls), "scale (min)");
controls.pad.chord_slots[0].degree = 2.0;
assert_eq!(display(&controls), "scale (maj)");
controls.pad.chord_slots[0].quality = 1.0;
assert_eq!(display(&controls), "maj");
}
#[test]
fn pad_chord_root_note_applies_degree_and_accidental() {
let flat_default = ChordSlotControls::default();
assert_eq!(pad_chord_root_note(&flat_default), 45);
let sharp_second = ChordSlotControls {
degree: 1.0,
accidental: 1.0,
..ChordSlotControls::default()
};
assert_eq!(pad_chord_root_note(&sharp_second), 48);
}
#[test]
fn custom_progression_pad_bass_and_arp_read_the_same_chord_source() {
let mut pad = PadControls {
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_count: 4.0,
..PadControls::default()
};
pad.chord_slots[2].degree = 3.0;
pad.chord_slots[2].accidental = -1.0;
pad.chord_slots[2].extension = 1.0;
let tones = pad_chord_tones(&pad, CUSTOM_PROGRESSION_INDEX, 2);
let root = bass_root_note(CUSTOM_PROGRESSION_INDEX, 2, &pad);
assert_eq!(root, pad_chord_root_note(&pad.chord_slots[2]));
assert_eq!(root, tones[0]);
assert_eq!(arp_cycle_notes(tones, 1), {
let mut sorted = tones;
sorted.sort_unstable();
sorted.to_vec()
});
}
#[test]
fn pad_chord_count_gates_step_wrap_in_every_progression_mode() {
let built_in = PadControls {
progression: 0.0,
chord_count: 2.0, ..PadControls::default()
};
assert_eq!(pad_chord_count(&built_in), 2);
let custom = PadControls {
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_count: 2.0,
..PadControls::default()
};
assert_eq!(pad_chord_count(&custom), 2);
}
#[test]
fn pad_engine_step_index_wraps_at_pad_chord_count_on_built_in_and_custom_progressions() {
for progression in [0.0, CUSTOM_PROGRESSION_INDEX as f32] {
let controls = PadControls {
chord_bars: 1.0,
progression,
chord_count: 2.0,
attack_time: 1.0,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
for chord in 1..=5 {
let sample = chord * SAMPLE_RATE as u64 * 2;
let _ = pad.next(&controls, 0.0, timing(sample, 120.0));
assert!(
pad.step_index < 2,
"progression {progression} ran past its chord count"
);
}
}
}
#[test]
fn bass_engine_step_index_wraps_at_pad_chord_count_in_custom_mode() {
let sample_rate = 48_000.0;
let mut bass = BassEngine::new(sample_rate);
let pad = PadControls {
chord_bars: 1.0,
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_count: 2.0,
..PadControls::default()
};
let bass_controls = BassControls::default();
for chord in 1..=5 {
let sample = chord * sample_rate as u64 * 2;
let timing = timing(sample, 120.0);
bass.next(&bass_controls, &pad, 0.0, timing);
assert!(bass.progression.step_index < 2);
}
}
#[test]
fn pad_engine_chord_count_change_finishes_the_current_chord_before_relooping() {
let mut controls = PadControls {
chord_bars: 1.0,
chord_count: 3.0,
attack_time: 1.0,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
advance_chords(&mut pad, &controls, 3);
assert_eq!(pad.step_index, 2);
let layers_before = pad.layers.len();
controls.chord_count = 2.0;
let _ = pad.next(&controls, 0.0, timing(SAMPLE_RATE as u64 * 6, 120.0));
assert_eq!(pad.step_index, 2);
assert_eq!(pad.layers.len(), layers_before);
let _ = pad.next(&controls, 0.0, timing(SAMPLE_RATE as u64 * 8, 120.0));
assert_eq!(pad.step_index, 0);
assert_eq!(pad.active_chord_count, 2);
}
#[test]
fn pad_engine_progression_change_finishes_the_current_loop_before_switching() {
let mut controls = PadControls {
chord_bars: 1.0,
chord_count: 3.0,
attack_time: 1.0,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
advance_chords(&mut pad, &controls, 3);
assert_eq!(pad.step_index, 2);
let layers_before = pad.layers.len();
controls.progression = 1.0;
let _ = pad.next(&controls, 0.0, timing(SAMPLE_RATE as u64 * 6, 120.0));
assert_eq!(pad.step_index, 2);
assert_eq!(pad.layers.len(), layers_before);
assert_eq!(pad.active_progression, 0);
let _ = pad.next(&controls, 0.0, timing(SAMPLE_RATE as u64 * 8, 120.0));
assert_eq!(pad.step_index, 0);
assert_eq!(pad.active_progression, 1);
}
#[test]
fn pad_engine_chord_slot_edit_retriggers_immediately() {
let mut controls = PadControls {
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_bars: 64.0,
attack_time: 0.001,
..PadControls::default()
};
let mut pad = pad_engine(&controls);
for sample in 0..10 {
let _ = pad.next(&controls, 0.0, timing(sample, 120.0));
}
let layers_before = pad.layers.len();
controls.chord_slots[0].degree = 1.0;
let _ = pad.next(&controls, 0.0, timing(10, 120.0));
assert!(
pad.layers.len() > layers_before,
"editing the current chord slot must push a new layer immediately"
);
}
#[test]
fn tempo_clock_preserves_beat_phase_when_bpm_changes() {
let mut clock = TempoClock::new(SAMPLE_RATE, 120.0);
let mut before = clock.tick(120.0);
for _ in 1..20_000 {
before = clock.tick(120.0);
}
let after = clock.tick(60.0);
assert!(after.beat > before.beat);
assert!(after.beat - before.beat < 0.001);
assert!(after.bpm < before.bpm);
assert!(after.bpm > 60.0);
}
#[test]
fn grid_trigger_keeps_next_hit_when_only_bpm_changes() {
let mut clock = TempoClock::new(SAMPLE_RATE, 120.0);
let mut trigger = GridTrigger::new();
for _ in 0..25_000 {
let timing = clock.tick(120.0);
let _ = trigger.pop(timing, 1.0, 0.0);
}
let before = trigger.next_hit.map(|hit| hit.beat);
let timing = clock.tick(60.0);
let fired = trigger.pop(timing, 1.0, 0.0);
let after = trigger.next_hit.map(|hit| hit.beat);
assert!(!fired);
assert_eq!(before, after);
}
#[test]
fn grid_trigger_fires_identically_for_same_params() {
let mut a = GridTrigger::new();
let mut b = GridTrigger::new();
let mut a_hits = Vec::new();
let mut b_hits = Vec::new();
for sample in 0..(SAMPLE_RATE as u64 * 6) {
let timing = timing(sample, 120.0);
if a.pop(timing, 2.0, 1.0) {
a_hits.push(sample);
}
if b.pop(timing, 2.0, 1.0) {
b_hits.push(sample);
}
}
assert!(a_hits.len() >= 3);
assert_eq!(a_hits, b_hits);
}
#[test]
fn grid_trigger_no_silence_after_bpm_decrease() {
let change_at = 50_000u64;
let mut clock = TempoClock::new(SAMPLE_RATE, 120.0);
let mut kick = GridTrigger::new();
let mut clap = GridTrigger::new();
let mut kick_hits: Vec<u64> = Vec::new();
let mut clap_hits: Vec<u64> = Vec::new();
for sample in 0..change_at {
let timing = clock.tick(120.0);
if kick.pop(timing, 1.0, 0.0) {
kick_hits.push(sample);
}
if clap.pop(timing, 2.0, 1.0) {
clap_hits.push(sample);
}
}
for sample in change_at..(SAMPLE_RATE as u64 * 8) {
let timing = clock.tick(60.0);
if kick.pop(timing, 1.0, 0.0) {
kick_hits.push(sample);
}
if clap.pop(timing, 2.0, 1.0) {
clap_hits.push(sample);
}
}
let one_beat_samples = (60.0 / 60.0 * SAMPLE_RATE as f64) as u64;
let first_post = kick_hits.iter().copied().find(|&s| s >= change_at);
assert!(
first_post.is_some_and(|s| s - change_at <= one_beat_samples),
"kick stalled after BPM decrease"
);
}
#[test]
fn grid_trigger_no_silence_after_interval_increase() {
let change_at = 50_000u64;
let mut trigger = GridTrigger::new();
let mut hits: Vec<u64> = Vec::new();
for sample in 0..change_at {
if trigger.pop(timing(sample, 120.0), 0.5, 0.0) {
hits.push(sample);
}
}
for sample in change_at..(SAMPLE_RATE as u64 * 8) {
if trigger.pop(timing(sample, 120.0), 4.0, 0.0) {
hits.push(sample);
}
}
let new_interval_samples = (4.0 * 60.0 / 120.0 * SAMPLE_RATE) as u64;
let first_post = hits.iter().copied().find(|&s| s >= change_at);
assert!(
first_post.is_some_and(|s| s - change_at <= new_interval_samples),
"trigger stalled after interval increase"
);
}
fn max_hit_gap(hit_beats: &[f64], total_beats: f64) -> f64 {
let mut max_gap = 0.0f64;
let mut prev = 0.0f64;
for &beat in hit_beats {
max_gap = max_gap.max(beat - prev);
prev = beat;
}
max_gap.max(total_beats - prev)
}
fn run_grid(
total_beats: f64,
mut step: impl FnMut(&mut GridTrigger, TimingContext) -> bool,
) -> Vec<f64> {
let samples = (total_beats * 60.0 / 120.0 * f64::from(SAMPLE_RATE)) as u64;
let mut trigger = GridTrigger::new();
let mut hit_beats = Vec::new();
for sample in 0..samples {
let t = timing(sample, 120.0);
if step(&mut trigger, t) {
hit_beats.push(t.beat);
}
}
hit_beats
}
#[test]
fn grid_trigger_survives_continuous_interval_sweep() {
let total_beats = 32.0;
let hit_beats = run_grid(total_beats, |trigger, t| {
let interval = 1.0 + 0.75 * (std::f64::consts::TAU * t.beat / 8.0).sin() as f32;
trigger.pop(t, interval, 0.0)
});
let max_gap = max_hit_gap(&hit_beats, total_beats);
assert!(
max_gap <= 2.25,
"trigger starved during interval sweep: max gap {max_gap:.2} beats"
);
}
#[test]
fn grid_trigger_survives_sliding_offset() {
let total_beats = 32.0;
let hit_beats = run_grid(total_beats, |trigger, t| {
let offset = 2.0 + 2.0 * (std::f64::consts::TAU * t.beat / 8.0).sin() as f32;
trigger.pop(t, 1.0, offset)
});
let max_gap = max_hit_gap(&hit_beats, total_beats);
assert!(
max_gap <= 1.5,
"trigger starved during offset slide: max gap {max_gap:.2} beats"
);
}
fn min_hit_gap(hit_beats: &[f64]) -> f64 {
hit_beats
.windows(2)
.map(|w| w[1] - w[0])
.fold(f64::INFINITY, f64::min)
}
#[test]
fn grid_trigger_no_double_fire_when_swing_ramps() {
let interval = 0.5f32;
let total_beats = 32.0;
let hit_beats = run_grid(total_beats, |trigger, t| {
let swing = (t.beat / total_beats) as f32;
trigger.pop_swung(t, interval, 0.0, swing)
});
let min_gap = min_hit_gap(&hit_beats);
assert!(
min_gap >= f64::from(interval) * 0.5 - 1e-6,
"swing ramp double-fired: min gap {min_gap:.4} beats < half interval"
);
assert!(hit_beats.len() as f64 >= total_beats / f64::from(interval) * 0.5);
}
#[test]
fn grid_trigger_no_double_fire_when_offset_steps() {
let interval = 1.0f32;
let total_beats = 32.0;
let hit_beats = run_grid(total_beats, |trigger, t| {
let offset = 0.1 * (t.beat as f32 * 0.5).floor();
trigger.pop(t, interval, offset)
});
let min_gap = min_hit_gap(&hit_beats);
assert!(
min_gap >= f64::from(interval) * 0.5 - 1e-6,
"offset step double-fired: min gap {min_gap:.4} beats < half interval"
);
}
#[test]
fn grid_trigger_no_double_hit_after_offset_nudge() {
let bpm = 120.0f64;
let interval = 1.0f32;
let mut offset = 0.0f32;
let mut nudged = false;
let hit_beats = run_grid(16.0, |trigger, t| {
let fired = trigger.pop(t, interval, offset);
if fired && !nudged {
offset += (0.010 * bpm / 60.0) as f32;
nudged = true;
}
fired
});
assert!(nudged, "test never reached a hit to nudge after");
let min_gap = min_hit_gap(&hit_beats);
assert!(
min_gap >= f64::from(interval) * 0.5 - 1e-6,
"double hit after offset nudge: min gap {min_gap:.4} beats < half interval"
);
}
#[test]
fn grid_trigger_no_double_hit_after_rate_nudge() {
let mut interval = 1.0f32;
let mut nudged = false;
let hit_beats = run_grid(16.0, |trigger, t| {
let fired = trigger.pop(t, interval, 0.0);
if fired && !nudged {
interval *= 0.4;
nudged = true;
}
fired
});
assert!(nudged, "test never reached a hit to nudge after");
let min_gap = min_hit_gap(&hit_beats);
assert!(
min_gap >= f64::from(interval) * 0.5 - 1e-6,
"double hit after rate nudge: min gap {min_gap:.4} beats < half interval"
);
}
fn automation_with_route(
target_id: &'static str,
depth_ratio: f32,
cycle_beats: f32,
) -> AutomationState {
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new(target_id),
LfoRoute {
depth_ratio,
cycle_beats,
phase_offset_beats: 0.0,
shape: LfoShape::Sine,
..LfoRoute::default()
},
);
automation
}
#[test]
fn modulated_control_value_snaps_like_the_engine() {
let spec = spec_by_id("kick.interval_beats").unwrap();
let route = LfoRoute {
depth_ratio: 0.4,
cycle_beats: 8.0,
phase_offset_beats: 0.0,
shape: LfoShape::Sine,
..LfoRoute::default()
};
let peak = modulated_control_value(spec, &route, 1.0, 2.0);
assert_close(peak, 2.0);
let trough = modulated_control_value(spec, &route, 1.0, 6.0);
assert_close(trough, 0.125);
}
#[test]
fn lfo_interval_modulation_snaps_to_power_of_two() {
let mut controls = FluidControls::default();
controls.kick.interval_beats = 1.0;
let automation = automation_with_route("kick.interval_beats", 0.4, 8.0);
for sample in (0..(SAMPLE_RATE as u64 * 16)).step_by(64) {
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(sample, 120.0));
let v = effective.kick.interval_beats;
assert!(
[0.125f32, 0.25, 0.5, 1.0, 2.0, 4.0]
.iter()
.any(|&q| (v - q).abs() < 1e-4),
"modulated interval {v} is not a power-of-two subdivision"
);
}
}
#[test]
fn lfo_offset_modulation_snaps_to_eighth_beats() {
let mut controls = FluidControls::default();
controls.kick.offset_beats = 2.0;
let automation = automation_with_route("kick.offset_beats", 0.4, 8.0);
for sample in (0..(SAMPLE_RATE as u64 * 16)).step_by(64) {
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(sample, 120.0));
let v = effective.kick.offset_beats;
let snapped = (v / 0.125).round() * 0.125;
assert!(
(v - snapped).abs() < 1e-4,
"modulated offset {v} is not on the 0.125-beat grid"
);
}
}
#[test]
fn lfo_interval_sweep_plays_on_grid_breakdown() {
let mut controls = FluidControls::default();
controls.kick.interval_beats = 1.0;
controls.kick.offset_beats = 0.0;
let automation = automation_with_route("kick.interval_beats", 0.4, 8.0);
let total_beats = 32.0;
let samples = (total_beats * 60.0 / 120.0 * f64::from(SAMPLE_RATE)) as u64;
let mut trigger = GridTrigger::new();
let mut hit_beats = Vec::new();
for sample in 0..samples {
let t = timing(sample, 120.0);
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, t);
if trigger.pop(
t,
effective.kick.interval_beats,
effective.kick.offset_beats,
) {
hit_beats.push(t.beat);
}
}
for &beat in &hit_beats {
let snapped = (beat / 0.125).round() * 0.125;
assert!(
(beat - snapped).abs() < 1e-3,
"hit at beat {beat:.4} is off the 0.125 grid"
);
}
let mut gaps: Vec<i64> = hit_beats
.windows(2)
.map(|w| ((w[1] - w[0]) / 0.125).round() as i64)
.collect();
gaps.sort_unstable();
gaps.dedup();
assert!(
gaps.len() >= 3,
"expected at least 3 distinct hit spacings, got {gaps:?}"
);
let max_gap = max_hit_gap(&hit_beats, total_beats);
assert!(
max_gap <= 2.0 + 1e-3,
"trigger starved during breakdown sweep: max gap {max_gap:.2} beats"
);
}
fn lfo_shape(shape: LfoShape) -> LfoRoute {
LfoRoute {
shape,
cycle_beats: 1.0,
depth_ratio: 1.0,
..LfoRoute::default()
}
}
fn env_ctx(beat: f64) -> ModContext {
ModContext {
beat,
kick_interval_beats: 1.0,
kick_offset_beats: 0.0,
}
}
#[test]
fn render_fluid_draws_envelope_submenu_and_lane() {
let controls = FluidControls::default();
let fluid = RippleField::new();
let items = tab_controls(Tab::Chords, &controls);
let mut automation = AutomationState::default();
let address = ControlAddress::new(items[3].id); automation.open_or_create(address).shape = LfoShape::SampleHold;
automation.open_or_create_envelope(address).amount = 0.5;
let buffer = render_to_buffer(RenderTest {
size: (120, 44),
tab: Tab::Chords,
cursor: 3,
submenu: 1,
beat: 2.5,
fluid: &fluid,
automation: &automation,
controls: &controls,
footer: None,
drill: ChordDrill::None,
active_chord: 0,
mute: &[false; TAB_COUNT],
});
let text = buffer_text(&buffer);
assert!(text.contains("attack"));
assert!(text.contains("decay"));
assert!(text.contains("trigger"));
}
#[test]
fn lfo_shapes_match_reference_curves() {
let tri = lfo_shape(LfoShape::Triangle);
assert_near(tri.wave_at(0.0), 0.0);
assert_near(tri.wave_at(0.25), 1.0);
assert_near(tri.wave_at(0.5), 0.0);
assert_near(tri.wave_at(0.75), -1.0);
let up = lfo_shape(LfoShape::RampUp);
assert_near(up.wave_at(0.0), -1.0);
assert_near(up.wave_at(0.5), 0.0);
assert_near(up.wave_at(0.75), 0.5);
let down = lfo_shape(LfoShape::RampDown);
assert_near(down.wave_at(0.0), 1.0);
assert_near(down.wave_at(0.5), 0.0);
assert_near(down.wave_at(0.75), -0.5);
let square = lfo_shape(LfoShape::Square);
assert!(square.wave_at(0.25) > 0.99, "square high near +1");
assert!(square.wave_at(0.75) < -0.99, "square low near -1");
}
#[test]
fn ramp_shapes_are_continuous_across_the_wrap() {
let eps = 1e-4;
for shape in [LfoShape::RampUp, LfoShape::RampDown] {
let route = lfo_shape(shape);
let before = route.wave_at(1.0 - eps);
let after = route.wave_at(1.0 + eps);
assert!(
(after - before).abs() < 0.1,
"{shape:?} jumps {} across the wrap",
(after - before).abs()
);
}
}
#[test]
fn sample_hold_is_stepped_and_seeded() {
let route = LfoRoute {
shape: LfoShape::SampleHold,
cycle_beats: 1.0,
depth_ratio: 1.0,
seed: 12345,
..LfoRoute::default()
};
assert_close(route.wave_at(0.1), route.wave_at(0.9));
assert!((route.wave_at(0.5) - route.wave_at(1.5)).abs() > 1e-6);
let twin = route;
for i in 0..64 {
let beat = f64::from(i) * 0.5;
assert_close(route.wave_at(beat), twin.wave_at(beat));
}
}
#[test]
fn render_fluid_draws_step_submenu() {
let controls = FluidControls::default();
let fluid = RippleField::new();
let items = tab_controls(Tab::Chords, &controls);
let mut automation = AutomationState::default();
let address = ControlAddress::new(items[3].id);
let route = automation.open_or_create(address);
route.shape = LfoShape::Steps;
route.depth_ratio = 0.5;
let buffer = render_to_buffer(RenderTest {
size: (120, 44),
tab: Tab::Chords,
cursor: 3,
submenu: 1,
beat: 2.5,
fluid: &fluid,
automation: &automation,
controls: &controls,
footer: None,
drill: ChordDrill::None,
active_chord: 0,
mute: &[false; TAB_COUNT],
});
let text = buffer_text(&buffer);
assert!(text.contains("steps"), "step count row present");
assert!(text.contains("glide"), "glide row present");
assert!(text.contains("step 1"), "first step value row present");
}
#[test]
fn steps_shape_defaults_to_three_neutral_then_a_full_up_step() {
let route = lfo_shape(LfoShape::Steps);
assert_eq!(route.active_step_count(), 4);
assert_near(route.wave_at(0.5), 0.0);
assert_near(route.wave_at(1.5), 0.0);
assert_near(route.wave_at(2.5), 0.0);
assert_near(route.wave_at(3.5), 1.0);
}
#[test]
fn steps_playhead_uses_the_same_interval_index_as_the_lfo_wave() {
let mut route = lfo_shape(LfoShape::Steps);
route.cycle_beats = 0.5;
route.step_count = 3;
assert_eq!(route.active_step_at(0.0), Some(0));
assert_eq!(route.active_step_at(0.5), Some(1));
assert_eq!(route.active_step_at(1.0), Some(2));
assert_eq!(route.active_step_at(1.5), Some(0));
route.shape = LfoShape::Sine;
assert_eq!(route.active_step_at(1.0), None);
}
#[test]
fn raising_step_count_extends_the_pattern() {
let mut route = lfo_shape(LfoShape::Steps);
assert_near(route.wave_at(3.5), 1.0);
assert_near(route.wave_at(7.5), 1.0);
route.set_step(StepTarget::Count, 8.0);
assert_near(route.wave_at(3.5), 1.0);
assert_near(route.wave_at(7.5), 0.0);
}
#[test]
fn steps_shape_is_continuous_with_glide() {
let mut route = lfo_shape(LfoShape::Steps); route.steps = [0.0; MAX_LFO_STEPS];
route.steps[0] = 1.0;
route.steps[1] = -1.0;
route.steps[2] = 1.0;
route.steps[3] = -1.0;
let eps = 1e-4;
for i in 0..400 {
let beat = f64::from(i) / 100.0; let jump = (route.wave_at(beat + eps) - route.wave_at(beat)).abs();
assert!(jump < 0.1, "steps jump {jump} at beat {beat}");
}
}
#[test]
fn steps_with_zero_glide_hold_flat_then_jump() {
let mut route = lfo_shape(LfoShape::Steps);
route.step_glide = 0.0;
route.steps = [0.0; MAX_LFO_STEPS];
route.steps[0] = -1.0;
route.steps[1] = 1.0;
assert_near(route.wave_at(0.4), route.wave_at(0.8));
assert!((route.wave_at(0.99) - route.wave_at(1.01)).abs() > 1.0);
}
#[test]
fn step_edits_clamp_count_glide_and_values() {
let mut route = lfo_shape(LfoShape::Steps);
route.set_step(StepTarget::Count, 99.0);
assert_eq!(route.active_step_count(), MAX_LFO_STEPS);
for _ in 0..100 {
route.adjust_step(StepTarget::Count, -1.0);
}
assert_eq!(route.active_step_count(), 1);
route.set_step(StepTarget::Value(0), -250.0);
assert_near(route.steps[0], -1.0);
route.set_step(StepTarget::Value(0), 50.0);
assert_near(route.steps[0], 0.5);
route.set_step(StepTarget::Glide, 40.0);
assert_near(route.step_glide, 0.4);
}
#[test]
fn song_code_round_trips_steps_shape() {
let mut automation = AutomationState::default();
let mut route = LfoRoute {
shape: LfoShape::Steps,
cycle_beats: 2.0,
depth_ratio: 0.7,
step_count: 5,
step_glide: 0.3,
..LfoRoute::default()
};
route.steps = [0.0; MAX_LFO_STEPS];
route.steps[0] = 1.0;
route.steps[1] = -0.5;
route.steps[2] = 0.25;
route.steps[3] = -1.0;
route.steps[4] = 0.8;
automation.set_route(ControlAddress::new("master.level"), route);
let song = SongState {
automation,
..SongState::from_controls(FluidControls::default())
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let got = decoded
.automation
.route(ControlAddress::new("master.level"))
.unwrap();
assert_eq!(got.shape, LfoShape::Steps);
assert_eq!(got.step_count, 5);
assert_quantized(got.step_glide, 0.3);
for i in 0..5 {
assert_quantized(got.steps[i], route.steps[i]);
}
}
#[test]
fn random_drift_is_deterministic_for_a_seed() {
let a = LfoRoute {
shape: LfoShape::RandomDrift,
cycle_beats: 2.0,
depth_ratio: 1.0,
seed: 777,
..LfoRoute::default()
};
let b = a;
for i in 0..128 {
let beat = f64::from(i) * 0.3;
assert_close(a.wave_at(beat), b.wave_at(beat));
assert!(a.wave_at(beat).abs() <= 1.0 + 1e-6);
}
}
#[test]
fn reseed_changes_pattern_but_stays_repeatable() {
let base = LfoRoute {
shape: LfoShape::SampleHold,
cycle_beats: 1.0,
depth_ratio: 1.0,
seed: 5,
..LfoRoute::default()
};
let sample =
|route: &LfoRoute| -> Vec<f32> { (0..32).map(|i| route.wave_at(f64::from(i))).collect() };
let original = sample(&base);
let mut rolled = base;
rolled.reseed();
let mut rolled_again = base;
rolled_again.reseed();
assert_eq!(sample(&rolled), sample(&rolled_again));
assert_ne!(sample(&base), sample(&rolled));
let _ = original;
}
#[test]
fn envelope_level_follows_attack_then_decay() {
let env = EnvelopeRoute {
amount: 1.0,
attack_beats: 2.0,
decay_beats: 2.0,
trigger: EnvTrigger::Once,
};
assert_near(env.level_at(env_ctx(0.0)), 0.0);
assert_near(env.level_at(env_ctx(1.0)), 0.5);
assert_near(env.level_at(env_ctx(2.0)), 1.0);
assert_near(env.level_at(env_ctx(3.0)), 0.5);
assert_near(env.level_at(env_ctx(4.0)), 0.0);
assert_near(env.level_at(env_ctx(9.0)), 0.0);
}
#[test]
fn envelope_macro_holds_at_peak_when_decay_is_zero() {
let env = EnvelopeRoute {
amount: 1.0,
attack_beats: 4.0,
decay_beats: 0.0,
trigger: EnvTrigger::Once,
};
assert_near(env.level_at(env_ctx(2.0)), 0.5);
assert_near(env.level_at(env_ctx(4.0)), 1.0);
assert_near(env.level_at(env_ctx(400.0)), 1.0);
}
#[test]
fn envelope_every_n_beats_retriggers() {
let env = EnvelopeRoute {
amount: 1.0,
attack_beats: 0.0,
decay_beats: 4.0,
trigger: EnvTrigger::EveryBeats(4.0),
};
assert_near(env.level_at(env_ctx(0.0)), 1.0);
assert!(env.level_at(env_ctx(3.9)) < 0.1);
assert_near(env.level_at(env_ctx(4.0)), 1.0);
}
#[test]
fn envelope_on_kick_tracks_the_kick_grid() {
let env = EnvelopeRoute {
amount: 1.0,
attack_beats: 0.0,
decay_beats: 1.0,
trigger: EnvTrigger::OnKick,
};
let ctx = |beat: f64| ModContext {
beat,
kick_interval_beats: 2.0,
kick_offset_beats: 0.0,
};
assert_near(env.level_at(ctx(2.0)), 1.0);
assert_near(env.level_at(ctx(2.5)), 0.5);
assert_near(env.level_at(ctx(4.0)), 1.0);
}
#[test]
fn envelope_amount_zero_is_audible_neutral() {
let mut controls = FluidControls::default();
controls.master.level = 0.5;
let mut automation = AutomationState::default();
automation.open_or_create_envelope(ControlAddress::new("master.level"));
apply_automation(
&mut controls,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 1.0),
);
assert_close(controls.master.level, 0.5);
}
#[test]
fn open_or_create_envelope_defaults_to_neutral_amount() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("master.level");
let env = automation.open_or_create_envelope(address);
assert_close(env.amount, 0.0);
assert_eq!(automation.active_kind(), Some(ModKind::Envelope));
}
#[test]
fn close_editor_deletes_zero_amount_envelope() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("master.level");
automation.open_or_create_envelope(address);
automation.close_editor();
assert!(automation.envelope(address).is_none());
automation.open_or_create_envelope(address).amount = 0.5;
automation.close_editor();
assert!(automation.envelope(address).is_some());
}
#[test]
fn lfo_and_envelope_coexist_on_one_control() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("pad.slot1.amount");
automation.open_or_create(address).depth_ratio = 0.3;
automation.open_or_create_envelope(address).amount = 0.4;
assert!(automation.route(address).is_some());
assert!(automation.envelope(address).is_some());
assert_eq!(automation.active_kind(), Some(ModKind::Envelope));
}
#[test]
fn automation_stack_sums_multiple_lfo_lanes_on_one_control() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.set_route(
address,
LfoRoute {
cycle_beats: 2.0,
depth_ratio: 0.1,
..LfoRoute::default()
},
);
assert!(automation.add_route(
address,
LfoRoute {
cycle_beats: 2.0,
depth_ratio: 0.2,
..LfoRoute::default()
},
));
let mut controls = FluidControls::default();
controls.master.level = 0.5;
apply_automation(
&mut controls,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
assert_near(controls.master.level, 0.8);
}
#[test]
fn automation_stack_caps_each_lane_family() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.set_route(address, LfoRoute::default());
for _ in 1..MAX_AUTOMATION_LANES_PER_KIND {
assert!(automation.add_route(address, LfoRoute::default()));
}
assert!(!automation.add_route(address, LfoRoute::default()));
}
#[test]
fn automation_stack_adds_opens_and_cycles_lanes() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.open_or_create(address).depth_ratio = 0.2;
assert!(automation.add_and_open(address, ModKind::Lfo));
let added = (
automation.active_lane_index(),
automation.active_lane_count(),
);
automation.cycle_open(address, ModKind::Lfo);
assert_eq!(added, (Some(1), Some(2)));
assert_eq!(automation.active_lane_index(), Some(0));
}
#[test]
fn remove_open_route_keeps_other_stacked_lanes() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.open_or_create(address).depth_ratio = 0.2;
assert!(automation.add_and_open(address, ModKind::Lfo));
automation.remove_open_route();
assert_eq!(automation.routes_for(address).count(), 1);
}
#[test]
fn automation_plan_declicks_an_envelope_retrigger() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.set_envelope(
address,
EnvelopeRoute {
amount: 0.5,
attack_beats: 0.0,
decay_beats: 1.0,
trigger: EnvTrigger::EveryBeats(1.0),
},
);
let mut plan = AutomationPlan::default();
plan.rebuild(&automation);
let mut before = FluidControls::default();
before.master.level = 0.5;
plan.apply(
&mut before,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.999),
);
let mut after = FluidControls::default();
after.master.level = 0.5;
plan.apply(
&mut after,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 1.0),
);
assert!(
(after.master.level - before.master.level).abs() < 0.01,
"retrigger jumped from {} to {}",
before.master.level,
after.master.level
);
}
#[test]
fn song_code_round_trips_stacked_lfo_lanes() {
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.set_route(
address,
LfoRoute {
depth_ratio: 0.2,
..LfoRoute::default()
},
);
assert!(automation.add_route(
address,
LfoRoute {
depth_ratio: 0.4,
shape: LfoShape::Triangle,
..LfoRoute::default()
},
));
let song = SongState {
automation,
..SongState::from_controls(FluidControls::default())
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let depths: Vec<_> = decoded
.automation
.routes_for(address)
.map(|route| route.depth_ratio)
.collect();
assert_eq!(depths, vec![0.2, 0.4]);
}
#[test]
fn combined_lfo_and_envelope_sum_and_clamp() {
let mut controls = FluidControls::default();
controls.master.level = 0.5;
let address = ControlAddress::new("master.level");
let mut automation = AutomationState::default();
automation.set_route(
address,
LfoRoute {
depth_ratio: 0.5,
cycle_beats: 2.0,
shape: LfoShape::Sine,
..LfoRoute::default()
},
);
automation.set_envelope(
address,
EnvelopeRoute {
amount: 0.5,
attack_beats: 0.0,
decay_beats: 64.0,
trigger: EnvTrigger::Once,
},
);
apply_automation(
&mut controls,
&automation,
TimingContext::new(f64::from(SAMPLE_RATE), 120.0, 0.5),
);
assert_close(controls.master.level, 1.0);
}
#[test]
fn song_code_round_trips_non_sine_lfo_shape() {
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new("master.level"),
LfoRoute {
cycle_beats: 4.0,
depth_ratio: 0.4,
shape: LfoShape::SampleHold,
phase_offset_beats: 0.0,
..LfoRoute::default()
},
);
let song = SongState {
automation,
..SongState::from_controls(FluidControls::default())
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let route = decoded
.automation
.route(ControlAddress::new("master.level"))
.unwrap();
assert_eq!(route.shape, LfoShape::SampleHold);
}
#[test]
fn song_code_round_trips_envelope_routes() {
let mut automation = AutomationState::default();
automation.set_envelope(
ControlAddress::new("pad.slot1.amount"),
EnvelopeRoute {
amount: 0.6,
attack_beats: 1.5,
decay_beats: 3.0,
trigger: EnvTrigger::OnKick,
},
);
let song = SongState {
automation,
..SongState::from_controls(FluidControls::default())
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let env = decoded
.automation
.envelope(ControlAddress::new("pad.slot1.amount"))
.unwrap();
assert_quantized(env.amount, 0.6);
assert_close(env.attack_beats, 1.5);
assert_close(env.decay_beats, 3.0);
assert_eq!(env.trigger, EnvTrigger::OnKick);
}
#[test]
fn clap_voice_starts_first_burst_at_local_sample_zero() {
let controls = ClapControls {
level: 1.0,
slap_count: 4.0,
slap_spread_ms: 40.0,
..ClapControls::default()
};
let mut rng = StdRng::seed_from_u64(99);
let mut voice = ClapVoice::new(&controls, SAMPLE_RATE, &mut rng);
assert_eq!(voice.scheduled.first().copied(), Some(0));
let _ = voice.next(&mut rng);
assert_eq!(voice.current, 1);
assert!(!voice.bursts.is_empty());
assert!(voice.scheduled.iter().all(|&sample| sample > 0));
}
#[test]
fn unit_conversion_round_trips_at_current_bpm() {
let bpm = 82.0;
assert_near(beats_to_ms(1.0, 120.0), 500.0);
assert_near(ms_to_beats(500.0, 120.0), 1.0);
let beats = 2.125;
assert_near(ms_to_beats(beats_to_ms(beats, bpm), bpm), beats);
assert_eq!(
unit_key("kick.level", Some("lfo.interval")),
"kick.level#lfo.interval"
);
assert_eq!(unit_key("perc.decay_ms", None), "perc.decay_ms");
}
#[test]
fn song_code_round_trips_seeded_lfo_and_envelope() {
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new("master.level"),
LfoRoute {
cycle_beats: 4.0,
depth_ratio: 0.4,
shape: LfoShape::SampleHold,
phase_offset_beats: 0.5,
seed: 0xDEAD_BEEF,
..LfoRoute::default()
},
);
automation.set_envelope(
ControlAddress::new("pad.slot1.amount"),
EnvelopeRoute {
amount: 0.7,
attack_beats: 1.25,
decay_beats: 6.0,
trigger: EnvTrigger::EveryBeats(8.0),
},
);
let song = SongState {
automation,
..SongState::from_controls(FluidControls::default())
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let route = decoded
.automation
.route(ControlAddress::new("master.level"))
.unwrap();
assert_close(route.cycle_beats, 4.0);
assert_quantized(route.depth_ratio, 0.4);
assert_eq!(route.shape, LfoShape::SampleHold);
assert_close(route.phase_offset_beats, 0.5);
assert_eq!(route.seed, 0xDEAD_BEEF);
let env = decoded
.automation
.envelope(ControlAddress::new("pad.slot1.amount"))
.unwrap();
assert_quantized(env.amount, 0.7);
assert_close(env.attack_beats, 1.25);
assert_close(env.decay_beats, 6.0);
assert_eq!(env.trigger, EnvTrigger::EveryBeats(8.0));
}
#[test]
fn enter_expands_into_the_owning_tab() {
assert_eq!(tab_owning_control("pad.level"), Some(Tab::Chords));
assert_eq!(tab_owning_control("bass.level"), Some(Tab::Bass));
assert_eq!(tab_owning_control("master.bpm"), Some(Tab::Master));
assert_eq!(tab_owning_control("nope.nope"), None);
}
#[test]
#[ignore]
fn engine_hot_path_timing() {
let mut automation = AutomationState::default();
automation.set_route(ControlAddress::new("pad.level"), LfoRoute::default());
automation.set_route(
ControlAddress::new("kick.interval_beats"),
LfoRoute::default(),
);
automation.set_route(ControlAddress::new("tonal.level"), LfoRoute::default());
let mut engine = engine_for(FluidControls::default(), automation);
let frames = SAMPLE_RATE as u64 * 10;
let start = Instant::now();
let mut acc = 0.0f32;
for _ in 0..frames {
let (l, r) = engine.next_stereo();
acc += l + r;
}
let elapsed = start.elapsed();
println!(
"10 s of audio in {elapsed:?} ({:.1}x realtime, acc {acc})",
10.0 / elapsed.as_secs_f64()
);
}
#[test]
fn arp_default_voice_type_matches_former_fixed_pluck_profile() {
let expected = TONAL_PIANO_PROFILES[5];
let actual = piano_profile(wrapped_index(
ArpControls::default().voice_type,
TONAL_SYNTH_TYPES.len(),
));
assert_eq!(actual.keyframes.len(), expected.keyframes.len());
for (a, e) in actual.keyframes.iter().zip(expected.keyframes.iter()) {
assert_eq!(a.midi, e.midi);
assert_eq!(a.decay_factor, e.decay_factor);
assert_eq!(a.harmonics, e.harmonics);
}
assert_eq!(actual.amplitude, expected.amplitude);
assert_eq!(actual.body_power, expected.body_power);
assert_eq!(actual.harmonic_tilt, expected.harmonic_tilt);
assert_eq!(actual.decay_low, expected.decay_low);
assert_eq!(actual.decay_high, expected.decay_high);
assert_eq!(actual.decay_scale, expected.decay_scale);
}
#[test]
fn arp_cycle_notes_duplicates_chord_up_whole_octaves_sorted() {
let chord = [45, 48, 52, 55];
assert_eq!(arp_cycle_notes(chord, 1), vec![45, 48, 52, 55]);
assert_eq!(
arp_cycle_notes(chord, 2),
vec![45, 48, 52, 55, 57, 60, 64, 67]
);
assert_eq!(
arp_cycle_notes(chord, 3),
vec![45, 48, 52, 55, 57, 60, 64, 67, 69, 72, 76, 79]
);
assert!(
arp_cycle_notes(chord, 2).is_sorted(),
"cycle notes must stay sorted ascending across octave spans"
);
}
#[test]
fn arp_pattern_labels_and_index_map_round_trip() {
assert_eq!(arp_pattern_label(0.0), "Up");
assert_eq!(arp_pattern_label(1.0), "Down");
assert_eq!(arp_pattern_label(2.0), "Up-Down");
assert_eq!(arp_pattern_label(3.0), "Random");
assert_eq!(arp_pattern_from_control(0.0), ArpPattern::Up);
assert_eq!(arp_pattern_from_control(1.0), ArpPattern::Down);
assert_eq!(arp_pattern_from_control(2.0), ArpPattern::UpDown);
assert_eq!(arp_pattern_from_control(3.0), ArpPattern::Random);
}
fn arp_advance_sequence(
pattern: ArpPattern,
len: usize,
count: usize,
rng: &mut StdRng,
) -> Vec<usize> {
let mut pos = 0usize;
let mut dir = 1i32;
let mut seq = Vec::with_capacity(count);
for _ in 0..count {
seq.push(pos);
let (next_pos, next_dir) = arp_advance(pos, pattern, len, dir, rng);
pos = next_pos;
dir = next_dir;
}
seq
}
#[test]
fn arp_pattern_up_cycles_ascending_across_octave_spans() {
let mut rng = StdRng::seed_from_u64(0);
assert_eq!(
arp_advance_sequence(ArpPattern::Up, 4, 9, &mut rng),
vec![0, 1, 2, 3, 0, 1, 2, 3, 0]
);
assert_eq!(
arp_advance_sequence(ArpPattern::Up, 8, 10, &mut rng),
vec![0, 1, 2, 3, 4, 5, 6, 7, 0, 1]
);
assert_eq!(
arp_advance_sequence(ArpPattern::Up, 12, 13, &mut rng),
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0]
);
}
#[test]
fn arp_pattern_down_cycles_descending() {
let mut rng = StdRng::seed_from_u64(0);
assert_eq!(
arp_advance_sequence(ArpPattern::Down, 4, 9, &mut rng),
vec![0, 3, 2, 1, 0, 3, 2, 1, 0]
);
assert_eq!(
arp_advance_sequence(ArpPattern::Down, 8, 5, &mut rng),
vec![0, 7, 6, 5, 4]
);
}
#[test]
fn arp_pattern_up_down_ping_pongs_without_repeating_endpoints() {
let mut rng = StdRng::seed_from_u64(0);
assert_eq!(
arp_advance_sequence(ArpPattern::UpDown, 4, 13, &mut rng),
vec![0, 1, 2, 3, 2, 1, 0, 1, 2, 3, 2, 1, 0]
);
assert_eq!(
arp_advance_sequence(ArpPattern::UpDown, 1, 4, &mut rng),
vec![0, 0, 0, 0]
);
}
#[test]
fn arp_random_pattern_is_deterministic_for_a_seed_and_differs_across_seeds() {
let seq_a = arp_advance_sequence(ArpPattern::Random, 8, 20, &mut StdRng::seed_from_u64(5));
let seq_b = arp_advance_sequence(ArpPattern::Random, 8, 20, &mut StdRng::seed_from_u64(5));
let seq_c = arp_advance_sequence(ArpPattern::Random, 8, 20, &mut StdRng::seed_from_u64(6));
assert_eq!(seq_a, seq_b, "same seed must reproduce the same sequence");
assert_ne!(
seq_a, seq_c,
"different seeds should (almost always) diverge"
);
assert!(
seq_a.iter().all(|&i| i < 8),
"random index must stay in range"
);
}
#[test]
fn arp_engine_reseed_via_fluid_engine_reproduces_random_pattern() {
let mut a = ArpEngine::new(SAMPLE_RATE);
a.rng = StdRng::seed_from_u64(9u64.wrapping_add(5));
let mut b = ArpEngine::new(SAMPLE_RATE);
b.rng = StdRng::seed_from_u64(9u64.wrapping_add(5));
let pad = PadControls::default();
let controls = ArpControls {
gain: 0.5,
pattern: 3.0, ..ArpControls::default()
};
let total = SAMPLE_RATE as u64 * 2;
let mut out_a = Vec::with_capacity(total as usize * 2);
let mut out_b = Vec::with_capacity(total as usize * 2);
for sample in 0..total {
let (l, r) = a.next(&controls, &pad, 0.0, timing(sample, 120.0));
out_a.push(l);
out_a.push(r);
let (l, r) = b.next(&controls, &pad, 0.0, timing(sample, 120.0));
out_b.push(l);
out_b.push(r);
}
assert_eq!(
out_a, out_b,
"identical reseed must render byte-identical audio"
);
}
#[test]
fn arp_chord_change_clamps_cycle_position_without_resetting_it() {
let mut arp = ArpEngine::new(SAMPLE_RATE);
arp.cycle_pos = 9;
let pad = PadControls::default();
let narrow = ArpControls {
gain: 0.5,
octaves: 1.0, pattern: 0.0, ..ArpControls::default()
};
arp.next(&narrow, &pad, 0.0, timing(0, 120.0));
assert_eq!(arp.cycle_pos, 0);
}
#[test]
fn arp_decay_sets_note_ring_independent_of_step() {
let pad = PadControls::default();
let t0 = timing(0, 120.0);
let step_samples = t0.beats_to_samples(ARP_RATE_BEATS_MAX);
let base = ArpControls {
gain: 0.5,
rate_beats: ARP_RATE_BEATS_MAX,
pattern: 0.0,
..ArpControls::default()
};
let mut short = ArpEngine::new(SAMPLE_RATE);
let mut long = ArpEngine::new(SAMPLE_RATE);
let short_controls = ArpControls {
decay: 0.1,
..base.clone()
};
let long_controls = ArpControls { decay: 3.0, ..base };
short.next(&short_controls, &pad, 0.0, t0);
long.next(&long_controls, &pad, 0.0, t0);
assert_eq!(short.voices.len(), 1);
assert_eq!(long.voices.len(), 1);
for _ in 0..step_samples {
short.voices[0].next();
long.voices[0].next();
}
assert!(
short.voices[0].is_done(),
"a short-decay arp note must end long before the next step"
);
assert!(
!long.voices[0].is_done(),
"a long decay must keep the arp note ringing past its step"
);
}
#[test]
fn arp_defaults_are_silent_and_do_not_change_default_render() {
let controls = FluidControls::default();
assert_close(controls.arp.gain, 0.0);
}
#[test]
fn arp_reverb_is_a_shared_module_not_a_bespoke_mix_control() {
let controls = FluidControls::default();
assert_eq!(controls.modules.arp[0].kind().unwrap().id, "room");
assert!(spec_by_id("arp.reverb_mix").is_none());
}
#[test]
fn kick_filter_is_a_shared_module_not_a_bespoke_control() {
let controls = FluidControls::default();
assert_eq!(controls.modules.kick[0].kind().unwrap().id, "filter");
assert_eq!(controls.modules.kick[1].kind().unwrap().id, "drive");
assert!(spec_by_id("kick.filter").is_none());
}
#[test]
fn palette_entries_cover_every_unique_control_id_exactly_once() {
let entries = palette_entries();
let mut ids: Vec<&str> = entries.iter().filter_map(PaletteEntry::id).collect();
ids.sort_unstable();
let before_dedup = ids.len();
ids.dedup();
assert_eq!(ids.len(), before_dedup, "no duplicate palette controls");
let module_rows = entries
.iter()
.filter(|e| matches!(e, PaletteEntry::Module { .. }))
.count();
let available = Tab::all()
.into_iter()
.flat_map(|tab| MODULE_CATALOG.iter().map(move |kind| (tab, *kind)))
.filter(|(tab, kind)| module_available_on(*kind, *tab))
.count();
assert_eq!(module_rows, available);
}
#[test]
fn palette_entries_jump_targets_stay_inside_their_tab_tables() {
for entry in palette_entries() {
let PaletteEntry::Control {
tab,
index_in_tab,
spec,
} = entry
else {
continue;
};
assert!(index_in_tab < tab_specs(tab).len());
assert_eq!(tab_specs(tab)[index_in_tab].id, spec.id);
}
}
#[test]
fn palette_fuzzy_ranks_word_start_matches_above_scattered_hits() {
let (word_start, _) =
fuzzy_score("rev", "room \u{b7} Reverb \u{b7} Pads \u{b7} module").expect("subsequence");
let (scattered, _) =
fuzzy_score("rev", "perc.interval_beats \u{b7} Perc \u{b7} Interval").expect("subsequence");
assert!(word_start > scattered);
}
#[test]
fn palette_empty_query_keeps_global_recent_controls_in_mru_order() {
let pal = PaletteState::new(
Tab::Bass,
&[
"perc.level",
"bass.attack_time",
"bass.decay_time",
"pad.level",
],
None,
);
let ids: Vec<&str> = pal
.matches
.iter()
.take(4)
.map(|matched| pal.entry(matched.entry_index).id().unwrap_or(""))
.collect();
assert_eq!(
ids,
[
"perc.level",
"bass.attack_time",
"bass.decay_time",
"pad.level"
]
);
}
#[test]
fn palette_first_ten_are_the_global_mru_across_tabs() {
let mut recent = RecentControls::default();
for id in [
"pad.attack_time",
"perc.level",
"bass.slot1.time",
"kick.click",
"tonal.decay",
"clap.filter",
"arp.rate_beats",
"master.bpm",
"pad.stereo_width",
"tonal.randomness",
] {
recent.touch(id);
}
let pal = PaletteState::new(Tab::Bass, recent.ids(), None);
let ids: Vec<&str> = pal
.matches
.iter()
.take(10)
.map(|matched| pal.entry(matched.entry_index).id().unwrap_or(""))
.collect();
assert_eq!(
ids,
[
"tonal.randomness",
"pad.stereo_width",
"master.bpm",
"arp.rate_beats",
"clap.filter",
"tonal.decay",
"kick.click",
"perc.level",
"pad.attack_time",
"bass.level",
]
);
}
fn palette_top_hit(current_tab: Tab, recent: &[&'static str], query: &str) -> &'static str {
let mut pal = PaletteState::new(current_tab, recent, None);
for character in query.chars() {
pal.push_char(character);
}
pal.entry(pal.matches[0].entry_index).id().unwrap_or("")
}
#[test]
fn palette_layer_name_lands_on_that_layers_level_over_the_mru() {
let recent = ["bass.decay_time", "bass.attack_time", "bass.slot1.time"];
assert_eq!(palette_top_hit(Tab::Master, &recent, "bass"), "bass.level");
assert_eq!(palette_top_hit(Tab::Master, &recent, "bas"), "bass.level");
}
#[test]
fn palette_layer_boost_matches_tab_name_and_id_namespace_alike() {
assert_eq!(palette_top_hit(Tab::Bass, &[], "pad"), "pad.level");
assert_eq!(palette_top_hit(Tab::Bass, &[], "pads"), "pad.level");
}
#[test]
fn palette_layer_boost_releases_once_the_query_outgrows_the_namespace() {
let recent = ["bass.decay_time"];
assert_eq!(
palette_top_hit(Tab::Master, &recent, "bass.d"),
"bass.decay_time"
);
}
#[test]
fn palette_empty_query_lists_current_page_before_unused_other_pages() {
let pal = PaletteState::new(Tab::Bass, &[], None);
let first_id = pal.entry(pal.matches[0].entry_index).id().unwrap_or("");
assert!(tab_specs(Tab::Bass).iter().any(|spec| spec.id == first_id));
}
#[test]
fn chords_drill_for_index_inverts_chords_flat_index() {
let controls = FluidControls::default();
for flat in 0..(10 + CHORD_SLOT_COUNT * 5) {
let (drill, row) = chords_drill_for_index(flat, &controls);
assert_eq!(chords_flat_index(drill, row), flat);
}
}
#[test]
fn chords_drill_for_index_keeps_module_slot_rows_out_of_the_chord_drill() {
let mut controls = FluidControls::default();
controls.modules.pad[1] = preset_slot("drive", 0.0);
let id = module_slot_collapsed_id(Tab::Chords, 1, &controls).expect("pads has a slot 2");
let flat = tab_specs(Tab::Chords)
.iter()
.position(|spec| spec.id == id)
.expect("slot 2 amount is a real control");
let (drill, selected) = chords_drill_for_index(flat, &controls);
assert_eq!(drill, ChordDrill::None);
let expected = chords_tab_controls(&controls, ChordDrill::None)
.iter()
.position(|item| item.id == id)
.expect("slot 2 amount renders once occupied");
assert_eq!(selected, expected);
}
#[test]
fn next_bar_beat_targets_the_following_downbeat() {
assert_eq!(next_bar_beat(12.3), 16.0);
assert_eq!(next_bar_beat(16.0), 20.0);
assert_eq!(next_bar_beat(0.0), 4.0);
}
#[test]
fn built_in_auto_states_are_container_v2() {
let states = decode_auto_states();
assert_eq!(states.len(), 20);
for state in &states {
let code = song::encode_song_code(state).unwrap();
let bytes = URL_SAFE_NO_PAD
.decode(code.strip_prefix("n1_").unwrap())
.unwrap();
assert_eq!(&bytes[..4], b"NOOI");
assert_eq!(bytes[4], 2, "re-encoded state must be container v2");
song::decode_song_code(&code).unwrap();
}
}
#[test]
fn song_codes_round_trip_every_non_tapered_control_exactly() {
let mut seen = std::collections::BTreeSet::new();
for base_spec in all_specs() {
if !seen.insert(base_spec.id) {
continue;
}
let defaults = FluidControls::default();
let spec = base_spec.contextual(&defaults);
if !matches!(spec.taper, Taper::Linear) && matches!(spec.step, Step::Linear(_)) {
continue;
}
let default_raw = (spec.get)(&defaults);
for rung in 0..=16 {
let raw = spec.min + (spec.max - spec.min) * rung as f32 / 16.0;
let mut controls = FluidControls::default();
base_spec.apply_quantized_value(raw, &mut controls);
let expected = if spec.quantize(raw) == spec.quantize(default_raw) {
default_raw
} else {
(spec.get)(&controls)
};
let code = song::encode_song_code(&SongState::from_controls(controls)).unwrap();
let decoded = song::decode_song_code(&code).unwrap().controls;
assert_eq!(
(spec.get)(&decoded),
expected,
"{} did not round-trip exactly at {raw}",
spec.id
);
}
}
}
#[test]
fn re_encoding_a_decoded_song_reproduces_the_same_code() {
for state in decode_auto_states() {
let once = song::encode_song_code(&state).unwrap();
let twice = song::encode_song_code(&song::decode_song_code(&once).unwrap()).unwrap();
assert_eq!(once, twice, "re-encoding changed the code");
}
}
const POSITION_STEP: f32 = 1.0 / 65_535.0;
fn assert_song_states_agree(a: &SongState, b: &SongState, label: &str) {
assert_eq!(a.muted, b.muted, "{label}: mute state");
for tab in Tab::all() {
for kind in GestureKind::ALL {
let before = a.gestures.envelope(tab, kind);
let after = b.gestures.envelope(tab, kind);
assert!(
(before.amount - after.amount).abs() <= POSITION_STEP,
"{label}: {} {} gesture amount drifted ({} -> {})",
tab.name(),
kind.name(),
before.amount,
after.amount
);
assert_eq!(
before.held,
after.held,
"{label}: {} {} held state",
tab.name(),
kind.name()
);
assert_eq!(
after.restored,
after.held,
"{label}: {} {} restored marker",
tab.name(),
kind.name()
);
assert_eq!(
after.at_seconds,
0.0,
"{label}: {} {} time origin",
tab.name(),
kind.name()
);
}
}
let mut seen = std::collections::BTreeSet::new();
for spec in all_specs() {
if !seen.insert(spec.id) {
continue;
}
let before = spec.quantized_value(&a.controls);
let after = spec.quantized_value(&b.controls);
if !matches!(spec.taper, Taper::Linear) && matches!(spec.step, Step::Linear(_)) {
let drift = (spec.ratio(before, &a.controls) - spec.ratio(after, &b.controls)).abs();
assert!(
drift <= POSITION_STEP,
"{label}: {} drifted {drift} in position space ({before} -> {after})",
spec.id
);
} else {
assert_eq!(
before, after,
"{label}: {} must round-trip exactly",
spec.id
);
}
}
let routes_a: Vec<_> = a.automation.routes().collect();
let routes_b: Vec<_> = b.automation.routes().collect();
assert_eq!(routes_a.len(), routes_b.len(), "{label}: LFO route count");
for ((address_a, route_a), (address_b, route_b)) in routes_a.iter().zip(&routes_b) {
let id = address_a.id();
assert_eq!(id, address_b.id(), "{label}: LFO route order");
assert_eq!(route_a.shape, route_b.shape, "{label}: {id} shape");
assert_eq!(
route_a.seed, route_b.seed,
"{label}: {id} seed must be exact"
);
assert_eq!(
route_a.cycle_beats, route_b.cycle_beats,
"{label}: {id} cycle_beats must be exact"
);
assert_eq!(
route_a.phase_offset_beats, route_b.phase_offset_beats,
"{label}: {id} phase_offset_beats must be exact"
);
assert_quantized_named(route_b.depth_ratio, route_a.depth_ratio, "depth_ratio");
if route_a.shape == LfoShape::Steps {
assert_eq!(
route_a.step_count, route_b.step_count,
"{label}: {id} steps"
);
assert_quantized_named(route_b.step_glide, route_a.step_glide, "step_glide");
for i in 0..route_a.active_step_count() {
assert_quantized_named(route_b.steps[i], route_a.steps[i], "step");
}
}
}
let envelopes_a: Vec<_> = a.automation.envelopes().collect();
let envelopes_b: Vec<_> = b.automation.envelopes().collect();
assert_eq!(
envelopes_a.len(),
envelopes_b.len(),
"{label}: envelope count"
);
for ((address_a, env_a), (address_b, env_b)) in envelopes_a.iter().zip(&envelopes_b) {
let id = address_a.id();
assert_eq!(id, address_b.id(), "{label}: envelope order");
assert_quantized_named(env_b.amount, env_a.amount, id);
assert_eq!(
env_a.attack_beats, env_b.attack_beats,
"{label}: {id} attack_beats must be exact"
);
assert_eq!(
env_a.decay_beats, env_b.decay_beats,
"{label}: {id} decay_beats must be exact"
);
assert_eq!(env_a.trigger, env_b.trigger, "{label}: {id} trigger");
}
match (&a.tonal_sequence, &b.tonal_sequence) {
(None, None) => {}
(Some(before), Some(after)) => {
assert_eq!(before.phrase, after.phrase, "{label}: tonal phrase");
assert_eq!(
before.notes, after.notes,
"{label}: tonal notes must be exact"
);
assert_eq!(
before.evolution_seed, after.evolution_seed,
"{label}: evolution_seed must be exact"
);
assert_eq!(
before.evolution_count, after.evolution_count,
"{label}: evolution_count must be exact"
);
}
_ => panic!("{label}: tonal sequence presence changed"),
}
}
#[test]
fn song_codes_round_trip_every_built_in_state() {
for (index, state) in decode_auto_states().iter().enumerate() {
let code = encode_song_code(state).unwrap();
let again = decode_song_code(&code).unwrap();
assert_song_states_agree(state, &again, &format!("AUTO_STATES[{index}]"));
}
}
#[test]
fn baked_in_auto_state_codes_are_container_v2_on_disk() {
let source = include_str!("auto.rs");
let codes: Vec<&str> = source
.lines()
.filter_map(|line| line.trim().strip_prefix('"')?.strip_suffix("\","))
.filter(|code| code.starts_with("n1_"))
.collect();
assert_eq!(codes.len(), 20);
for code in codes {
let bytes = URL_SAFE_NO_PAD
.decode(code.strip_prefix("n1_").unwrap())
.unwrap();
assert_eq!(bytes[4], 2, "{code} is not a container-v2 code");
}
}
#[test]
fn song_code_skips_unknown_automation_target_indexes() {
let mut payload = Vec::new();
payload.extend_from_slice(&1u16.to_le_bytes()); payload.extend_from_slice(&u16::MAX.to_le_bytes()); payload.extend_from_slice(&4.0f32.to_le_bytes()); payload.extend_from_slice(&32_768u16.to_le_bytes()); payload.push(0); payload.extend_from_slice(&0.25f32.to_le_bytes()); payload.extend_from_slice(&7u32.to_le_bytes()); payload.extend_from_slice(&0u16.to_le_bytes()); payload.extend_from_slice(&0u16.to_le_bytes()); payload.extend_from_slice(&0u16.to_le_bytes()); let code = song::code_from_records(
song::CONTAINER_VERSION,
&[
(
song::SNAPSHOT_RECORD,
&song::snapshot_payload(&FluidControls::default()),
),
(song::AUTOMATION_RECORD, &payload),
],
);
let decoded = song::decode_song_code(&code).unwrap();
assert_eq!(decoded.automation.routes().count(), 0);
}
#[test]
fn container_v1_song_codes_are_rejected_with_an_explanation() {
let code = song::code_from_records(1, &[]);
let Err(err) = song::decode_song_code(&code) else {
panic!("a container-v1 code must not decode");
};
assert_eq!(err, song::SongCodeError::UnsupportedVersion(1));
let message = err.to_string();
assert!(
message.contains("older nooise") && message.contains("version 2"),
"unhelpful message: {message}"
);
}
#[test]
fn lead_defaults_are_silent_and_carry_a_drive_module() {
let controls = FluidControls::default();
assert_close(controls.lead.level, 0.0);
assert_eq!(controls.modules.lead[0].kind().unwrap().id, "drive");
assert!(
spec_by_id("lead.drive").is_none(),
"drive is the shared module"
);
}
#[test]
fn lead_step_is_derived_from_the_transport_and_wraps_at_the_live_count() {
for step in 0..8 {
assert_eq!(lead_step_at(step as f64 * 0.5, 0.5, 0.0, 8), step);
}
assert_eq!(
lead_step_at(4.0, 0.5, 0.0, 8),
0,
"wraps after the live count"
);
assert_eq!(
lead_step_at(4.0, 0.5, 0.0, 3),
2,
"wraps at a shorter live count"
);
assert_eq!(lead_step_at(1.5 + 0.15, 0.5, 0.0, 8), 3);
assert_eq!(lead_step_at(1.5 - 1e-6, 0.5, 0.0, 8), 3);
assert_eq!(lead_step_at(1.0, 0.5, 1.0, 8), 0);
}
#[test]
fn lead_step_rows_live_only_inside_the_pattern_drill() {
let mut controls = FluidControls::default();
controls.lead.step_count = 3.0;
let root: Vec<_> = lead_tab_controls(&controls, interaction::LeadDrill::None)
.into_iter()
.map(|item| item.id)
.collect();
assert!(
root.iter().all(|id| lead_step_index(id).is_none()),
"root page carries no step rows: {root:?}"
);
assert!(
root.contains(&LEAD_STEPS_ID),
"the Steps row opens the lane"
);
let lane: Vec<_> =
lead_tab_controls(&controls, interaction::LeadDrill::Pattern { return_to: 0 })
.into_iter()
.map(|item| item.id)
.collect();
assert_eq!(lane, ["lead.step1", "lead.step2", "lead.step3"]);
assert_eq!(lead_step_index("lead.step16"), Some(15));
assert_eq!(lead_step_index("lead.step17"), None);
assert_eq!(lead_step_index(LEAD_STEPS_ID), None);
}
#[test]
fn lead_steps_past_the_default_length_rest() {
let controls = LeadControls::default();
let live = lead_live_step_count(controls.step_count);
assert!(
controls.steps[live..].iter().all(|step| *step == 0.0),
"lengthening the lane must add rests, not notes"
);
assert!(controls.steps[..live].iter().any(|step| *step != 0.0));
}
#[test]
fn lead_pattern_off_silences_the_lane_but_not_the_keys() {
let pad = PadControls::default();
let mut controls = LeadControls {
level: 0.5,
attack: 0.005,
decay: 0.05,
pattern: LeadPattern::Off.value(),
..LeadControls::default()
};
let peak = |lead: &mut LeadEngine, c: &LeadControls, samples: u64| -> f32 {
(0..samples)
.map(|sample| lead.next(c, &pad, 0.0, timing(sample, 120.0)).0.abs())
.fold(0.0, f32::max)
};
let two_beats = SAMPLE_RATE as u64; let mut lead = LeadEngine::new(SAMPLE_RATE);
assert_eq!(
peak(&mut lead, &controls, two_beats),
0.0,
"Off: the lane is silent"
);
let mut lead = LeadEngine::new(SAMPLE_RATE);
lead.observe(LeadPlayState {
presses: 1,
tone: 1,
held: false,
});
assert!(
peak(&mut lead, &controls, 2_000) > 0.05,
"Off: a played key still sounds"
);
controls.pattern = LeadPattern::Play.value();
let mut lead = LeadEngine::new(SAMPLE_RATE);
assert!(
peak(&mut lead, &controls, two_beats) > 0.05,
"Play: the lane sounds"
);
}
#[test]
fn lead_capture_keeps_the_last_phrase_on_the_shortest_lane_that_fits() {
let press = |beat, tone| LeadPress { beat, tone };
let presses = [press(7.95, 1), press(8.55, 2), press(9.0, 3), press(9.6, 5)];
let capture = lead_capture(&presses, 10.0, 0.5, 0.0).unwrap();
assert_eq!(capture.count, 4);
assert_eq!(&capture.steps[..4], &[1.0, 2.0, 3.0, 5.0]);
assert!(capture.steps[4..].iter().all(|s| *s == 0.0));
let capture = lead_capture(&[press(9.0, 4), press(11.0, 7)], 11.5, 0.5, 0.0).unwrap();
assert_eq!(capture.count, 8);
assert_eq!(capture.steps[2], 4.0);
assert_eq!(capture.steps[6], 7.0);
let capture = lead_capture(
&[press(0.0, 9), press(1.0, 9), press(6.0, 2)],
6.5,
0.5,
0.0,
)
.unwrap();
assert_eq!(capture.count, 4);
assert_eq!(capture.steps, {
let mut steps = [0.0; LEAD_STEP_COUNT];
steps[0] = 2.0; steps
});
assert_eq!(lead_capture(&[press(0.0, 1)], 20.0, 0.5, 0.0), None);
assert_eq!(lead_capture(&[], 0.0, 0.5, 0.0), None);
let mut buffer = LeadPhraseBuffer::default();
for beat in 0..200 {
buffer.push(press(beat as f64, 1));
}
assert_eq!(buffer.presses().len(), 128, "the buffer is bounded");
assert_eq!(buffer.presses()[0].beat, 72.0, "and forgets the oldest");
}
#[test]
fn lead_retrigger_keeps_the_envelope_continuous() {
let attack = 0.02;
let mut voice = LeadVoice::new(220.0, lead_shape(attack, 1.0), SAMPLE_RATE);
let recipe = &LEAD_TYPES[3]; let mut last = 0.0;
for _ in 0..(SAMPLE_RATE * attack * 0.5) as usize {
last = voice.next(0.0, recipe);
}
voice.retrigger(220.0, lead_shape(attack, 1.0));
let next = voice.next(0.0, recipe);
let slope_bound = 220.0 * std::f32::consts::TAU / SAMPLE_RATE * 2.0;
assert!(
(next - last).abs() < slope_bound,
"retrigger stepped the output: {last} -> {next}"
);
}
fn lead_shape(attack: f32, decay: f32) -> LeadShape {
LeadShape {
attack,
decay,
hold: false,
}
}
#[test]
fn lead_held_key_sustains_until_its_release_and_silences_the_lane() {
let pad = PadControls::default();
let rests = LeadControls {
level: 0.5,
attack: 0.005,
decay: 0.05,
step_count: 1.0,
steps: [0.0; LEAD_STEP_COUNT],
..LeadControls::default()
};
let peak = |lead: &mut LeadEngine, c: &LeadControls, from: u64, samples: u64| -> f32 {
(from..from + samples)
.map(|sample| lead.next(c, &pad, 0.0, timing(sample, 120.0)).0.abs())
.fold(0.0, f32::max)
};
let held = LeadPlayState {
presses: 1,
tone: 3,
held: true,
};
let mut lead = LeadEngine::new(SAMPLE_RATE);
lead.observe(held);
let half_second = (SAMPLE_RATE * 0.5) as u64; assert!(
peak(&mut lead, &rests, 0, half_second) > 0.05,
"the held key sounds"
);
assert!(
peak(&mut lead, &rests, half_second, 2_000) > 0.05,
"still sounding half a second in: held, not decaying"
);
lead.observe(LeadPlayState {
held: false,
..held
});
let released_at = half_second + 2_000;
let four_decays = (SAMPLE_RATE * 0.2) as u64;
peak(&mut lead, &rests, released_at, four_decays);
assert_eq!(
peak(&mut lead, &rests, released_at + four_decays, 2_000),
0.0,
"released, the note decayed out"
);
let lane = LeadControls {
steps: [1.0; LEAD_STEP_COUNT],
..rests.clone()
};
let mut lead = LeadEngine::new(SAMPLE_RATE);
lead.observe(held);
peak(&mut lead, &lane, 0, 2_000);
let held_hz = lead.voice.as_ref().unwrap().hz();
let two_seconds = (SAMPLE_RATE * 2.0) as u64;
peak(&mut lead, &lane, 2_000, two_seconds);
assert_close_named(
lead.voice.as_ref().unwrap().hz(),
held_hz,
"pitch under a held key",
);
lead.observe(LeadPlayState {
held: false,
..held
});
assert!(
peak(&mut lead, &lane, 2_000 + two_seconds, half_second) > 0.05,
"the lane plays again once the key is up"
);
}
#[test]
fn lead_tones_reach_the_chord_an_octave_up_and_the_root_two_up() {
let pad = PadControls::default();
let reach = lead_reach(LeadFollow::Chord, &pad, 0, 6); assert_eq!(lead_step_tone(0.0), None, "value 0 is a rest");
assert_eq!(lead_step_label(0.0, &reach), "Rest");
assert_eq!(reach.note(lead_step_tone(1.0).unwrap(), 0.0), 48);
assert_eq!(reach.note(lead_step_tone(4.0).unwrap(), 0.0), 64);
assert_eq!(reach.note(lead_step_tone(5.0).unwrap(), 0.0), 60);
assert_eq!(reach.note(lead_step_tone(9.0).unwrap(), 0.0), 72);
assert_eq!(reach.note(lead_step_tone(1.0).unwrap(), -1.0), 36);
assert_eq!(
lead_step_tone(99.0),
lead_step_tone(9.0),
"an out-of-table value clamps to the last tone"
);
let labels: Vec<_> = (1..=LEAD_TONE_COUNT)
.map(|tone| reach.label(tone))
.collect();
assert_eq!(labels, ["1", "2", "3", "4", "1'", "2'", "3'", "4'", "1''"]);
}
#[test]
fn lead_scale_follow_walks_the_progression_scale_from_its_tonic() {
let pad = PadControls::default();
let scale = progression_scale(&pad, 0);
let notes: Vec<_> = (1..=scale.len())
.map(|tone| scale.note(tone, 0.0))
.collect();
assert_eq!(notes, [45, 47, 48, 50, 52, 55]);
assert_eq!(
scale.note(7, 0.0),
57,
"the seventh tone wraps to A an octave up"
);
assert_eq!(scale.label(7), "1'");
assert_eq!(
lead_reach(LeadFollow::Scale, &pad, 0, 3),
lead_reach(LeadFollow::Scale, &pad, 0, 6)
);
assert_ne!(
lead_reach(LeadFollow::Chord, &pad, 0, 3),
lead_reach(LeadFollow::Chord, &pad, 0, 6)
);
assert_eq!(progression_scale(&pad, 4).len(), 8);
let custom = PadControls {
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_count: 1.0,
..PadControls::default()
};
assert_eq!(
progression_scale(&custom, CUSTOM_PROGRESSION_INDEX).len(),
3
);
assert_eq!(LeadFollow::from_value(1.0), LeadFollow::Scale);
assert_eq!(LeadFollow::from_value(0.4), LeadFollow::Chord);
}
#[test]
fn lead_glide_slides_the_pitch_toward_the_new_note_without_a_jump() {
let mut voice = LeadVoice::new(220.0, lead_shape(0.001, 1.0), SAMPLE_RATE);
voice.retrigger(440.0, lead_shape(0.001, 1.0));
let glide = 0.1;
voice.next(glide, &LEAD_TYPES[0]);
assert!(voice.hz() < 230.0, "pitch jumped: {}", voice.hz());
for _ in 0..(SAMPLE_RATE * glide) as usize {
voice.next(glide, &LEAD_TYPES[0]);
}
assert!(voice.hz() > 440.0 * 0.95, "pitch stalled: {}", voice.hz());
let mut instant = LeadVoice::new(220.0, lead_shape(0.001, 1.0), SAMPLE_RATE);
instant.retrigger(440.0, lead_shape(0.001, 1.0));
instant.next(0.0, &LEAD_TYPES[0]);
assert_close(instant.hz(), 440.0);
}
#[test]
fn lead_engine_is_mono_and_plays_the_lane_against_the_pad_chord() {
let pad = PadControls::default();
let controls = LeadControls {
level: 0.5,
..LeadControls::default()
};
let mut lead = LeadEngine::new(SAMPLE_RATE);
let mut peak = 0.0f32;
let mut silent = LeadEngine::new(SAMPLE_RATE);
for sample in 0..SAMPLE_RATE as u64 * 4 {
let (l, r) = lead.next(&controls, &pad, 0.0, timing(sample, 120.0));
assert_eq!(l, r, "the lead is centred");
assert!(l.is_finite());
peak = peak.max(l.abs());
let (sl, _) = silent.next(&LeadControls::default(), &pad, 0.0, timing(sample, 120.0));
assert_eq!(sl, 0.0, "a Level of 0 plays nothing");
}
assert!(peak > 0.05, "the lane never sounded: peak {peak}");
assert!(peak < 1.0, "the lead clips on its own: peak {peak}");
assert!(silent.voice.is_none(), "a silent lead keeps no voice alive");
}
#[test]
fn song_code_round_trips_the_lead_lane() {
let decoded = round_trip(|c| {
c.lead.level = 0.4;
c.lead.glide = 0.25;
c.lead.octave = 1.0;
c.lead.step_count = 5.0;
c.lead.steps[0] = 9.0;
c.lead.steps[4] = 0.0;
c.lead.steps[15] = 3.0;
});
assert_near(decoded.lead.level, 0.4);
assert_near(decoded.lead.glide, 0.25);
assert_close(decoded.lead.octave, 1.0);
assert_close(decoded.lead.step_count, 5.0);
assert_close(decoded.lead.steps[0], 9.0);
assert_close(decoded.lead.steps[4], 0.0);
assert_close(decoded.lead.steps[15], 3.0);
}
#[test]
fn mute_bits_are_stable_when_a_tab_is_added_mid_strip() {
assert_eq!(Tab::Master.mute_bit(), 7);
assert_eq!(Tab::Lead.mute_bit(), 8);
assert_eq!(MUTE_BYTES, 2);
let mut seen = std::collections::BTreeSet::new();
for tab in Tab::all() {
assert!(
seen.insert(tab.mute_bit()),
"{} shares a mute bit",
tab.name()
);
}
let mut song = SongState::from_controls(FluidControls::default());
song.muted[Tab::Lead as usize] = true;
song.muted[Tab::Master as usize] = true;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.muted, song.muted);
}
#[test]
fn lead_engine_plays_a_pressed_tone_once_per_press_even_on_a_resting_lane() {
let pad = PadControls::default();
let controls = LeadControls {
level: 0.5,
step_count: 1.0,
steps: [0.0; LEAD_STEP_COUNT],
..LeadControls::default()
};
let mut lead = LeadEngine::new(SAMPLE_RATE);
let quiet_for = |lead: &mut LeadEngine, from: u64, samples: u64| -> f32 {
(from..from + samples)
.map(|sample| {
lead.next(&controls, &pad, 0.0, timing(sample, 120.0))
.0
.abs()
})
.fold(0.0, f32::max)
};
assert_eq!(
quiet_for(&mut lead, 0, 4_000),
0.0,
"an all-rest lane is silent"
);
lead.observe(LeadPlayState {
presses: 1,
tone: 5,
held: false,
});
assert!(quiet_for(&mut lead, 4_000, 4_000) > 0.05, "a press sounds");
let mut lead_again = LeadEngine::with_play_state(
SAMPLE_RATE,
LeadPlayState {
presses: 1,
tone: 5,
held: false,
},
);
lead_again.observe(LeadPlayState {
presses: 1,
tone: 5,
held: false,
});
assert_eq!(
quiet_for(&mut lead_again, 0, 4_000),
0.0,
"a caught-up engine replays nothing"
);
let mut silent = LeadEngine::new(SAMPLE_RATE);
silent.observe(LeadPlayState {
presses: 1,
tone: 5,
held: false,
});
let muted = LeadControls::default();
for sample in 0..64 {
silent.next(&muted, &pad, 0.0, timing(sample, 120.0));
}
assert!(silent.voice.is_none());
assert_eq!(quiet_for(&mut silent, 64, 4_000), 0.0);
}
#[test]
fn lead_types_render_at_a_matched_level() {
let samples = SAMPLE_RATE as usize * 2;
let types: Vec<SoundVariant> = LEAD_TYPES
.iter()
.map(|recipe| {
let mut voice = LeadVoice::new(220.0, lead_shape(0.005, 10.0), SAMPLE_RATE);
let step: Box<dyn FnMut() -> (f32, f32)> = Box::new(move || {
let sample = voice.next(0.0, recipe);
(sample, sample)
});
(recipe.label, step)
})
.collect();
assert_types_differ_but_balanced("lead", samples, types);
let rms: Vec<f32> = LEAD_TYPES
.iter()
.map(|recipe| {
let mut voice = LeadVoice::new(220.0, lead_shape(0.005, 10.0), SAMPLE_RATE);
let out: Vec<f32> = (0..samples).map(|_| voice.next(0.0, recipe)).collect();
crate::synth::fm::rms(&out)
})
.collect();
let max = rms.iter().cloned().fold(f32::MIN, f32::max);
let min = rms.iter().cloned().fold(f32::MAX, f32::min);
assert!(max / min < 1.1, "lead types drift apart in level: {rms:?}");
}
#[test]
fn lead_at_full_level_sits_under_the_bass() {
let pad = PadControls::default();
let n = SAMPLE_RATE as u64 * 8;
let mut lead = LeadEngine::new(SAMPLE_RATE);
let lc = LeadControls {
level: 1.0,
..LeadControls::default()
};
let l: Vec<f32> = (0..n)
.map(|s| lead.next(&lc, &pad, 0.0, timing(s, 120.0)).0)
.collect();
let mut bass = BassEngine::new(SAMPLE_RATE);
let bc = BassControls {
level: 1.0,
voice_type: 1.0,
..BassControls::default()
};
let b: Vec<f32> = (0..n)
.map(|s| bass.next(&bc, &pad, 0.0, timing(s, 120.0)).0)
.collect();
let ratio = crate::synth::fm::rms(&l) / crate::synth::fm::rms(&b);
assert!((0.45..0.9).contains(&ratio), "lead/bass rms ratio {ratio}");
}
#[test]
fn apply_ratio_lands_on_the_dial_evenly_for_every_scale() {
let mut c = FluidControls::default();
let level = spec_by_id("pad.level").unwrap();
level.apply_ratio(0.0, &mut c);
assert_close(c.pad.level, 0.0);
level.apply_ratio(1.0, &mut c);
assert_close(c.pad.level, 1.0);
level.apply_ratio(0.5, &mut c);
assert_close(c.pad.level, 0.5);
let interval = spec_by_id("perc.interval_beats").unwrap();
interval.apply_ratio(0.0, &mut c);
assert_close(c.perc.interval_beats, interval.min);
interval.apply_ratio(1.0, &mut c);
assert_close(c.perc.interval_beats, interval.max);
interval.apply_ratio(0.37, &mut c);
let landed = c.perc.interval_beats;
interval.apply_delta(1.0, &mut c);
interval.apply_delta(-1.0, &mut c);
assert_close(c.perc.interval_beats, landed);
let bars = spec_by_id("pad.chord_bars").unwrap();
for ratio in [0.0, 0.2, 0.5, 0.8, 1.0] {
bars.apply_ratio(ratio, &mut c);
let bars_value = c.pad.chord_bars;
assert!(
bars_value.log2().fract() == 0.0,
"{bars_value} is not a power of two"
);
}
let step = spec_by_id("lead.step3").unwrap();
step.apply_ratio(0.49, &mut c);
assert_close(c.lead.steps[2], c.lead.steps[2].round());
step.apply_ratio(1.0, &mut c);
assert_close(c.lead.steps[2], 9.0);
}