mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-22 02:58:32 +00:00
Merge pull request #181 from tidalcycles/optimize-scheduler
scheduler improvements
This commit is contained in:
commit
72f4d3efa8
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user