pub mod bounce;
mod build;
pub mod document;
mod noise;
pub mod worklet;
use std::collections::HashMap;
use anyhow::Result;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::{
AnalyserNode, AudioBuffer, AudioBufferSourceNode, AudioContext, AudioNode,
AudioScheduledSourceNode, GainNode,
};
use awsm_audio_schema::{AssetId, Graph, Listener, NodeId};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub struct Player {
ctx: AudioContext,
master: GainNode,
analyser: AnalyserNode,
inner: Vec<AudioNode>,
sources: Vec<AudioScheduledSourceNode>,
params: Vec<(NodeId, Vec<(&'static str, web_sys::AudioParam)>)>,
song_voices: Vec<Voice>,
bus_nodes: Vec<(NodeId, AudioNode)>,
buffers: HashMap<AssetId, AudioBuffer>,
modules: HashMap<AssetId, js_sys::WebAssembly::Module>,
worklet_ready: bool,
mic: Option<web_sys::MediaStream>,
listener: Option<Listener>,
}
const MAX_SONG_VOICES: usize = 4096;
pub struct TriggerPart {
pub target: NodeId,
pub instrument: Graph,
pub notes: Vec<SongVoiceSpec>,
}
pub struct ControlLanePart {
pub target: NodeId,
pub param: String,
pub points: Vec<(f64, f32, awsm_audio_schema::Curve)>,
}
pub struct AudioClipPart {
pub buffer: AssetId,
pub start: f64,
pub offset: f64,
pub length: f64,
pub gain: f32,
pub gain_curve: Vec<(f64, f32)>,
pub looping: bool,
pub speed: f64,
}
pub struct SongVoiceSpec {
pub start: f64,
pub end: f64,
pub semitones: i32,
pub velocity: f32,
}
struct Voice {
gain: GainNode,
nodes: Vec<AudioNode>,
sources: Vec<AudioScheduledSourceNode>,
stop_at: f64,
}
impl Voice {
fn teardown(self) {
for s in &self.sources {
let _ = s.stop();
}
for n in &self.nodes {
let _ = n.disconnect();
}
let _ = self.gain.disconnect();
}
}
#[allow(clippy::too_many_arguments)]
fn spawn_voices(
ctx: &web_sys::BaseAudioContext,
bus_nodes: &[(NodeId, AudioNode)],
buffers: &HashMap<AssetId, AudioBuffer>,
modules: &HashMap<AssetId, js_sys::WebAssembly::Module>,
worklet_ready: bool,
mic: Option<&web_sys::MediaStream>,
parts: &[TriggerPart],
t0: f64,
out: &mut Vec<Voice>,
room: usize,
) -> Result<f64> {
const ATTACK: f64 = 0.004;
const RELEASE: f64 = 0.08;
let mut end_time = t0;
'outer: for part in parts {
let Some(target) = bus_nodes
.iter()
.find(|(id, _)| *id == part.target)
.map(|(_, n)| n.clone())
else {
continue;
};
for note in &part.notes {
if out.len() >= room {
break 'outer;
}
let on = t0 + note.start;
let off = t0 + note.end.max(note.start);
let gain = ctx
.create_gain()
.map_err(|e| anyhow::anyhow!("song gain: {e:?}"))?;
let g = gain.gain();
let _ = g.set_value_at_time(0.0, on);
let _ = g.set_target_at_time(note.velocity.clamp(0.0, 1.0), on, ATTACK);
let _ = g.set_target_at_time(0.0, off, RELEASE / 3.0);
gain.connect_with_audio_node(&target)
.map_err(|e| anyhow::anyhow!("song voice→bus: {e:?}"))?;
let graph = part.instrument.transposed(note.semitones);
let built = build::build_graph(
ctx,
&graph,
&gain,
buffers,
modules,
mic,
worklet_ready,
false,
on,
)?;
let stop_at = off + RELEASE * 3.0;
end_time = end_time.max(stop_at);
for s in &built.sources {
let _ = s.start_with_when(on);
let _ = s.stop_with_when(stop_at);
}
out.push(Voice {
gain,
nodes: built.inner,
sources: built.sources,
stop_at,
});
}
}
Ok(end_time)
}
fn apply_control(
params: &[(NodeId, Vec<(&'static str, web_sys::AudioParam)>)],
parts: &[ControlLanePart],
at: f64,
) {
use awsm_audio_schema::Curve;
const EPS: f32 = 1e-4;
for part in parts {
let Some(param) = params
.iter()
.find(|(id, _)| *id == part.target)
.and_then(|(_, ps)| {
ps.iter()
.find(|(n, _)| *n == part.param)
.map(|(_, p)| p.clone())
})
else {
continue;
};
let mut pts = part.points.clone();
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut prev: Option<(f64, f32)> = None;
for (i, (secs, value, curve)) in pts.iter().enumerate() {
let t = at + secs.max(0.0);
let v = *value;
if i == 0 {
let _ = param.set_value_at_time(v, t);
prev = Some((t, v));
continue;
}
match curve {
Curve::Step => {
let _ = param.set_value_at_time(v, t);
}
Curve::Linear => {
let _ = param.linear_ramp_to_value_at_time(v, t);
}
Curve::Exponential => {
if let Some((pt, pv)) = prev {
if pv.abs() < EPS {
let _ = param.set_value_at_time(EPS, pt);
}
}
let target = if v.abs() < EPS { EPS } else { v };
let _ = param.exponential_ramp_to_value_at_time(target, t);
}
Curve::Smooth => {
if let Some((pt, pv)) = prev {
const N: usize = 24;
let mut curve_vals = vec![0.0f32; N];
for (k, slot) in curve_vals.iter_mut().enumerate() {
let x = k as f32 / (N - 1) as f32;
let s = x * x * (3.0 - 2.0 * x);
*slot = pv + (v - pv) * s;
}
let dur = (t - pt).max(0.001);
let _ = param.set_value_curve_at_time(&mut curve_vals, pt, dur);
} else {
let _ = param.linear_ramp_to_value_at_time(v, t);
}
}
}
prev = Some((t, v));
}
}
}
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));
}
}
impl Player {
pub fn new() -> Result<Self> {
let ctx = AudioContext::new().map_err(|e| anyhow::anyhow!("AudioContext: {e:?}"))?;
let master = ctx
.create_gain()
.map_err(|e| anyhow::anyhow!("master gain: {e:?}"))?;
let analyser = ctx
.create_analyser()
.map_err(|e| anyhow::anyhow!("analyser: {e:?}"))?;
analyser.set_fft_size(2048);
master
.connect_with_audio_node(&analyser)
.map_err(|e| anyhow::anyhow!("master→analyser: {e:?}"))?;
analyser
.connect_with_audio_node(&ctx.destination())
.map_err(|e| anyhow::anyhow!("analyser→destination: {e:?}"))?;
Ok(Self {
ctx,
master,
analyser,
inner: Vec::new(),
sources: Vec::new(),
params: Vec::new(),
song_voices: Vec::new(),
bus_nodes: Vec::new(),
buffers: HashMap::new(),
modules: HashMap::new(),
worklet_ready: false,
mic: None,
listener: None,
})
}
pub fn add_worklet_shim(&self) -> Result<js_sys::Promise> {
let parts = js_sys::Array::new();
parts.push(&JsValue::from_str(&worklet::shim_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 = self
.ctx
.audio_worklet()
.map_err(|e| anyhow::anyhow!("audioWorklet: {e:?}"))?;
wl.add_module(&url)
.map_err(|e| anyhow::anyhow!("addModule: {e:?}"))
}
pub fn mark_worklet_ready(&mut self) {
self.worklet_ready = true;
}
pub fn worklet_ready(&self) -> bool {
self.worklet_ready
}
pub fn compile_module(bytes: &js_sys::Uint8Array) -> js_sys::Promise {
js_sys::WebAssembly::compile(bytes.as_ref())
}
pub fn store_module(&mut self, id: AssetId, module: js_sys::WebAssembly::Module) {
self.modules.insert(id, module);
}
pub fn has_module(&self, id: &AssetId) -> bool {
self.modules.contains_key(id)
}
pub fn decode(&self, data: &js_sys::ArrayBuffer) -> Result<js_sys::Promise> {
self.ctx
.decode_audio_data(data)
.map_err(|e| anyhow::anyhow!("decodeAudioData: {e:?}"))
}
pub fn store_buffer(&mut self, id: AssetId, buffer: AudioBuffer) {
self.buffers.insert(id, buffer);
}
pub fn store_pcm(
&mut self,
id: AssetId,
sample_rate: f32,
channels: &[Vec<f32>],
) -> Result<()> {
let ch = channels.len().max(1) as u32;
let len = channels.iter().map(Vec::len).max().unwrap_or(1).max(1) as u32;
let buffer = self
.ctx
.create_buffer(ch, len, sample_rate)
.map_err(|e| anyhow::anyhow!("create_buffer: {e:?}"))?;
for (i, data) in channels.iter().enumerate() {
buffer
.copy_to_channel(data, i as i32)
.map_err(|e| anyhow::anyhow!("copy_to_channel: {e:?}"))?;
}
self.buffers.insert(id, buffer);
Ok(())
}
pub fn request_mic(&self) -> Result<js_sys::Promise> {
let nav = web_sys::window()
.ok_or_else(|| anyhow::anyhow!("no window"))?
.navigator();
let devices = nav
.media_devices()
.map_err(|e| anyhow::anyhow!("mediaDevices: {e:?}"))?;
let constraints = web_sys::MediaStreamConstraints::new();
constraints.set_audio(&JsValue::TRUE);
devices
.get_user_media_with_constraints(&constraints)
.map_err(|e| anyhow::anyhow!("getUserMedia: {e:?}"))
}
pub fn set_mic(&mut self, stream: web_sys::MediaStream) {
self.mic = Some(stream);
}
pub fn set_listener(&mut self, listener: Option<Listener>) {
self.listener = listener;
}
pub fn set_master_gain(&self, gain: f32) {
self.master.gain().set_value(gain);
}
pub fn play(&mut self, graph: &Graph, looping: bool) -> Result<()> {
self.stop();
let t0 = self.ctx.current_time();
let built = build::build_graph(
&self.ctx,
graph,
&self.master,
&self.buffers,
&self.modules,
self.mic.as_ref(),
self.worklet_ready,
looping,
t0,
)?;
self.inner = built.inner;
self.sources = built.sources;
self.params = built.params;
self.bus_nodes = built.nodes;
if let Some(l) = &self.listener {
build::apply_listener(&self.ctx, l, t0);
}
for s in &self.sources {
let _ = s.start();
}
let _ = self.ctx.resume();
Ok(())
}
pub fn resume(&self) {
let _ = self.ctx.resume();
}
pub fn scope(&self, id: NodeId) -> Vec<u8> {
let Some((_, node)) = self.bus_nodes.iter().find(|(n, _)| *n == id) else {
return Vec::new();
};
if let Some(an) = node.dyn_ref::<AnalyserNode>() {
let mut buf = vec![0u8; an.fft_size() as usize];
an.get_byte_time_domain_data(&mut buf);
buf
} else {
Vec::new()
}
}
pub fn stop(&mut self) {
for s in self.sources.drain(..) {
let _ = s.stop();
}
for n in self.inner.drain(..) {
let _ = n.disconnect();
}
self.params.clear();
self.bus_nodes.clear();
for v in self.song_voices.drain(..) {
v.teardown();
}
}
pub fn current_time(&self) -> f64 {
self.ctx.current_time()
}
pub fn sample_rate(&self) -> u32 {
self.ctx.sample_rate() as u32
}
pub fn clip_buffers(&self) -> std::collections::HashMap<AssetId, AudioBuffer> {
self.buffers.clone()
}
pub fn has_buffer(&self, id: AssetId) -> bool {
self.buffers.contains_key(&id)
}
pub fn bounce_job(
&self,
graph: Graph,
parts: Vec<TriggerPart>,
control: Vec<ControlLanePart>,
duration_secs: f64,
loop_secs: Option<f64>,
) -> bounce::BounceJob {
bounce::BounceJob {
graph,
parts,
control,
duration_secs,
loop_secs,
sample_rate: self.ctx.sample_rate(),
buffers: self.buffers.clone(),
modules: self.modules.clone(),
shim_source: worklet::shim_source(),
}
}
pub fn arrange_audio_begin(&mut self) {
self.stop();
let _ = self.ctx.resume();
}
pub fn schedule_audio_clips(&mut self, clips: &[AudioClipPart], at: f64) -> Result<f64> {
let now = self.ctx.current_time();
let mut i = 0;
while i < self.song_voices.len() {
if self.song_voices[i].stop_at <= now {
self.song_voices.swap_remove(i).teardown();
} else {
i += 1;
}
}
let mut end = at;
for c in clips {
let Some(buf) = self.buffers.get(&c.buffer).cloned() else {
continue;
};
let when = at + c.start.max(0.0);
let dur = c.length.max(0.0);
let off = c.offset.max(0.0);
let speed = if c.speed > 0.0 { c.speed } else { 1.0 };
if dur <= 0.0 {
continue;
}
let buf_dur = buf.duration();
let span = dur * speed;
let stretched = c.looping && span > (buf_dur - off) + 1e-3;
let (src, g) = self.new_clip_source(&buf)?;
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);
}
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);
}
let stop_at = when + dur + 0.05;
end = end.max(stop_at);
self.song_voices.push(Voice {
gain: g,
nodes: Vec::new(),
sources: vec![sched],
stop_at,
});
}
let _ = self.ctx.resume();
Ok(end)
}
fn new_clip_source(&self, buf: &AudioBuffer) -> Result<(AudioBufferSourceNode, GainNode)> {
let src = self
.ctx
.create_buffer_source()
.map_err(|e| anyhow::anyhow!("buffer source: {e:?}"))?;
src.set_buffer(Some(buf));
let g = self
.ctx
.create_gain()
.map_err(|e| anyhow::anyhow!("clip gain: {e:?}"))?;
src.connect_with_audio_node(&g)
.map_err(|e| anyhow::anyhow!("clip src→gain: {e:?}"))?;
g.connect_with_audio_node(&self.master)
.map_err(|e| anyhow::anyhow!("clip gain→master: {e:?}"))?;
Ok((src, g))
}
pub fn play_arrangement(&mut self, arrangement: &Graph, looping: bool) -> Result<()> {
self.stop();
let t0 = self.ctx.current_time();
let built = build::build_graph(
&self.ctx,
arrangement,
&self.master,
&self.buffers,
&self.modules,
self.mic.as_ref(),
self.worklet_ready,
looping,
t0,
)?;
self.bus_nodes = built.nodes;
self.inner = built.inner;
self.sources = built.sources;
self.params = built.params;
if let Some(l) = &self.listener {
build::apply_listener(&self.ctx, l, t0);
}
for s in &self.sources {
let _ = s.start();
}
let _ = self.ctx.resume();
Ok(())
}
pub fn schedule_triggers(&mut self, parts: &[TriggerPart], at: f64) -> Result<(usize, f64)> {
let now = self.ctx.current_time();
let mut i = 0;
while i < self.song_voices.len() {
if self.song_voices[i].stop_at <= now {
self.song_voices.swap_remove(i).teardown();
} else {
i += 1;
}
}
let before = self.song_voices.len();
let end_time = spawn_voices(
self.ctx.as_ref(),
&self.bus_nodes,
&self.buffers,
&self.modules,
self.worklet_ready,
self.mic.as_ref(),
parts,
at,
&mut self.song_voices,
MAX_SONG_VOICES,
)?;
let count = self.song_voices.len() - before;
let _ = self.ctx.resume();
Ok((count, end_time))
}
pub fn schedule_control(&self, parts: &[ControlLanePart], at: f64) {
apply_control(&self.params, parts, at);
}
pub fn set_param_live(&self, node: NodeId, param: &str, value: f32, glide: f64) {
let now = self.ctx.current_time();
let apply = |params: &[(NodeId, Vec<(&'static str, web_sys::AudioParam)>)]| {
if let Some(p) = params
.iter()
.find(|(id, _)| *id == node)
.and_then(|(_, ps)| ps.iter().find(|(name, _)| *name == param).map(|(_, p)| p))
{
if glide <= 0.0 {
let _ = p.set_value_at_time(value, now);
} else {
let _ = p.set_target_at_time(value, now, glide / 3.0);
}
}
};
apply(&self.params);
}
pub fn live_params(&self) -> Vec<(NodeId, Vec<&'static str>)> {
self.params
.iter()
.map(|(id, ps)| (*id, ps.iter().map(|(name, _)| *name).collect()))
.collect()
}
pub fn param_value(&self, node: NodeId, param: &str) -> Option<f32> {
self.params
.iter()
.find(|(id, _)| *id == node)
.and_then(|(_, ps)| {
ps.iter()
.find(|(name, _)| *name == param)
.map(|(_, p)| p.value())
})
}
pub fn voice_count(&self) -> usize {
self.song_voices.len()
}
pub fn waveform_len(&self) -> usize {
self.analyser.fft_size() as usize
}
pub fn peak(&self) -> f32 {
let mut buf = vec![128u8; self.analyser.fft_size() as usize];
self.analyser.get_byte_time_domain_data(&mut buf);
buf.iter()
.map(|&b| (f32::from(b) - 128.0).abs() / 128.0)
.fold(0.0, f32::max)
}
pub fn context_state(&self) -> String {
format!("{:?}", self.ctx.state())
}
pub fn read_waveform(&self, buf: &mut [u8]) {
self.analyser.get_byte_time_domain_data(buf);
}
}