1use crate::assets::{MusicPattern, Sfx, SfxEffect, Waveform, CHANNELS, SFX_LEN};
9use std::sync::{Arc, Mutex};
10
11const INTERNAL_RATE: f32 = 22050.0;
14
15const SAMPLES_PER_TICK: f32 = 183.0;
17
18const ANTICLICK_RAMP_SECONDS: f32 = 0.0025;
22
23const NOISE_CUTOFF_SCALE: f32 = 8.858923;
26
27const NOISE_GAIN: f32 = 0.3;
31
32fn pitch_to_freq(pitch: f32) -> f32 {
33 440.0 * ((pitch - 33.0) / 12.0).exp2()
35}
36
37fn sfx_loops(sfx: &Sfx) -> bool {
39 sfx.loop_end > sfx.loop_start
40}
41
42fn sfx_steps(sfx: &Sfx) -> usize {
45 if sfx.loop_end > sfx.loop_start {
46 sfx.loop_end as usize
47 } else if sfx.loop_start > 0 {
48 sfx.loop_start as usize
49 } else {
50 SFX_LEN
51 }
52}
53
54fn sfx_duration(sfx: &Sfx) -> f32 {
56 sfx_steps(sfx) as f32 * sfx.speed.max(1) as f32 * SAMPLES_PER_TICK / INTERNAL_RATE
57}
58
59fn tonal_wave(wave: Waveform, t: f32, buzz: bool, t_phaser: f32) -> f32 {
64 match wave {
65 Waveform::Triangle => {
66 let mut ret = 1.0 - (4.0 * t - 2.0).abs();
67 if buzz {
68 let a = 0.875;
69 let bret = if t < a {
70 2.0 * t / a - 1.0
71 } else {
72 2.0 * (1.0 - t) / (1.0 - a) - 1.0
73 };
74 ret = ret * 0.75 + bret * 0.25;
75 }
76 ret * 0.5
77 }
78 Waveform::TiltedSaw => {
79 let a = if buzz { 0.975 } else { 0.875 };
80 let ret = if t < a {
81 2.0 * t / a - 1.0
82 } else {
83 2.0 * (1.0 - t) / (1.0 - a) - 1.0
84 };
85 ret * 0.5
86 }
87 Waveform::Saw => {
88 let base = if t < 0.5 { t } else { t - 1.0 };
91 let ret = if buzz { base * 0.83 } else { base };
92 0.653 * ret
93 }
94 Waveform::Square => {
95 if t < if buzz { 0.4 } else { 0.5 } {
96 0.25
97 } else {
98 -0.25
99 }
100 }
101 Waveform::Pulse => {
102 if t < if buzz { 0.255 } else { 0.316 } {
103 0.25
104 } else {
105 -0.25
106 }
107 }
108 Waveform::Organ => {
109 let mut ret = if t < 0.5 {
110 3.0 - (24.0 * t - 6.0).abs()
111 } else {
112 1.0 - (16.0 * t - 12.0).abs()
113 };
114 if buzz {
115 ret = if t < 0.5 { ret * 2.0 + 3.0 } else { ret };
116 ret = if t < 0.5 && ret > -1.875 {
117 ret * 0.2 - 1.0
118 } else {
119 ret + 0.5
120 };
121 }
122 ret / 9.0
123 }
124 Waveform::Phaser => {
125 let mut ret = 2.0 - (8.0 * t - 4.0).abs();
126 ret += 1.0 - (4.0 * t_phaser - 2.0).abs();
127 if buzz {
128 ret += 0.25 - ((2.0 * t + 0.5).fract() - 0.5).abs();
129 ret += 0.125 - (0.5 * (4.0 * t).fract() - 0.25).abs();
130 }
131 ret / 6.0
132 }
133 Waveform::Noise => 0.0,
135 }
136}
137
138fn drawn_wave(w: &crate::assets::CustomWave, phase: f32) -> f32 {
142 let n = w.samples.len();
143 let fpos = phase * n as f32;
144 let i0 = (fpos as usize) % n;
145 let i1 = (i0 + 1) % n;
146 let frac = fpos - fpos.floor();
147 let a = w.samples[i0] as f32 / 16.0;
148 let b = w.samples[i1] as f32 / 16.0;
149 a + (b - a) * frac
150}
151
152struct Voice {
154 sfx_index: usize,
155 sfx: Sfx,
156 step: usize,
158 t_in_step: f32,
160 amp: f32,
163 phase: f32,
165 phase2: f32,
167 phase_b: f32,
169 prev_pitch: f32,
171 from_music: bool,
173 noise: u32,
175 noise_level: f32,
176 lp: f32,
178 echo: Vec<f32>,
181 echo_pos: usize,
182}
183
184impl Voice {
185 fn new(sfx_index: usize, sfx: Sfx, from_music: bool) -> Self {
186 let first_pitch = sfx.notes[0].pitch as f32;
187 let echo_ticks = match sfx.reverb {
190 1 => 2.0,
191 2 => 4.0,
192 _ => 0.0,
193 };
194 let echo_len = (echo_ticks * SAMPLES_PER_TICK).round() as usize;
195 Self {
196 sfx_index,
197 sfx,
198 step: 0,
199 t_in_step: 0.0,
200 amp: 0.0,
202 phase: 0.0,
203 phase2: 0.0,
204 phase_b: 0.0,
205 prev_pitch: first_pitch,
206 from_music,
207 noise: 0x1234_5678,
208 noise_level: 0.0,
209 lp: 0.0,
210 echo: vec![0.0; echo_len],
211 echo_pos: 0,
212 }
213 }
214
215 fn step_duration(&self) -> f32 {
216 self.sfx.speed.max(1) as f32 * SAMPLES_PER_TICK / INTERNAL_RATE
217 }
218
219 fn sample(
227 &mut self,
228 dt: f32,
229 total_t: f32,
230 inst_waves: &[u8; 8],
231 inst_drawn: &[Option<crate::assets::CustomWave>; 8],
232 ) -> Option<f32> {
233 if self.step >= SFX_LEN {
234 return None;
235 }
236 let note = self.sfx.notes[self.step];
237 let frac = self.t_in_step / self.step_duration();
238
239 let base_pitch = note.pitch as f32;
241 let mut pitch = base_pitch;
242 let mut vol = note.volume as f32 / 7.0;
243 match SfxEffect::from_u8(note.effect) {
244 SfxEffect::None => {}
245 SfxEffect::Slide => pitch = self.prev_pitch + (base_pitch - self.prev_pitch) * frac,
246 SfxEffect::Vibrato => {
247 pitch += 0.25 * (total_t * 2.0 * std::f32::consts::PI * 8.0).sin()
248 }
249 SfxEffect::Drop => pitch = base_pitch * (1.0 - frac),
250 SfxEffect::FadeIn => vol *= frac,
251 SfxEffect::FadeOut => vol *= 1.0 - frac,
252 SfxEffect::ArpFast | SfxEffect::ArpSlow => {
253 let rate = if note.effect == 6 { 32.0 } else { 16.0 };
254 let group = self.step / 4 * 4;
255 let idx = (total_t * rate) as usize % 4;
256 pitch = self.sfx.notes[(group + idx).min(SFX_LEN - 1)].pitch as f32;
257 }
258 }
259
260 let drawn = note.instrument().and_then(|slot| inst_drawn[slot as usize]);
264 let bass = drawn.is_some_and(|w| w.bass);
265 let freq = pitch_to_freq(pitch) * if bass { 0.5 } else { 1.0 };
266 let wave = match note.instrument() {
267 Some(slot) => Waveform::from_u8(inst_waves[slot as usize]),
268 None => Waveform::from_u8(note.wave),
269 };
270
271 self.phase = (self.phase + freq * dt).fract();
273 self.phase_b = (self.phase_b + freq * (109.0 / 110.0) * dt).fract();
275 let raw = if let Some(w) = &drawn {
276 drawn_wave(w, self.phase) * 0.5
277 } else if wave == Waveform::Noise {
278 self.noise = self.noise.wrapping_mul(1664525).wrapping_add(1013904223);
282 let white = (self.noise >> 16) as f32 / 32768.0 - 1.0;
283 let scale = freq * dt * NOISE_CUTOFF_SCALE;
284 self.noise_level = (self.noise_level + scale * white) / (1.0 + scale);
285 let factor = 1.0 - pitch / 63.0;
286 let mut n = self.noise_level * 1.5 * (1.0 + factor * factor) * NOISE_GAIN;
287 if self.sfx.noiz {
288 n *= 2.0
291 * if self.phase < 0.5 {
292 self.phase
293 } else {
294 self.phase - 1.0
295 };
296 }
297 n
298 } else {
299 let mut s = tonal_wave(wave, self.phase, self.sfx.buzz, self.phase_b);
300 if self.sfx.detune > 0 {
303 let ratio = if self.sfx.detune == 1 { 1.0073 } else { 2.0 };
304 self.phase2 = (self.phase2 + freq * ratio * dt).fract();
305 s = (s + tonal_wave(wave, self.phase2, self.sfx.buzz, self.phase_b)) * 0.5;
306 }
307 s
308 };
309
310 let max_step = dt / ANTICLICK_RAMP_SECONDS;
313 self.amp += (vol - self.amp).clamp(-max_step, max_step);
314
315 let mut out = raw * self.amp;
316
317 if self.sfx.dampen > 0 {
319 let fc = if self.sfx.dampen == 1 { 2200.0 } else { 900.0 };
320 let rc = 1.0 / (2.0 * std::f32::consts::PI * fc);
321 let alpha = dt / (rc + dt);
322 self.lp += alpha * (out - self.lp);
323 out = self.lp;
324 }
325
326 if !self.echo.is_empty() {
328 let delayed = self.echo[self.echo_pos];
329 self.echo[self.echo_pos] = (out + delayed * 0.45).clamp(-1.0, 1.0);
330 self.echo_pos = (self.echo_pos + 1) % self.echo.len();
331 out = (out + delayed * 0.5).clamp(-1.0, 1.0);
332 }
333
334 self.t_in_step += dt;
336 if self.t_in_step >= self.step_duration() {
337 self.t_in_step = 0.0;
338 self.prev_pitch = base_pitch;
339 self.step += 1;
340 let (ls, le) = (self.sfx.loop_start as usize, self.sfx.loop_end as usize);
341 if le > ls {
342 if self.step >= le {
346 self.step = ls;
347 }
348 } else if ls > 0 && self.step >= ls {
349 self.step = SFX_LEN;
352 }
353 }
354 Some(out.clamp(-1.0, 1.0))
356 }
357}
358
359struct MusicState {
361 pattern: usize,
362 remaining: f32,
364}
365
366pub struct Synth {
368 sample_rate: f32,
369 t: f32,
370 sfx: Vec<Sfx>,
371 music: Vec<MusicPattern>,
372 voices: [Option<Voice>; CHANNELS],
373 music_state: Option<MusicState>,
374 token_counter: i32,
376 current_token: i32,
378 music_gain: f32,
380 music_gain_target: f32,
382 music_gain_step: f32,
384 stop_when_silent: bool,
386 reserved_channels: u8,
388 resample_frac: f32,
390 prev_internal: f32,
392 cur_internal: f32,
393 lp1: f32,
395 lp2: f32,
396}
397
398impl Synth {
399 pub fn new(sample_rate: f32) -> Self {
400 Self {
401 sample_rate,
402 t: 0.0,
403 sfx: Vec::new(),
404 music: Vec::new(),
405 voices: [None, None, None, None],
406 music_state: None,
407 token_counter: 0,
408 current_token: 0,
409 music_gain: 1.0,
410 music_gain_target: 1.0,
411 music_gain_step: 0.0,
412 stop_when_silent: false,
413 reserved_channels: 0,
414 resample_frac: 1.0,
416 prev_internal: 0.0,
417 cur_internal: 0.0,
418 lp1: 0.0,
419 lp2: 0.0,
420 }
421 }
422
423 pub fn load(&mut self, sfx: Vec<Sfx>, music: Vec<MusicPattern>) {
425 self.sfx = sfx;
426 self.music = music;
427 }
428
429 pub fn stop_all(&mut self) {
431 self.voices = [None, None, None, None];
432 self.music_state = None;
433 self.current_token = 0;
434 self.music_gain = 1.0;
435 self.music_gain_target = 1.0;
436 self.music_gain_step = 0.0;
437 self.stop_when_silent = false;
438 self.reserved_channels = 0;
439 }
440
441 pub fn play_sfx(&mut self, n: i32, channel: i32) {
444 if n < 0 {
445 if (0..CHANNELS as i32).contains(&channel) {
446 self.voices[channel as usize] = None;
447 }
448 return;
449 }
450 let Some(sfx) = self.sfx.get(n as usize).cloned() else {
451 return;
452 };
453 let ch = if (0..CHANNELS as i32).contains(&channel) {
454 channel as usize
455 } else {
456 let reserved = self.reserved_channels;
460 let free = |i: usize| reserved & (1 << i) == 0;
461 let idle = (0..CHANNELS).find(|&i| self.voices[i].is_none() && free(i));
462 let non_music = (0..CHANNELS)
463 .find(|&i| free(i) && self.voices[i].as_ref().is_some_and(|v| !v.from_music));
464 let any_free = (0..CHANNELS).rev().find(|&i| free(i));
465 idle.or(non_music).or(any_free).unwrap_or(CHANNELS - 1)
466 };
467 self.voices[ch] = Some(Voice::new(n as usize, sfx, false));
468 }
469
470 pub fn play_music(&mut self, n: i32, fade_duration: i32, channel_mask: i32, token: i32) -> i32 {
477 if n < 0 {
478 let matches = token <= 0 || (self.current_token != 0 && token == self.current_token);
479 if matches {
480 self.begin_stop(fade_duration);
481 }
482 return 0;
483 }
484 if self.music_state.is_some() && !self.stop_when_silent {
486 return 0;
487 }
488 self.reserved_channels = (channel_mask & 0x0F) as u8;
489 self.start_pattern(n as usize);
490 self.setup_fade_in(fade_duration);
491 self.token_counter = self.token_counter.wrapping_add(1);
492 if self.token_counter == 0 {
493 self.token_counter = 1;
494 }
495 self.current_token = self.token_counter;
496 self.current_token
497 }
498
499 fn setup_fade_in(&mut self, fade_duration: i32) {
501 self.stop_when_silent = false;
502 if fade_duration <= 0 {
503 self.music_gain = 1.0;
504 self.music_gain_target = 1.0;
505 self.music_gain_step = 0.0;
506 } else {
507 let fade_seconds = fade_duration as f32 / 1000.0;
508 self.music_gain = 0.0;
509 self.music_gain_target = 1.0;
510 self.music_gain_step = 1.0 / (fade_seconds * INTERNAL_RATE);
511 }
512 }
513
514 fn begin_stop(&mut self, fade_duration: i32) {
516 if self.music_state.is_none() {
517 return;
518 }
519 if fade_duration <= 0 {
520 self.stop_music();
521 return;
522 }
523 let fade_seconds = fade_duration as f32 / 1000.0;
524 self.music_gain_target = 0.0;
525 self.music_gain_step = -1.0 / (fade_seconds * INTERNAL_RATE);
526 self.stop_when_silent = true;
527 }
528
529 fn advance_music_gain(&mut self) {
532 if self.music_gain_step == 0.0 {
533 return;
534 }
535 self.music_gain += self.music_gain_step;
536 let reached = if self.music_gain_step > 0.0 {
537 self.music_gain >= self.music_gain_target
538 } else {
539 self.music_gain <= self.music_gain_target
540 };
541 if reached {
542 self.music_gain = self.music_gain_target;
543 self.music_gain_step = 0.0;
544 if self.stop_when_silent {
545 self.stop_music();
546 }
547 }
548 }
549
550 pub fn stop_music(&mut self) {
551 for v in &mut self.voices {
552 if v.as_ref().is_some_and(|v| v.from_music) {
553 *v = None;
554 }
555 }
556 self.music_state = None;
557 self.current_token = 0;
558 self.music_gain = 1.0;
559 self.music_gain_target = 1.0;
560 self.music_gain_step = 0.0;
561 self.stop_when_silent = false;
562 self.reserved_channels = 0;
563 }
564
565 pub fn playing_pattern(&self) -> Option<usize> {
567 self.music_state.as_ref().map(|m| m.pattern)
568 }
569
570 fn start_pattern(&mut self, n: usize) {
571 let Some(pat) = self.music.get(n).copied() else {
572 self.music_state = None;
573 return;
574 };
575 let mut timekeeper: Option<f32> = None;
579 let mut longest = 0.0f32;
580 for (ch, slot) in pat.channels.iter().enumerate() {
581 if let Some(sfx_idx) = slot {
583 if let Some(sfx) = self.sfx.get(*sfx_idx as usize).cloned() {
584 let dur = sfx_duration(&sfx);
585 longest = longest.max(dur);
586 if timekeeper.is_none() && !sfx_loops(&sfx) {
587 timekeeper = Some(dur);
588 }
589 self.voices[ch] = Some(Voice::new(*sfx_idx as usize, sfx, true));
590 }
591 } else if self.voices[ch].as_ref().is_some_and(|v| v.from_music) {
592 self.voices[ch] = None;
593 }
594 }
595 let length = timekeeper.unwrap_or(longest);
596 if length == 0.0 {
597 self.music_state = None;
598 return;
599 }
600 self.music_state = Some(MusicState {
601 pattern: n,
602 remaining: length,
603 });
604 }
605
606 fn advance_music(&mut self) {
607 let Some(state) = &self.music_state else {
608 return;
609 };
610 let cur = state.pattern;
611 let pat = self.music.get(cur).copied().unwrap_or_default();
612 if pat.stop_at_end {
613 self.stop_music();
614 return;
615 }
616 if pat.loop_back {
617 let target = (0..=cur)
619 .rev()
620 .find(|&i| self.music.get(i).is_some_and(|p| p.loop_start))
621 .unwrap_or(0);
622 self.start_pattern(target);
623 return;
624 }
625 let next = cur + 1;
626 if self.music.get(next).is_some_and(|p| !p.is_empty()) {
627 self.start_pattern(next);
628 } else {
629 self.stop_music();
630 }
631 }
632
633 pub fn next_sample(&mut self) -> f32 {
641 let ratio = INTERNAL_RATE / self.sample_rate;
643 self.resample_frac += ratio;
644 while self.resample_frac >= 1.0 {
645 self.prev_internal = self.cur_internal;
646 self.cur_internal = self.render_internal();
647 self.resample_frac -= 1.0;
648 }
649 let mut out =
650 self.prev_internal + (self.cur_internal - self.prev_internal) * self.resample_frac;
651 let fc = 11_000.0;
654 let dt_dev = 1.0 / self.sample_rate;
655 let alpha = dt_dev / (1.0 / (2.0 * std::f32::consts::PI * fc) + dt_dev);
656 self.lp1 += alpha * (out - self.lp1);
657 self.lp2 += alpha * (self.lp1 - self.lp2);
658 out = self.lp2;
659 out
660 }
661
662 fn render_internal(&mut self) -> f32 {
664 let dt = 1.0 / INTERNAL_RATE;
665 self.t += dt;
666
667 if let Some(state) = &mut self.music_state {
668 state.remaining -= dt;
669 if state.remaining <= 0.0 {
670 self.advance_music();
671 }
672 }
673
674 let mut inst_waves = [0u8; 8];
677 let mut inst_drawn: [Option<crate::assets::CustomWave>; 8] = Default::default();
678 for i in 0..8 {
679 if let Some(s) = self.sfx.get(i) {
680 inst_waves[i] = s.notes[0].wave_index();
681 inst_drawn[i] = s.custom_wave;
682 }
683 }
684
685 let mut music_mix = 0.0;
686 let mut sfx_mix = 0.0;
687 for v in &mut self.voices {
688 if let Some(voice) = v {
689 let from_music = voice.from_music;
690 match voice.sample(dt, self.t, &inst_waves, &inst_drawn) {
691 Some(s) => {
692 if from_music {
693 music_mix += s;
694 } else {
695 sfx_mix += s;
696 }
697 }
698 None => *v = None,
699 }
700 }
701 }
702 self.advance_music_gain();
703 (sfx_mix + music_mix * self.music_gain).clamp(-1.0, 1.0)
704 }
705
706 pub fn channel_sfx(&self) -> [Option<usize>; CHANNELS] {
708 let mut out = [None; CHANNELS];
709 for (i, v) in self.voices.iter().enumerate() {
710 out[i] = v.as_ref().map(|v| v.sfx_index);
711 }
712 out
713 }
714
715 pub fn channel_step(&self) -> [Option<usize>; CHANNELS] {
718 let mut out = [None; CHANNELS];
719 for (i, v) in self.voices.iter().enumerate() {
720 out[i] = v.as_ref().map(|v| v.step);
721 }
722 out
723 }
724}
725
726#[derive(Clone)]
728pub struct AudioHandle {
729 synth: Arc<Mutex<Synth>>,
730}
731
732impl AudioHandle {
733 pub fn new(synth: Arc<Mutex<Synth>>) -> Self {
734 Self { synth }
735 }
736
737 pub fn dummy() -> Self {
739 Self {
740 synth: Arc::new(Mutex::new(Synth::new(44100.0))),
741 }
742 }
743
744 pub fn with_synth<R>(&self, f: impl FnOnce(&mut Synth) -> R) -> R {
745 let mut guard = self.synth.lock().unwrap_or_else(|e| e.into_inner());
749 f(&mut guard)
750 }
751
752 pub fn play_sfx(&self, n: i32, channel: i32) {
753 self.with_synth(|s| s.play_sfx(n, channel));
754 }
755
756 pub fn channel_step(&self) -> [Option<usize>; CHANNELS] {
758 self.with_synth(|s| s.channel_step())
759 }
760
761 pub fn play_music(&self, n: i32, fade_duration: i32, channel_mask: i32, token: i32) -> i32 {
762 self.with_synth(|s| s.play_music(n, fade_duration, channel_mask, token))
763 }
764
765 pub fn stop_all(&self) {
766 self.with_synth(|s| s.stop_all());
767 }
768
769 pub fn load(&self, sfx: Vec<Sfx>, music: Vec<MusicPattern>) {
770 self.with_synth(|s| s.load(sfx, music));
771 }
772}
773
774#[cfg(feature = "audio")]
776pub struct AudioOutput {
777 _stream: cpal::Stream,
778 handle: AudioHandle,
779}
780
781#[cfg(feature = "audio")]
782impl AudioOutput {
783 pub fn start() -> Option<Self> {
786 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
787 let host = cpal::default_host();
788 let device = host.default_output_device()?;
789 let config = device.default_output_config().ok()?;
790 let sample_rate = config.sample_rate() as f32;
791 let channels = config.channels() as usize;
792 let synth = Arc::new(Mutex::new(Synth::new(sample_rate)));
793 let cb_synth = synth.clone();
794 let stream = device
795 .build_output_stream(
796 config.into(),
797 move |data: &mut [f32], _| {
798 let mut synth = cb_synth.lock().unwrap();
799 for frame in data.chunks_mut(channels) {
800 let s = synth.next_sample();
801 for out in frame {
802 *out = s;
803 }
804 }
805 },
806 |err| eprintln!("Pixel8 audio error: {err}"),
807 None,
808 )
809 .ok()?;
810 stream.play().ok()?;
811 Some(Self {
812 _stream: stream,
813 handle: AudioHandle::new(synth),
814 })
815 }
816
817 pub fn handle(&self) -> AudioHandle {
818 self.handle.clone()
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use crate::assets::{Note, SFX_COUNT};
826
827 fn test_sfx() -> Vec<Sfx> {
828 let mut sfx = vec![Sfx::default(); SFX_COUNT];
829 for note in sfx[0].notes.iter_mut() {
830 *note = Note {
831 pitch: 33,
832 wave: 0,
833 volume: 5,
834 effect: 0,
835 };
836 }
837 sfx
838 }
839
840 #[test]
841 fn pitch_33_is_a440() {
842 assert!((pitch_to_freq(33.0) - 440.0).abs() < 0.01);
843 assert!((pitch_to_freq(45.0) - 880.0).abs() < 0.01);
844 }
845
846 #[test]
847 fn sfx_produces_sound_then_ends() {
848 let mut synth = Synth::new(44100.0);
849 synth.load(test_sfx(), vec![MusicPattern::default(); 64]);
850 synth.play_sfx(0, 0);
851 let mut peak = 0.0f32;
852 for _ in 0..1000 {
853 peak = peak.max(synth.next_sample().abs());
854 }
855 assert!(peak > 0.01, "voice should be audible");
856 for _ in 0..(44100 * 5) {
858 synth.next_sample();
859 }
860 assert_eq!(synth.channel_sfx()[0], None, "voice should end");
861 }
862
863 #[test]
864 fn custom_instrument_borrows_its_waveform() {
865 use crate::assets::NOTE_CUSTOM_FLAG;
866 let mut sfx = vec![Sfx::default(); SFX_COUNT];
868 for note in sfx[1].notes.iter_mut() {
869 *note = Note {
870 pitch: 33,
871 wave: 6,
872 volume: 5,
873 effect: 0,
874 };
875 }
876 for note in sfx[0].notes.iter_mut() {
878 *note = Note {
879 pitch: 33,
880 wave: NOTE_CUSTOM_FLAG | 1,
881 volume: 5,
882 effect: 0,
883 };
884 }
885 let mut synth = Synth::new(44100.0);
886 synth.load(sfx, vec![MusicPattern::default(); 64]);
887 synth.play_sfx(0, 0);
888 let mut peak = 0.0f32;
889 for _ in 0..1000 {
890 peak = peak.max(synth.next_sample().abs());
891 }
892 assert!(peak > 0.01, "a custom-instrument note should be audible");
893 }
894
895 #[test]
896 fn sfx_filters_stay_audible_and_bounded() {
897 let mut sfx = test_sfx();
900 sfx[0].noiz = true;
901 sfx[0].buzz = true;
902 sfx[0].detune = 2;
903 sfx[0].reverb = 2;
904 sfx[0].dampen = 1;
905 let mut synth = Synth::new(44100.0);
906 synth.load(sfx, vec![MusicPattern::default(); 64]);
907 synth.play_sfx(0, 0);
908 let mut peak = 0.0f32;
909 for _ in 0..44100 {
910 let s = synth.next_sample();
911 assert!(s.is_finite() && s.abs() <= 1.0, "sample out of range: {s}");
912 peak = peak.max(s.abs());
913 }
914 assert!(peak > 0.01, "filtered voice should still be audible");
915 }
916
917 #[test]
918 fn noise_is_smooth_not_crackly() {
919 let mut sfx = vec![Sfx::default(); SFX_COUNT];
924 for note in sfx[0].notes.iter_mut() {
925 *note = Note {
926 pitch: 17,
927 wave: 6,
928 volume: 7,
929 effect: 0,
930 };
931 }
932 sfx[0].speed = 16;
933 sfx[0].buzz = true;
934 sfx[0].noiz = false;
935
936 let mut synth = Synth::new(48000.0);
937 synth.load(sfx, vec![MusicPattern::default(); 64]);
938 synth.play_sfx(0, 0);
939
940 let mut buf = Vec::with_capacity(24000);
942 for _ in 0..24000 {
943 buf.push(synth.next_sample());
944 }
945 let mut max_jump = 0.0f32;
946 for i in 257..buf.len() {
947 max_jump = max_jump.max((buf[i] - buf[i - 1]).abs());
948 }
949 let peak = buf[256..].iter().fold(0.0f32, |m, s| m.max(s.abs()));
950
951 assert!(peak > 0.01, "noise should be audible: peak {peak}");
957 assert!(
958 max_jump < 0.15,
959 "noise should be smooth, not crackly: max jump {max_jump}"
960 );
961 }
962
963 #[test]
964 fn note_transitions_do_not_click() {
965 let mut sfx = vec![Sfx::default(); SFX_COUNT];
970 sfx[0].speed = 16;
971 sfx[0].notes[0] = Note {
972 pitch: 33,
973 wave: 0,
974 volume: 7,
975 effect: 0,
976 };
977 sfx[0].notes[1] = Note {
978 pitch: 33,
979 wave: 0,
980 volume: 0,
981 effect: 0,
982 };
983 let mut synth = Synth::new(48000.0);
984 synth.load(sfx, vec![MusicPattern::default(); 64]);
985 synth.play_sfx(0, 0);
986
987 let mut buf = Vec::with_capacity(14400);
990 for _ in 0..14400 {
991 buf.push(synth.next_sample());
992 }
993 let mut max_jump = 0.0f32;
994 for i in 1..buf.len() {
995 max_jump = max_jump.max((buf[i] - buf[i - 1]).abs());
996 }
997 let peak = buf.iter().fold(0.0f32, |m, s| m.max(s.abs()));
998
999 assert!(
1007 max_jump < 0.04,
1008 "amplitude jump should be smooth: {max_jump}"
1009 );
1010 assert!(peak > 0.01, "the note should still be audible: {peak}");
1011 }
1012
1013 #[test]
1014 fn empty_sfx_slot_is_ignored() {
1015 let mut synth = Synth::new(44100.0);
1016 synth.load(test_sfx(), vec![]);
1017 synth.play_sfx(63, -1);
1018 for _ in 0..100 {
1019 assert_eq!(synth.next_sample(), 0.0);
1020 }
1021 }
1022
1023 #[test]
1024 fn music_plays_and_stops() {
1025 let mut synth = Synth::new(44100.0);
1026 let mut music = vec![MusicPattern::default(); 64];
1027 music[0].channels[0] = Some(0);
1028 music[0].stop_at_end = true;
1029 synth.load(test_sfx(), music);
1030 synth.play_music(0, 0, 0, 0);
1031 assert_eq!(synth.playing_pattern(), Some(0));
1032 for _ in 0..(44100 * 5) {
1033 synth.next_sample();
1034 }
1035 assert_eq!(synth.playing_pattern(), None);
1036 }
1037
1038 #[test]
1039 fn music_loops_back() {
1040 let mut synth = Synth::new(44100.0);
1041 let mut music = vec![MusicPattern::default(); 64];
1042 music[0].channels[0] = Some(0);
1043 music[0].loop_start = true;
1044 music[1].channels[0] = Some(0);
1045 music[1].loop_back = true;
1046 synth.load(test_sfx(), music);
1047 synth.play_music(1, 0, 0, 0);
1048 for _ in 0..(44100 * 5) {
1049 synth.next_sample();
1050 }
1051 assert_eq!(synth.playing_pattern(), Some(0), "should loop to start");
1052 }
1053
1054 #[test]
1055 fn pattern_length_follows_first_non_looping_channel() {
1056 let mut sfx = vec![Sfx::default(); SFX_COUNT];
1059 for (i, &spd) in [4u8, 16].iter().enumerate() {
1060 sfx[i].speed = spd;
1061 for n in sfx[i].notes.iter_mut() {
1062 *n = Note {
1063 pitch: 33,
1064 wave: 0,
1065 volume: 5,
1066 effect: 0,
1067 };
1068 }
1069 }
1070 let mut music = vec![MusicPattern::default(); 64];
1071 music[0].channels = [Some(0), Some(1), None, None];
1072 music[0].stop_at_end = true;
1073 let mut synth = Synth::new(44100.0);
1074 synth.load(sfx, music);
1075 synth.play_music(0, 0, 0, 0);
1076 let mut n = 0;
1077 while synth.playing_pattern().is_some() && n < 44100 * 5 {
1078 synth.next_sample();
1079 n += 1;
1080 }
1081 let secs = n as f32 / 44100.0;
1082 assert!(
1083 (secs - 1.062).abs() < 0.03,
1084 "pattern should track ch0, got {secs}s"
1085 );
1086 }
1087
1088 #[test]
1089 fn auto_channel_avoids_music() {
1090 let mut synth = Synth::new(44100.0);
1091 let mut sfx = test_sfx();
1092 sfx[1] = sfx[0].clone();
1093 let mut music = vec![MusicPattern::default(); 64];
1094 music[0].channels[0] = Some(0);
1095 synth.load(sfx, music);
1096 synth.play_music(0, 0, 0, 0);
1097 synth.play_sfx(1, -1);
1098 let chans = synth.channel_sfx();
1099 assert_eq!(chans[0], Some(0), "music keeps channel 0");
1100 assert!(chans[1..].contains(&Some(1)), "sfx lands elsewhere");
1101 }
1102
1103 #[test]
1104 fn drawn_waveform_instrument_drives_output() {
1105 use crate::assets::{CustomWave, Note, NOTE_CUSTOM_FLAG, SFX_COUNT, SFX_LEN};
1106 let mut sfx = vec![Sfx::default(); SFX_COUNT];
1107 sfx[1].custom_wave = Some(CustomWave {
1112 samples: [15; SFX_LEN],
1113 bass: false,
1114 });
1115 for note in sfx[0].notes.iter_mut() {
1116 *note = Note {
1117 pitch: 33,
1118 wave: NOTE_CUSTOM_FLAG | 1,
1119 volume: 5,
1120 effect: 0,
1121 };
1122 }
1123 let mut synth = Synth::new(44100.0);
1124 synth.load(sfx, vec![MusicPattern::default(); 64]);
1125 synth.play_sfx(0, 0);
1126 let mut sum = 0.0f32;
1127 let n = 2000;
1128 for _ in 0..n {
1129 let s = synth.next_sample();
1130 assert!(s.is_finite() && s.abs() <= 1.0, "sample out of range: {s}");
1131 sum += s;
1132 }
1133 assert!(
1134 sum / n as f32 > 0.05,
1135 "drawn samples should drive the output"
1136 );
1137 }
1138
1139 #[test]
1140 fn channel_step_tracks_playback() {
1141 let mut synth = Synth::new(44100.0);
1142 synth.load(test_sfx(), vec![MusicPattern::default(); 64]);
1143 assert_eq!(synth.channel_step(), [None, None, None, None]);
1144 synth.play_sfx(0, 0);
1145 assert_eq!(synth.channel_step()[0], Some(0));
1147 for _ in 0..(44100 / 5) {
1150 synth.next_sample();
1151 }
1152 assert_eq!(synth.channel_step()[0], Some(1));
1153 }
1154
1155 #[test]
1156 fn second_start_is_refused_while_playing() {
1157 let mut synth = Synth::new(44100.0);
1158 let mut music = vec![MusicPattern::default(); 64];
1159 music[0].channels[0] = Some(0);
1160 music[1].channels[0] = Some(0);
1161 synth.load(test_sfx(), music);
1162 let token = synth.play_music(0, 0, 0, 0);
1163 assert!(token != 0, "first start mints a nonzero token");
1164 assert_eq!(synth.play_music(1, 0, 0, 0), 0);
1166 assert_eq!(synth.playing_pattern(), Some(0), "first song keeps playing");
1167 }
1168
1169 #[test]
1170 fn stale_token_does_not_stop_a_later_song() {
1171 let mut synth = Synth::new(44100.0);
1172 let mut music = vec![MusicPattern::default(); 64];
1173 music[0].channels[0] = Some(0);
1174 music[0].stop_at_end = true; music[1].channels[0] = Some(0);
1176 synth.load(test_sfx(), music);
1177 let stale = synth.play_music(0, 0, 0, 0);
1178 for _ in 0..(44100 * 5) {
1179 synth.next_sample(); }
1181 assert_eq!(synth.playing_pattern(), None, "one-shot ended on its own");
1182 let fresh = synth.play_music(1, 0, 0, 0);
1183 assert!(fresh != 0 && fresh != stale, "new song gets a fresh token");
1184 synth.play_music(-1, 0, 0, stale);
1186 assert_eq!(synth.playing_pattern(), Some(1), "stale token is a no-op");
1187 synth.play_music(-1, 0, 0, fresh);
1189 assert_eq!(synth.playing_pattern(), None);
1190 }
1191
1192 #[test]
1193 fn music_fades_in_from_silence() {
1194 let mut synth = Synth::new(44100.0);
1195 let mut music = vec![MusicPattern::default(); 64];
1196 music[0].channels[0] = Some(0);
1198 music[0].loop_start = true;
1199 music[1].channels[0] = Some(0);
1200 music[1].loop_back = true;
1201 synth.load(test_sfx(), music);
1202 synth.play_music(0, 1000, 0, 0); assert!(
1204 synth.music_gain < 0.05,
1205 "starts near silent: {}",
1206 synth.music_gain
1207 );
1208 for _ in 0..(44100 / 2) {
1209 synth.next_sample();
1210 }
1211 assert!(
1212 synth.music_gain > 0.4 && synth.music_gain < 0.6,
1213 "~half after 0.5s: {}",
1214 synth.music_gain
1215 );
1216 for _ in 0..44100 {
1217 synth.next_sample();
1218 }
1219 assert!(
1220 (synth.music_gain - 1.0).abs() < 1e-3,
1221 "reaches full: {}",
1222 synth.music_gain
1223 );
1224 }
1225
1226 #[test]
1227 fn music_fades_out_then_stops() {
1228 let mut synth = Synth::new(44100.0);
1229 let mut music = vec![MusicPattern::default(); 64];
1230 music[0].channels[0] = Some(0);
1231 music[0].loop_start = true; music[1].channels[0] = Some(0);
1233 music[1].loop_back = true;
1234 synth.load(test_sfx(), music);
1235 let token = synth.play_music(0, 0, 0, 0);
1236 synth.play_music(-1, 1000, 0, token); assert!(synth.stop_when_silent, "fading out");
1238 assert_eq!(
1239 synth.playing_pattern(),
1240 Some(0),
1241 "still playing while fading"
1242 );
1243 for _ in 0..(44100 / 2) {
1244 synth.next_sample();
1245 }
1246 assert!(synth.playing_pattern().is_some(), "still fading at 0.5s");
1247 for _ in 0..(44100 / 2 + 200) {
1248 synth.next_sample();
1249 }
1250 assert_eq!(synth.playing_pattern(), None, "stops once silent");
1251 }
1252
1253 #[test]
1254 fn reserved_channel_is_not_auto_selected_for_sfx() {
1255 let mut synth = Synth::new(44100.0);
1256 let mut music = vec![MusicPattern::default(); 64];
1257 music[0].channels[0] = Some(0); synth.load(test_sfx(), music);
1259 synth.play_music(0, 0, 0b0010, 0);
1263 synth.play_sfx(1, -1); let chans = synth.channel_sfx();
1265 assert_ne!(
1266 chans[1],
1267 Some(1),
1268 "sfx must avoid the reserved idle channel 1"
1269 );
1270 assert!(
1271 chans[2..].contains(&Some(1)),
1272 "sfx landed on a free channel"
1273 );
1274 }
1275
1276 #[test]
1277 fn explicit_channel_overrides_reservation() {
1278 let mut synth = Synth::new(44100.0);
1279 let mut music = vec![MusicPattern::default(); 64];
1280 music[0].channels[0] = Some(0);
1281 synth.load(test_sfx(), music);
1282 synth.play_music(0, 0, 0b0001, 0); synth.play_sfx(1, 0); assert_eq!(synth.channel_sfx()[0], Some(1), "explicit request wins");
1285 }
1286
1287 fn goertzel(samples: &[f32], freq: f32, fs: f32) -> f32 {
1290 let omega = 2.0 * std::f32::consts::PI * freq / fs;
1291 let coeff = 2.0 * omega.cos();
1292 let mut s_prev = 0.0f32;
1293 let mut s_prev2 = 0.0f32;
1294 for &x in samples {
1295 let s = x + coeff * s_prev - s_prev2;
1296 s_prev2 = s_prev;
1297 s_prev = s;
1298 }
1299 let real = s_prev - s_prev2 * omega.cos();
1300 let imag = s_prev2 * omega.sin();
1301 (real * real + imag * imag).sqrt()
1302 }
1303
1304 #[test]
1305 fn tick_duration_matches_pico8() {
1306 let mut sfx = Sfx {
1310 speed: 16,
1311 ..Default::default()
1312 };
1313 for n in sfx.notes.iter_mut() {
1314 *n = Note {
1315 pitch: 33,
1316 wave: 0,
1317 volume: 5,
1318 effect: 0,
1319 };
1320 }
1321 let expected = 32.0 * 16.0 * 183.0 / 22050.0;
1322 assert!((sfx_duration(&sfx) - expected).abs() < 1e-4);
1323 }
1324
1325 #[test]
1326 fn no_aliasing_above_internal_nyquist() {
1327 let mut sfx = Sfx {
1339 speed: 1,
1340 ..Default::default()
1341 };
1342 for n in sfx.notes.iter_mut() {
1343 *n = Note {
1344 pitch: 63,
1345 wave: 2,
1346 volume: 7,
1347 effect: 0,
1348 };
1349 }
1350 let mut all = vec![Sfx::default(); SFX_COUNT];
1351 all[0] = sfx;
1352 let fs = 48000.0;
1353 let mut synth = Synth::new(fs);
1354 synth.load(all, vec![MusicPattern::default(); 64]);
1355 synth.play_sfx(0, 0);
1356 let mut buf = Vec::with_capacity(24000);
1357 for i in 0..24000 {
1358 let s = synth.next_sample();
1359 if i >= 512 {
1360 buf.push(s);
1361 }
1362 }
1363 let fund = goertzel(&buf, 2490.0, fs);
1366 let high: f32 = [12445.0, 14934.0, 17423.0, 19912.0]
1367 .iter()
1368 .map(|&f| goertzel(&buf, f, fs))
1369 .sum();
1370 let ratio = high / fund;
1371 assert!(
1372 ratio < 0.30,
1373 "high-band/fundamental ratio {ratio} should be small (fund={fund}, high={high})"
1374 );
1375 }
1376
1377 #[test]
1378 fn start_is_allowed_while_fading_out() {
1379 let mut synth = Synth::new(44100.0);
1380 let mut music = vec![MusicPattern::default(); 64];
1381 music[0].channels[0] = Some(0);
1382 music[0].loop_start = true;
1383 music[1].channels[0] = Some(0);
1384 music[1].loop_back = true;
1385 synth.load(test_sfx(), music);
1386 let a = synth.play_music(0, 0, 0, 0);
1387 synth.play_music(-1, 1000, 0, a); let b = synth.play_music(0, 0, 0, 0); assert!(b != 0 && b != a, "took over during fade-out");
1390 assert!(
1391 !synth.stop_when_silent,
1392 "the new song plays at full, not fading"
1393 );
1394 }
1395
1396 #[test]
1397 fn waveform_amplitudes_match_pico8() {
1398 let cases = [
1401 (Waveform::Triangle, 0.5),
1402 (Waveform::TiltedSaw, 0.5),
1403 (Waveform::Saw, 0.327),
1404 (Waveform::Square, 0.25),
1405 (Waveform::Pulse, 0.25),
1406 (Waveform::Organ, 0.333),
1407 ];
1408 for (wave, expected) in cases {
1409 let mut peak = 0.0f32;
1410 for i in 0..10000 {
1411 let t = i as f32 / 10000.0;
1412 peak = peak.max(tonal_wave(wave, t, false, t).abs());
1413 }
1414 assert!(
1415 (peak - expected).abs() <= 0.02,
1416 "{wave:?} peak {peak} should match {expected}"
1417 );
1418 }
1419 let mut peak = 0.0f32;
1422 for i in 0..10000 {
1423 let t = i as f32 / 10000.0;
1424 peak = peak.max(tonal_wave(Waveform::Phaser, t, false, t).abs());
1425 }
1426 assert!(
1427 (0.25..=0.85).contains(&peak),
1428 "Phaser peak {peak} should be in 0.25..=0.85"
1429 );
1430 }
1431
1432 #[test]
1433 fn buzz_changes_duty_cycle() {
1434 assert!(tonal_wave(Waveform::Square, 0.45, false, 0.0) > 0.0);
1437 assert!(tonal_wave(Waveform::Square, 0.45, true, 0.0) < 0.0);
1438 assert!(tonal_wave(Waveform::Pulse, 0.28, false, 0.0) > 0.0);
1439 assert!(tonal_wave(Waveform::Pulse, 0.28, true, 0.0) < 0.0);
1440 }
1441}