//! Synthesised sound effects using the Web Audio API. //! //! All public functions are no-ops on non-WASM targets so callers need no //! `#[cfg]` guards themselves. #[cfg(target_arch = "wasm32")] mod inner { use std::cell::RefCell; use web_sys::{AudioContext, OscillatorType}; thread_local! { static CTX: RefCell> = const { RefCell::new(None) }; } fn with_ctx(f: F) { CTX.with(|cell| { let mut opt = cell.borrow_mut(); if opt.is_none() { *opt = AudioContext::new().ok(); } if let Some(ctx) = opt.as_ref() { f(ctx); } }); } /// Schedule a single oscillator tone with an exponential gain decay. /// /// - `start_offset`: seconds from `ctx.current_time()` when the tone starts /// - `duration`: how long (in seconds) until gain reaches ~0 fn play_tone( ctx: &AudioContext, freq: f32, gain: f32, duration: f64, start_offset: f64, wave: OscillatorType, ) { let t0 = ctx.current_time() + start_offset; let t1 = t0 + duration; let Ok(osc) = ctx.create_oscillator() else { return; }; let Ok(gain_node) = ctx.create_gain() else { return; }; osc.set_type(wave); osc.frequency().set_value(freq); let gain_param = gain_node.gain(); let _ = gain_param.set_value_at_time(gain, t0); // exponential_ramp requires a positive target; 0.001 is inaudible let _ = gain_param.exponential_ramp_to_value_at_time(0.001, t1); let dest = ctx.destination(); let _ = osc.connect_with_audio_node(&gain_node); let _ = gain_node.connect_with_audio_node(&dest); let _ = osc.start_with_when(t0); let _ = osc.stop_with_when(t1); } /// Short wooden clack: sine fundamental + triangle body resonance, ~80 ms. pub fn play_checker_move() { with_ctx(|ctx| { // Sine at 300 Hz for the clean attack click play_tone(ctx, 300.0, 0.55, 0.080, 0.000, OscillatorType::Sine); // Triangle at 150 Hz for the woody body resonance play_tone(ctx, 150.0, 0.35, 0.070, 0.005, OscillatorType::Triangle); // Sub at 80 Hz for weight play_tone(ctx, 80.0, 0.20, 0.060, 0.008, OscillatorType::Triangle); }); } /// Cinematic dice roll: ~500 ms of rolling texture + 5 impact transients. /// /// Two layers: /// - A dense series of detuned sawtooth bursts that thin out over time, /// modelling the continuous scrape/rattle of dice tumbling. /// - Five percussive impacts (square clicks + triangle thuds) whose /// inter-arrival gap shrinks as the dice decelerate and settle. pub fn play_dice_roll_cinematic() { with_ctx(|ctx| { // ── Continuous rolling texture ───────────────────────────────── // 16 steps over 440 ms; each step is two detuned sawtooth waves // (the interference between them produces a noise-like texture). // Gain fades by ~55 % from first to last step. const N: u32 = 16; for i in 0..N { let t = i as f64 * 0.028; let g = 0.017 * (1.0 - i as f32 / N as f32 * 0.55); // Quasi-random frequencies so each step sounds different. let f1 = 310.0 + (i as f32 * 29.3 % 280.0); let f2 = 480.0 + (i as f32 * 43.7 % 220.0); play_tone(ctx, f1, g, 0.028, t, OscillatorType::Sawtooth); play_tone(ctx, f2, g * 0.70, 0.028, t, OscillatorType::Sawtooth); } // ── Impact transients ────────────────────────────────────────── // Gaps narrow toward the end (0.13 → 0.11 → 0.10 → 0.08 s), // mimicking dice decelerating and settling. let impacts: &[(f64, f32)] = &[(0.00, 1.00), (0.13, 0.8), (0.24, 0.54), (0.34, 0.30)]; for &(t_off, amp) in impacts { // Hard click: bright square partials → percussive attack for &freq in &[700.0f32, 1_050.0, 1_500.0] { play_tone(ctx, freq, amp * 0.03, 0.022, t_off, OscillatorType::Square); } // Woody body thud: two low triangle partials play_tone( ctx, 130.0, amp * 0.05, 0.070, t_off, OscillatorType::Triangle, ); play_tone( ctx, 68.0, amp * 0.07, 0.090, t_off, OscillatorType::Triangle, ); } }); } /// Ascending three-note chime (C5 – E5 – G5). pub fn play_points_scored() { with_ctx(|ctx| { let notes: [(f32, f64); 3] = [(523.25, 0.0), (659.25, 0.14), (783.99, 0.28)]; for (freq, offset) in notes { play_tone(ctx, freq, 0.28, 0.30, offset, OscillatorType::Sine); } }); } /// Triumphant four-note fanfare (C5 – E5 – G5 – C6). pub fn play_hole_scored() { with_ctx(|ctx| { let notes: [(f32, f64, f64); 4] = [ (523.25, 0.0, 0.35), (659.25, 0.17, 0.35), (783.99, 0.34, 0.35), (1046.5, 0.51, 0.55), ]; for (freq, offset, dur) in notes { play_tone(ctx, freq, 0.32, dur, offset, OscillatorType::Sine); } }); } } // ── Public API: WASM delegates to `inner`, other targets are no-ops ─────────── #[cfg(target_arch = "wasm32")] pub use inner::{ play_checker_move, play_dice_roll_cinematic, play_hole_scored, play_points_scored, }; #[cfg(not(target_arch = "wasm32"))] pub fn play_checker_move() {} #[cfg(not(target_arch = "wasm32"))] pub fn play_dice_roll_cinematic() {} #[cfg(not(target_arch = "wasm32"))] pub fn play_points_scored() {} #[cfg(not(target_arch = "wasm32"))] pub fn play_hole_scored() {}