use js_sys::{Array, Function, Reflect, Uint8Array};
use std::cell::RefCell;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{AudioContext, AudioBuffer, AudioBufferSourceNode, GainNode, OscillatorNode, OscillatorType, PannerNode};
struct Tone {
osc: OscillatorNode,
lfo: OscillatorNode,
lfo_gain: GainNode,
amp: GainNode,
panner: PannerNode,
}
struct BgmState {
#[allow(dead_code)]
buffer: AudioBuffer,
source: Option<AudioBufferSourceNode>,
gain: GainNode,
volume: f32,
}
struct MusicSlot {
#[allow(dead_code)]
buffer: AudioBuffer,
source: Option<AudioBufferSourceNode>,
gain: GainNode,
start_ctx_time: f64,
duration: f64,
}
struct AudioState {
ctx: AudioContext,
tones: Vec<Option<Tone>>,
master: GainNode,
bgm: Option<BgmState>,
music_slots: Vec<Option<MusicSlot>>,
sample_bufs: Vec<Option<AudioBuffer>>,
current_music: usize,
}
thread_local! {
static AUDIO: RefCell<Option<AudioState>> = RefCell::new(None);
static AUDIO_DISABLED: RefCell<bool> = RefCell::new(false);
}
fn has_audio_context_ctor() -> bool {
let g = js_sys::global();
Reflect::has(&g, &JsValue::from_str("AudioContext")).unwrap_or(false)
|| Reflect::has(&g, &JsValue::from_str("webkitAudioContext")).unwrap_or(false)
}
fn js_call(obj: &JsValue, method: &str, args: &[f64]) {
if let Ok(func_val) = Reflect::get(obj, &JsValue::from_str(method)) {
if let Some(func) = func_val.dyn_ref::<Function>() {
let arr = Array::new();
for &a in args {
arr.push(&JsValue::from_f64(a));
}
let _ = func.apply(obj, &arr);
}
}
}
fn ensure_init() -> bool {
if AUDIO_DISABLED.with(|d| *d.borrow()) {
return false;
}
AUDIO.with(|a| {
if a.borrow().is_some() {
return true;
}
if !has_audio_context_ctor() {
AUDIO_DISABLED.with(|d| *d.borrow_mut() = true);
return false;
}
match AudioContext::new() {
Ok(ctx) => {
let master = match ctx.create_gain() {
Ok(g) => g,
Err(_) => return false,
};
master.gain().set_value(1.0);
master.connect_with_audio_node(&ctx.destination()).ok();
*a.borrow_mut() = Some(AudioState {
ctx,
tones: (0..16).map(|_| None).collect(),
master,
bgm: None,
music_slots: Vec::new(),
sample_bufs: Vec::new(),
current_music: 0,
});
true
},
Err(e) => {
web_sys::console::warn_1(&e);
AUDIO_DISABLED.with(|d| *d.borrow_mut() = true);
false
},
}
})
}
pub fn set_tone(
idx: usize,
x: f32,
y: f32,
z: f32,
_w: f32,
freq: f32,
amp: f32,
lfo_rate: f32,
lfo_depth: f32,
) {
if !ensure_init() {
return;
}
AUDIO.with(|a| {
let mut opt = a.borrow_mut();
if let Some(state) = opt.as_mut() {
if idx >= state.tones.len() {
return;
}
if state.tones[idx].is_none() {
let ctx = &state.ctx;
let result = (|| -> Option<Tone> {
let osc = ctx.create_oscillator().ok()?;
let lfo = ctx.create_oscillator().ok()?;
let lfo_gain = ctx.create_gain().ok()?;
let amp_node = ctx.create_gain().ok()?;
let panner = ctx.create_panner().ok()?;
osc.set_type(OscillatorType::Sine);
lfo.set_type(OscillatorType::Sine);
lfo.connect_with_audio_node(&lfo_gain).ok()?;
lfo_gain.connect_with_audio_param(&osc.frequency()).ok()?;
osc.connect_with_audio_node(&_node).ok()?;
amp_node.connect_with_audio_node(&panner).ok()?;
panner.connect_with_audio_node(&state.master).ok()?;
osc.start().ok()?;
lfo.start().ok()?;
Some(Tone { osc, lfo, lfo_gain, amp: amp_node, panner })
})();
state.tones[idx] = result;
}
if let Some(tone) = &state.tones[idx] {
tone.osc.frequency().set_value(freq);
tone.amp.gain().set_value(amp);
tone.lfo.frequency().set_value(lfo_rate);
tone.lfo_gain.gain().set_value(freq * lfo_depth);
js_call(
tone.panner.as_ref(),
"setPosition",
&[x as f64, y as f64, z as f64],
);
}
}
});
}
pub fn set_listener(cry: f32, sry: f32, crx: f32, srx: f32) {
AUDIO.with(|a| {
let opt = a.borrow();
if let Some(state) = opt.as_ref() {
let listener = state.ctx.listener();
let fx = sry;
let fy = -srx * cry;
let fz = -crx * cry;
js_call(
listener.as_ref(),
"setOrientation",
&[fx as f64, fy as f64, fz as f64, 0.0, 1.0, 0.0],
);
}
});
}
pub fn set_master_volume(vol: f32) {
AUDIO.with(|a| {
if let Some(state) = a.borrow().as_ref() {
state.master.gain().set_value(vol);
}
});
}
pub fn load_bgm(path: &str, vol: f32) {
if !ensure_init() {
return;
}
let bytes = match fetch_audio_sync(path) {
Ok(b) => b,
Err(e) => {
web_sys::console::warn_1(&format!("Failed to fetch BGM {}: {}", path, e).into());
return;
}
};
AUDIO.with(|a| {
let mut opt = a.borrow_mut();
if let Some(state) = opt.as_mut() {
let ctx = state.ctx.clone();
let gain = match ctx.create_gain() {
Ok(g) => g,
Err(_) => return,
};
gain.gain().set_value(vol);
if gain.connect_with_audio_node(&state.master).is_err() {
return;
}
match decode_audio_sync(&ctx, &bytes) {
Ok(buffer) => {
if let Some(bgm) = &state.bgm {
if let Some(src) = &bgm.source {
#[allow(deprecated)]
src.stop().ok();
}
}
let source = match ctx.create_buffer_source() {
Ok(s) => s,
Err(_) => return,
};
source.set_buffer(Some(&buffer));
source.set_loop(true);
if source.connect_with_audio_node(&gain).is_err() {
return;
}
ctx.resume().ok();
if source.start().is_err() {
return;
}
state.bgm = Some(BgmState {
buffer,
source: Some(source),
gain,
volume: vol,
});
},
Err(e) => {
web_sys::console::warn_1(&format!("Failed to decode audio: {}", e).into());
}
}
}
});
}
fn pcm_to_audio_buffer(ctx: &AudioContext, stereo: &[f32], channels: usize, rate: u32) -> Option<AudioBuffer> {
if channels == 0 || stereo.is_empty() || rate == 0 {
return None;
}
let ch = channels.min(2) as u32;
let frames = stereo.len() / channels;
let buf = ctx.create_buffer(ch, frames as u32, rate as f32).ok()?;
let left: Vec<f32> = stereo.iter().step_by(channels).copied().collect();
buf.copy_to_channel(&left, 0).ok()?;
if channels > 1 {
let right: Vec<f32> = stereo[1..].iter().step_by(channels).copied().collect();
buf.copy_to_channel(&right, 1).ok()?;
}
Some(buf)
}
pub fn play_music(slot_id: usize, stereo: &[f32], channels: usize, rate: u32, vol: f32) {
if !ensure_init() {
return;
}
AUDIO.with(|a| {
let mut opt = a.borrow_mut();
let state = match opt.as_mut() {
Some(s) => s,
None => return,
};
if let Some(Some(old)) = state.music_slots.get_mut(slot_id) {
if let Some(src) = old.source.take() {
#[allow(deprecated)]
src.stop().ok();
}
}
let duration = stereo.len() as f64 / channels.max(1) as f64 / rate.max(1) as f64;
let buf = match pcm_to_audio_buffer(&state.ctx, stereo, channels, rate) {
Some(b) => b,
None => {
web_sys::console::warn_1(&"play_music: pcm_to_audio_buffer failed".into());
return;
},
};
let gain = match state.ctx.create_gain() {
Ok(g) => g,
Err(_) => return,
};
gain.gain().set_value(vol);
if gain.connect_with_audio_node(&state.master).is_err() {
return;
}
let source = match state.ctx.create_buffer_source() {
Ok(s) => s,
Err(_) => return,
};
source.set_buffer(Some(&buf));
source.set_loop(true);
if source.connect_with_audio_node(&gain).is_err() {
return;
}
state.ctx.resume().ok();
if source.start().is_err() {
return;
}
let start = state.ctx.current_time();
while state.music_slots.len() <= slot_id {
state.music_slots.push(None);
}
state.music_slots[slot_id] = Some(MusicSlot {
buffer: buf,
source: Some(source),
gain,
start_ctx_time: start,
duration,
});
state.current_music = slot_id;
});
}
pub fn music_position(slot_id: usize) -> f64 {
AUDIO.with(|a| {
let opt = a.borrow();
if let Some(state) = opt.as_ref() {
if let Some(Some(slot)) = state.music_slots.get(slot_id) {
let elapsed = state.ctx.current_time() - slot.start_ctx_time;
if slot.duration > 0.0 {
return elapsed % slot.duration;
}
return elapsed.max(0.0);
}
}
0.0
})
}
pub fn current_music_position() -> f64 {
AUDIO.with(|a| {
let opt = a.borrow();
if let Some(state) = opt.as_ref() {
music_position_inner(state, state.current_music)
} else {
0.0
}
})
}
fn music_position_inner(state: &AudioState, slot_id: usize) -> f64 {
if let Some(Some(slot)) = state.music_slots.get(slot_id) {
let elapsed = state.ctx.current_time() - slot.start_ctx_time;
if slot.duration > 0.0 {
return elapsed % slot.duration;
}
return elapsed.max(0.0);
}
0.0
}
pub fn set_music_volume(slot_id: usize, vol: f32) {
AUDIO.with(|a| {
let opt = a.borrow();
if let Some(state) = opt.as_ref() {
if let Some(Some(slot)) = state.music_slots.get(slot_id) {
slot.gain.gain().set_value(vol);
}
}
});
}
pub fn stop_music(slot_id: usize) {
AUDIO.with(|a| {
let mut opt = a.borrow_mut();
if let Some(state) = opt.as_mut() {
if let Some(Some(slot)) = state.music_slots.get_mut(slot_id) {
if let Some(src) = slot.source.take() {
#[allow(deprecated)]
src.stop().ok();
}
}
}
});
}
pub fn add_sample(stereo: &[f32], channels: usize, rate: u32) -> i32 {
if !ensure_init() {
return -1;
}
AUDIO.with(|a| {
let mut opt = a.borrow_mut();
if let Some(state) = opt.as_mut() {
if let Some(buf) = pcm_to_audio_buffer(&state.ctx, stereo, channels, rate) {
let id = state.sample_bufs.len();
state.sample_bufs.push(Some(buf));
return id as i32;
}
}
-1
})
}
pub fn play_sample(id: usize, x: f32, y: f32, z: f32, vol: f32, looping: bool) {
if !ensure_init() {
return;
}
AUDIO.with(|a| {
let opt = a.borrow();
if let Some(state) = opt.as_ref() {
let buf = match state.sample_bufs.get(id).and_then(|b| b.as_ref()) {
Some(b) => b,
None => return,
};
let gain = match state.ctx.create_gain() {
Ok(g) => g,
Err(_) => return,
};
gain.gain().set_value(vol);
let panner = match state.ctx.create_panner() {
Ok(p) => p,
Err(_) => return,
};
js_call(panner.as_ref(), "setPosition", &[x as f64, y as f64, z as f64]);
if gain.connect_with_audio_node(&panner).is_err() {
return;
}
if panner.connect_with_audio_node(&state.master).is_err() {
return;
}
let source = match state.ctx.create_buffer_source() {
Ok(s) => s,
Err(_) => return,
};
source.set_buffer(Some(buf));
source.set_loop(looping);
if source.connect_with_audio_node(&gain).is_err() {
return;
}
state.ctx.resume().ok();
source.start().ok();
}
});
}
pub fn resume() {
AUDIO.with(|a| {
if let Some(state) = a.borrow().as_ref() {
state.ctx.resume().ok();
}
});
}
pub fn set_bgm_volume(vol: f32) {
AUDIO.with(|a| {
if let Some(state) = a.borrow_mut().as_mut() {
if let Some(bgm) = &mut state.bgm {
bgm.volume = vol;
bgm.gain.gain().set_value(vol);
}
}
});
}
fn fetch_audio_sync(path: &str) -> Result<Vec<u8>, String> {
let script = format!(
r#"(function() {{
var xhr = new XMLHttpRequest();
xhr.open('GET', '{}', false);
xhr.responseType = 'arraybuffer';
xhr.send(null);
if (xhr.status !== 200 && xhr.status !== 0) {{
throw new Error('HTTP ' + xhr.status);
}}
return new Uint8Array(xhr.response || new ArrayBuffer(0));
}})()"#,
path.replace('\\', "\\\\").replace('\'', "\\'")
);
let result = js_sys::eval(&script)
.map_err(|e| format!("XHR failed: {:?}", e))?;
let arr = Uint8Array::new(&result);
let mut bytes = vec![0u8; arr.length() as usize];
arr.copy_to(&mut bytes);
Ok(bytes)
}
fn decode_audio_sync(ctx: &AudioContext, data: &[u8]) -> Result<AudioBuffer, String> {
let array = Uint8Array::new_with_length(data.len() as u32);
array.copy_from(data);
let promise = ctx
.decode_audio_data(&array.buffer())
.map_err(|e| format!("decode_audio_data failed: {:?}", e))?;
let script = format!(
r#"(function(promise) {{
var result = null;
var error = null;
var done = false;
promise.then(function(r) {{ result = r; done = true; }})
.catch(function(e) {{ error = e; done = true; }});
var start = Date.now();
while (!done && Date.now() - start < 10000) {{
// Spin-wait up to 10 seconds
}}
if (error) throw error;
if (!done) throw new Error('Timeout decoding audio');
return result;
}})(arguments[0])"#
);
let func = js_sys::Function::new_with_args("promise", &script);
let args = Array::new();
args.push(&promise);
let result = func
.apply(&JsValue::NULL, &args)
.map_err(|e| format!("Audio decode failed: {:?}", e))?;
result
.dyn_into::<AudioBuffer>()
.map_err(|_| "Result is not an AudioBuffer".to_string())
}