use std::cell::RefCell;
use js_sys::{Function, Reflect};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{AudioContext, GainNode, OscillatorType};
const MAX_VOICES: usize = 64;
thread_local! {
static ENGINE: RefCell<Option<Engine>> = const { RefCell::new(None) };
}
struct Engine {
ctx: AudioContext,
master: GainNode,
voices: Vec<Option<Voice>>,
}
struct Voice {
node: JsValue,
_onended: Closure<dyn FnMut()>,
}
fn with_engine<R>(f: impl FnOnce(&mut Engine) -> R) -> Option<R> {
ENGINE.with(|cell| {
let mut slot = cell.borrow_mut();
if slot.is_none() {
let ctx = AudioContext::new().ok()?;
let master = ctx.create_gain().ok()?;
master.gain().set_value(0.3);
let _ = master.connect_with_audio_node(&ctx.destination());
*slot = Some(Engine { ctx, master, voices: Vec::new() });
}
let eng = slot.as_mut()?;
let _ = eng.ctx.resume();
Some(f(eng))
})
}
fn push_voice(eng: &mut Engine, voice: Voice) -> i32 {
let live = eng.voices.iter().filter(|v| v.is_some()).count();
if live >= MAX_VOICES {
if let Some(slot) = eng.voices.iter_mut().find(|s| s.is_some()) {
if let Some(old) = slot.take() {
stop_node(&old.node);
}
}
}
if let Some(i) = eng.voices.iter().position(|s| s.is_none()) {
eng.voices[i] = Some(voice);
i as i32
} else {
eng.voices.push(Some(voice));
(eng.voices.len() - 1) as i32
}
}
fn stop_node(node: &JsValue) {
if let Ok(f) = Reflect::get(node, &JsValue::from_str("stop")) {
if let Ok(f) = f.dyn_into::<Function>() {
let _ = f.call0(node);
}
}
}
fn osc_type(wave: i32) -> OscillatorType {
match wave {
1 => OscillatorType::Square,
2 => OscillatorType::Sawtooth,
3 => OscillatorType::Triangle,
_ => OscillatorType::Sine,
}
}
pub(crate) fn play_tone(freq: i32, dur_ms: i32, wave: i32, delay_ms: i32) -> i32 {
with_engine(|eng| {
let osc = match eng.ctx.create_oscillator() {
Ok(o) => o,
Err(_) => return -1,
};
let gain = match eng.ctx.create_gain() {
Ok(g) => g,
Err(_) => return -1,
};
osc.set_type(osc_type(wave));
osc.frequency().set_value(freq.max(1) as f32);
let t0 = eng.ctx.current_time() + (delay_ms.max(0) as f64) / 1000.0;
let dur = (dur_ms.max(1) as f64) / 1000.0;
let g = gain.gain();
let _ = g.set_value_at_time(0.0, t0);
let _ = g.linear_ramp_to_value_at_time(1.0, t0 + 0.004);
let _ = g.set_value_at_time(1.0, (t0 + dur - 0.004).max(t0 + 0.004));
let _ = g.linear_ramp_to_value_at_time(0.0, t0 + dur);
let _ = osc.connect_with_audio_node(&gain);
let _ = gain.connect_with_audio_node(&eng.master);
let _ = osc.start_with_when(t0);
let _ = osc.stop_with_when(t0 + dur);
let node: JsValue = osc.clone().into();
let onended = Closure::<dyn FnMut()>::new(move || {});
osc.set_onended(Some(onended.as_ref().unchecked_ref()));
push_voice(eng, Voice { node, _onended: onended })
})
.unwrap_or(-1)
}
pub(crate) fn play_noise(dur_ms: i32) -> i32 {
with_engine(|eng| {
let sr = eng.ctx.sample_rate();
let frames = sr as u32; let buf = match eng.ctx.create_buffer(1, frames, sr) {
Ok(b) => b,
Err(_) => return -1,
};
let mut data = vec![0f32; frames as usize];
let mut s: u32 = 0x2545_F491;
for x in data.iter_mut() {
s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
*x = ((s >> 8) as f32 / 8_388_608.0) - 1.0;
}
if buf.copy_to_channel(&data, 0).is_err() {
return -1;
}
let src = match eng.ctx.create_buffer_source() {
Ok(s) => s,
Err(_) => return -1,
};
src.set_buffer(Some(&buf));
let gain = match eng.ctx.create_gain() {
Ok(g) => g,
Err(_) => return -1,
};
let t0 = eng.ctx.current_time();
let dur = (dur_ms.max(1) as f64) / 1000.0;
let g = gain.gain();
let _ = g.set_value_at_time(0.8, t0);
let _ = g.linear_ramp_to_value_at_time(0.0, t0 + dur);
let _ = src.connect_with_audio_node(&gain);
let _ = gain.connect_with_audio_node(&eng.master);
let _ = src.start_with_when(t0);
let scheduled: &web_sys::AudioScheduledSourceNode = src.as_ref();
let _ = scheduled.stop_with_when(t0 + dur);
let node: JsValue = src.clone().into();
let onended = Closure::<dyn FnMut()>::new(move || {});
scheduled.set_onended(Some(onended.as_ref().unchecked_ref()));
push_voice(eng, Voice { node, _onended: onended })
})
.unwrap_or(-1)
}
pub(crate) fn stop_handle(handle: i32) {
ENGINE.with(|cell| {
if let Some(eng) = cell.borrow_mut().as_mut() {
if handle < 0 {
for slot in eng.voices.iter_mut() {
if let Some(v) = slot.take() {
stop_node(&v.node);
}
}
} else if let Some(slot) = eng.voices.get_mut(handle as usize) {
if let Some(v) = slot.take() {
stop_node(&v.node);
}
}
}
});
}
pub(crate) fn set_master_volume(pct: i32) {
with_engine(|eng| {
eng.master.gain().set_value((pct.clamp(0, 100) as f32) / 100.0);
});
}
pub(crate) fn stop_all() {
ENGINE.with(|cell| {
if let Some(eng) = cell.borrow_mut().as_mut() {
for slot in eng.voices.iter_mut() {
if let Some(v) = slot.take() {
stop_node(&v.node);
}
}
let _ = eng.ctx.suspend();
}
});
}