mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 05:05:26 -04:00
Adding structs and traits
This commit is contained in:
@@ -6,8 +6,8 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
const ac = getAudioContext();
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error(
|
||||
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
|
||||
'error',
|
||||
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
|
||||
let {
|
||||
note = 'C2',
|
||||
s,
|
||||
s = 'triangle',
|
||||
bank,
|
||||
source,
|
||||
gain = 0.8,
|
||||
@@ -50,24 +50,24 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
loopBegin = 0,
|
||||
loopEnd = 1,
|
||||
attack = 0.001,
|
||||
decay = 0.05,
|
||||
decay = 0.005,
|
||||
sustain = 1,
|
||||
release = 0.1,
|
||||
release = 0.001,
|
||||
lpattack = 0.0001,
|
||||
lpdecay = 0.2,
|
||||
lpsustain = 0.6,
|
||||
lprelease = 0.2,
|
||||
lpenv = 1,
|
||||
lpenv = 0,
|
||||
hpattack = 0.0001,
|
||||
hpdecay = 0.2,
|
||||
hpsustain = 0.6,
|
||||
hprelease = 0.2,
|
||||
hpenv = 1,
|
||||
hpenv = 0,
|
||||
bpattack = 0.0001,
|
||||
bpdecay = 0.2,
|
||||
bpsustain = 0.6,
|
||||
bprelease = 0.2,
|
||||
bpenv = 1,
|
||||
bpenv = 0,
|
||||
n = 0,
|
||||
freq,
|
||||
} = value;
|
||||
@@ -101,21 +101,21 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
let map = getSound(s).data.samples;
|
||||
if (Array.isArray(map)) {
|
||||
sampleUrl =
|
||||
path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', '');
|
||||
path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', '');
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(map)
|
||||
.filter((k) => !k.startsWith('_'))
|
||||
.reduce(
|
||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||
null,
|
||||
);
|
||||
.filter((k) => !k.startsWith('_'))
|
||||
.reduce(
|
||||
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
|
||||
null,
|
||||
);
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
sampleUrl =
|
||||
path !== undefined
|
||||
? path + map[closest][n % map[closest].length].replace('./', '')
|
||||
: map[closest][n % map[closest].length].replace('./', '');
|
||||
path !== undefined
|
||||
? path + map[closest][n % map[closest].length].replace('./', '')
|
||||
: map[closest][n % map[closest].length].replace('./', '');
|
||||
}
|
||||
}
|
||||
if (isNote(note)) {
|
||||
@@ -183,9 +183,9 @@ const hap2value = (hap) => {
|
||||
return { ...hap.value, velocity: hap.context.velocity };
|
||||
};
|
||||
export const webaudioDesktopOutputTrigger = (t, hap, ct, cps) =>
|
||||
desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||
desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||
export const webaudioDesktopOutput = (hap, deadline, hapDuration) =>
|
||||
desktopAudio(hap2value(hap), deadline, hapDuration);
|
||||
desktopAudio(hap2value(hap), deadline, hapDuration);
|
||||
|
||||
Pattern.prototype.webaudio = function () {
|
||||
return this.onTrigger(webaudioDesktopOutputTrigger);
|
||||
|
||||
Generated
+122
@@ -110,6 +110,7 @@ name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"midir",
|
||||
"mini-moka",
|
||||
"quick_cache",
|
||||
"reqwest",
|
||||
"rosc",
|
||||
@@ -280,6 +281,12 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.13.1"
|
||||
@@ -328,6 +335,37 @@ dependencies = [
|
||||
"system-deps 6.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_toml"
|
||||
version = "0.15.3"
|
||||
@@ -746,6 +784,19 @@ dependencies = [
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.0",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
@@ -878,6 +929,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-chain"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -1930,6 +1990,21 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mini-moka"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23e0b72e7c9042467008b10279fc732326bd605459ae03bda88825909dd19b56"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-utils",
|
||||
"dashmap",
|
||||
"skeptic",
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -2545,6 +2620,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.28.2"
|
||||
@@ -3142,6 +3228,21 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "skeptic"
|
||||
version = "0.13.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8"
|
||||
dependencies = [
|
||||
"bytecount",
|
||||
"cargo_metadata",
|
||||
"error-chain",
|
||||
"glob",
|
||||
"pulldown-cmark",
|
||||
"tempfile",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.8"
|
||||
@@ -3489,6 +3590,12 @@ dependencies = [
|
||||
"version-compare 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.16.2"
|
||||
@@ -4032,6 +4139,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee8098afad3fb0c54a9007aab6804558410503ad676d4633f9c2559a00ac0f"
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.4"
|
||||
@@ -4044,6 +4157,15 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
|
||||
@@ -24,6 +24,7 @@ rosc = "0.10.1"
|
||||
web-audio-api = { git = "https://github.com/orottier/web-audio-api-rs.git", branch = "main" }
|
||||
reqwest = "0.11.20"
|
||||
quick_cache = "0.4.0"
|
||||
mini-moka = "0.10.2"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
||||
+148
-248
@@ -54,206 +54,170 @@ pub struct FilterADSR {
|
||||
pub env: f64,
|
||||
}
|
||||
|
||||
pub fn superdough_sample(
|
||||
message: &WebAudioMessage,
|
||||
context: &mut AudioContext,
|
||||
audio_buffer: AudioBuffer,
|
||||
) {
|
||||
let now = context.current_time();
|
||||
let mut chain: Vec<&dyn AudioNode> = Vec::new();
|
||||
let compressor = context.create_dynamics_compressor();
|
||||
compressor.connect(&context.destination());
|
||||
compressor.threshold().set_value(-50.0);
|
||||
let delay = context.create_delay(1.);
|
||||
|
||||
let audio_buffer_duration = audio_buffer.duration();
|
||||
let mut src = context.create_buffer_source();
|
||||
src.set_buffer(audio_buffer);
|
||||
chain.push(&src);
|
||||
let filters = create_filters(context, message);
|
||||
for f in &filters {
|
||||
chain.push(f);
|
||||
}
|
||||
let env = context.create_gain();
|
||||
chain.push(&env);
|
||||
|
||||
|
||||
if message.delay.wet > 0.0 {
|
||||
create_delay(message, context, &env, &delay, now, &compressor);
|
||||
}
|
||||
// Connect nodes and play sample
|
||||
connect_nodes(chain, &compressor);
|
||||
src.playback_rate().set_value(message.speed);
|
||||
let src = play_sample(message, now, audio_buffer_duration, src, filters, &env);
|
||||
src.set_onended(move |_| {
|
||||
delay.disconnect();
|
||||
});
|
||||
pub trait WebAudioPlayer {
|
||||
fn set_adsr(&mut self, t: f64, adsr: &ADSR, velocity: f32, duration: f64);
|
||||
fn play(&mut self, t: f64, message: &WebAudioMessage, duration: f64,);
|
||||
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode>;
|
||||
}
|
||||
|
||||
pub struct Synth {
|
||||
pub oscillator: OscillatorNode,
|
||||
pub envelope: GainNode,
|
||||
}
|
||||
|
||||
pub fn superdough_synth(
|
||||
message: &WebAudioMessage,
|
||||
context: &mut AudioContext,
|
||||
|
||||
) {
|
||||
let now = context.current_time();
|
||||
let mut chain: Vec<&dyn AudioNode> = Vec::new();
|
||||
let compressor = context.create_dynamics_compressor();
|
||||
compressor.connect(&context.destination());
|
||||
compressor.threshold().set_value(-50.0);
|
||||
let env = context.create_gain();
|
||||
let delay = context.create_delay(1.);
|
||||
|
||||
let osc = context.create_oscillator();
|
||||
// Create nodes for synth playback
|
||||
chain.push(&osc);
|
||||
let filters = create_filters(context, message);
|
||||
for f in &filters {
|
||||
chain.push(f);
|
||||
impl Synth {
|
||||
pub fn new(context: &mut AudioContext, message: &WebAudioMessage) -> Self {
|
||||
let oscillator = context.create_oscillator();
|
||||
let envelope = context.create_gain();
|
||||
Self { oscillator, envelope }
|
||||
}
|
||||
chain.push(&env);
|
||||
|
||||
|
||||
if message.delay.wet > 0.0 {
|
||||
create_delay(message, context, &env, &delay, now, &compressor);
|
||||
};
|
||||
|
||||
// Connect nodes and play synth
|
||||
connect_nodes(chain, &compressor);
|
||||
let osc = play_synth(&message, now, osc, filters, &env);
|
||||
|
||||
|
||||
osc.set_onended(move |_| {
|
||||
delay.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
fn create_delay(message: &WebAudioMessage, context: &mut AudioContext, env: &GainNode, delay: &DelayNode, now: f64, compressor: &DynamicsCompressorNode) {
|
||||
let output = context.create_gain();
|
||||
output.connect(compressor);
|
||||
|
||||
delay.delay_time().set_value(message.delay.delay_time);
|
||||
delay.connect(&output);
|
||||
|
||||
let feedback = context.create_gain();
|
||||
feedback.gain().set_value(message.delay.feedback);
|
||||
feedback.connect(delay);
|
||||
delay.connect(&feedback);
|
||||
|
||||
let pre_gain = context.create_gain();
|
||||
pre_gain.gain().set_value_at_time(message.delay.wet, now + message.delay.delay_time as f64);
|
||||
pre_gain.connect(&feedback);
|
||||
|
||||
let input = context.create_gain();
|
||||
input.connect(&pre_gain);
|
||||
env.connect(&input);
|
||||
}
|
||||
|
||||
fn play_synth(message: &&WebAudioMessage, now: f64, mut osc: OscillatorNode, filters: Vec<BiquadFilterNode>, env: &GainNode) -> OscillatorNode {
|
||||
osc.set_type(create_osc_type(&message));
|
||||
osc.frequency().set_value(message.note);
|
||||
osc.start();
|
||||
match message.adsr.adsr_on {
|
||||
1 => apply_synth_adsr(&env, message, now),
|
||||
_ => apply_default_synth_adsr(&env, message, now),
|
||||
};
|
||||
for f in filters {
|
||||
apply_filter_adsr(&f, message, &f.type_(), now);
|
||||
pub fn set_frequency(&mut self, frequency: &f32) {
|
||||
self.oscillator.frequency().set_value(*frequency);
|
||||
}
|
||||
osc.stop_at(now + message.duration + message.adsr.release + 2.);
|
||||
|
||||
osc
|
||||
}
|
||||
|
||||
fn create_osc_type(message: &WebAudioMessage) -> OscillatorType {
|
||||
match message.waveform.as_str() {
|
||||
"sine" => OscillatorType::Sine,
|
||||
"square" => OscillatorType::Square,
|
||||
"triangle" => OscillatorType::Triangle,
|
||||
"saw" => OscillatorType::Sawtooth,
|
||||
"sawtooth" => OscillatorType::Sawtooth,
|
||||
_ => panic!("Invalid oscillator type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn play_sample(message: &WebAudioMessage, now: f64, audio_buffer_duration: f64, mut src: AudioBufferSourceNode, filters: Vec<BiquadFilterNode>, env: &GainNode) -> AudioBufferSourceNode {
|
||||
let (start_at, stop_at) = if message.speed < 0.0 {
|
||||
(audio_buffer_duration, now + message.duration + 0.2)
|
||||
} else {
|
||||
(message.begin * audio_buffer_duration, now + message.duration + message.adsr.release)
|
||||
};
|
||||
if message.looper.is_loop > 0 {
|
||||
src.set_loop(true);
|
||||
src.set_loop_start(message.looper.loop_start);
|
||||
src.set_loop_end(message.looper.loop_end);
|
||||
src.start_at_with_offset(
|
||||
now,
|
||||
src.loop_start(),
|
||||
);
|
||||
match message.adsr.adsr_on {
|
||||
1 => apply_drum_adsr(&env, message, now),
|
||||
_ => apply_default_drum_adsr(&env, message, now),
|
||||
pub fn set_waveform(&mut self, waveform: &str) {
|
||||
match waveform {
|
||||
"sine" => self.oscillator.set_type(OscillatorType::Sine),
|
||||
"square" => self.oscillator.set_type(OscillatorType::Square),
|
||||
"triangle" => self.oscillator.set_type(OscillatorType::Triangle),
|
||||
"saw" | "sawtooth" => self.oscillator.set_type(OscillatorType::Sawtooth),
|
||||
_ => {}
|
||||
}
|
||||
for f in filters {
|
||||
apply_filter_adsr(&f, message, &f.type_(), now);
|
||||
}
|
||||
}
|
||||
|
||||
impl WebAudioPlayer for Synth {
|
||||
fn set_adsr(&mut self, t: f64, adsr: &ADSR, velocity: f32, duration: f64) {
|
||||
self.envelope.gain()
|
||||
.set_value_at_time(0.0, t)
|
||||
.linear_ramp_to_value_at_time(velocity, t + adsr.attack)
|
||||
.linear_ramp_to_value_at_time((adsr.sustain + 0.001) * velocity, t + adsr.attack + adsr.decay)
|
||||
.set_value_at_time(adsr.sustain * velocity, t + duration)
|
||||
.linear_ramp_to_value_at_time(0.0, t + duration + adsr.release);
|
||||
}
|
||||
|
||||
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
|
||||
self.oscillator.start();
|
||||
self.oscillator.stop_at(t + message.duration + release);
|
||||
}
|
||||
|
||||
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
|
||||
let mut filters = Vec::new();
|
||||
if message.bpf.frequency > 0.0 {
|
||||
let mut bpf = context.create_biquad_filter();
|
||||
bpf.set_type(Bandpass);
|
||||
bpf.frequency().set_value(message.bpf.frequency);
|
||||
bpf.q().set_value(message.bpf.resonance);
|
||||
filters.push(bpf);
|
||||
} else if message.lpf.frequency > 0.0 {
|
||||
let mut lpf = context.create_biquad_filter();
|
||||
lpf.set_type(Lowpass);
|
||||
lpf.frequency().set_value(message.lpf.frequency);
|
||||
lpf.q().set_value(message.lpf.resonance);
|
||||
filters.push(lpf);
|
||||
} else if message.hpf.frequency > 0.0 {
|
||||
let mut hpf = context.create_biquad_filter();
|
||||
hpf.set_type(Highpass);
|
||||
hpf.frequency().set_value(message.hpf.frequency);
|
||||
hpf.q().set_value(message.hpf.resonance);
|
||||
filters.push(hpf);
|
||||
}
|
||||
src.stop_at(now + message.duration + message.adsr.release);
|
||||
} else {
|
||||
src.start_at_with_offset(
|
||||
now,
|
||||
start_at,
|
||||
);
|
||||
match message.adsr.adsr_on {
|
||||
1 => apply_drum_adsr(&env, message, now),
|
||||
_ => apply_default_drum_adsr(&env, message, now),
|
||||
}
|
||||
for f in filters {
|
||||
apply_filter_adsr(&f, message, &f.type_(), now);
|
||||
|
||||
if !filters.is_empty() {
|
||||
self.oscillator.connect(filters.first().unwrap());
|
||||
filters.last().unwrap().connect(&self.envelope);
|
||||
} else {
|
||||
self.oscillator.connect(&self.envelope);
|
||||
};
|
||||
src.stop_at(stop_at);
|
||||
|
||||
filters
|
||||
}
|
||||
src
|
||||
}
|
||||
|
||||
fn create_filters(context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
|
||||
let mut filters = Vec::new();
|
||||
|
||||
if message.bpf.frequency > 0.0 {
|
||||
let mut bpf = context.create_biquad_filter();
|
||||
bpf.set_type(Bandpass);
|
||||
bpf.frequency().set_value(message.bpf.frequency);
|
||||
bpf.q().set_value(message.bpf.resonance);
|
||||
filters.push(bpf);
|
||||
} else if message.lpf.frequency > 0.0 && message.hpf.frequency > 0.0 {
|
||||
let mut lpf = context.create_biquad_filter();
|
||||
let mut hpf = context.create_biquad_filter();
|
||||
lpf.set_type(Lowpass);
|
||||
lpf.frequency().set_value(message.lpf.frequency);
|
||||
lpf.q().set_value(message.lpf.resonance);
|
||||
hpf.set_type(Highpass);
|
||||
hpf.frequency().set_value(message.hpf.frequency);
|
||||
hpf.q().set_value(message.hpf.resonance);
|
||||
lpf.connect(&hpf);
|
||||
filters.push(lpf);
|
||||
filters.push(hpf);
|
||||
} else if message.lpf.frequency > 0.0 {
|
||||
let mut lpf = context.create_biquad_filter();
|
||||
lpf.set_type(Lowpass);
|
||||
lpf.frequency().set_value(message.lpf.frequency);
|
||||
lpf.q().set_value(message.lpf.resonance);
|
||||
filters.push(lpf);
|
||||
} else if message.hpf.frequency > 0.0 {
|
||||
let mut hpf = context.create_biquad_filter();
|
||||
hpf.set_type(Highpass);
|
||||
hpf.frequency().set_value(message.hpf.frequency);
|
||||
hpf.q().set_value(message.hpf.resonance);
|
||||
filters.push(hpf);
|
||||
}
|
||||
|
||||
filters
|
||||
pub struct Sampler {
|
||||
pub sample: AudioBufferSourceNode,
|
||||
pub envelope: GainNode,
|
||||
}
|
||||
|
||||
fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) {
|
||||
impl Sampler {
|
||||
pub fn new(context: &mut AudioContext, message: &WebAudioMessage, audio_buffer: AudioBuffer) -> Self {
|
||||
let mut sample = context.create_buffer_source();
|
||||
sample.set_buffer(audio_buffer);
|
||||
let envelope = context.create_gain();
|
||||
Self { sample, envelope }
|
||||
}
|
||||
}
|
||||
|
||||
impl WebAudioPlayer for Sampler {
|
||||
fn set_adsr(&mut self, t: f64, adsr: &ADSR, velocity: f32, duration: f64) {
|
||||
self.envelope.gain()
|
||||
.set_value_at_time(0.0, t)
|
||||
.linear_ramp_to_value_at_time(velocity, t + adsr.attack)
|
||||
.linear_ramp_to_value_at_time((adsr.sustain + 0.00001) * velocity, t + adsr.attack + adsr.decay)
|
||||
.set_value_at_time(adsr.sustain * velocity, t + duration)
|
||||
.linear_ramp_to_value_at_time(0.0, t + duration + adsr.release);
|
||||
}
|
||||
|
||||
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
|
||||
let buffer_duration = release;
|
||||
let (start_at, stop_at) = if message.speed < 0.0 {
|
||||
(buffer_duration, t + message.duration + 0.2)
|
||||
} else {
|
||||
(message.begin * buffer_duration, t + message.duration + message.adsr.release)
|
||||
};
|
||||
if message.looper.is_loop > 0 {
|
||||
self.sample.set_loop(true);
|
||||
self.sample.set_loop_start(message.looper.loop_start);
|
||||
self.sample.set_loop_end(message.looper.loop_end);
|
||||
self.sample.start_at_with_offset(
|
||||
t,
|
||||
self.sample.loop_start(),
|
||||
);
|
||||
self.sample.stop_at(t + message.duration + message.adsr.release);
|
||||
} else {
|
||||
self.sample.start_at_with_offset(
|
||||
t,
|
||||
start_at,
|
||||
);
|
||||
self.sample.stop_at(stop_at);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
|
||||
let mut filters = Vec::new();
|
||||
if message.bpf.frequency > 0.0 {
|
||||
let mut bpf = context.create_biquad_filter();
|
||||
bpf.set_type(Bandpass);
|
||||
bpf.frequency().set_value(message.bpf.frequency);
|
||||
bpf.q().set_value(message.bpf.resonance);
|
||||
filters.push(bpf);
|
||||
} else if message.lpf.frequency > 0.0 {
|
||||
let mut lpf = context.create_biquad_filter();
|
||||
lpf.set_type(Lowpass);
|
||||
lpf.frequency().set_value(message.lpf.frequency);
|
||||
lpf.q().set_value(message.lpf.resonance);
|
||||
filters.push(lpf);
|
||||
} else if message.hpf.frequency > 0.0 {
|
||||
let mut hpf = context.create_biquad_filter();
|
||||
hpf.set_type(Highpass);
|
||||
hpf.frequency().set_value(message.hpf.frequency);
|
||||
hpf.q().set_value(message.hpf.resonance);
|
||||
filters.push(hpf);
|
||||
}
|
||||
|
||||
if !filters.is_empty() {
|
||||
self.sample.connect(filters.first().unwrap());
|
||||
filters.last().unwrap().connect(&self.envelope);
|
||||
} else {
|
||||
self.sample.connect(&self.envelope);
|
||||
};
|
||||
filters
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) {
|
||||
let env = match filter {
|
||||
Lowpass => message.lpenv,
|
||||
Highpass => message.hpenv,
|
||||
@@ -281,67 +245,3 @@ fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage,
|
||||
filter_node.frequency().set_value_at_time(sustain_level, now + message.duration);
|
||||
filter_node.frequency().linear_ramp_to_value_at_time(min, now + message.duration + env.release.max(0.1));
|
||||
}
|
||||
|
||||
//TODO fix envelopes
|
||||
fn apply_adsr(
|
||||
envelope: &GainNode,
|
||||
message: &WebAudioMessage,
|
||||
now: f64,
|
||||
attack: f64,
|
||||
sustain: f32,
|
||||
decay: f64,
|
||||
release: f64,
|
||||
) {
|
||||
envelope
|
||||
.gain()
|
||||
.set_value_at_time(0.0, now)
|
||||
.linear_ramp_to_value_at_time(message.velocity, now + attack)
|
||||
.exponential_ramp_to_value_at_time(
|
||||
(sustain + 0.00001) * message.velocity,
|
||||
now + attack + decay,
|
||||
)
|
||||
.set_value_at_time(sustain * message.velocity, now + message.duration)
|
||||
.linear_ramp_to_value_at_time(0.00001, now + message.duration + release);
|
||||
}
|
||||
|
||||
fn apply_synth_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) {
|
||||
apply_adsr(
|
||||
envelope,
|
||||
message,
|
||||
now,
|
||||
message.adsr.attack,
|
||||
message.adsr.sustain,
|
||||
message.adsr.decay,
|
||||
message.adsr.release,
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_default_synth_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) {
|
||||
apply_adsr(envelope, message, now, 0.001, message.velocity, 0.05, 0.07);
|
||||
}
|
||||
|
||||
fn apply_drum_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) {
|
||||
apply_adsr(
|
||||
envelope,
|
||||
message,
|
||||
now,
|
||||
message.adsr.attack,
|
||||
message.adsr.sustain,
|
||||
message.adsr.decay,
|
||||
message.adsr.release,
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_default_drum_adsr(envelope: &GainNode, message: &WebAudioMessage, now: f64) {
|
||||
apply_adsr(envelope, message, now, 0.001, message.velocity, 0.5, 0.5);
|
||||
}
|
||||
|
||||
fn connect_nodes<'a>(nodes: Vec<&'a (dyn AudioNode + 'a)>, compressor: &DynamicsCompressorNode) -> Vec<&'a (dyn AudioNode)> {
|
||||
for i in 0..nodes.len() - 1 {
|
||||
nodes[i].connect(nodes[i + 1]);
|
||||
}
|
||||
if let Some(last_node) = nodes.last() {
|
||||
last_node.connect(compressor);
|
||||
}
|
||||
nodes
|
||||
}
|
||||
+129
-58
@@ -4,19 +4,22 @@ use std::{
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use quick_cache::sync::Cache;
|
||||
// use mini_moka::sync::Cache;
|
||||
use std::sync::mpsc::{RecvError, SendError};
|
||||
use std::time::{Instant, SystemTime};
|
||||
use mini_moka::sync::Cache;
|
||||
use reqwest::Url;
|
||||
|
||||
use tokio::{fs, sync::{mpsc, Mutex}, time::Instant};
|
||||
use tokio::{fs, sync::{mpsc, Mutex}};
|
||||
use serde::Deserialize;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use web_audio_api::{AudioBuffer, context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}};
|
||||
use web_audio_api::context::BaseAudioContext;
|
||||
use crate::superdough::{ADSR, BPF, Delay, FilterADSR, HPF, Loop, LPF, superdough_sample, superdough_synth};
|
||||
use web_audio_api::node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, DelayNode, DynamicsCompressorNode, GainNode, OscillatorNode, OscillatorType};
|
||||
use web_audio_api::node::BiquadFilterType::{Bandpass, Highpass, Lowpass};
|
||||
use crate::superdough::{ADSR, apply_filter_adsr, BPF, Delay, FilterADSR, HPF, Loop, LPF, Sampler, Synth, WebAudioPlayer};
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebAudioMessage {
|
||||
pub note: f32,
|
||||
pub instant: Instant,
|
||||
@@ -52,6 +55,8 @@ pub fn init(
|
||||
async_output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
|
||||
) {
|
||||
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
|
||||
let message_queue: Arc<Mutex<Vec<WebAudioMessage>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
/* ...........................................................
|
||||
Listen For incoming messages and add to queue
|
||||
@@ -70,20 +75,7 @@ pub fn init(
|
||||
});
|
||||
|
||||
let message_queue_clone = Arc::clone(&message_queue);
|
||||
|
||||
/* ...........................................................
|
||||
Prepare audio context
|
||||
............................................................*/
|
||||
let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() {
|
||||
Ok("playback") => AudioContextLatencyCategory::Playback,
|
||||
_ => AudioContextLatencyCategory::default(),
|
||||
};
|
||||
let mut audio_context = AudioContext::new(AudioContextOptions {
|
||||
latency_hint,
|
||||
..AudioContextOptions::default()
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let cache: Cache<String, AudioBuffer> = Cache::new(10_000);
|
||||
/* ...........................................................
|
||||
Process queued messages
|
||||
............................................................*/
|
||||
@@ -96,51 +88,130 @@ pub fn init(
|
||||
return true;
|
||||
};
|
||||
|
||||
match message.waveform.as_str() {
|
||||
"sine" | "square" | "triangle" | "saw" | "sawtooth" => {
|
||||
superdough_synth(message.clone(), &mut audio_context);
|
||||
}
|
||||
_ => {
|
||||
let url = Url::parse(&*message.sampleurl).expect("failed to parse url");
|
||||
let filename = url.path_segments()
|
||||
.and_then(Iterator::last)
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.unwrap_or("tmp.ben");
|
||||
let file_path = format!("samples/{}{}", message.dirname, filename);
|
||||
match sender.send(message.clone()) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {}
|
||||
};
|
||||
|
||||
if let Some(audio_buffer) = cache.get(&file_path) {
|
||||
superdough_sample(
|
||||
message.clone(),
|
||||
&mut audio_context,
|
||||
audio_buffer.clone(),
|
||||
);
|
||||
} else if let Ok(file) = File::open(&file_path) {
|
||||
let audio_buffer = audio_context.decode_audio_data_sync(file)
|
||||
.unwrap_or_else(|_| panic!("Failed to decode audio data"));
|
||||
cache.insert(file_path.clone(), audio_buffer.clone());
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
if tokio::fs::metadata(&file_path).await.is_err() {
|
||||
let response = reqwest::get(url)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Failed to send GET request"));
|
||||
|
||||
let bytes = response.bytes().await.unwrap();
|
||||
let path = Path::new(&file_path);
|
||||
let mut file = create_file_and_dirs(path).await;
|
||||
file.write_all(&bytes)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Failed to write to file"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
});
|
||||
|
||||
/* ...........................................................
|
||||
Create and setup WebAudio
|
||||
............................................................*/
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() {
|
||||
Ok("playback") => AudioContextLatencyCategory::Playback,
|
||||
_ => AudioContextLatencyCategory::default(),
|
||||
};
|
||||
let mut audio_context = AudioContext::new(AudioContextOptions {
|
||||
latency_hint,
|
||||
..AudioContextOptions::default()
|
||||
});
|
||||
let mut temp_src = audio_context.create_buffer_source();
|
||||
let compressor = audio_context.create_dynamics_compressor();
|
||||
compressor.threshold().set_value(-50.0);
|
||||
compressor.connect(&audio_context.destination());
|
||||
let delay = audio_context.create_delay(1.);
|
||||
let output = audio_context.create_gain();
|
||||
output.connect(&compressor);
|
||||
delay.connect(&output);
|
||||
let feedback = audio_context.create_gain();
|
||||
feedback.connect(&delay);
|
||||
delay.connect(&feedback);
|
||||
let pre_gain = audio_context.create_gain();
|
||||
pre_gain.connect(&feedback);
|
||||
let input = audio_context.create_gain();
|
||||
input.connect(&pre_gain);
|
||||
let cache: Cache<String, AudioBuffer> = Cache::builder()
|
||||
.max_capacity(31 * 1024 * 1024)
|
||||
.time_to_idle(Duration::from_secs(40))
|
||||
.build();
|
||||
loop {
|
||||
match receiver.recv() {
|
||||
Ok(message) => {
|
||||
match message.waveform.as_str() {
|
||||
"sine" | "square" | "triangle" | "saw" | "sawtooth" => {
|
||||
let t = audio_context.current_time();
|
||||
delay.delay_time().set_value(message.delay.delay_time);
|
||||
feedback.gain().set_value(message.delay.feedback);
|
||||
pre_gain.gain().set_value_at_time(message.delay.wet, t + message.delay.delay_time as f64);
|
||||
|
||||
let mut synth = Synth::new(&mut audio_context, &message);
|
||||
synth.set_frequency(&message.note);
|
||||
synth.set_waveform(&message.waveform);
|
||||
let filters = synth.set_filters(&mut audio_context, &message);
|
||||
if message.delay.wet > 0.0 {
|
||||
synth.envelope.connect(&input);
|
||||
}
|
||||
synth.envelope.connect(&compressor);
|
||||
synth.play(t, &message, message.adsr.release);
|
||||
synth.set_adsr(t, &message.adsr, message.velocity, message.duration);
|
||||
if message.lpenv.env > 0.0 || message.hpenv.env > 0.0 || message.bpenv.env > 0.0 {
|
||||
for f in filters {
|
||||
apply_filter_adsr(&f, &message, &f.type_(), t);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let url = Url::parse(&*message.sampleurl).expect("failed to parse url");
|
||||
let filename = url.path_segments()
|
||||
.and_then(Iterator::last)
|
||||
.and_then(|name| if name.is_empty() { None } else { Some(name) })
|
||||
.unwrap_or("tmp.ben");
|
||||
let file_path = format!("/Users/vasiliymilovidov/samples/{}{}", message.dirname, filename);
|
||||
let file_path_clone = file_path.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if tokio::fs::metadata(&file_path.clone()).await.is_err() {
|
||||
let response = reqwest::get(url)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Failed to send GET request"));
|
||||
|
||||
let bytes = response.bytes().await.unwrap();
|
||||
let path = Path::new(&file_path);
|
||||
let mut file = create_file_and_dirs(path).await;
|
||||
file.write_all(&bytes)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Failed to write to file"));
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(audio_buffer) = cache.get(&file_path_clone) {
|
||||
let t = audio_context.current_time();
|
||||
delay.delay_time().set_value(message.delay.delay_time);
|
||||
feedback.gain().set_value(message.delay.feedback);
|
||||
pre_gain.gain().set_value_at_time(message.delay.wet, t + message.delay.delay_time as f64);
|
||||
let audio_buffer_duration = audio_buffer.duration();
|
||||
let mut sampler = Sampler::new(&mut audio_context, &message, audio_buffer);
|
||||
sampler.sample.playback_rate().set_value(message.speed);
|
||||
sampler.set_adsr(t, &message.adsr, message.velocity, message.duration);
|
||||
let filters = sampler.set_filters(&mut audio_context, &message);
|
||||
if message.lpenv.env > 0.0 || message.hpenv.env > 0.0 || message.bpenv.env > 0.0 {
|
||||
for f in filters {
|
||||
apply_filter_adsr(&f, &message, &f.type_(), t);
|
||||
}
|
||||
}
|
||||
if message.delay.wet > 0.0 {
|
||||
sampler.envelope.connect(&input);
|
||||
};
|
||||
sampler.envelope.connect(&compressor);
|
||||
sampler.play(t, &message, audio_buffer_duration);
|
||||
} else if let Ok(file) = File::open(&file_path_clone) {
|
||||
let audio_buffer = audio_context.decode_audio_data_sync(file)
|
||||
.unwrap_or_else(|_| panic!("Failed to decode audio data"));
|
||||
cache.insert(file_path_clone.clone(), audio_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn async_process_model(
|
||||
@@ -266,4 +337,4 @@ async fn create_file_and_dirs(path: &Path) -> fs::File {
|
||||
}
|
||||
let file = fs::File::create(path).await.unwrap();
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user