use std::collections::HashMap;
use anyhow::Result;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
AudioBuffer, AudioNode, AudioScheduledSourceNode, BaseAudioContext, OfflineAudioContext,
};
use awsm_audio_schema::{AssetId, Graph};
use crate::{
apply_control, build, spawn_voices, AudioClipPart, ControlLanePart, TriggerPart, Voice,
};
pub struct BounceJob {
pub graph: Graph,
pub parts: Vec<TriggerPart>,
pub control: Vec<ControlLanePart>,
pub duration_secs: f64,
pub loop_secs: Option<f64>,
pub sample_rate: f32,
pub buffers: HashMap<AssetId, AudioBuffer>,
pub modules: HashMap<AssetId, js_sys::WebAssembly::Module>,
pub shim_source: String,
}
pub async fn render(job: BounceJob) -> Result<(Vec<Vec<f32>>, u32)> {
let sr = job.sample_rate.max(8000.0);
let frames = (job.duration_secs.max(0.05) * sr as f64).ceil() as u32;
let octx =
OfflineAudioContext::new_with_number_of_channels_and_length_and_sample_rate(2, frames, sr)
.map_err(|e| anyhow::anyhow!("offline ctx: {e:?}"))?;
let base: &BaseAudioContext = octx.as_ref();
let worklet_ready = add_shim(base, &job.shim_source).await.unwrap_or(false);
let master = base
.create_gain()
.map_err(|e| anyhow::anyhow!("offline master: {e:?}"))?;
master
.connect_with_audio_node(&base.destination())
.map_err(|e| anyhow::anyhow!("offline master→dest: {e:?}"))?;
let built = build::build_graph(
base,
&job.graph,
&master,
&job.buffers,
&job.modules,
None,
worklet_ready,
false,
0.0,
)?;
for s in &built.sources {
let _ = s.start();
}
let mut voices: Vec<Voice> = Vec::new();
spawn_voices(
base,
&built.nodes,
&job.buffers,
&job.modules,
worklet_ready,
None,
&job.parts,
0.0,
&mut voices,
usize::MAX,
)?;
apply_control(&built.params, &job.control, 0.0);
let promise = octx
.start_rendering()
.map_err(|e| anyhow::anyhow!("startRendering: {e:?}"))?;
let rendered = JsFuture::from(promise)
.await
.map_err(|e| anyhow::anyhow!("offline render: {e:?}"))?;
let buffer: AudioBuffer = rendered
.dyn_into()
.map_err(|_| anyhow::anyhow!("render result is not an AudioBuffer"))?;
let _ = (&built.inner, &voices);
let nch = buffer.number_of_channels() as usize;
let mut channels: Vec<Vec<f32>> = Vec::with_capacity(nch);
for ch in 0..nch {
channels.push(
buffer
.get_channel_data(ch as u32)
.map_err(|e| anyhow::anyhow!("channel {ch}: {e:?}"))?,
);
}
match job.loop_secs {
Some(loop_secs) if loop_secs > 0.0 => {
fold_loop_tail(&mut channels, (loop_secs * sr as f64).round() as usize);
}
_ => trim_trailing_silence(&mut channels, sr as u32),
}
Ok((channels, sr as u32))
}
pub async fn render_clips(
clips: Vec<AudioClipPart>,
buffers: std::collections::HashMap<AssetId, AudioBuffer>,
sample_rate: f32,
duration_secs: f64,
) -> Result<(Vec<Vec<f32>>, u32)> {
let sr = sample_rate.max(8000.0);
let frames = (duration_secs.max(0.05) * sr as f64).ceil() as u32;
let octx =
OfflineAudioContext::new_with_number_of_channels_and_length_and_sample_rate(2, frames, sr)
.map_err(|e| anyhow::anyhow!("offline ctx: {e:?}"))?;
let base: &BaseAudioContext = octx.as_ref();
let master = base
.create_gain()
.map_err(|e| anyhow::anyhow!("offline master: {e:?}"))?;
master
.connect_with_audio_node(&base.destination())
.map_err(|e| anyhow::anyhow!("offline master→dest: {e:?}"))?;
let mut keep: Vec<AudioNode> = Vec::new();
for c in &clips {
let Some(buf) = buffers.get(&c.buffer) else {
continue;
};
let dur = c.length.max(0.0);
if dur <= 0.0 {
continue;
}
let off = c.offset.max(0.0);
let speed = if c.speed > 0.0 { c.speed } else { 1.0 };
let when = c.start.max(0.0);
let buf_dur = buf.duration();
let span = dur * speed; let stretched = c.looping && span > (buf_dur - off) + 1e-3;
let src = base
.create_buffer_source()
.map_err(|e| anyhow::anyhow!("buffer source: {e:?}"))?;
src.set_buffer(Some(buf));
let g = base
.create_gain()
.map_err(|e| anyhow::anyhow!("clip gain: {e:?}"))?;
apply_clip_gain_curve(&g.gain(), c.gain, &c.gain_curve, when);
if (speed - 1.0).abs() > 1e-6 {
src.playback_rate().set_value(speed as f32);
}
src.connect_with_audio_node(&g)
.map_err(|e| anyhow::anyhow!("clip src→gain: {e:?}"))?;
g.connect_with_audio_node(&master)
.map_err(|e| anyhow::anyhow!("clip gain→master: {e:?}"))?;
let sched: AudioScheduledSourceNode = src.clone().unchecked_into();
if stretched {
src.set_loop(true);
src.set_loop_start(off);
src.set_loop_end(buf_dur);
let _ = src.start_with_when_and_grain_offset(when, off);
let _ = sched.stop_with_when(when + dur);
} else {
let _ = src.start_with_when_and_grain_offset_and_grain_duration(when, off, span);
}
keep.push(src.unchecked_into());
keep.push(g.unchecked_into());
}
let promise = octx
.start_rendering()
.map_err(|e| anyhow::anyhow!("startRendering: {e:?}"))?;
let rendered = JsFuture::from(promise)
.await
.map_err(|e| anyhow::anyhow!("offline render: {e:?}"))?;
let buffer: AudioBuffer = rendered
.dyn_into()
.map_err(|_| anyhow::anyhow!("render result is not an AudioBuffer"))?;
let _ = &keep;
let nch = buffer.number_of_channels() as usize;
let mut channels: Vec<Vec<f32>> = Vec::with_capacity(nch);
for ch in 0..nch {
channels.push(
buffer
.get_channel_data(ch as u32)
.map_err(|e| anyhow::anyhow!("channel {ch}: {e:?}"))?,
);
}
Ok((channels, sr as u32))
}
fn apply_clip_gain_curve(
param: &web_sys::AudioParam,
fallback: f32,
points: &[(f64, f32)],
at: f64,
) {
if points.is_empty() {
param.set_value(fallback);
return;
}
let mut pts = points.to_vec();
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (t0, v0) = pts[0];
let _ = param.set_value_at_time(v0, at + t0.max(0.0));
for (secs, gain) in pts.into_iter().skip(1) {
let _ = param.linear_ramp_to_value_at_time(gain, at + secs.max(0.0));
}
}
fn fold_loop_tail(channels: &mut [Vec<f32>], period: usize) {
if period == 0 {
return;
}
for ch in channels.iter_mut() {
if ch.len() > period {
for i in period..ch.len() {
ch[(i - period) % period] += ch[i];
}
}
ch.truncate(period);
}
}
async fn add_shim(base: &BaseAudioContext, source: &str) -> Result<bool> {
let parts = js_sys::Array::new();
parts.push(&JsValue::from_str(source));
let bag = web_sys::BlobPropertyBag::new();
bag.set_type("text/javascript");
let blob = web_sys::Blob::new_with_str_sequence_and_options(&parts, &bag)
.map_err(|e| anyhow::anyhow!("blob: {e:?}"))?;
let url = web_sys::Url::create_object_url_with_blob(&blob)
.map_err(|e| anyhow::anyhow!("blob url: {e:?}"))?;
let wl = base
.audio_worklet()
.map_err(|e| anyhow::anyhow!("audioWorklet: {e:?}"))?;
let p = wl
.add_module(&url)
.map_err(|e| anyhow::anyhow!("addModule: {e:?}"))?;
JsFuture::from(p)
.await
.map_err(|e| anyhow::anyhow!("addModule await: {e:?}"))?;
Ok(true)
}
fn trim_trailing_silence(channels: &mut [Vec<f32>], sample_rate: u32) {
const THRESH: f32 = 0.001; let len = channels.iter().map(|c| c.len()).max().unwrap_or(0);
let mut last = 0usize;
for i in 0..len {
if channels
.iter()
.any(|c| c.get(i).is_some_and(|s| s.abs() > THRESH))
{
last = i;
}
}
let pad = (sample_rate as usize) * 30 / 1000;
let keep = (last + pad + 1).min(len);
for c in channels.iter_mut() {
c.truncate(keep);
}
}