mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 05:05:26 -04:00
wip
This commit is contained in:
@@ -15,7 +15,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
|
||||
let {
|
||||
note = 'C3',
|
||||
s = 'triangle',
|
||||
s,
|
||||
bank = '',
|
||||
source,
|
||||
gain = 0.8,
|
||||
@@ -70,44 +70,52 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
bpenv = 1,
|
||||
n = 0,
|
||||
} = value;
|
||||
bank = getSound(s).data.samples;
|
||||
|
||||
// SAMPLES
|
||||
value.duration = hapDuration;
|
||||
if (isNote(note)) {
|
||||
note = noteToMidi(note);
|
||||
}
|
||||
const baseUrl = getSound(s).data.baseUrl;
|
||||
let path;
|
||||
if (baseUrl === './piano/') {
|
||||
path = 'https://strudel.tidalcycles.org/';
|
||||
} else if (baseUrl === './EmuSP12/') {
|
||||
path = 'https://strudel.tidalcycles.org/';
|
||||
}
|
||||
|
||||
value.duration = hapDuration;
|
||||
let transpose = 0;
|
||||
transpose = note - 36;
|
||||
let sampleUrl;
|
||||
let key;
|
||||
if (Array.isArray(bank)) {
|
||||
sampleUrl = path !== undefined ? path + bank[n % bank.length].replace('./', '') : bank[n % bank.length].replace('./', '');
|
||||
if (s === 'sine' || s === 'square' || s === 'saw' || s === 'triangle') {
|
||||
sampleUrl = 'none';
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - note;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.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 + bank[closest][n % bank[closest].length].replace('./', '') : bank[closest][n % bank[closest].length].replace('./', '');
|
||||
bank = getSound(s).data.samples;
|
||||
console.log('bank', bank)
|
||||
|
||||
// if (bank && s) {
|
||||
// s = `${bank}_${s}`;
|
||||
// }
|
||||
|
||||
let path = '';
|
||||
if (getSound(s).data.baseUrl !== undefined) {
|
||||
const baseUrl = getSound(s).data.baseUrl;
|
||||
if (baseUrl === './piano/') {
|
||||
path = 'https://strudel.tidalcycles.org/';
|
||||
} else if (baseUrl === './EmuSP12/') {
|
||||
path = 'https://strudel.tidalcycles.org/';
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(bank)) {
|
||||
sampleUrl = path !== undefined ? path + bank[n % bank.length].replace('./', '') : bank[n % bank.length].replace('./', '');
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - note;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.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 + bank[closest][n % bank[closest].length].replace('./', '') : bank[closest][n % bank[closest].length].replace('./', '');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('sampleUrl', sampleUrl)
|
||||
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||
// const sound = getSoundData(s);
|
||||
|
||||
// SYNTHS
|
||||
if (delay !== 0) {
|
||||
delay = Math.abs(delay);
|
||||
delayfeedback = Math.abs(delayfeedback);
|
||||
@@ -127,16 +135,13 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv],
|
||||
bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv],
|
||||
};
|
||||
|
||||
const offset = (t - getAudioContext().currentTime) * 1000;
|
||||
const roundedOffset = Math.round(offset);
|
||||
const messagesfromjs = [];
|
||||
|
||||
messagesfromjs.push({
|
||||
note: midiToFreq(note),
|
||||
offset: roundedOffset,
|
||||
waveform: s,
|
||||
bank: bank,
|
||||
lpf: packages.lpf,
|
||||
hpf: packages.hpf,
|
||||
bpf: packages.bpf,
|
||||
@@ -144,7 +149,7 @@ export const desktopAudio = async (value, deadline, hapDuration) => {
|
||||
velocity: velocity,
|
||||
delay: packages.delay,
|
||||
orbit: orbit,
|
||||
speed: speed,
|
||||
speed: speed * playbackRate,
|
||||
begin: begin,
|
||||
end: end,
|
||||
looper: packages.loop,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::fs::File;
|
||||
use web_audio_api::AudioBuffer;
|
||||
use web_audio_api::context::{AudioContext, BaseAudioContext};
|
||||
use web_audio_api::node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, DelayNode, DynamicsCompressorNode, GainNode, OscillatorNode, OscillatorType};
|
||||
use web_audio_api::node::BiquadFilterType::{Bandpass, Highpass, Lowpass};
|
||||
@@ -54,6 +55,80 @@ pub struct FilterADSR {
|
||||
pub env: f64,
|
||||
}
|
||||
|
||||
|
||||
pub fn 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 env = context.create_gain();
|
||||
let delay = context.create_delay(1.);
|
||||
// Play synth or sample
|
||||
|
||||
|
||||
// Create nodes for sample playback
|
||||
// let audio_buffer = context.decode_audio_data_sync(file).unwrap();
|
||||
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, context, &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 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.);
|
||||
// Play synth or sample
|
||||
|
||||
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);
|
||||
}
|
||||
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, context, &compressor);
|
||||
let osc = play_synth(&message, now, osc, filters, &env);
|
||||
|
||||
|
||||
osc.set_onended(move |_| {
|
||||
delay.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub fn superdough(message: &WebAudioMessage, context: &mut AudioContext) {
|
||||
let now = context.current_time();
|
||||
let mut chain: Vec<&dyn AudioNode> = Vec::new();
|
||||
@@ -85,11 +160,11 @@ pub fn superdough(message: &WebAudioMessage, context: &mut AudioContext) {
|
||||
|
||||
|
||||
osc.set_onended(move |_| {
|
||||
delay.disconnect();
|
||||
delay.disconnect();
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
match File::open(format!("samples/{}/{}.wav", message.bank, message.waveform)) {
|
||||
match File::open(format!("samples/{}.wav", message.waveform)) {
|
||||
Ok(file) => {
|
||||
|
||||
// Create nodes for sample playback
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
time::Duration,
|
||||
thread::sleep,
|
||||
};
|
||||
use std::fs::File;
|
||||
use mini_moka::sync::Cache;
|
||||
use reqwest::Url;
|
||||
|
||||
@@ -11,14 +12,12 @@ use tokio::{
|
||||
time::Instant,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
// use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use web_audio_api::{
|
||||
context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions},
|
||||
node::AudioNode,
|
||||
};
|
||||
use crate::superdough::{ADSR, BPF, Delay, FilterADSR, HPF, Loop, LPF, superdough};
|
||||
use web_audio_api::{AudioBuffer, context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}, node::AudioNode};
|
||||
use web_audio_api::context::BaseAudioContext;
|
||||
use crate::superdough::{ADSR, BPF, Delay, FilterADSR, HPF, Loop, LPF, sample, superdough, superdough_synth};
|
||||
|
||||
const BLOCK_SIZE: usize = 128;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebAudioMessage {
|
||||
@@ -26,7 +25,6 @@ pub struct WebAudioMessage {
|
||||
pub instant: Instant,
|
||||
pub offset: f64,
|
||||
pub waveform: String,
|
||||
pub bank: String,
|
||||
pub lpf: LPF,
|
||||
pub hpf: HPF,
|
||||
pub bpf: BPF,
|
||||
@@ -90,8 +88,7 @@ pub fn init(
|
||||
/* ...........................................................
|
||||
Prepare audio context
|
||||
............................................................*/
|
||||
// let cache = Cache::new(10_000);
|
||||
// let cache_clone_1 = cache.clone();
|
||||
let cache:Cache<String, AudioBuffer> = Cache::new(10_000);
|
||||
/* ...........................................................
|
||||
Process queued messages
|
||||
............................................................*/
|
||||
@@ -104,30 +101,47 @@ pub fn init(
|
||||
return true;
|
||||
};
|
||||
|
||||
let url = Url::parse(&*message.sampleurl).unwrap();
|
||||
let fname = 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 = "/Users/vasiliymilovidov/samples/".to_owned() + fname;
|
||||
let url_copy = url.clone();
|
||||
|
||||
// superdough(message.clone(), &mut audio_context);
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
match tokio::fs::metadata(&file_path).await {
|
||||
Ok(_) => {
|
||||
println!("File exists");
|
||||
}
|
||||
Err(_) => {
|
||||
println!("File does not exist, downloading");
|
||||
let resp = reqwest::get(url_copy).await.unwrap().text().await.unwrap();
|
||||
let bytes = resp.bytes().await.unwrap();
|
||||
let mut out = tokio::fs::File::create(file_path).await.unwrap();
|
||||
out.write_all(&bytes).await.unwrap();
|
||||
}
|
||||
match message.waveform.as_str() {
|
||||
"sine" | "square" | "triangle" | "saw" => {
|
||||
// superdough(message.clone(), &mut audio_context);
|
||||
superdough_synth(message.clone(), &mut audio_context);
|
||||
}
|
||||
});
|
||||
_ => {
|
||||
let url = Url::parse(&*message.sampleurl).unwrap();
|
||||
let fname = 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 = "/Users/vasiliymilovidov/samples/".to_owned() + fname;
|
||||
let url_copy = url.clone();
|
||||
let file_path_copy = file_path.clone();
|
||||
|
||||
match cache.get(&file_path_copy) {
|
||||
Some(buff) => {
|
||||
sample(message.clone(), &mut audio_context, buff.clone());
|
||||
return false;
|
||||
}
|
||||
None => {
|
||||
match File::open(file_path_copy.clone()) {
|
||||
Ok(file) => {
|
||||
let buff = audio_context.decode_audio_data_sync(file).unwrap();
|
||||
cache.insert(file_path_copy.clone(), buff.clone());
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
match tokio::fs::metadata(&file_path).await {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let resp = reqwest::get(url_copy).await.unwrap();
|
||||
let bytes = resp.bytes().await.unwrap();
|
||||
let mut out = tokio::fs::File::create(file_path).await.unwrap();
|
||||
out.write_all(&bytes).await.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
@@ -151,7 +165,6 @@ pub struct MessageFromJS {
|
||||
note: f32,
|
||||
offset: f64,
|
||||
waveform: String,
|
||||
bank: String,
|
||||
lpf: (f32, f32),
|
||||
hpf: (f32, f32),
|
||||
bpf: (f32, f32),
|
||||
@@ -186,7 +199,6 @@ pub async fn sendwebaudio(
|
||||
instant: Instant::now(),
|
||||
offset: m.offset,
|
||||
waveform: m.waveform,
|
||||
bank: m.bank,
|
||||
lpf: LPF {
|
||||
frequency: m.lpf.0,
|
||||
resonance: m.lpf.1,
|
||||
|
||||
Reference in New Issue
Block a user