work on scheduler:

- simplify clockworker (no audioContext)
- tick based scheduler with cps
- support cps in .out
This commit is contained in:
Felix Roos 2022-08-11 22:58:37 +02:00
parent bf3e90baf5
commit 9478915cf4
4 changed files with 56 additions and 45 deletions

View File

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

View File

@ -1,6 +1,8 @@
<div style="position: absolute; top: 0; right: 0; padding: 4px">
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
<button id="stop" style="font-size: 2em">stop</button>
<button id="stop" style="margin-bottom: 4px; font-size: 2em">stop</button><br />
<button id="slower" style="font-size: 2em">-</button>
<button id="faster" style="font-size: 2em">+</button>
</div>
<textarea
style="font-size: 2em; background: #bce865; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
@ -14,7 +16,7 @@ Loading...</textarea
// import * as strudel from '@strudel.cycles/core';
import * as strudel from '../../core/index.mjs';
import * as util from '../../core/util.mjs';
import '@strudel.cycles/core/euclid.mjs';
import '../../core/euclid.mjs';
// import { Scheduler, getAudioContext } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.0.4';
import { Scheduler, getAudioContext } from '../index.mjs';
@ -33,11 +35,12 @@ Loading...</textarea
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.freq()
.velocity(.5)
.wave(cat('sawtooth','square').fast(2))
.adsr(0.01,.02,.5,0.1)
.filter('lowshelf',800,25)
.out()`;
.s('sawtooth')
.cutoff(800)
.out()
`;
try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
@ -61,4 +64,6 @@ Loading...</textarea
document.getElementById('start').addEventListener('click', () => scheduler.start());
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
</script>

View File

@ -5,35 +5,40 @@ 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,
);
startedAt;
audioContext;
cps = 1;
constructor({ audioContext, interval = 0.1, onEvent, latency = 0.1 }) {
this.audioContext = audioContext;
this.worker = new ClockWorker((tick, interval) => {
const begin = tick * interval * this.cps;
const end = (tick + 1) * interval * this.cps;
const haps = this.pattern.queryArc(begin, end);
haps.forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
if (e.context.onTrigger) {
this.lastEnd = end;
const ctxTime = e.whole.begin / this.cps + this.startedAt + 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.startedAt = this.audioContext.currentTime;
this.worker.start();
}
stop() {
@ -42,4 +47,7 @@ export class Scheduler {
setPattern(pat) {
this.pattern = pat;
}
setCps(cps = 1) {
this.cps = cps;
}
}

View File

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