move core repl + add package

This commit is contained in:
Felix Roos 2022-04-01 20:54:54 +02:00
parent 47a046ca1d
commit 7fb9c3bf1e
9 changed files with 8982 additions and 154 deletions

3
.gitignore vendored
View File

@ -26,4 +26,5 @@ node_modules/
.DS_Store
repl-parcel
mytunes.ts
doc
doc
.parcel-cache

5129
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,17 +4,14 @@ const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction
const createWorker = (func) => new Worker(urlifyFunction(func));
// this class is basically the tale of two clocks
class Metro {
class ClockWorker {
worker;
audioContext;
startedAt = 0;
lastEnd;
lookahead = 0.2; // query offset
interval = 0.2; // query span
constructor(audioContext, callback, interval = this.interval, lookahead = this.lookahead) {
lastEnd = 0;
constructor(audioContext, callback, interval = this.interval) {
this.audioContext = audioContext;
this.interval = interval;
this.lookahead = lookahead;
this.worker = createWorker(() => {
// we cannot use closures here!
let interval;
@ -46,25 +43,27 @@ class Metro {
};
});
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.startedAt + this.lookahead;
const end = begin + this.interval;
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, this.lookahead);
callback(begin, end);
}
};
}
start() {
console.log('start...');
this.audioContext.resume();
delete this.lastEnd;
this.startedAt = this.audioContext.currentTime;
this.worker.postMessage('start');
}
stop() {
console.log('stop...');
this.worker.postMessage('stop');
}
}
export default Metro;
export default ClockWorker;

View File

@ -0,0 +1,62 @@
<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>
</div>
<textarea
style="font-size: 2em; background: #e8d565; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
id="text"
spellcheck="false"
>
Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from '@strudel.cycles/core';
import '@strudel.cycles/core/euclid.mjs';
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
const { cat, State, TimeSpan } = strudel;
Object.assign(window, strudel); // add strudel to eval scope
const audioContext = getAudioContext();
const interval = 0.1;
const lookahead = 0.5;
const scheduler = new Scheduler(audioContext, interval, lookahead);
let initialCode = `sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.velocity(.5)
.osc(cat('sawtooth','square').fast(2))
.adsr(0.01,.02,.5,0.1)
.filter('lowshelf',800,25)
.out()`;
try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
initialCode = atob(base64);
} catch (err) {
console.warn('failed to decode', err);
}
const input = document.getElementById('text');
input.value = initialCode;
const evaluate = () => {
try {
const pattern = eval(input.value);
console.log('value', input.value);
console.log('pattern', pattern);
scheduler.setPattern(pattern);
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
} catch (err) {
console.warn(err);
}
};
evaluate();
input.addEventListener('input', () => evaluate());
document.getElementById('start').addEventListener('click', () => scheduler.start());
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
</script>

View File

@ -0,0 +1,3 @@
export { default as ClockWorker } from './clockworker.mjs';
export { default as Scheduler } from './scheduler.mjs';
export * from './webaudio.mjs';

3851
packages/webaudio/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.0.1",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"directories": {
"example": "examples"
},
"scripts": {
"example": "parcel examples/repl.html"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"parcel": "^2.4.1"
}
}

View File

@ -1,10 +1,13 @@
import ClockWorker from './clockworker.mjs';
class Scheduler {
metro;
constructor(interval = 0.2, lookahead = 0.2) {
this.metro = new Metro(
worker;
pattern;
constructor(audioContext, interval = 0.2) {
this.worker = new ClockWorker(
audioContext,
(begin, end, lookahead) => {
pattern.query(new State(new TimeSpan(begin - lookahead, end - lookahead))).forEach((e) => {
(begin, end) => {
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
@ -16,13 +19,20 @@ class Scheduler {
});
},
interval,
lookahead,
);
}
start() {
this.metro.start();
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.worker.start();
}
stop() {
this.metro.stop();
this.worker.stop();
}
setPattern(pat) {
this.pattern = pat;
}
}
export default Scheduler;

View File

@ -1,7 +1,12 @@
import { Pattern } from '@strudel.cycles/core';
let audioContext;
export const getAudioContext = () => audioContext || new AudioContext();
export const getAudioContext = () => {
if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext;
};
const lookahead = 0.2;