@@ -246,3 +246,93 @@ impl MusicEngine {
246246pub 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+ }
0 commit comments