mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-18 00:26:04 -04:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3af2210c5e | |||
| d6aa73fd28 | |||
| f0083cfe45 | |||
| 66f32dd678 | |||
| 7faf1e1366 | |||
| e6ae16ca51 | |||
| 6af5250501 | |||
| 7c0dd9a6cc | |||
| 5f6b2223e1 | |||
| 864157ac84 | |||
| c89ee5ddb3 | |||
| 5a255350b4 | |||
| 8919524432 | |||
| f79a64de60 | |||
| 3fdeccdb23 | |||
| fb52227a92 | |||
| e376f25d92 | |||
| d2a715af0f | |||
| 3c230986e7 | |||
| f529d09227 | |||
| b7988f2158 | |||
| 2a0d8c3f77 | |||
| 429fcaf05a | |||
| 4603d16162 | |||
| 6d4a7f96eb | |||
| 4c277afea9 |
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
@@ -19,3 +19,25 @@ cd repl
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Publish Packages
|
||||
|
||||
To publish, just run:
|
||||
|
||||
```sh
|
||||
npx lerna version
|
||||
```
|
||||
|
||||
This will publish all packages that changed since the last version.
|
||||
|
||||
## Style
|
||||
|
||||
For now, please try to copy the style of surrounding code. VS Code users can install the 'prettier' add-on which will use the .prettierrc configuration file for automatic formatting.
|
||||
|
||||
## Community
|
||||
|
||||
There is a #strudel channel on the TidalCycles discord: https://discord.com/invite/HGEdXmRkzT
|
||||
|
||||
You can also ask questions and find related discussions on the tidal club forum: https://club.tidalcycles.org/
|
||||
|
||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||
@@ -0,0 +1,26 @@
|
||||
<input
|
||||
type="text"
|
||||
id="text"
|
||||
value="cat('a', 'b')"
|
||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
||||
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||
const input = document.getElementById('text');
|
||||
const getEvents = () => {
|
||||
const code = document.getElementById('text').value;
|
||||
const pattern = eval(code);
|
||||
const events = pattern.firstCycle();
|
||||
console.log(code, '->', events);
|
||||
document.getElementById('output').innerHTML = events.map((e) => e.show()).join('<br/>');
|
||||
};
|
||||
getEvents();
|
||||
input.addEventListener('input', () => getEvents());
|
||||
</script>
|
||||
<p>
|
||||
This page shows how skypack can be used to import strudel core directly into a simple html page. <br />
|
||||
No server, no bundler and no build setup is needed to run this!
|
||||
</p>
|
||||
@@ -0,0 +1,35 @@
|
||||
<body style="margin: 0">
|
||||
<input
|
||||
type="text"
|
||||
id="text"
|
||||
value="cat('lime', slowcat('steelblue','darkblue'),'yellow','salmon','black').every(2,rev).fast(16)"
|
||||
style="width: 100%; font-size: 1.5em; background: black; color: white; outline: none; position: absolute; top: 0"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
||||
// this adds all strudel functions to the global scope, to be used by eval
|
||||
Object.assign(window, strudel);
|
||||
// setup elements
|
||||
const input = document.getElementById('text');
|
||||
const canvas = document.getElementById('canvas');
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
input.focus(); // autofocus
|
||||
input.setSelectionRange(input.value.length, input.value.length); // move cursor to end
|
||||
paint(input.value); // initial paint
|
||||
input.addEventListener('input', (e) => paint(e.target.value)); // repaint on input
|
||||
|
||||
function paint(code) {
|
||||
const pattern = eval(code); // run code
|
||||
const events = pattern.firstCycle(); // query first cycle
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
|
||||
events.forEach((event) => {
|
||||
ctx.fillStyle = event.value;
|
||||
ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration * canvas.width, canvas.height);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
@@ -0,0 +1,237 @@
|
||||
<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 'https://cdn.skypack.dev/@strudel.cycles/core@0.0.2';
|
||||
import 'https://cdn.skypack.dev/@strudel.cycles/core@0.0.2/euclid.mjs';
|
||||
const { cat, State, TimeSpan } = strudel;
|
||||
let pattern;
|
||||
Object.assign(window, strudel); // add strudel to eval scope
|
||||
const input = document.getElementById('text');
|
||||
|
||||
let initialCode = `sequence(880, [440, 660], 440, 660)
|
||||
.div(slowcat(3,2).slow(2))
|
||||
.off(1/8,mul(2))
|
||||
.off(1/4,mul(3))
|
||||
.legato(2)
|
||||
.slow(2)`;
|
||||
try {
|
||||
initialCode = atob(decodeURIComponent(window.location.href.split('#')[1]));
|
||||
} catch (err) {
|
||||
console.warn('failed to decode', err);
|
||||
}
|
||||
input.value = initialCode;
|
||||
|
||||
const evaluate = () => {
|
||||
try {
|
||||
pattern = eval(input.value);
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
input.addEventListener('input', () => evaluate());
|
||||
|
||||
// helpers to create a worker dynamically without needing a server / extra file
|
||||
const stringifyFunction = (func) => '(' + func + ')();';
|
||||
const urlifyFunction = (func) =>
|
||||
URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
|
||||
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||
|
||||
const interval = 0.1;
|
||||
const lookahead = 0.5;
|
||||
|
||||
// this class is basically the tale of two clocks
|
||||
class Metro {
|
||||
worker;
|
||||
audioContext;
|
||||
interval = 0.2; // query span
|
||||
lastEnd = 0;
|
||||
constructor(audioContext, callback, interval = this.interval) {
|
||||
this.audioContext = audioContext;
|
||||
this.interval = interval;
|
||||
this.worker = createWorker(() => {
|
||||
// we cannot use closures here!
|
||||
let interval;
|
||||
let timerID = null; // this is clock #1 (the sloppy js clock)
|
||||
const clear = () => {
|
||||
if (timerID) {
|
||||
clearInterval(timerID);
|
||||
timerID = null;
|
||||
}
|
||||
};
|
||||
const start = () => {
|
||||
clear();
|
||||
if (!interval) {
|
||||
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
|
||||
}
|
||||
timerID = setInterval(() => postMessage('tick'), interval * 1000);
|
||||
};
|
||||
self.onmessage = function (e) {
|
||||
if (e.data == 'start') {
|
||||
start();
|
||||
} else if (e.data.interval) {
|
||||
interval = e.data.interval;
|
||||
if (timerID) {
|
||||
start();
|
||||
}
|
||||
} else if (e.data == 'stop') {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
});
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
start() {
|
||||
console.log('start...');
|
||||
this.audioContext.resume();
|
||||
this.worker.postMessage('start');
|
||||
}
|
||||
stop() {
|
||||
console.log('stop...');
|
||||
this.worker.postMessage('stop');
|
||||
}
|
||||
}
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
const metro = new Metro(
|
||||
audioContext,
|
||||
(begin, end) => {
|
||||
pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
|
||||
if (!e.part.begin.equals(e.whole.begin)) {
|
||||
return;
|
||||
}
|
||||
if (e.context.createAudioNode) {
|
||||
e.context.createAudioNode(e);
|
||||
} else {
|
||||
// fallback sine wave
|
||||
const attack = 0.01;
|
||||
const decay = 0.05;
|
||||
const sustain = 0.5;
|
||||
const release = 0.01;
|
||||
const velocity = (e.context?.velocity || 1) * 0.1;
|
||||
const osc = audioContext.createOscillator();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.value = e.value;
|
||||
osc.start(e.whole.begin);
|
||||
const envelope = adsr(attack, decay, sustain, release, velocity, e.whole.begin, e.whole.end);
|
||||
osc.stop(e.whole.end + release);
|
||||
osc.connect(envelope);
|
||||
envelope.connect(audioContext.destination);
|
||||
}
|
||||
});
|
||||
},
|
||||
interval,
|
||||
lookahead,
|
||||
);
|
||||
|
||||
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = audioContext.createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||
// for some reason, using exponential ramping creates little cracklings
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
strudel.Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||
return this._withEvent((event) => {
|
||||
return event.setContext({
|
||||
...event.context,
|
||||
createAudioNode: (e) => createAudioNode(e, event.context.createAudioNode?.(event)),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
strudel.Pattern.prototype._osc = function (type) {
|
||||
return this.withAudioNode((e) => {
|
||||
const osc = audioContext.createOscillator();
|
||||
osc.type = type;
|
||||
osc.frequency.value = e.value; // expects frequency..
|
||||
osc.start(e.whole.begin.valueOf() + lookahead);
|
||||
osc.stop(e.whole.end.valueOf() + lookahead); // release?
|
||||
// osc.connect(audioContext.destination);
|
||||
return osc;
|
||||
});
|
||||
};
|
||||
strudel.Pattern.prototype.define('osc', (type, pat) => pat.osc(type), { patternified: true });
|
||||
strudel.Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
|
||||
return this.withAudioNode((e, node) => {
|
||||
const velocity = e.context?.velocity || 1;
|
||||
const envelope = adsr(
|
||||
a,
|
||||
d,
|
||||
s,
|
||||
r,
|
||||
velocity,
|
||||
e.whole.begin.valueOf() + lookahead,
|
||||
e.whole.end.valueOf() + lookahead,
|
||||
);
|
||||
node?.connect(envelope);
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
strudel.Pattern.prototype.filter = function (type = 'lowshelf', frequency = 1000, gain = 25) {
|
||||
return this.withAudioNode((e, node) => {
|
||||
const filter = audioContext.createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.gain.value = gain;
|
||||
node?.connect(filter);
|
||||
return filter;
|
||||
});
|
||||
};
|
||||
|
||||
strudel.Pattern.prototype.out = function () {
|
||||
const master = audioContext.createGain();
|
||||
master.gain.value = 0.1;
|
||||
master.connect(audioContext.destination);
|
||||
return this.withAudioNode((e, node) => {
|
||||
if (!node) {
|
||||
console.warn('out: no source! call .osc() first');
|
||||
}
|
||||
node?.connect(master);
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('start').addEventListener('click', () => metro.start());
|
||||
document.getElementById('stop').addEventListener('click', () => metro.stop());
|
||||
evaluate(); // evaluate initial code
|
||||
/*
|
||||
sequence(1,3/2,1,2).stack(
|
||||
stack(5,3,4).velocity(.8).slow(2).legato(.2)
|
||||
//.echo(3, 1/4, .5)
|
||||
).mul(110) // frequencies
|
||||
.div(slowcat(slowcat(2,5/4, 3/2),slowcat(3,4/3)).slow(2))
|
||||
.echoWith(
|
||||
16, // n of partials / cutoff
|
||||
tri2.slow(128).div(2), // time between partials
|
||||
(x,n)=>x // n = partial index 0..n
|
||||
.mul((n+1)*3/2)
|
||||
.legato(2)
|
||||
.velocity(1/((n+1)**1.6)) // amplitude falloff
|
||||
).legato(2) // note length
|
||||
*/
|
||||
</script>
|
||||
+937
-819
File diff suppressed because it is too large
Load Diff
@@ -7,3 +7,7 @@ This package adds midi functionality to strudel Patterns.
|
||||
```sh
|
||||
npm i @strudel.cycles/midi --save
|
||||
```
|
||||
|
||||
## Dev Notes
|
||||
|
||||
- is this package really necessary? currently, /tone also depends on webmidi through @tonejs/piano. Either move piano out of /tone or merge /midi into /tone...
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "midi.mjs",
|
||||
"repository": {
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.0.2",
|
||||
"@strudel.cycles/tone": "^0.0.3",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ const pattern = mini('a [b c*2]');
|
||||
|
||||
const events = pattern.firstCycle().map((e) => e.show());
|
||||
console.log(events);
|
||||
|
||||
document.getElementById('app').innerHTML = events.join('<br/>');
|
||||
```
|
||||
|
||||
yields:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "mini.mjs",
|
||||
"type": "module",
|
||||
@@ -26,6 +26,6 @@
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/eval": "^0.0.2",
|
||||
"@strudel.cycles/tone": "^0.0.2"
|
||||
"@strudel.cycles/tone": "^0.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,33 @@ This package adds Tone.js functions to strudel Patterns.
|
||||
npm i @strudel.cycles/tone --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
The following example will create a pattern and play it back with tone.js:
|
||||
|
||||
```js
|
||||
import { sequence, stack, State, TimeSpan } from '@strudel.cycles/core';
|
||||
import { Tone, polysynth, osc, out } from '@strudel.cycles/tone';
|
||||
|
||||
const pattern = sequence('c3', ['eb3', stack('g3', 'bb3')]).tone(polysynth().set(osc('sawtooth4')).chain(out()));
|
||||
|
||||
document.getElementById('play').addEventListener('click', async () => {
|
||||
await Tone.start();
|
||||
Tone.getTransport().stop();
|
||||
const events = pattern.query(new State(new TimeSpan(0, 4))).filter((e) => e.whole.begin.equals(e.part.begin));
|
||||
events.forEach((event) =>
|
||||
Tone.getTransport().schedule((time) => event.context.onTrigger(time, event), event.whole.begin.valueOf()),
|
||||
);
|
||||
Tone.getTransport().start('+0.1');
|
||||
});
|
||||
```
|
||||
|
||||
[open in codesandbox](https://codesandbox.io/s/strudel-tone-example-5ph2te?file=/src/index.js:0-708)
|
||||
|
||||
## API
|
||||
|
||||
See "Tone API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
- `@tonejs/piano` has peer dependency on `webmidi@^2.5.1`! A newer version of webmidi will break.
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "tone.mjs",
|
||||
"type": "module",
|
||||
|
||||
Reference in New Issue
Block a user