Merge pull request #181 from tidalcycles/optimize-scheduler

scheduler improvements
This commit is contained in:
Felix Roos 2022-08-13 16:29:53 +02:00 committed by GitHub
commit 72f4d3efa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 49 deletions

View File

@ -9,14 +9,12 @@ const stringifyFunction = (func) => '(' + func + ')();';
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' })); const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
const createWorker = (func) => new Worker(urlifyFunction(func)); 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 { export class ClockWorker {
worker; worker;
audioContext; interval = 0.1; // query span
interval = 0.2; // query span tick = 0;
lastEnd = 0; constructor(callback, interval = this.interval) {
constructor(audioContext, callback, interval = this.interval) {
this.audioContext = audioContext;
this.interval = interval; this.interval = interval;
this.worker = createWorker(() => { this.worker = createWorker(() => {
// we cannot use closures here! // we cannot use closures here!
@ -33,6 +31,7 @@ export class ClockWorker {
if (!interval) { if (!interval) {
throw new Error('no interval set! call worker.postMessage({interval}) before starting.'); throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
} }
postMessage('tick');
timerID = setInterval(() => postMessage('tick'), interval * 1000); timerID = setInterval(() => postMessage('tick'), interval * 1000);
}; };
self.onmessage = function (e) { self.onmessage = function (e) {
@ -50,25 +49,23 @@ export class ClockWorker {
}); });
this.worker.postMessage({ interval }); this.worker.postMessage({ interval });
// const round = (n, d) => Math.round(n * d) / d; // const round = (n, d) => Math.round(n * d) / d;
const precision = 100;
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
if (e.data === 'tick') { 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 with query span, using clock #2 (the audio clock)
callback(begin, end); callback(this.tick++, this.interval);
} }
}; };
} }
start() { start() {
console.log('start...'); // console.log('start...');
this.audioContext.resume();
this.worker.postMessage('start'); this.worker.postMessage('start');
} }
stop() { stop() {
console.log('stop...'); // console.log('stop...');
this.worker.postMessage('stop'); this.worker.postMessage('stop');
this.tick = 0;
}
setInterval(interval) {
this.worker.postMessage({ interval });
} }
} }

View File

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

View File

@ -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 { ClockWorker } from './clockworker.mjs';
import { State, TimeSpan } from '@strudel.cycles/core';
export class Scheduler { export class Scheduler {
worker; worker;
pattern; pattern;
constructor({ audioContext, interval = 0.2, onEvent, latency = 0.2 }) { phase;
this.worker = new ClockWorker( audioContext;
audioContext, cps = 1;
(begin, end) => { constructor({ audioContext, interval = 0.1, onEvent, latency = 0.1 }) {
this.pattern.query(new State(new TimeSpan(begin + latency, end + latency))).forEach((e) => { this.audioContext = audioContext;
if (!e.part.begin.equals(e.whole.begin)) { this.worker = new ClockWorker((tick, interval) => {
return; const begin = this.phase;
} const end = this.phase + interval * this.cps;
if (e.context.onTrigger) { this.phase = end;
// TODO: kill first param, as it's contained in e const haps = this.pattern.queryArc(begin, end);
e.context.onTrigger(e.whole.begin, e, audioContext.currentTime, 1 /* cps */); haps.forEach((e) => {
} if (typeof e.value?.cps === 'number') {
if (onEvent) { this.setCps(e.value?.cps);
onEvent?.(e); }
} if (!e.part.begin.equals(e.whole.begin)) {
}); return;
}, }
interval, 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() { start() {
if (!this.pattern) { if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.'); throw new Error('Scheduler: no pattern set! call .setPattern first.');
} }
this.audioContext.resume();
this.phase = 0;
this.worker.start(); this.worker.start();
} }
stop() { stop() {
@ -42,4 +50,7 @@ export class Scheduler {
setPattern(pat) { setPattern(pat) {
this.pattern = pat; this.pattern = pat;
} }
setCps(cps = 1) {
this.cps = cps;
}
} }

View File

@ -125,7 +125,8 @@ const splitSN = (s, n) => {
}; };
Pattern.prototype.out = function () { 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 { try {
const ac = getAudioContext(); const ac = getAudioContext();
// calculate correct time (tone.js workaround) // calculate correct time (tone.js workaround)
@ -175,7 +176,7 @@ Pattern.prototype.out = function () {
freq = fromMidi(n); // + 48); freq = fromMidi(n); // + 48);
} }
// make oscillator // make oscillator
const o = getOscillator({ t, s, freq, duration: hap.duration, release }); const o = getOscillator({ t, s, freq, duration: hapDuration, release });
chain.push(o); chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested // level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain(); const g = ac.createGain();
@ -183,7 +184,7 @@ Pattern.prototype.out = function () {
chain.push(g); chain.push(g);
// TODO: make adsr work with samples without pops // TODO: make adsr work with samples without pops
// envelope // 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); chain.push(adsr);
} else { } else {
// load sample // load sample
@ -221,7 +222,7 @@ Pattern.prototype.out = function () {
} }
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop // 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; // let duration = bufferSource.buffer.duration;
const offset = begin * duration; const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed); duration = ((end - begin) * duration) / Math.abs(speed);