Skip to content

Commit 55ce01b

Browse files
committed
tests: add unit+integration tests for music; fix clippy format args; rustfmt
1 parent f9cab5f commit 55ce01b

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

src/core/music.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,93 @@ impl MusicEngine {
246246
pub fn midi_to_hz(midi: f32) -> f32 {
247247
440.0 * (2.0_f32).powf((midi - 69.0) / 12.0)
248248
}
249+
250+
#[cfg(test)]
251+
mod tests {
252+
use super::*;
253+
254+
fn make_engine() -> MusicEngine {
255+
let configs = vec![
256+
VoiceConfig {
257+
waveform: Waveform::Sine,
258+
base_position: Vec3::new(-1.0, 0.0, 0.0),
259+
},
260+
VoiceConfig {
261+
waveform: Waveform::Saw,
262+
base_position: Vec3::new(1.0, 0.0, 0.0),
263+
},
264+
VoiceConfig {
265+
waveform: Waveform::Triangle,
266+
base_position: Vec3::new(0.0, 0.0, -1.0),
267+
},
268+
];
269+
let params = EngineParams::default();
270+
MusicEngine::new(configs, params, 42)
271+
}
272+
273+
#[test]
274+
fn midi_to_hz_matches_a4_and_octave() {
275+
let a4 = midi_to_hz(69.0);
276+
assert!((a4 - 440.0).abs() < 1e-4);
277+
278+
let a5 = midi_to_hz(81.0);
279+
assert!((a5 - 880.0).abs() < 1e-3);
280+
assert!((a5 / a4 - 2.0).abs() < 1e-4);
281+
}
282+
283+
#[test]
284+
fn midi_to_hz_is_monotonic_over_range() {
285+
let mut prev = midi_to_hz(20.0);
286+
for m in 21..=100 {
287+
let f = midi_to_hz(m as f32);
288+
assert!(f > prev, "frequency not increasing at midi {m}");
289+
prev = f;
290+
}
291+
}
292+
293+
#[test]
294+
fn engine_tick_emits_some_events_over_time() {
295+
let mut engine = make_engine();
296+
let mut events = Vec::new();
297+
// Simulate a few seconds worth of eighth-note ticks
298+
let seconds_per_beat = 60.0 / engine.params.bpm as f64;
299+
for _ in 0..200 {
300+
engine.tick(Duration::from_secs_f64(seconds_per_beat / 2.0), &mut events);
301+
}
302+
assert!(!events.is_empty(), "expected some scheduled events");
303+
for ev in &events {
304+
assert!(ev.voice_index < engine.voices.len());
305+
assert!(ev.frequency_hz > 0.0);
306+
assert!(ev.velocity >= 0.0 && ev.velocity <= 1.0);
307+
assert!(ev.duration_sec > 0.0);
308+
}
309+
}
310+
311+
#[test]
312+
fn engine_toggle_mute_affects_state() {
313+
let mut engine = make_engine();
314+
assert!(!engine.voices[1].muted);
315+
engine.toggle_mute(1);
316+
assert!(engine.voices[1].muted);
317+
engine.toggle_mute(1);
318+
assert!(!engine.voices[1].muted);
319+
}
320+
321+
#[test]
322+
fn engine_toggle_solo_sets_other_voices_muted() {
323+
let mut engine = make_engine();
324+
engine.toggle_solo(2);
325+
for (i, v) in engine.voices.iter().enumerate() {
326+
if i == 2 {
327+
assert!(!v.muted);
328+
} else {
329+
assert!(v.muted);
330+
}
331+
}
332+
// Toggle again clears solo
333+
engine.toggle_solo(2);
334+
for v in engine.voices.iter() {
335+
assert!(!v.muted);
336+
}
337+
}
338+
}

tests/music_tests.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Host-side integration tests for core music engine.
2+
// The main crate is wasm-only, so we include the pure-Rust module directly.
3+
4+
#![allow(dead_code)]
5+
mod music {
6+
include!("../src/core/music.rs");
7+
}
8+
9+
use music::*;
10+
use std::time::Duration;
11+
12+
fn make_engine() -> MusicEngine {
13+
let configs = vec![
14+
VoiceConfig {
15+
waveform: Waveform::Sine,
16+
base_position: glam::Vec3::new(-1.0, 0.0, 0.0),
17+
},
18+
VoiceConfig {
19+
waveform: Waveform::Saw,
20+
base_position: glam::Vec3::new(1.0, 0.0, 0.0),
21+
},
22+
VoiceConfig {
23+
waveform: Waveform::Triangle,
24+
base_position: glam::Vec3::new(0.0, 0.0, -1.0),
25+
},
26+
];
27+
let params = EngineParams::default();
28+
MusicEngine::new(configs, params, 42)
29+
}
30+
31+
#[test]
32+
fn midi_to_hz_matches_a4_and_octave() {
33+
let a4 = midi_to_hz(69.0);
34+
assert!((a4 - 440.0).abs() < 1e-4);
35+
let a5 = midi_to_hz(81.0);
36+
assert!((a5 - 880.0).abs() < 1e-3);
37+
assert!((a5 / a4 - 2.0).abs() < 1e-4);
38+
}
39+
40+
#[test]
41+
fn midi_to_hz_is_monotonic_over_range() {
42+
let mut prev = midi_to_hz(20.0);
43+
for m in 21..=100 {
44+
let f = midi_to_hz(m as f32);
45+
assert!(f > prev, "frequency not increasing at midi {m}");
46+
prev = f;
47+
}
48+
}
49+
50+
#[test]
51+
fn engine_tick_emits_some_events_over_time() {
52+
let mut engine = make_engine();
53+
let mut events = Vec::new();
54+
let seconds_per_beat = 60.0 / engine.params.bpm as f64;
55+
for _ in 0..200 {
56+
engine.tick(Duration::from_secs_f64(seconds_per_beat / 2.0), &mut events);
57+
}
58+
assert!(!events.is_empty(), "expected some scheduled events");
59+
for ev in &events {
60+
assert!(ev.voice_index < engine.voices.len());
61+
assert!(ev.frequency_hz > 0.0);
62+
assert!(ev.velocity >= 0.0 && ev.velocity <= 1.0);
63+
assert!(ev.duration_sec > 0.0);
64+
}
65+
}
66+
67+
#[test]
68+
fn engine_toggle_mute_and_solo() {
69+
let mut engine = make_engine();
70+
assert!(!engine.voices[1].muted);
71+
engine.toggle_mute(1);
72+
assert!(engine.voices[1].muted);
73+
engine.toggle_mute(1);
74+
assert!(!engine.voices[1].muted);
75+
76+
engine.toggle_solo(2);
77+
for (i, v) in engine.voices.iter().enumerate() {
78+
if i == 2 {
79+
assert!(!v.muted);
80+
} else {
81+
assert!(v.muted);
82+
}
83+
}
84+
engine.toggle_solo(2);
85+
for v in engine.voices.iter() {
86+
assert!(!v.muted);
87+
}
88+
}

0 commit comments

Comments
 (0)