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 assert_close(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < f32::EPSILON,
"expected {expected}, got {actual}"
);
}
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 assert_near(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < 1e-5,
"expected {expected}, got {actual}"
);
}
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 single_macro_route(slot: usize, amount: f32) -> MacroRoute {
let mut amounts = [0.0; MACRO_COUNT];
amounts[slot] = amount;
MacroRoute { amounts }
}
fn append_record_to_code(code: &str, record_type: u8, payload: &[u8]) -> String {
let encoded = code.strip_prefix("n1_").unwrap();
let mut bytes = URL_SAFE_NO_PAD.decode(encoded).unwrap();
song::write_record(record_type, payload, &mut bytes).unwrap();
format!("n1_{}", URL_SAFE_NO_PAD.encode(bytes))
}
fn write_test_str(value: &str, out: &mut Vec<u8>) {
out.push(value.len() as u8);
out.extend_from_slice(value.as_bytes());
}
fn automation_payload(target_id: &str, route: LfoRoute) -> Vec<u8> {
let mut payload = Vec::new();
payload.push(2);
payload.extend_from_slice(&1u16.to_le_bytes());
write_test_str(target_id, &mut payload);
payload.extend_from_slice(&route.cycle_beats.to_le_bytes());
payload.extend_from_slice(&route.depth_ratio.to_le_bytes());
payload.push(0);
payload.extend_from_slice(&route.phase_offset_beats.to_le_bytes());
payload
}
fn buffer_text(buffer: &Buffer) -> String {
buffer
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>()
}
#[allow(clippy::too_many_arguments)] fn render_to_buffer(
width: u16,
height: u16,
items: &[ControlItem],
tab: Tab,
cursor: usize,
submenu: usize,
beat: f64,
fluid: &FluidState,
automation: &AutomationState,
controls: &FluidControls,
footer: Option<&str>,
drill: ChordDrill,
active_chord: u64,
mute: &MuteState,
) -> Buffer {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
render(
f,
items,
tab,
cursor,
submenu,
beat,
NumericDisplay::empty(),
fluid,
automation,
controls,
footer,
&FlippedUnits::new(),
drill,
active_chord,
mute,
)
})
.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 = tonal_note_hz(45, 0.0);
assert_close(tonal_note_hz(45, 12.0), flat * 2.0);
assert_close(tonal_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, tonal_note_hz(36, 0.0));
let high = piano_harmonic_decay_rates(profile, 60, tonal_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);
}
#[test]
fn tonal_attack_control_changes_piano_voice_onset() {
let profile = piano_profile(1);
let mut fast = PianoTonalVoice::new(profile, 60, 440.0, 0.0, 1.0, SAMPLE_RATE, 0.0, 2.0);
let mut slow = PianoTonalVoice::new(profile, 60, 440.0, 0.0, 1.0, SAMPLE_RATE, 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, 60, 440.0, 0.0, 1.0, SAMPLE_RATE, 0.0, 2.0);
let mut short_decay = PianoTonalVoice::new(profile, 60, 440.0, 0.0, 1.0, SAMPLE_RATE, 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!(tonal_synth_type_label(0.0), "Sine");
assert_eq!(tonal_synth_type_label(1.0), "Rhodes");
assert_eq!(tonal_synth_type_label(2.0), "Wurli");
assert_eq!(tonal_synth_type_label(3.0), "Felt");
assert_eq!(tonal_synth_type_label(4.0), "Marimba");
assert_eq!(tonal_synth_type_label(5.0), "Kalimba");
assert_eq!(tonal_synth_type_label(6.0), "Pluck");
assert_eq!(tonal_synth_type_label(7.0), "Dulcet");
assert_eq!(tonal_synth_type_label(8.0), "Cloud Keys");
assert_eq!(tonal_synth_type_label(9.0), "Haze");
}
#[test]
fn bass_low_pass_reduces_high_energy_without_thinning_low_notes() {
fn filtered_sine_rms(hz: f32, cutoff_hz: f32) -> f32 {
let mut low_pass = BassLowPass::new();
let total = SAMPLE_RATE as u64 * 2;
let warmup = SAMPLE_RATE as u64 / 2;
let mut sum = 0.0f32;
let mut count = 0u64;
for sample in 0..total {
let phase = TAU * hz * sample as f32 / SAMPLE_RATE;
let filtered = low_pass.process(phase.sin(), cutoff_hz, SAMPLE_RATE);
if sample >= warmup {
sum += filtered * filtered;
count += 1;
}
}
(sum / count as f32).sqrt()
}
let cutoff = 300.0;
let low = filtered_sine_rms(80.0, cutoff);
let high = filtered_sine_rms(cutoff * 8.0, cutoff);
assert!(low > 0.4, "low note rms should stay strong, got {low}");
assert!(high < 0.3, "high content should be reduced, got {high}");
}
#[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 mut sum = 0.0f32;
let mut count = 0u64;
for sample in 0..total {
let phase = TAU * hz * sample as f32 / SAMPLE_RATE;
let filtered = low_cut.process(phase.sin());
if sample >= warmup {
sum += filtered * filtered;
count += 1;
}
}
(sum / count as f32).sqrt()
}
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.0, 0.5), 7);
assert_eq!(tonal_cycle_step(3.75, 4.0, 0.0, 1.0), 3);
assert_eq!(tonal_cycle_step(4.0, 4.0, 0.0, 0.5), 0);
}
#[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 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 chords_tab_shows_type_row_with_letter_display() {
let mut controls = FluidControls::default();
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[3].id, "pad.type");
assert_eq!(rows[3].label, "Type");
assert_eq!(rows[3].display, "Warm");
controls.pad.voice_type = 1.0;
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[3].display, "Dark");
controls.pad.voice_type = 2.0;
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[3].display, "Glass");
}
#[test]
fn tab_previous_wraps_back_one_tab() {
assert_eq!(Tab::Master.previous(), Tab::Macros);
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 = FluidState::new();
let items = tab_controls(Tab::Master, &controls);
let automation = AutomationState::default();
render_to_buffer(
100,
32,
&items,
Tab::Master,
0,
0,
0.0,
&fluid,
&automation,
&controls,
None,
ChordDrill::None,
0,
&[None; 9],
);
}
#[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 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(
route.field_ratio(LfoField::Interval),
index as f32 / denominator,
);
}
route.cycle_beats = 6.0;
assert_near(
route.field_ratio(LfoField::Interval),
(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 offset = spec_by_id("kick.offset_beats").unwrap();
let offset_ratios = [0.0, 0.125, 0.25, 0.5].map(|value| offset.ratio(value));
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));
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() {
for spec in all_specs() {
assert_near(spec.ratio(spec.min), 0.0);
assert_near(spec.ratio(spec.max), 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 same_key_toggles_editor_closed_and_keeps_the_route() {
let controls = FluidControls::default();
let items = tab_controls(Tab::Master, &controls);
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let address = ControlAddress::new(items[0].id);
let mut sub = 0usize;
open_modulator(&mut automation, &items, 0, ModKind::Lfo, &mut sub);
automation.edit(|state| state.route_mut(address).unwrap().depth_ratio = 0.4);
assert!(automation.state().is_editor_open());
open_modulator(&mut automation, &items, 0, ModKind::Lfo, &mut sub);
assert!(!automation.state().is_editor_open());
assert_close(automation.state().route(address).unwrap().depth_ratio, 0.4);
}
#[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.set_macro_route(address, single_macro_route(0, 0.5));
automation.clear_control(address);
assert!(automation.route(address).is_none());
assert!(automation.macro_route(address).is_none());
}
#[test]
fn macro_route_scales_target_into_its_range() {
let mut controls = FluidControls::default();
controls.master.level = 0.2;
controls.macros.values[0] = 0.5;
let mut automation = AutomationState::default();
automation.set_macro_route(
ControlAddress::new("master.level"),
single_macro_route(0, 1.0),
);
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(0, 120.0));
assert_near(effective.master.level, 0.7);
automation.set_macro_route(
ControlAddress::new("master.level"),
single_macro_route(0, -1.0),
);
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(0, 120.0));
assert_near(effective.master.level, 0.0);
}
#[test]
fn a_control_can_ride_several_macro_sliders_at_once() {
let mut controls = FluidControls::default();
controls.master.level = 0.2;
controls.macros.values[0] = 0.5;
controls.macros.values[2] = 1.0;
let mut automation = AutomationState::default();
let mut route = MacroRoute::default();
route.amounts[0] = 0.4;
route.amounts[2] = -0.3;
automation.set_macro_route(ControlAddress::new("master.level"), route);
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(0, 120.0));
assert_near(effective.master.level, 0.1);
automation
.macro_route_mut(ControlAddress::new("master.level"))
.unwrap()
.amounts[2] = 0.0;
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(0, 120.0));
assert_near(effective.master.level, 0.4);
}
#[test]
fn field_macro_on_lfo_amount_is_off_by_default_and_only_appears_after_v() {
let controls = FluidControls::default();
let items = tab_controls(Tab::Master, &controls);
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let address = ControlAddress::new(items[0].id);
let key = unit_key(address.id(), Some("lfo.amount"));
automation.edit(|state| {
state.open_or_create(address);
});
assert_eq!(
lfo_submenu_rows(automation.state(), address).len(),
LfoField::ALL.len(),
"no macro rows before v is pressed"
);
assert!(automation.state().field_macro(&key).is_none());
automation.edit(|state| state.toggle_open_field(key.clone()));
assert_eq!(
lfo_submenu_rows(automation.state(), address).len(),
LfoField::ALL.len() + MacroField::ALL.len(),
"one row per macro slider nests in once expanded"
);
assert!(automation.state().field_macro(&key).is_some());
automation.edit(|state| state.toggle_open_field(key.clone()));
assert!(automation.state().field_macro(&key).is_none());
assert_eq!(
lfo_submenu_rows(automation.state(), address).len(),
LfoField::ALL.len()
);
}
#[test]
fn close_one_level_collapses_the_nested_field_macro_before_the_whole_lfo_editor() {
let controls = FluidControls::default();
let items = tab_controls(Tab::Master, &controls);
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let address = ControlAddress::new(items[0].id);
let key = unit_key(address.id(), Some("lfo.amount"));
automation.edit(|state| {
state.open_or_create(address).depth_ratio = 0.3;
});
automation.edit(|state| state.toggle_open_field(key.clone()));
automation.edit(|state| {
state.field_macro_mut(&key).unwrap().amounts[0] = 0.5;
});
let mut lfo_selected = 5;
close_one_level(&mut automation, &mut lfo_selected);
assert!(automation.state().is_editor_open());
assert_eq!(automation.state().active_kind(), Some(ModKind::Lfo));
assert!(automation.state().field_macro(&key).is_some());
assert_eq!(
lfo_selected, 1,
"cursor lands back on the amount field's own row"
);
close_one_level(&mut automation, &mut lfo_selected);
assert!(!automation.state().is_editor_open());
assert_eq!(lfo_selected, 0);
assert_close(automation.state().route(address).unwrap().depth_ratio, 0.3);
}
#[test]
fn close_one_level_prunes_a_neutral_field_macro_left_open() {
let controls = FluidControls::default();
let items = tab_controls(Tab::Master, &controls);
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let address = ControlAddress::new(items[0].id);
let key = unit_key(address.id(), Some("lfo.amount"));
automation.edit(|state| {
state.open_or_create(address);
});
automation.edit(|state| state.toggle_open_field(key.clone()));
let mut lfo_selected = 5;
close_one_level(&mut automation, &mut lfo_selected);
assert!(
automation.state().field_macro(&key).is_none(),
"left neutral, the field macro prunes on close like every other route"
);
assert!(automation.state().is_editor_open());
assert_eq!(lfo_selected, 1);
}
#[test]
fn macro_stacked_on_lfo_amount_via_v_scales_the_depth() {
let mut controls = FluidControls::default();
controls.master.level = 0.2;
controls.macros.values[0] = 0.5;
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new("master.level"),
LfoRoute {
depth_ratio: 0.0,
cycle_beats: 2.0,
..LfoRoute::default()
},
);
automation.set_field_macro(
unit_key("master.level", Some("lfo.amount")),
single_macro_route(0, 1.0),
);
let half_beat = SAMPLE_RATE as u64 / 4; let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(half_beat, 120.0));
assert_near(effective.master.level, 0.7);
controls.macros.values[0] = 0.0;
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(half_beat, 120.0));
assert_near(effective.master.level, 0.2);
}
#[test]
fn macro_sliders_own_lfos_never_take_a_stacked_field_macro() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("macro.1");
automation.set_route(ControlAddress::new("macro.1"), LfoRoute::default());
automation.set_field_macro(
unit_key("macro.1", Some("lfo.amount")),
single_macro_route(1, 1.0),
);
let controls = FluidControls::default();
let route = automation.route(address).unwrap();
let effective = effective_lfo_route(&automation, &controls, address, route);
assert_close(effective.depth_ratio, route.depth_ratio);
}
#[test]
fn macro_slider_lfo_fields_do_not_expose_stacked_macro_rows() {
let mut automation = AutomationState::default();
let address = ControlAddress::new("macro.1");
automation.open_or_create(address);
automation.toggle_open_field(unit_key("macro.1", Some("lfo.amount")));
assert_eq!(
lfo_submenu_rows(&automation, address).len(),
LfoField::ALL.len(),
"macro sliders may have LFOs, but their LFO fields do not take nested macro routes"
);
}
#[test]
fn macro_own_lfo_feeds_targets_in_the_same_pass() {
let mut controls = FluidControls::default();
controls.master.level = 0.0;
controls.macros.values[0] = 0.0;
let mut automation = AutomationState::default();
automation.set_route(
ControlAddress::new("macro.1"),
LfoRoute {
depth_ratio: 1.0,
cycle_beats: 2.0,
shape: LfoShape::Sine,
..LfoRoute::default()
},
);
automation.set_macro_route(
ControlAddress::new("master.level"),
single_macro_route(0, 1.0),
);
let sample = (f64::from(SAMPLE_RATE) * 0.25) as u64;
let mut effective = controls.clone();
apply_automation(&mut effective, &automation, timing(sample, 120.0));
assert!(
effective.macros.values[0] > 0.99,
"macro slider should sit at its LFO peak, got {}",
effective.macros.values[0]
);
assert!(
effective.master.level > 0.99,
"target should follow the modulated macro, got {}",
effective.master.level
);
}
#[test]
fn envelope_opens_only_on_macros_and_macros_never_target_macros() {
let controls = FluidControls::default();
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let mut sub = 0usize;
let master_items = tab_controls(Tab::Master, &controls);
open_modulator(
&mut automation,
&master_items,
0,
ModKind::Envelope,
&mut sub,
);
assert!(!automation.state().is_editor_open());
let macro_items = tab_controls(Tab::Macros, &controls);
open_modulator(&mut automation, ¯o_items, 0, ModKind::Macro, &mut sub);
assert!(!automation.state().is_editor_open());
open_modulator(
&mut automation,
¯o_items,
0,
ModKind::Envelope,
&mut sub,
);
assert_eq!(automation.state().active_kind(), Some(ModKind::Envelope));
automation.edit(AutomationState::close_editor);
open_modulator(&mut automation, &master_items, 0, ModKind::Macro, &mut sub);
assert_eq!(automation.state().active_kind(), Some(ModKind::Macro));
}
#[test]
fn engine_publishes_beat_telemetry() {
let controls = Arc::new(ArcSwap::from_pointee(FluidControls::default()));
let automation = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let telemetry = Arc::new(FluidTelemetry::default());
let bpm = f64::from(controls.load().master.bpm);
let mut engine = FluidEngine::new(
44_100.0,
controls,
automation,
no_morph(),
Arc::clone(&telemetry),
);
for _ in 0..512 {
engine.next_stereo();
}
let expected = 256.0 * bpm / (60.0 * 44_100.0);
let beat = telemetry.beat();
assert!(beat > 0.0);
assert!(
(beat - expected).abs() / expected < 0.01,
"expected ~{expected}, got {beat}"
);
}
const GOLDEN_RENDER_SAMPLES: usize = 48_000;
const GOLDEN_RENDER_CHECKSUM: u64 = 0x75c0_c29c_d1ec_3941;
fn fold_sample_bits(hash: u64, bits: u32) -> u64 {
(hash ^ u64::from(bits)).wrapping_mul(0x100000001b3)
}
#[test]
fn golden_render_is_byte_identical_for_a_seed() {
let controls = Arc::new(ArcSwap::from_pointee(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()
}));
let automation = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let telemetry = Arc::new(FluidTelemetry::default());
let mut engine = FluidEngine::new(SAMPLE_RATE, controls, automation, no_morph(), telemetry);
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 ambient_reverb_send_ducks_dry_sources_by_mix() {
let mut send = AmbientReverbSend::new(SAMPLE_RATE);
let frame = send.process((1.0, -1.0), (0.5, -0.5), (0.0, 0.0), 1.0, 0.5, 0.0);
assert_near(frame.pad_l, AmbientReverbSend::dry_gain(1.0));
assert_near(frame.pad_r, -AmbientReverbSend::dry_gain(1.0));
assert_near(frame.tonal_l, 0.5 * AmbientReverbSend::dry_gain(0.5));
assert_near(frame.tonal_r, -0.5 * AmbientReverbSend::dry_gain(0.5));
assert_close(frame.wet_l, 0.0);
assert_close(frame.wet_r, 0.0);
}
fn assert_reverb_does_not_boost_rms(label: &str, rms: impl Fn(f32) -> f32) {
let dry = rms(0.0);
let wet = rms(1.0);
assert!(
wet <= dry * 1.05,
"full reverb should not make {label} much louder: dry rms {dry}, wet rms {wet}"
);
}
#[test]
fn full_pad_reverb_does_not_boost_pad_rms() {
fn pad_rms(reverb_mix: f32) -> f32 {
let controls = PadControls {
reverb_mix,
attack_time: 0.01,
release_time: 0.1,
..PadControls::default()
};
let mut pad = PadEngine::new(SAMPLE_RATE, &controls, Arc::new(FluidTelemetry::default()));
pad.rng = StdRng::seed_from_u64(7);
let mut send = AmbientReverbSend::new(SAMPLE_RATE);
let mut sum = 0.0;
let mut count = 0;
let total = SAMPLE_RATE as u64 * 4;
let warmup = SAMPLE_RATE as u64;
for sample in 0..total {
let dry = pad.next(&controls, 0.0, timing(sample, 120.0));
let frame = send.process(dry, (0.0, 0.0), (0.0, 0.0), controls.reverb_mix, 0.0, 0.0);
if sample >= warmup {
let l = frame.pad_l + frame.wet_l;
let r = frame.pad_r + frame.wet_r;
sum += l * l + r * r;
count += 2;
}
}
(sum / count as f32).sqrt()
}
assert_reverb_does_not_boost_rms("pad", pad_rms);
}
#[test]
fn full_tonal_reverb_does_not_boost_tonal_rms() {
fn tonal_rms(reverb_mix: f32) -> f32 {
let controls = TonalControls {
level: 0.8,
randomness: 0.0,
step_interval_beats: 4.0,
reverb_mix,
..TonalControls::default()
};
let mut tonal = TonalEngine::new(SAMPLE_RATE);
tonal.rng = StdRng::seed_from_u64(11);
let mut send = AmbientReverbSend::new(SAMPLE_RATE);
let mut sum = 0.0;
let mut count = 0;
let total = SAMPLE_RATE as u64 * 4;
let warmup = SAMPLE_RATE as u64;
for sample in 0..total {
let dry = tonal.next(&controls, 0.0, timing(sample, 120.0));
let frame = send.process((0.0, 0.0), dry, (0.0, 0.0), 0.0, controls.reverb_mix, 0.0);
if sample >= warmup {
let l = frame.tonal_l + frame.wet_l;
let r = frame.tonal_r + frame.wet_r;
sum += l * l + r * r;
count += 2;
}
}
(sum / count as f32).sqrt()
}
assert_reverb_does_not_boost_rms("tonal", tonal_rms);
}
#[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 = FluidState::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(
120,
40,
&items,
Tab::Master,
0,
1,
beat,
&fluid,
&automation,
&controls,
None,
ChordDrill::None,
0,
&[None; 9],
)
};
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 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_close(controls.master.drive, 0.1);
assert_close(controls.master.comp_threshold, -8.0);
assert_close(controls.perc.decay_ms, 200.0);
assert_close(controls.perc.filter, 0.7);
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, 250.0);
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_close(controls.clap.room, 0.0);
}
#[test]
fn apply_min_moves_selected_control_to_floor() {
let mut controls = FluidControls::default();
controls.master.drive = 0.8;
apply_min(Tab::Master, 9, &mut controls);
assert_close(controls.master.drive, 0.0);
controls.master.bpm = 120.0;
apply_min(Tab::Master, 7, &mut controls);
assert_close(controls.master.bpm, MASTER_BPM_MIN);
controls.master.tone = 0.5;
apply_min(Tab::Master, 13, &mut controls);
assert_close(controls.master.tone, -1.0);
controls.pad.chord_bars = 16.0;
apply_min(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, 8, 42.0, &mut controls);
assert_close(controls.master.level, 0.42);
apply_value(Tab::Master, 8, 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, 5, 1.13, &mut controls);
assert_close(controls.kick.interval_beats, 1.25);
apply_value(Tab::Kick, 5, 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 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, Timing, Gain, Gain, Continuous,
Continuous, Timing, Continuous, Discrete,
],
),
(Tab::Perc, vec![Gain, Gain, Timing, Timing, Timing, Gain]),
(
Tab::Chords,
vec![
Gain, Timing, Timing, Discrete, Timing, Discrete, Discrete, Gain, Gain, Gain, Gain,
Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete,
Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete,
Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete,
Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete,
],
),
(
Tab::Bass,
vec![
Gain, Continuous, Timing, Timing, Discrete, Timing, Timing, Discrete, Discrete,
Gain,
],
),
(
Tab::Kick,
vec![
Gain, Gain, Timing, Timing, Discrete, Timing, Timing, Continuous, Gain, Gain,
],
),
(
Tab::Tonal,
vec![
Gain, Timing, Timing, Discrete, Discrete, Discrete, Timing, Timing, Timing, Gain,
Gain, Continuous, Gain,
],
),
(
Tab::Clap,
vec![
Gain, Gain, Timing, Timing, Timing, Discrete, Timing, Gain, Gain,
],
),
(
Tab::Arp,
vec![
Gain, Timing, Timing, Discrete, Timing, Timing, Gain, Discrete, 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"
);
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_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 controls_swap = Arc::new(ArcSwap::from_pointee(decoded.controls));
let automation = Arc::new(ArcSwap::from_pointee(decoded.automation));
let telemetry = Arc::new(FluidTelemetry::default());
let mut engine = FluidEngine::new(
SAMPLE_RATE,
controls_swap,
automation,
no_morph(),
telemetry,
);
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
}
fn assert_close_named(actual: f32, expected: f32, name: &str) {
assert!(
(actual - expected).abs() < f32::EPSILON,
"{name}: expected {expected}, got {actual}"
);
}
#[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 = 2.0);
assert_close_named(decoded.pad.voice_type, 2.0, "pad.type");
let decoded = round_trip(|c| c.bass.cutoff = 500.0);
assert_close_named(decoded.bass.cutoff, 500.0, "bass.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.arp.reverb_mix = 0.9);
assert_close_named(decoded.arp.reverb_mix, 0.9, "arp.reverb_mix");
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_close_named(decoded.tonal.attack, 0.2, "tonal.attack");
assert_close_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.bass.cutoff, default.bass.cutoff, "bass.cutoff");
assert_close_named(decoded.tonal.octave, default.tonal.octave, "tonal.octave");
assert_close_named(
decoded.arp.reverb_mix,
default.arp.reverb_mix,
"arp.reverb_mix",
);
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 {
controls,
automation,
};
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::encode_song_code(&SongState::from_controls(controls)).unwrap();
let code = append_record_to_code(&code, 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_skips_unknown_control_ids() {
let code = song::encode_song_code(&SongState::default()).unwrap();
let mut payload = Vec::new();
let id = b"future.control.id";
payload.extend_from_slice(&1u16.to_le_bytes());
payload.push(id.len() as u8);
payload.extend_from_slice(id);
payload.extend_from_slice(&0.75f32.to_le_bytes());
let code = append_record_to_code(&code, song::SNAPSHOT_RECORD, &payload);
let decoded = song::decode_song_code(&code).unwrap();
assert_close(
decoded.controls.master.level,
FluidControls::default().master.level,
);
}
#[test]
fn song_code_skips_unknown_automation_target_control_ids() {
let code = song::encode_song_code(&SongState::default()).unwrap();
let payload = automation_payload(
"future.control.id",
LfoRoute {
depth_ratio: 0.2,
..LfoRoute::default()
},
);
let code = append_record_to_code(&code, song::AUTOMATION_RECORD, &payload);
let decoded = song::decode_song_code(&code).unwrap();
assert_eq!(decoded.automation.routes().count(), 0);
}
#[test]
fn launch_line_is_cli_launchable() {
let line = launch_line(&SongState::default()).unwrap();
assert!(line.starts_with("nooise n1_"));
}
#[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.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.pad.reverb_mix = 0.0;
controls.perc.filter = 0.5;
controls.kick.click = 0.0;
controls.kick.drive = 0.0;
controls.kick.filter = 0.0;
controls.tonal.randomness = 0.0;
controls.clap.filter = 0.5;
controls.clap.body = 0.0;
controls.master.level = 0.0;
controls.master.drive = 0.0;
controls.bass.drive = 0.0;
let mut smoothers = GainSmoothers::new(&controls);
controls.pad.level = 1.0;
controls.pad.reverb_mix = 1.0;
controls.perc.filter = 1.0;
controls.kick.click = 0.2;
controls.kick.drive = 1.0;
controls.kick.filter = 1.0;
controls.tonal.randomness = 1.0;
controls.clap.filter = 1.0;
controls.clap.body = 1.0;
controls.master.level = 0.5;
controls.master.drive = 1.0;
controls.master.bpm = 123.0;
controls.bass.drive = 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.pad.reverb_mix > 0.0 && next.pad.reverb_mix < 1.0);
assert!(next.perc.filter > 0.5 && next.perc.filter < 1.0);
assert!(next.kick.click > 0.0 && next.kick.click < 0.2);
assert!(next.kick.drive > 0.0 && next.kick.drive < 1.0);
assert!(next.kick.filter > 0.0 && next.kick.filter < 1.0);
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.master.drive > 0.0 && next.master.drive < 1.0);
assert!(next.bass.drive > 0.0 && next.bass.drive < 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.unwrap().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");
assert_eq!(rows[6].display, "A");
controls.pad.progression = 2.0;
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[6].display, "C");
controls.pad.progression = CUSTOM_PROGRESSION_INDEX as f32;
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[6].display, "Custom");
}
#[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_min(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, ChordDrill::Progression);
assert_eq!(
rows.iter().map(|r| r.label).collect::<Vec<_>>(),
vec!["Chord 1 Root", "Chord 2 Root", "Chord 3 Root"]
);
controls.pad.chord_count = 8.0;
let rows = chords_tab_controls(&controls, ChordDrill::Progression);
assert_eq!(rows.len(), 8);
assert_eq!(rows[7].label, "Chord 8 Root");
}
#[test]
fn chords_tab_controls_slot_shows_accidental_extension_inversion() {
let controls = FluidControls::default();
let rows = chords_tab_controls(&controls, ChordDrill::Slot(2));
assert_eq!(
rows.iter().map(|r| r.label).collect::<Vec<_>>(),
vec![
"Chord 3 Accidental",
"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(ChordDrill::Progression, 0), 11);
assert_eq!(chords_flat_index(ChordDrill::Progression, 2), 19);
assert_eq!(chords_flat_index(ChordDrill::Slot(2), 0), 20);
let controls = FluidControls::default();
let expected = tab_controls(Tab::Chords, &controls)[20].id;
assert_eq!(expected, "pad.chord3_accidental");
}
#[test]
fn chords_footer_signals_drill_depth() {
assert_eq!(chords_footer(Tab::Chords, ChordDrill::None), None);
assert_eq!(chords_footer(Tab::Master, ChordDrill::Progression), None);
assert_eq!(
chords_footer(Tab::Chords, ChordDrill::Progression),
Some("Progression Enter: open chord Esc: back".to_string())
);
assert_eq!(
chords_footer(Tab::Chords, ChordDrill::Slot(2)),
Some("Chord 3 Esc: back".to_string())
);
}
#[test]
fn render_fluid_shows_chords_drill_breadcrumb_and_footer() {
let controls = FluidControls::default();
let fluid = FluidState::new();
let automation = AutomationState::default();
let rows = chords_tab_controls(&controls, ChordDrill::Slot(1));
let footer = chords_footer(Tab::Chords, ChordDrill::Slot(1));
let buffer = render_to_buffer(
120,
40,
&rows,
Tab::Chords,
0,
0,
0.0,
&fluid,
&automation,
&controls,
footer.as_deref(),
ChordDrill::Slot(1),
0,
&[None; 9],
);
let text = buffer_text(&buffer);
assert!(text.contains("Chords › Chord 2"));
assert!(text.contains("Chord 2 Esc: back"));
}
fn render_progression(controls: &FluidControls, active_chord: u64) -> String {
let fluid = FluidState::new();
let automation = AutomationState::default();
let rows = chords_tab_controls(controls, ChordDrill::Progression);
let backend = TestBackend::new(120, 40);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
render(
f,
&rows,
Tab::Chords,
0,
0,
0.0,
NumericDisplay::empty(),
&fluid,
&automation,
controls,
None,
&FlippedUnits::new(),
ChordDrill::Progression,
active_chord,
&[None; 9],
)
})
.unwrap();
buffer_text(terminal.backend().buffer())
}
#[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 = FluidState::new();
let automation = AutomationState::default();
let rows = chords_tab_controls(&controls, ChordDrill::Slot(2));
let draw = |active_chord: u64| {
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
terminal
.draw(|f| {
render(
f,
&rows,
Tab::Chords,
0,
0,
0.0,
NumericDisplay::empty(),
&fluid,
&automation,
&controls,
None,
&FlippedUnits::new(),
ChordDrill::Slot(2),
active_chord,
&[None; 9],
)
})
.unwrap();
buffer_text(terminal.backend().buffer())
};
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[4].id, "bass.type");
assert_eq!(rows[4].label, "Type");
assert_eq!(rows[4].display, "Sub");
assert_eq!(rows[7].label, "Rhythm");
assert_eq!(rows[7].display, "A");
controls.bass.voice_type = 1.0;
let rows = tab_controls(Tab::Bass, &controls);
assert_eq!(rows[4].display, "Saw");
controls.bass.voice_type = 2.0;
let rows = tab_controls(Tab::Bass, &controls);
assert_eq!(rows[4].display, "Pluck");
controls.bass.rhythm = 3.0;
let rows = tab_controls(Tab::Bass, &controls);
assert_eq!(rows[7].display, "D");
}
#[test]
fn bass_controls_adjust_and_clamp() {
let mut controls = FluidControls::default();
apply_delta(Tab::Bass, 7, 1.0, &mut controls);
assert_close(controls.bass.rhythm, 1.0);
controls.bass.rhythm = 3.0;
apply_delta(Tab::Bass, 7, 1.0, &mut controls);
assert_close(controls.bass.rhythm, 3.0);
controls.bass.octave = -1.0;
apply_delta(Tab::Bass, 8, -1.0, &mut controls);
apply_delta(Tab::Bass, 8, -1.0, &mut controls);
assert_close(controls.bass.octave, -3.0);
apply_min(Tab::Bass, 0, &mut controls);
assert_close(controls.bass.level, 0.0);
controls.bass.decay_time = 0.4;
apply_delta(Tab::Bass, 3, 1.0, &mut controls);
assert!(controls.bass.decay_time > 0.4);
apply_min(Tab::Bass, 3, &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.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, 0.0, 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, 0.15, sample_rate);
let mut legacy = SubBassVoice::new(110.0, 0.01, 0.05, 0.15, 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 sum_sq = vec![0.0f32; types.len()];
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 (energy, diff) = step();
sum_sq[i] += energy;
diff_samples[i].push(diff);
}
}
let rms: Vec<f32> = sum_sq
.iter()
.map(|&s| (s / samples as f32).sqrt())
.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, 0.0, sample_rate);
let step: Box<dyn FnMut() -> (f32, f32)> = Box::new(move || {
let s = voice.next();
(s * s, s)
});
(name, step)
})
.collect();
assert_types_differ_but_balanced("bass", samples, types);
}
#[test]
fn pad_type_zero_matches_legacy_warm_tone_exactly() {
let sample_rate = 48_000.0;
let mut dispatched = PadTone::new(0, 220.0, 0.2, 0.15, 0.5, 1.0, sample_rate);
let mut legacy = WarmPadTone::new(220.0, 0.2, 0.15, 0.5, 1.0, sample_rate);
for _ in 0..(sample_rate * 0.3) as usize {
assert_eq!(
dispatched.next_stereo(0.8, 0.5, 0.5),
legacy.next_stereo(0.8, 0.5, 0.5)
);
}
}
#[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")]
.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, 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 = SubKickVoice::new(&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 kick_types_produce_differing_but_comparably_balanced_audio() {
let sample_rate = 48_000.0;
let samples = (sample_rate * 0.3) as usize;
let types: Vec<SoundVariant> = [(0usize, "sub"), (1, "warm"), (2, "wood"), (3, "felt")]
.into_iter()
.map(|(voice_type, name)| {
let controls = KickControls {
level: 0.6,
..Default::default()
};
let mut construct_rng = StdRng::seed_from_u64(7);
let mut voice = KickVoice::new(voice_type, &controls, sample_rate, &mut construct_rng);
let mut click_rng = StdRng::seed_from_u64(99);
let step: Box<dyn FnMut() -> (f32, f32)> = Box::new(move || {
let (l, r) = voice.next(&mut click_rng);
((l * l + r * r) / 2.0, l)
});
(name, step)
})
.collect();
assert_types_differ_but_balanced("kick", samples, types);
}
#[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_reverb_mix_row_shifted_to_index_four() {
let controls = FluidControls::default();
let rows = tab_controls(Tab::Chords, &controls);
assert_eq!(rows[7].label, "Reverb Mix");
}
#[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_min(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_min(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_min(Tab::Kick, 5, &mut controls);
assert_close(controls.kick.interval_beats, 0.125);
controls.kick.interval_beats = 0.125;
apply_delta(Tab::Kick, 5, -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 {
let sum_sq: f32 = window.iter().map(|x| x * x).sum();
window_rms.push((sum_sq / window.len() as f32).sqrt());
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(), 6);
assert_eq!(rows[3].label, "Interval");
assert_close(rows[3].min, 0.125);
assert_close(rows[3].max, 4.25);
assert_eq!(rows[4].label, "Offset");
assert_close(rows[4].min, 0.0);
assert_close(rows[4].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[3].display, "Continuous");
}
#[test]
fn perc_interval_and_offset_adjust_and_clamp() {
let mut controls = FluidControls::default();
apply_delta(Tab::Perc, 3, 1.0, &mut controls);
assert_close(controls.perc.interval_beats, 0.5);
controls.perc.interval_beats = 0.25;
apply_delta(Tab::Perc, 3, -1.0, &mut controls);
assert_close(controls.perc.interval_beats, 0.125);
apply_delta(Tab::Perc, 3, 1.0, &mut controls);
assert_close(controls.perc.interval_beats, 0.25);
controls.perc.interval_beats = 4.25;
apply_delta(Tab::Perc, 3, 1.0, &mut controls);
assert_close(controls.perc.interval_beats, 4.25);
apply_delta(Tab::Perc, 4, 1.0, &mut controls);
assert_close(controls.perc.offset_beats, 0.125);
controls.perc.offset_beats = 4.0;
apply_delta(Tab::Perc, 4, 1.0, &mut controls);
assert_close(controls.perc.offset_beats, 4.0);
apply_min(Tab::Perc, 3, &mut controls);
assert_close(controls.perc.interval_beats, 0.125);
apply_min(Tab::Perc, 4, &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, 4, 0.03, &mut controls);
assert_close(controls.perc.offset_beats, 0.0);
apply_value(Tab::Perc, 4, 0.09, &mut controls);
assert_close(controls.perc.offset_beats, 0.125);
apply_value(Tab::Perc, 4, 0.3, &mut controls);
assert_close(controls.perc.offset_beats, 0.25);
controls.perc.offset_beats = 0.125;
apply_delta(Tab::Perc, 4, -1.0, &mut controls);
assert_close(controls.perc.offset_beats, 0.0);
apply_delta(Tab::Perc, 4, -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 = PadEngine::new(SAMPLE_RATE, &controls, Arc::new(FluidTelemetry::default()));
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 = PadEngine::new(SAMPLE_RATE, &controls, Arc::new(FluidTelemetry::default()));
for chord in 1..=9 {
let sample = chord * SAMPLE_RATE as u64 * 2;
let _ = pad.next(&controls, 0.0, timing(sample, 120.0));
}
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_retriggers_immediately() {
let mut controls = PadControls {
chord_bars: 64.0, attack_time: 0.001,
..PadControls::default()
};
let mut pad = PadEngine::new(SAMPLE_RATE, &controls, Arc::new(FluidTelemetry::default()));
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!(
pad.layers.len() > layers_before,
"switching progression must push a new layer immediately, without waiting for chord_trigger"
);
}
#[test]
fn pad_chord_notes_with_slot_builds_notes_from_root_extension_and_inversion() {
let slot = ChordSlotControls {
degree: 1.0,
accidental: -1.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 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, 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_only_in_custom_mode() {
let built_in = PadControls {
progression: 0.0,
chord_count: 2.0, ..PadControls::default()
};
assert_eq!(pad_chord_count(&built_in), 8);
let custom = PadControls {
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_count: 2.0,
..PadControls::default()
};
assert_eq!(pad_chord_count(&custom), 2);
}
#[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.step_index < 2);
}
}
#[test]
fn pad_engine_step_index_wraps_at_pad_chord_count_in_custom_mode() {
let controls = PadControls {
chord_bars: 1.0,
progression: CUSTOM_PROGRESSION_INDEX as f32,
chord_count: 2.0,
attack_time: 1.0,
..PadControls::default()
};
let mut pad = PadEngine::new(SAMPLE_RATE, &controls, Arc::new(FluidTelemetry::default()));
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);
}
}
#[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 = PadEngine::new(SAMPLE_RATE, &controls, Arc::new(FluidTelemetry::default()));
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 = FluidState::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(
120,
44,
&items,
Tab::Chords,
3,
1,
2.5,
&fluid,
&automation,
&controls,
None,
ChordDrill::None,
0,
&[None; 9],
);
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 = FluidState::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(
120,
44,
&items,
Tab::Chords,
3,
1,
2.5,
&fluid,
&automation,
&controls,
None,
ChordDrill::None,
0,
&[None; 9],
);
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 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 {
controls: FluidControls::default(),
automation,
};
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_near(got.step_glide, 0.3);
for i in 0..5 {
assert_near(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.reverb_mix");
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 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 {
controls: FluidControls::default(),
automation,
};
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.reverb_mix"),
EnvelopeRoute {
amount: 0.6,
attack_beats: 1.5,
decay_beats: 3.0,
trigger: EnvTrigger::OnKick,
},
);
let song = SongState {
controls: FluidControls::default(),
automation,
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
let env = decoded
.automation
.envelope(ControlAddress::new("pad.reverb_mix"))
.unwrap();
assert_close(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 flipped_time_fields_step_in_ms_and_snap_back_onto_the_beat_grid() {
let mut c = FluidControls::default();
c.master.bpm = 120.0; c.perc.decay_ms = 470.0;
let controls = Arc::new(ArcSwap::from_pointee(c));
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let mut flipped = FlippedUnits::new();
flipped.insert(unit_key("perc.interval_beats", None));
adjust_lfo_or_control(
&mut automation,
0,
&controls,
Tab::Perc,
3,
1.0,
0.0,
&flipped,
);
assert_near(
beats_to_ms(controls.load().perc.interval_beats, 120.0),
140.0,
);
flipped.remove(&unit_key("perc.interval_beats", None));
snap_after_unit_flip(&mut automation, 0, &controls, Tab::Perc, 3, false, 0.0);
assert_close(controls.load().perc.interval_beats, 0.25);
snap_after_unit_flip(&mut automation, 0, &controls, Tab::Perc, 2, true, 0.0);
assert_near(controls.load().perc.decay_ms, 500.0);
flipped.insert(unit_key("perc.decay_ms", None));
adjust_lfo_or_control(
&mut automation,
0,
&controls,
Tab::Perc,
2,
1.0,
0.0,
&flipped,
);
assert_near(controls.load().perc.decay_ms, 562.5);
}
#[test]
fn flipped_lfo_interval_steps_in_ms_and_keeps_exact_values() {
let controls = Arc::new(ArcSwap::from_pointee(FluidControls::default()));
{
let mut c = FluidControls::clone(&controls.load());
c.master.bpm = 120.0;
controls.store(Arc::new(c));
}
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let address = ControlAddress::new("master.level");
automation.edit(|state| {
state.open_or_create(address);
});
let mut flipped = FlippedUnits::new();
flipped.insert(unit_key("master.level", Some("lfo.interval")));
adjust_lfo_or_control(
&mut automation,
2,
&controls,
Tab::Master,
0,
1.0,
0.0,
&flipped,
);
assert_near(
beats_to_ms(
automation.state().route(address).unwrap().cycle_beats,
120.0,
),
1010.0,
);
flipped.clear();
snap_after_unit_flip(&mut automation, 2, &controls, Tab::Master, 0, false, 0.0);
assert_near(
beats_to_ms(
automation.state().route(address).unwrap().cycle_beats,
120.0,
),
1010.0,
);
}
#[test]
fn song_code_v5_round_trips_seed_macro_envelope_and_field_macro() {
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_field_macro(
unit_key("master.level", Some("lfo.amount")),
single_macro_route(1, 0.35),
);
let mut pad_route = MacroRoute::default();
pad_route.amounts[2] = -0.55;
pad_route.amounts[3] = 0.2;
automation.set_macro_route(ControlAddress::new("pad.level"), pad_route);
automation.set_envelope(
ControlAddress::new("pad.reverb_mix"),
EnvelopeRoute {
amount: 0.7,
attack_beats: 1.25,
decay_beats: 6.0,
trigger: EnvTrigger::EveryBeats(8.0),
},
);
let song = SongState {
controls: FluidControls::default(),
automation,
};
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_close(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 field_macro = decoded
.automation
.field_macro(&unit_key("master.level", Some("lfo.amount")))
.unwrap();
assert_close(field_macro.amounts[1], 0.35);
let macro_route = decoded
.automation
.macro_route(ControlAddress::new("pad.level"))
.unwrap();
assert_close(macro_route.amounts[2], -0.55);
assert_close(macro_route.amounts[3], 0.2);
let env = decoded
.automation
.envelope(ControlAddress::new("pad.reverb_mix"))
.unwrap();
assert_close(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 song_code_decodes_hand_built_v2_automation_payload() {
let code = song::encode_song_code(&SongState::default()).unwrap();
let mut payload = Vec::new();
payload.push(2u8); payload.extend_from_slice(&1u16.to_le_bytes());
write_test_str("master.level", &mut payload);
payload.extend_from_slice(&4.0f32.to_le_bytes()); payload.extend_from_slice(&0.4f32.to_le_bytes()); payload.push(0); payload.extend_from_slice(&0.25f32.to_le_bytes()); let code = append_record_to_code(&code, song::AUTOMATION_RECORD, &payload);
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_close(route.depth_ratio, 0.4);
assert_eq!(route.shape, LfoShape::Sine);
assert_close(route.phase_offset_beats, 0.25);
assert_eq!(route.seed, 0);
assert!(decoded.automation.macro_routes().next().is_none());
assert!(decoded.automation.envelopes().next().is_none());
}
#[test]
fn song_code_decodes_hand_built_v4_single_target_macro_into_one_slot() {
let code = song::encode_song_code(&SongState::default()).unwrap();
let mut payload = Vec::new();
payload.push(4u8); payload.extend_from_slice(&0u16.to_le_bytes()); payload.extend_from_slice(&1u16.to_le_bytes()); write_test_str("pad.level", &mut payload);
payload.push(2); payload.extend_from_slice(&(-0.6f32).to_le_bytes()); payload.extend_from_slice(&0u16.to_le_bytes()); payload.extend_from_slice(&0u16.to_le_bytes()); let code = append_record_to_code(&code, song::AUTOMATION_RECORD, &payload);
let decoded = song::decode_song_code(&code).unwrap();
let route = decoded
.automation
.macro_route(ControlAddress::new("pad.level"))
.unwrap();
for (i, amount) in route.amounts.iter().enumerate() {
if i == 2 {
assert_close(*amount, -0.6);
} else {
assert_close(*amount, 0.0);
}
}
}
#[test]
fn song_code_does_not_serialize_neutral_macro_routes() {
let mut automation = AutomationState::default();
automation.set_macro_route(ControlAddress::new("master.level"), MacroRoute::default());
automation.set_macro_route(ControlAddress::new("pad.level"), MacroRoute::default());
let song = SongState {
controls: FluidControls::default(),
automation,
};
let code = song::encode_song_code(&song).unwrap();
let decoded = song::decode_song_code(&code).unwrap();
assert!(decoded.automation.macro_routes().next().is_none());
assert!(
decoded
.automation
.macro_route(ControlAddress::new("master.level"))
.is_none()
);
assert!(
decoded
.automation
.macro_route(ControlAddress::new("pad.level"))
.is_none()
);
}
#[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("macro.1"), Some(Tab::Macros));
assert_eq!(tab_owning_control("master.bpm"), Some(Tab::Master));
assert_eq!(tab_owning_control("nope.nope"), None);
}
#[test]
fn macro_toggle_hides_but_keeps_the_assignment() {
let controls = FluidControls::default();
let items = tab_controls(Tab::Master, &controls);
let shared = Arc::new(ArcSwap::from_pointee(AutomationState::default()));
let mut automation = PublishedAutomation::new(AutomationState::default(), shared);
let address = ControlAddress::new(items[0].id);
let mut sub = 0usize;
open_modulator(&mut automation, &items, 0, ModKind::Macro, &mut sub);
automation.edit(|state| {
let route = state.macro_route_mut(address).unwrap();
route.amounts[1] = 0.5;
});
open_modulator(&mut automation, &items, 0, ModKind::Macro, &mut sub);
assert!(!automation.state().is_editor_open());
let route = automation.state().macro_route(address).unwrap();
assert_close(route.amounts[1], 0.5);
}
#[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());
automation.set_route(ControlAddress::new("macro.1"), LfoRoute::default());
automation.set_field_macro(
unit_key("pad.level", Some("lfo.amount")),
single_macro_route(0, 0.5),
);
automation.set_macro_route(
ControlAddress::new("perc.level"),
single_macro_route(0, 0.4),
);
automation.set_macro_route(
ControlAddress::new("bass.level"),
single_macro_route(1, -0.3),
);
automation.set_envelope(
ControlAddress::new("macro.1"),
EnvelopeRoute {
amount: 0.5,
..EnvelopeRoute::default()
},
);
let controls = Arc::new(ArcSwap::from_pointee(FluidControls::default()));
let automation = Arc::new(ArcSwap::from_pointee(automation));
let telemetry = Arc::new(FluidTelemetry::default());
let mut engine = FluidEngine::new(SAMPLE_RATE, controls, automation, no_morph(), telemetry);
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(tonal_synth_type_index(ArpControls::default().voice_type));
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_reuses_shared_ambient_reverb_send_alongside_pad_and_tonal() {
let mut send = AmbientReverbSend::new(SAMPLE_RATE);
let arp_mix = ArpControls::default().reverb_mix;
let frame = send.process((0.0, 0.0), (0.0, 0.0), (1.0, -1.0), 0.0, 0.0, arp_mix);
assert_near(frame.arp_l, AmbientReverbSend::dry_gain(arp_mix));
assert_near(frame.arp_r, -AmbientReverbSend::dry_gain(arp_mix));
}
#[test]
fn toggle_mute_zeroes_and_restores_the_track_level() {
let mut c = FluidControls::default();
c.perc.level = 0.65;
let controls = Arc::new(ArcSwap::from_pointee(c));
let mut mute: MuteState = [None; 9];
toggle_mute(&controls, Tab::Perc, &mut mute);
assert_close(controls.load().perc.level, 0.0);
assert!(mute[Tab::Perc as usize].is_some());
toggle_mute(&controls, Tab::Perc, &mut mute);
assert_close(controls.load().perc.level, 0.65);
assert!(mute[Tab::Perc as usize].is_none());
}
#[test]
fn toggle_mute_on_master_is_independent_of_track_mute() {
let mut c = FluidControls::default();
c.master.level = 0.8;
c.bass.level = 0.5;
let controls = Arc::new(ArcSwap::from_pointee(c));
let mut mute: MuteState = [None; 9];
toggle_mute(&controls, Tab::Master, &mut mute);
assert_close(controls.load().master.level, 0.0);
assert_close(controls.load().bass.level, 0.5);
toggle_mute(&controls, Tab::Bass, &mut mute);
assert_close(controls.load().bass.level, 0.0);
assert_close(controls.load().master.level, 0.0);
toggle_mute(&controls, Tab::Master, &mut mute);
assert_close(controls.load().master.level, 0.8);
assert_close(controls.load().bass.level, 0.0);
}
#[test]
fn toggle_mute_on_macros_tab_is_a_no_op() {
let c = FluidControls::default();
let controls = Arc::new(ArcSwap::from_pointee(c));
let mut mute: MuteState = [None; 9];
toggle_mute(&controls, Tab::Macros, &mut mute);
assert!(mute[Tab::Macros as usize].is_none());
}
#[test]
fn render_shows_a_mute_marker_on_muted_tabs_only() {
let controls = FluidControls::default();
let fluid = FluidState::new();
let items = tab_controls(Tab::Bass, &controls);
let automation = AutomationState::default();
let mut mute: MuteState = [None; 9];
mute[Tab::Perc as usize] = Some(0.7);
let buffer = render_to_buffer(
120,
44,
&items,
Tab::Bass,
0,
0,
0.0,
&fluid,
&automation,
&controls,
None,
ChordDrill::None,
0,
&mute,
);
let text = buffer_text(&buffer);
assert!(text.contains("Perc (M)"), "muted tab must show a marker");
assert!(
!text.contains("Bass (M)"),
"unmuted tab must not show a marker"
);
}