use rand::rngs::ThreadRng;
use rand::Rng;
use std::fs::File;
use std::{thread, time};
use web_audio_api::context::{AudioContext, BaseAudioContext};
use web_audio_api::node::{AudioNode, AudioScheduledSourceNode};
use web_audio_api::AudioBuffer;
struct Scheduler {
period: f64,
lookahead: f64,
queue: Vec<f64>,
audio_context: AudioContext,
engine: Option<ScrubEngine>,
}
impl Scheduler {
fn new(audio_context: AudioContext) -> Self {
Self {
period: 0.05,
lookahead: 0.1,
queue: Vec::new(),
audio_context,
engine: None,
}
}
fn add(&mut self, engine: Option<ScrubEngine>, start_time: f64) {
self.engine = engine;
self.queue.push(start_time);
loop {
self.tick();
thread::sleep(time::Duration::from_millis((self.period * 1000.) as u64));
}
}
fn tick(&mut self) {
let now = self.audio_context.current_time();
self.queue.sort_by(|a, b| b.partial_cmp(a).unwrap());
let mut head = self.queue.last().cloned();
while head.is_some() && head.unwrap() < now + self.lookahead {
self.queue.pop();
let trigger_time = head.unwrap();
let next_time = self
.engine
.as_mut()
.unwrap()
.trigger_grain(&self.audio_context, trigger_time);
if let Some(time) = next_time {
self.queue.push(time);
self.queue.sort_by(|a, b| b.partial_cmp(a).unwrap());
}
head = self.queue.last().cloned();
}
}
}
struct ScrubEngine {
audio_buffer: AudioBuffer,
grain_period: f64,
grain_duration: f64,
position: f64,
incr_position: f64,
rng: ThreadRng,
}
impl ScrubEngine {
fn new(audio_buffer: AudioBuffer) -> Self {
let grain_period = 0.01;
let grain_duration = 0.2;
let speed = 1. / 2.;
Self {
audio_buffer,
grain_period,
grain_duration,
position: 0.,
incr_position: grain_period * speed, rng: rand::thread_rng(),
}
}
fn trigger_grain(&mut self, audio_context: &AudioContext, trigger_time: f64) -> Option<f64> {
let start_time = trigger_time + self.rng.gen::<f64>() * 0.003;
let env = audio_context.create_gain();
env.gain().set_value(0.);
env.connect(&audio_context.destination());
let src = audio_context.create_buffer_source();
src.set_buffer(self.audio_buffer.clone());
src.connect(&env);
env.gain()
.set_value_at_time(0., start_time)
.linear_ramp_to_value_at_time(1., start_time + self.grain_duration / 2.)
.linear_ramp_to_value_at_time(0., start_time + self.grain_duration);
src.start_at_with_offset(start_time, self.position);
src.stop_at(start_time + self.grain_duration);
if self.position + self.incr_position
> self.audio_buffer.duration() - (self.grain_duration + 0.2)
|| self.position + self.incr_position < 0.
{
self.incr_position *= -1.;
}
self.position += self.incr_position;
Some(trigger_time + self.grain_period)
}
}
fn main() {
env_logger::init();
println!("++ scrub into file forward and backward at 0.5 speed");
let audio_context = AudioContext::default();
let file = File::open("samples/sample.wav").unwrap();
let audio_buffer = audio_context.decode_audio_data_sync(file).unwrap();
let scrub_engine = ScrubEngine::new(audio_buffer);
let start_time = audio_context.current_time();
let mut scheduler = Scheduler::new(audio_context);
scheduler.add(Some(scrub_engine), start_time);
}