Merge branch 'supradough' of https://github.com/tidalcycles/strudel into supradough

This commit is contained in:
Jade (Rose) Rowland 2025-06-08 19:25:47 +02:00
commit 1167fdb910
5 changed files with 213 additions and 20 deletions

View file

@ -6,7 +6,19 @@ class DoughProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.dough = new Dough(sampleRate, currentTime);
this.port.onmessage = (event) => this.dough.scheduleSpawn(event.data);
this.port.onmessage = (event) => {
if (event.data.spawn) {
this.dough.scheduleSpawn(event.data.spawn);
} else if (event.data.sample) {
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
} else if (event.data.samples) {
event.data.samples.forEach(([name, channels, sampleRate]) => {
this.dough.loadSample(name, channels, sampleRate);
});
} else {
console.log('unrecognized event type', event.data);
}
};
}
process(inputs, outputs, params) {
if (this.disconnected) {

View file

@ -397,6 +397,22 @@ export class Distort {
}
// distortion could be expressed as a function, because it's stateless
export class BufferPlayer {
static samples = new Map();
buffer; // { channels: Float32Array, sampleRate: number }
pos = 0;
sampleFreq = 261.626; // middle c
update(freq, channel = 0) {
if (this.pos >= this.buffer.channels[channel].length) {
return 0;
}
const speed = ((freq / this.sampleFreq) * this.buffer.sampleRate) / SAMPLE_RATE;
let s = this.buffer.channels[channel][Math.floor(this.pos)];
this.pos = this.pos + speed;
return s;
}
}
export function _rangex(sig, min, max) {
let logmin = Math.log(min);
let range = Math.log(max) - logmin;
@ -417,7 +433,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
let oscillators = {
let shapes = {
sine: SineOsc,
saw: SawOsc,
zaw: ZawOsc,
@ -464,6 +480,7 @@ const defaultDefaultValues = {
z: 'triangle',
pan: 0.5,
fmh: 1,
fmenv: 0, // differs from superdough
};
let getDefaultValue = (key) => defaultDefaultValues[key];
@ -471,7 +488,7 @@ let getDefaultValue = (key) => defaultDefaultValues[key];
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
const note2midi = (note, defaultOctave = 3) => {
const [pc, acc = '', oct = defaultOctave] =
let [pc, acc = '', oct = ''] =
String(note)
.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)
?.slice(1) || [];
@ -480,13 +497,14 @@ const note2midi = (note, defaultOctave = 3) => {
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
oct = Number(oct || defaultOctave);
return (oct + 1) * 12 + chroma + offset;
};
const getFrequency = (value) => {
let { note, freq } = value;
note = note || 36;
if (typeof note === 'string') {
note = note2midi(note); // e.g. c3 => 48
note = note2midi(note, 3); // e.g. c3 => 48
}
if (!freq && typeof note === 'number') {
freq = Math.pow(2, (note - 69) / 12) * 440;
@ -553,6 +571,7 @@ export class DoughVoice {
this.fft = this.fft ?? getDefaultValue('fft');
this.pan = this.pan ?? getDefaultValue('pan');
this.orbit = this.orbit ?? getDefaultValue('orbit');
this.fmenv = this.fmenv ?? getDefaultValue('fmenv');
[this.attack, this.decay, this.sustain, this.release] = getADSRValues([
this.attack,
@ -564,8 +583,30 @@ export class DoughVoice {
this._holdEnd = this._begin + this._duration; // needed for gate
this._end = this._holdEnd + this.release + 0.01; // needed for despawn
const SourceClass = oscillators[this.s] ?? TriOsc;
this._sound = new SourceClass();
this.s ??= 'triangle';
if (this.s === 'saw' || this.s === 'sawtooth') {
this.s = 'zaw'; // polyblepped saw when fm is applied
}
if (shapes[this.s]) {
const SourceClass = shapes[this.s];
this._sound = new SourceClass();
} else if (BufferPlayer.samples.has(this.s)) {
this._sample = new BufferPlayer();
const buffer = BufferPlayer.samples.get(this.s);
this._sample.buffer = buffer;
} else {
console.warn('sound not found', this.s);
}
if (this.penv) {
this._penv = new ADSR();
[this.pattack, this.pdecay, this.psustain, this.prelease] = getADSRValues([
this.pattack,
this.pdecay,
this.psustain,
this.prelease,
]);
}
if (this.fmi) {
this._fm = new SineOsc();
@ -644,7 +685,7 @@ export class DoughVoice {
return 1 - Math.log(c) * this.eighthOverLogHalf;
}
update(t) {
if (!this._sound) {
if (!this._sound && !this._sample) {
return 0;
}
let s = 0;
@ -655,18 +696,25 @@ export class DoughVoice {
let fmi = this.fmi;
if (this._fmenv) {
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease) ** 2;
fmi = /* 2 ** */ this.fmenv * env * fmi; // todo: find good scaling
fmi = this.fmenv * env * fmi;
}
const modfreq = freq * this.fmh;
const modgain = modfreq * fmi;
freq = freq + this._fm.update(modfreq) * modgain;
}
if (this._penv) {
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease) ** 2;
freq = freq + env * this.penv;
}
// sound source
if (this.s === 'pulse') {
if (this._sound && this.s === 'pulse') {
s = this._sound.update(freq, this.pw ?? 0.5);
} else {
} else if (this._sound) {
s = this._sound.update(freq);
} else if (this._sample) {
s = this._sample.update(freq, 0); // tbd: stereo samples...
}
s = s * this.gain * this.velocity;
@ -708,11 +756,13 @@ export class DoughVoice {
this._crush && (s = this._crush.update(s, this.crush));
this._distort && (s = this._distort.update(s, this.distort, this.distortvol));
/* Math.random() > 0.99 && console.log('gate', gate); */
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
s = s * env;
s = s * this.postgain * 0.2;
s = s * this.postgain;
if (!this._sample) {
s = s * 0.2; // turn down waveforms
}
if (this.pan === 0.5) {
this.l = this.r = s; // mono
@ -744,6 +794,9 @@ export class Dough {
this._delayL = new Delay();
this._delayR = new Delay();
}
loadSample(name, channels, sampleRate) {
BufferPlayer.samples.set(name, { channels, sampleRate });
}
scheduleSpawn(value) {
if (value._begin === undefined) {
throw new Error('[dough]: scheduleSpawn expected _begin to be set');
@ -752,7 +805,8 @@ export class Dough {
throw new Error('[dough]: scheduleSpawn expected _duration to be set');
}
value.sampleRate = this.sampleRate;
const time = value._begin; // set from supradough.mjs
// convert seconds to samples
const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs
this.schedule({ time, type: 'spawn', arg: value });
}
spawn(value) {

View file

@ -1,4 +1,4 @@
import _workletUrl from './dough-worklet.mjs?audioworklet';
import _workletUrl from './dough-worklet.mjs?url'; // todo: change ?url to ?audioworklet before build (?audioworklet doesn't hot reload)
export * from './dough.mjs';
export const workletUrl = _workletUrl;

View file

@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from './spectrum.mjs';
export * from './supradough.mjs';
export * from 'superdough';

View file

@ -20,10 +20,136 @@ Pattern.prototype.supradough = function () {
return this.onTrigger((_, hap, __, cps, begin) => {
hap.value._begin = begin;
hap.value._duration = hap.duration / cps;
if (!doughWorklet) {
initDoughWorklet();
}
doughWorklet.port.postMessage(hap.value);
!doughWorklet && initDoughWorklet();
doughWorklet.port.postMessage({ spawn: hap.value });
}, 1);
};
let samples = {
casio: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/high.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/low.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/casio/noise.wav',
],
crow: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/000_crow.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/001_crow2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/002_crow3.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/crow/003_crow4.wav',
],
insect: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/000_everglades_conehead.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/001_robust_shieldback.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/insect/002_seashore_meadow_katydid.wav',
],
wind: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/000_wind1.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/001_wind10.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/002_wind2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/003_wind3.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/004_wind4.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/005_wind5.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/006_wind6.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/007_wind7.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/008_wind8.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/wind/009_wind9.wav',
],
jazz: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/000_BD.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/001_CB.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/002_FX.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/003_HH.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/004_OH.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/005_P1.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/006_P2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/jazz/007_SN.wav',
],
metal: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/000_0.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/001_1.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/002_2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/003_3.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/004_4.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/005_5.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/006_6.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/007_7.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/008_8.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/metal/009_9.wav',
],
east: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/000_nipon_wood_block.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/001_ohkawa_mute.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/002_ohkawa_open.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/003_shime_hi.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/004_shime_hi_2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/005_shime_mute.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/006_taiko_1.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/007_taiko_2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/east/008_taiko_3.wav',
],
space: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/000_0.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/001_1.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/002_11.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/003_12.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/004_13.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/005_14.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/006_15.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/007_16.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/008_17.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/009_18.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/010_2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/011_3.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/012_4.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/013_5.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/014_6.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/015_7.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/016_8.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/space/017_9.wav',
],
numbers: [
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/0.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/1.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/2.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/3.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/4.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/5.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/6.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/7.wav',
'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/numbers/8.wav',
],
piano: ['https://raw.githubusercontent.com/felixroos/dough-samples/refs/heads/main/piano/C3v8.mp3'],
flute: ['https://raw.githubusercontent.com/felixroos/samples/refs/heads/main/flute/c4.mp3'],
bd: [
'https://raw.githubusercontent.com/geikha/tidal-drum-machines/15eac73c5e878550f91d864a4863e014799403f1/machines/RolandTR909/rolandtr909-bd/Bassdrum-01.wav',
],
};
// for some reason, only piano and flute work.. is it because mp3??
async function loadSampleChannels(key, url) {
const buffer = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buf) => getAudioContext().decodeAudioData(buf));
let channels = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
return [key, channels, buffer.sampleRate];
}
let loaded = false;
export async function doughsample() {
!doughWorklet && initDoughWorklet();
if (loaded) {
return;
}
loaded = true;
const sampleMap = await Promise.all(
Object.entries(samples).map(async ([key, url]) => {
url = url[0];
return loadSampleChannels(key, url);
}),
);
console.log('sampleMap', sampleMap);
doughWorklet.port.postMessage({ samples: sampleMap });
}