move scheduler to core + move audioctx to userland

This commit is contained in:
Felix Roos 2022-08-17 21:26:00 +02:00
parent 1cd31cfce9
commit 7af6188a8a
5 changed files with 55 additions and 43 deletions

View File

@ -1,6 +1,6 @@
/* /*
clockworker.mjs - <short description TODO> clockworker.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/webaudio/clockworker.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */

View File

@ -1,8 +1,8 @@
<div style="position: absolute; top: 0; right: 0; padding: 4px"> <div style="position: absolute; bottom: 0; right: 0; padding: 4px; width: 100vw; display: flex">
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br /> <button id="start" style="font-size: 2em">start</button>
<button id="stop" style="margin-bottom: 4px; font-size: 2em">stop</button><br /> <button id="stop" style="font-size: 2em">stop</button>
<button id="slower" style="font-size: 2em">-</button> <button id="slower" style="font-size: 2em">slower</button>
<button id="faster" style="font-size: 2em">+</button> <button id="faster" style="font-size: 2em">faster</button>
</div> </div>
<textarea <textarea
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0" style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
@ -14,31 +14,47 @@ 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 util from '@strudel.cycles/core/util.mjs';
import '@strudel.cycles/core/euclid.mjs';
import { Scheduler, getAudioContext } from '../index.mjs';
const { cat, State, TimeSpan } = strudel; const { cat, State, TimeSpan, Scheduler, getPlayableNoteValue, getFreq } = strudel;
Object.assign(window, strudel); // add strudel to eval scope Object.assign(window, strudel); // add strudel to eval scope
const ctx = new AudioContext();
const scheduler = new Scheduler({ const scheduler = new Scheduler({
audioContext: getAudioContext(), // audioContext: getAudioContext(),
interval: 0.1, interval: 0.1,
onEvent: (e) => { onTrigger: (hap, time, duration) => {
e.context?.createAudioNode?.(e); const freq = getFrequency(hap);
const osc = ctx.createOscillator();
const gain = 0.2;
osc.frequency.value = freq;
osc.type = 'triangle';
const onset = ctx.currentTime + time;
const offset = onset + duration;
const attack = 0.05;
const release = 0.05;
osc.start(onset);
osc.stop(offset + release);
const g = ctx.createGain();
g.gain.setValueAtTime(gain, onset);
g.gain.linearRampToValueAtTime(gain, onset + attack);
g.gain.setValueAtTime(gain, offset - release);
g.gain.linearRampToValueAtTime(0, offset);
osc.connect(g);
g.connect(ctx.destination);
}, },
}); });
let initialCode = `sequence(1,2).mul(55/2) // frequencies let initialCode = `sequence(1,2).mul(55/2) // frequencies
.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() .freq()
.velocity(.5) .velocity(.5)
.s('sawtooth') .s('sawtooth')
.cutoff(800) .cutoff(800)
.out() .out()
`; `;
try { try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]); const base64 = decodeURIComponent(window.location.href.split('#')[1]);
@ -59,8 +75,10 @@ Loading...</textarea
}; };
evaluate(); evaluate();
input.addEventListener('input', () => evaluate()); input.addEventListener('input', () => evaluate());
document.getElementById('start').addEventListener('click', () => {
document.getElementById('start').addEventListener('click', () => scheduler.start()); ctx.resume();
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('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1)); document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));

View File

@ -15,6 +15,8 @@ export * from './state.mjs';
export * from './timespan.mjs'; export * from './timespan.mjs';
export * from './util.mjs'; export * from './util.mjs';
export * from './speak.mjs'; export * from './speak.mjs';
export * from './clockworker.mjs';
export * from './scheduler.mjs';
export { default as gist } from './gist.js'; export { default as gist } from './gist.js';
// below won't work with runtime.mjs (json import fails) // below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json'; /* import * as p from './package.json';

View File

@ -1,6 +1,6 @@
/* /*
scheduler.mjs - <short description TODO> scheduler.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/webaudio/scheduler.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/scheduler.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@ -10,29 +10,24 @@ export class Scheduler {
worker; worker;
pattern; pattern;
phase; phase;
audioContext;
cps = 1; cps = 1;
constructor({ audioContext, interval = 0.1, onEvent, latency = 0.1 }) { constructor({ interval = 0.1, onTrigger, latency = 0.1 }) {
this.audioContext = audioContext; this.worker = new ClockWorker((_, interval) => {
this.worker = new ClockWorker((tick, interval) => {
const begin = this.phase; const begin = this.phase;
const end = this.phase + interval * this.cps; const end = this.phase + interval * this.cps;
this.phase = end; this.phase = end;
const haps = this.pattern.queryArc(begin, end); const haps = this.pattern.queryArc(begin, end);
haps.forEach((e) => { haps.forEach((hap) => {
if (typeof e.value?.cps === 'number') { if (typeof hap.value?.cps === 'number') {
this.setCps(e.value?.cps); this.setCps(hap.value?.cps);
} }
if (!e.part.begin.equals(e.whole.begin)) { if (!hap.part.begin.equals(hap.whole.begin)) {
return; return;
} }
if (e.context.onTrigger) { const deadline = (hap.whole.begin - begin) / this.cps + latency;
const ctxTime = (e.whole.begin - begin) / this.cps + this.audioContext.currentTime + latency; // TODO: use legato / duration of objectified value
e.context.onTrigger(ctxTime, e, this.audioContext.currentTime, this.cps); const duration = hap.duration / this.cps;
} onTrigger?.(hap, deadline, duration);
if (onEvent) {
onEvent?.(e);
}
}); });
}, interval); }, interval);
} }
@ -40,7 +35,6 @@ export class Scheduler {
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.phase = 0;
this.worker.start(); this.worker.start();
} }

View File

@ -4,7 +4,5 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export * from './clockworker.mjs';
export * from './scheduler.mjs';
export * from './webaudio.mjs'; export * from './webaudio.mjs';
export * from './sampler.mjs'; export * from './sampler.mjs';