mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 05:38:35 +00:00
begin webaudio package
This commit is contained in:
parent
fb52227a92
commit
47a046ca1d
70
packages/webaudio/metro.mjs
Normal file
70
packages/webaudio/metro.mjs
Normal file
@ -0,0 +1,70 @@
|
||||
// helpers to create a worker dynamically without needing a server / extra file
|
||||
const stringifyFunction = (func) => '(' + func + ')();';
|
||||
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
|
||||
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||
|
||||
// this class is basically the tale of two clocks
|
||||
class Metro {
|
||||
worker;
|
||||
audioContext;
|
||||
startedAt = 0;
|
||||
lastEnd;
|
||||
lookahead = 0.2; // query offset
|
||||
interval = 0.2; // query span
|
||||
constructor(audioContext, callback, interval = this.interval, lookahead = this.lookahead) {
|
||||
this.audioContext = audioContext;
|
||||
this.interval = interval;
|
||||
this.lookahead = lookahead;
|
||||
this.worker = createWorker(() => {
|
||||
// we cannot use closures here!
|
||||
let interval;
|
||||
let timerID = null; // this is clock #1 (the sloppy js clock)
|
||||
const clear = () => {
|
||||
if (timerID) {
|
||||
clearInterval(timerID);
|
||||
timerID = null;
|
||||
}
|
||||
};
|
||||
const start = () => {
|
||||
clear();
|
||||
if (!interval) {
|
||||
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
|
||||
}
|
||||
timerID = setInterval(() => postMessage('tick'), interval * 1000);
|
||||
};
|
||||
self.onmessage = function (e) {
|
||||
if (e.data == 'start') {
|
||||
start();
|
||||
} else if (e.data.interval) {
|
||||
interval = e.data.interval;
|
||||
if (timerID) {
|
||||
start();
|
||||
}
|
||||
} else if (e.data == 'stop') {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
});
|
||||
this.worker.postMessage({ interval });
|
||||
this.worker.onmessage = (e) => {
|
||||
if (e.data === 'tick') {
|
||||
const begin = this.lastEnd || this.startedAt + this.lookahead;
|
||||
const end = begin + this.interval;
|
||||
this.lastEnd = end;
|
||||
// callback with query span, using clock #2 (the audio clock)
|
||||
callback(begin, end, this.lookahead);
|
||||
}
|
||||
};
|
||||
}
|
||||
start() {
|
||||
this.audioContext.resume();
|
||||
delete this.lastEnd;
|
||||
this.startedAt = this.audioContext.currentTime;
|
||||
this.worker.postMessage('start');
|
||||
}
|
||||
stop() {
|
||||
this.worker.postMessage('stop');
|
||||
}
|
||||
}
|
||||
|
||||
export default Metro;
|
||||
28
packages/webaudio/scheduler.mjs
Normal file
28
packages/webaudio/scheduler.mjs
Normal file
@ -0,0 +1,28 @@
|
||||
class Scheduler {
|
||||
metro;
|
||||
constructor(interval = 0.2, lookahead = 0.2) {
|
||||
this.metro = new Metro(
|
||||
audioContext,
|
||||
(begin, end, lookahead) => {
|
||||
pattern.query(new State(new TimeSpan(begin - lookahead, end - lookahead))).forEach((e) => {
|
||||
if (!e.part.begin.equals(e.whole.begin)) {
|
||||
return;
|
||||
}
|
||||
if (e.context.createAudioNode) {
|
||||
e.context.createAudioNode(e);
|
||||
} else {
|
||||
console.warn('unplayable event: no audio node');
|
||||
}
|
||||
});
|
||||
},
|
||||
interval,
|
||||
lookahead,
|
||||
);
|
||||
}
|
||||
start() {
|
||||
this.metro.start();
|
||||
}
|
||||
stop() {
|
||||
this.metro.stop();
|
||||
}
|
||||
}
|
||||
69
packages/webaudio/webaudio.mjs
Normal file
69
packages/webaudio/webaudio.mjs
Normal file
@ -0,0 +1,69 @@
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => audioContext || new AudioContext();
|
||||
|
||||
const lookahead = 0.2;
|
||||
|
||||
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||
// for some reason, using exponential ramping creates little cracklings
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||
return this._withEvent((event) => {
|
||||
return event.setContext({
|
||||
...event.context,
|
||||
createAudioNode: (e) => createAudioNode(e, event.context.createAudioNode?.(event)),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype._osc = function (type) {
|
||||
return this.withAudioNode((e) => {
|
||||
const osc = getAudioContext().createOscillator();
|
||||
osc.type = type;
|
||||
osc.frequency.value = e.value; // expects frequency..
|
||||
osc.start(e.whole.begin.valueOf() + lookahead);
|
||||
osc.stop(e.whole.end.valueOf() + lookahead); // release?
|
||||
return osc;
|
||||
});
|
||||
};
|
||||
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
|
||||
return this.withAudioNode((e, node) => {
|
||||
const velocity = e.context?.velocity || 1;
|
||||
const envelope = adsr(a, d, s, r, velocity, e.whole.begin.valueOf() + lookahead, e.whole.end.valueOf() + lookahead);
|
||||
node?.connect(envelope);
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
Pattern.prototype.filter = function (type = 'lowshelf', frequency = 1000, gain = 25) {
|
||||
return this.withAudioNode((e, node) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.gain.value = gain;
|
||||
node?.connect(filter);
|
||||
return filter;
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.out = function () {
|
||||
const master = getAudioContext().createGain();
|
||||
master.gain.value = 0.1;
|
||||
master.connect(getAudioContext().destination);
|
||||
return this.withAudioNode((e, node) => {
|
||||
if (!node) {
|
||||
console.warn('out: no source! call .osc() first');
|
||||
}
|
||||
node?.connect(master);
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.define('osc', (type, pat) => pat.osc(type), { patternified: true });
|
||||
Loading…
x
Reference in New Issue
Block a user