diff --git a/packages/webaudio/clockworker.mjs b/packages/webaudio/clockworker.mjs
index c13f79da..e1ec9005 100644
--- a/packages/webaudio/clockworker.mjs
+++ b/packages/webaudio/clockworker.mjs
@@ -9,14 +9,12 @@ 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
+// this is just a setInterval with a counter, running in a worker
export class ClockWorker {
worker;
- audioContext;
- interval = 0.2; // query span
- lastEnd = 0;
- constructor(audioContext, callback, interval = this.interval) {
- this.audioContext = audioContext;
+ interval = 0.1; // query span
+ tick = 0;
+ constructor(callback, interval = this.interval) {
this.interval = interval;
this.worker = createWorker(() => {
// we cannot use closures here!
@@ -33,6 +31,7 @@ export class ClockWorker {
if (!interval) {
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
}
+ postMessage('tick');
timerID = setInterval(() => postMessage('tick'), interval * 1000);
};
self.onmessage = function (e) {
@@ -50,25 +49,23 @@ export class ClockWorker {
});
this.worker.postMessage({ interval });
// const round = (n, d) => Math.round(n * d) / d;
- const precision = 100;
this.worker.onmessage = (e) => {
if (e.data === 'tick') {
- const begin = this.lastEnd || this.audioContext.currentTime;
- const end = this.audioContext.currentTime + this.interval; // DONT reference begin here!
- this.lastEnd = end;
// callback with query span, using clock #2 (the audio clock)
- callback(begin, end);
+ callback(this.tick++, this.interval);
}
};
}
start() {
- console.log('start...');
- this.audioContext.resume();
+ // console.log('start...');
this.worker.postMessage('start');
}
stop() {
- console.log('stop...');
+ // console.log('stop...');
this.worker.postMessage('stop');
+ this.tick = 0;
+ }
+ setInterval(interval) {
+ this.worker.postMessage({ interval });
}
}
-
diff --git a/packages/webaudio/examples/repl.html b/packages/webaudio/examples/repl.html
index 97e945d3..b34d956f 100644
--- a/packages/webaudio/examples/repl.html
+++ b/packages/webaudio/examples/repl.html
@@ -1,9 +1,11 @@
-
+
+
+
diff --git a/packages/webaudio/scheduler.mjs b/packages/webaudio/scheduler.mjs
index e4719307..618fec96 100644
--- a/packages/webaudio/scheduler.mjs
+++ b/packages/webaudio/scheduler.mjs
@@ -5,35 +5,43 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { ClockWorker } from './clockworker.mjs';
-import { State, TimeSpan } from '@strudel.cycles/core';
export class Scheduler {
worker;
pattern;
- constructor({ audioContext, interval = 0.2, onEvent, latency = 0.2 }) {
- this.worker = new ClockWorker(
- audioContext,
- (begin, end) => {
- this.pattern.query(new State(new TimeSpan(begin + latency, end + latency))).forEach((e) => {
- if (!e.part.begin.equals(e.whole.begin)) {
- return;
- }
- if (e.context.onTrigger) {
- // TODO: kill first param, as it's contained in e
- e.context.onTrigger(e.whole.begin, e, audioContext.currentTime, 1 /* cps */);
- }
- if (onEvent) {
- onEvent?.(e);
- }
- });
- },
- interval,
- );
+ phase;
+ audioContext;
+ cps = 1;
+ constructor({ audioContext, interval = 0.1, onEvent, latency = 0.1 }) {
+ this.audioContext = audioContext;
+ this.worker = new ClockWorker((tick, interval) => {
+ const begin = this.phase;
+ const end = this.phase + interval * this.cps;
+ this.phase = end;
+ const haps = this.pattern.queryArc(begin, end);
+ haps.forEach((e) => {
+ if (typeof e.value?.cps === 'number') {
+ this.setCps(e.value?.cps);
+ }
+ if (!e.part.begin.equals(e.whole.begin)) {
+ return;
+ }
+ if (e.context.onTrigger) {
+ const ctxTime = (e.whole.begin - begin) / this.cps + this.audioContext.currentTime + latency;
+ e.context.onTrigger(ctxTime, e, this.audioContext.currentTime, this.cps);
+ }
+ if (onEvent) {
+ onEvent?.(e);
+ }
+ });
+ }, interval);
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
+ this.audioContext.resume();
+ this.phase = 0;
this.worker.start();
}
stop() {
@@ -42,4 +50,7 @@ export class Scheduler {
setPattern(pat) {
this.pattern = pat;
}
+ setCps(cps = 1) {
+ this.cps = cps;
+ }
}
diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs
index aa6262ed..e177d0fd 100644
--- a/packages/webaudio/webaudio.mjs
+++ b/packages/webaudio/webaudio.mjs
@@ -125,7 +125,8 @@ const splitSN = (s, n) => {
};
Pattern.prototype.out = function () {
- return this.onTrigger(async (t, hap, ct) => {
+ return this.onTrigger(async (t, hap, ct, cps) => {
+ const hapDuration = hap.duration / cps;
try {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
@@ -175,7 +176,7 @@ Pattern.prototype.out = function () {
freq = fromMidi(n); // + 48);
}
// make oscillator
- const o = getOscillator({ t, s, freq, duration: hap.duration, release });
+ const o = getOscillator({ t, s, freq, duration: hapDuration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
@@ -183,7 +184,7 @@ Pattern.prototype.out = function () {
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
- const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
+ const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} else {
// load sample
@@ -221,7 +222,7 @@ Pattern.prototype.out = function () {
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop
- let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration;
+ let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);