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);