mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-15 15:23:54 -04:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3abe6db3de | |||
| b9ca9d3b06 | |||
| 9bbc04843d | |||
| ddf61a6438 | |||
| ba35a81e9b | |||
| 13133583ca | |||
| e91b981740 | |||
| 1458ee64ec | |||
| 4e2a5e1663 | |||
| c98b0e9687 | |||
| 4e25990de9 | |||
| 4b51f1a3fc | |||
| 5490b01004 | |||
| 43176e1f1d | |||
| fffd3c07ca | |||
| 952bd38e70 | |||
| 8589f5cc89 | |||
| ec594d52f8 | |||
| 848cbf9c60 | |||
| e02be7dd9a | |||
| 33d35e47a6 | |||
| 18d62b32c2 | |||
| 58ed51b49c | |||
| 9f9fe529e4 | |||
| aad721890a | |||
| e63a5d10c8 | |||
| 15681262de | |||
| b5d3aa81f9 | |||
| a688061616 | |||
| f788d347ec | |||
| a33ce67dd4 | |||
| 8a22c5627a | |||
| 7b529a30f4 | |||
| 4910b34f26 | |||
| 6a73b43e3a | |||
| b59b26460d | |||
| aa324ae25d | |||
| ff412f6396 | |||
| ac148b2f32 | |||
| 6f5d096e6d | |||
| ff1e6714c6 | |||
| 41d82eaadf | |||
| be6b011204 | |||
| 65e48c05f0 | |||
| cee08ea67d | |||
| 35ef26c013 | |||
| b08a0b8102 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 6059c69995 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 5506d18bc5 |
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
```bash
|
||||
npm run setup
|
||||
npm run repl
|
||||
pnpm run setup
|
||||
pnpm run repl
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"lerna": "^6.5.1",
|
||||
"prettier": "^2.8.1",
|
||||
"rollup-plugin-visualizer": "^5.8.1"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ const generic_params = [
|
||||
*
|
||||
*/
|
||||
[['s', 'n', 'gain'], 'sound'],
|
||||
/**
|
||||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
*/
|
||||
['source', 'src'],
|
||||
/**
|
||||
* Selects the given index from the sample map.
|
||||
* Numbers too high will wrap around.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.6.8",
|
||||
"version": "0.7.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1127,6 +1127,8 @@ function _composeOp(a, b, func) {
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
export const pm = polymeter;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||
Pattern.prototype.factories = {
|
||||
@@ -1284,6 +1286,20 @@ export function timeCat(...timepats) {
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to arrange multiple patterns together over multiple cycles.
|
||||
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
|
||||
*/
|
||||
export function arrange(...sections) {
|
||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||
return timeCat(...sections).slow(total);
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
@@ -1360,11 +1376,6 @@ export function polymeter(...args) {
|
||||
return polymeterSteps(0, ...args);
|
||||
}
|
||||
|
||||
// alias
|
||||
export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
export const mask = curry((a, b) => reify(b).mask(a));
|
||||
export const struct = curry((a, b) => reify(b).struct(a));
|
||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||
|
||||
@@ -4,7 +4,7 @@ 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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
||||
}
|
||||
note = note ?? n;
|
||||
if (typeof note === 'string') {
|
||||
return toMidi(note);
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
|
||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
toMidi,
|
||||
fromMidi,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe('toMidi', () => {
|
||||
describe('noteToMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
expect(toMidi('A4')).toEqual(69);
|
||||
expect(toMidi('C4')).toEqual(60);
|
||||
expect(toMidi('Db4')).toEqual(61);
|
||||
expect(toMidi('C3')).toEqual(48);
|
||||
expect(toMidi('Cb3')).toEqual(47);
|
||||
expect(toMidi('Cbb3')).toEqual(46);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
expect(noteToMidi('A4')).toEqual(69);
|
||||
expect(noteToMidi('C4')).toEqual(60);
|
||||
expect(noteToMidi('Db4')).toEqual(61);
|
||||
expect(noteToMidi('C3')).toEqual(48);
|
||||
expect(noteToMidi('Cb3')).toEqual(47);
|
||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C##3')).toEqual(50);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
describe('midiToFreq', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
expect(midiToFreq(69)).toEqual(440);
|
||||
expect(midiToFreq(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('freqToMidi', () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const tokenizeNote = (note) => {
|
||||
};
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const toMidi = (note) => {
|
||||
export const noteToMidi = (note) => {
|
||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
@@ -28,7 +28,7 @@ export const toMidi = (note) => {
|
||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const fromMidi = (n) => {
|
||||
export const midiToFreq = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
return freqToMidi(freq);
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
return toMidi(note);
|
||||
return noteToMidi(note);
|
||||
}
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
@@ -62,9 +62,9 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
if (typeof noteOrMidi === 'number') {
|
||||
return fromMidi(noteOrMidi);
|
||||
return midiToFreq(noteOrMidi);
|
||||
}
|
||||
return fromMidi(toMidi(noteOrMidi));
|
||||
return midiToFreq(noteToMidi(noteOrMidi));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,7 @@ export const getPlayableNoteValue = (hap) => {
|
||||
}
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = fromMidi(hap.value);
|
||||
note = midiToFreq(hap.value);
|
||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||
note = hap.value; // legacy workaround.. will be removed in the future
|
||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||
@@ -110,9 +110,9 @@ export const getFrequency = (hap) => {
|
||||
return getFreq(value.note || value.n || value.value);
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(hap.value);
|
||||
value = midiToFreq(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
value = midiToFreq(noteToMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
}
|
||||
@@ -170,7 +170,7 @@ export function parseNumeral(numOrString) {
|
||||
return asNumber;
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return toMidi(numOrString);
|
||||
return noteToMidi(numOrString);
|
||||
}
|
||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.0",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.0",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { toMidi } from '@strudel.cycles/core';
|
||||
import { noteToMidi } from '@strudel.cycles/core';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
@@ -114,7 +114,7 @@ Pattern.prototype.midi = function (output) {
|
||||
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
const midiNumber = toMidi(note);
|
||||
const midiNumber = noteToMidi(note);
|
||||
device.playNote(midiNumber, midichan, {
|
||||
time,
|
||||
duration,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
+153
-226
@@ -180,8 +180,8 @@ function peg$parse(input, options) {
|
||||
var peg$c6 = "\"";
|
||||
var peg$c7 = "'";
|
||||
var peg$c8 = "#";
|
||||
var peg$c9 = "_";
|
||||
var peg$c10 = "^";
|
||||
var peg$c9 = "^";
|
||||
var peg$c10 = "_";
|
||||
var peg$c11 = "[";
|
||||
var peg$c12 = "]";
|
||||
var peg$c13 = "{";
|
||||
@@ -217,9 +217,7 @@ function peg$parse(input, options) {
|
||||
var peg$r2 = /^[0-9]/;
|
||||
var peg$r3 = /^[ \n\r\t]/;
|
||||
var peg$r4 = /^[0-9a-zA-Z~]/;
|
||||
var peg$r5 = /^[A-Za-z]/;
|
||||
var peg$r6 = /^[A-Za-z0-9_]/;
|
||||
var peg$r7 = /^[^\n]/;
|
||||
var peg$r5 = /^[^\n]/;
|
||||
|
||||
var peg$e0 = peg$otherExpectation("number");
|
||||
var peg$e1 = peg$literalExpectation(".", false);
|
||||
@@ -237,84 +235,81 @@ function peg$parse(input, options) {
|
||||
var peg$e13 = peg$literalExpectation("'", false);
|
||||
var peg$e14 = peg$classExpectation([["0", "9"], ["a", "z"], ["A", "Z"], "~"], false, false);
|
||||
var peg$e15 = peg$literalExpectation("#", false);
|
||||
var peg$e16 = peg$literalExpectation("_", false);
|
||||
var peg$e17 = peg$literalExpectation("^", false);
|
||||
var peg$e18 = peg$classExpectation([["A", "Z"], ["a", "z"]], false, false);
|
||||
var peg$e19 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_"], false, false);
|
||||
var peg$e20 = peg$literalExpectation("[", false);
|
||||
var peg$e21 = peg$literalExpectation("]", false);
|
||||
var peg$e22 = peg$literalExpectation("{", false);
|
||||
var peg$e23 = peg$literalExpectation("}", false);
|
||||
var peg$e24 = peg$literalExpectation("%", false);
|
||||
var peg$e25 = peg$literalExpectation("<", false);
|
||||
var peg$e26 = peg$literalExpectation(">", false);
|
||||
var peg$e27 = peg$literalExpectation("@", false);
|
||||
var peg$e28 = peg$literalExpectation("!", false);
|
||||
var peg$e29 = peg$literalExpectation("(", false);
|
||||
var peg$e30 = peg$literalExpectation(")", false);
|
||||
var peg$e31 = peg$literalExpectation("/", false);
|
||||
var peg$e32 = peg$literalExpectation("*", false);
|
||||
var peg$e33 = peg$literalExpectation("?", false);
|
||||
var peg$e34 = peg$literalExpectation(":", false);
|
||||
var peg$e35 = peg$literalExpectation("struct", false);
|
||||
var peg$e36 = peg$literalExpectation("target", false);
|
||||
var peg$e37 = peg$literalExpectation("euclid", false);
|
||||
var peg$e38 = peg$literalExpectation("slow", false);
|
||||
var peg$e39 = peg$literalExpectation("rotL", false);
|
||||
var peg$e40 = peg$literalExpectation("rotR", false);
|
||||
var peg$e41 = peg$literalExpectation("fast", false);
|
||||
var peg$e42 = peg$literalExpectation("scale", false);
|
||||
var peg$e43 = peg$literalExpectation("//", false);
|
||||
var peg$e44 = peg$classExpectation(["\n"], true, false);
|
||||
var peg$e45 = peg$literalExpectation("cat", false);
|
||||
var peg$e46 = peg$literalExpectation("$", false);
|
||||
var peg$e47 = peg$literalExpectation("setcps", false);
|
||||
var peg$e48 = peg$literalExpectation("setbpm", false);
|
||||
var peg$e49 = peg$literalExpectation("hush", false);
|
||||
var peg$e16 = peg$literalExpectation("^", false);
|
||||
var peg$e17 = peg$literalExpectation("_", false);
|
||||
var peg$e18 = peg$literalExpectation("[", false);
|
||||
var peg$e19 = peg$literalExpectation("]", false);
|
||||
var peg$e20 = peg$literalExpectation("{", false);
|
||||
var peg$e21 = peg$literalExpectation("}", false);
|
||||
var peg$e22 = peg$literalExpectation("%", false);
|
||||
var peg$e23 = peg$literalExpectation("<", false);
|
||||
var peg$e24 = peg$literalExpectation(">", false);
|
||||
var peg$e25 = peg$literalExpectation("@", false);
|
||||
var peg$e26 = peg$literalExpectation("!", false);
|
||||
var peg$e27 = peg$literalExpectation("(", false);
|
||||
var peg$e28 = peg$literalExpectation(")", false);
|
||||
var peg$e29 = peg$literalExpectation("/", false);
|
||||
var peg$e30 = peg$literalExpectation("*", false);
|
||||
var peg$e31 = peg$literalExpectation("?", false);
|
||||
var peg$e32 = peg$literalExpectation(":", false);
|
||||
var peg$e33 = peg$literalExpectation("struct", false);
|
||||
var peg$e34 = peg$literalExpectation("target", false);
|
||||
var peg$e35 = peg$literalExpectation("euclid", false);
|
||||
var peg$e36 = peg$literalExpectation("slow", false);
|
||||
var peg$e37 = peg$literalExpectation("rotL", false);
|
||||
var peg$e38 = peg$literalExpectation("rotR", false);
|
||||
var peg$e39 = peg$literalExpectation("fast", false);
|
||||
var peg$e40 = peg$literalExpectation("scale", false);
|
||||
var peg$e41 = peg$literalExpectation("//", false);
|
||||
var peg$e42 = peg$classExpectation(["\n"], true, false);
|
||||
var peg$e43 = peg$literalExpectation("cat", false);
|
||||
var peg$e44 = peg$literalExpectation("$", false);
|
||||
var peg$e45 = peg$literalExpectation("setcps", false);
|
||||
var peg$e46 = peg$literalExpectation("setbpm", false);
|
||||
var peg$e47 = peg$literalExpectation("hush", false);
|
||||
|
||||
var peg$f0 = function() { return parseFloat(text()); };
|
||||
var peg$f1 = function(chars) { return new AtomStub(chars.join("")) };
|
||||
var peg$f2 = function(h, t) { return new MemoryStub(h + t.join("")) };
|
||||
var peg$f3 = function(s) { return s };
|
||||
var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
|
||||
var peg$f5 = function(a) { return a };
|
||||
var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
|
||||
var peg$f7 = function(a) { return x => x.options_['weight'] = a };
|
||||
var peg$f8 = function(a) { return x => x.options_['reps'] = a };
|
||||
var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
|
||||
var peg$f13 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f14 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
var peg$f2 = function(s) { return s };
|
||||
var peg$f3 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
|
||||
var peg$f4 = function(a) { return a };
|
||||
var peg$f5 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
|
||||
var peg$f6 = function(a) { return x => x.options_['weight'] = a };
|
||||
var peg$f7 = function(a) { return x => x.options_['reps'] = a };
|
||||
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
|
||||
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||
for (const op of ops) {
|
||||
op(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var peg$f15 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f16 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f17 = function(tail) { return { alignment: 'rand', list: tail }; };
|
||||
var peg$f18 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
|
||||
var peg$f19 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f20 = function(sc) { return sc; };
|
||||
var peg$f21 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f22 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f23 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f24 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f25 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f26 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f27 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f28 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f29 = function(s, v) { return v};
|
||||
var peg$f30 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f31 = function(sg) {return sg};
|
||||
var peg$f32 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f33 = function(sc) { return sc };
|
||||
var peg$f34 = function(c) { return c };
|
||||
var peg$f35 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f36 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f37 = function() { return new CommandStub("hush")};
|
||||
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||
var peg$f19 = function(sc) { return sc; };
|
||||
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
|
||||
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||
var peg$f28 = function(s, v) { return v};
|
||||
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||
var peg$f30 = function(sg) {return sg};
|
||||
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||
var peg$f32 = function(sc) { return sc };
|
||||
var peg$f33 = function(c) { return c };
|
||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||
var peg$f36 = function() { return new CommandStub("hush")};
|
||||
var peg$currPos = 0;
|
||||
var peg$savedPos = 0;
|
||||
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||
@@ -839,7 +834,7 @@ function peg$parse(input, options) {
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
||||
}
|
||||
if (s0 === peg$FAILED) {
|
||||
if (input.charCodeAt(peg$currPos) === 95) {
|
||||
if (input.charCodeAt(peg$currPos) === 94) {
|
||||
s0 = peg$c9;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
@@ -847,7 +842,7 @@ function peg$parse(input, options) {
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e16); }
|
||||
}
|
||||
if (s0 === peg$FAILED) {
|
||||
if (input.charCodeAt(peg$currPos) === 94) {
|
||||
if (input.charCodeAt(peg$currPos) === 95) {
|
||||
s0 = peg$c10;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
@@ -890,68 +885,6 @@ function peg$parse(input, options) {
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parsememory() {
|
||||
var s0, s1, s2, s3, s4, s5;
|
||||
|
||||
s0 = peg$currPos;
|
||||
s1 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 94) {
|
||||
s2 = peg$c10;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
if (peg$r5.test(input.charAt(peg$currPos))) {
|
||||
s3 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = [];
|
||||
if (peg$r6.test(input.charAt(peg$currPos))) {
|
||||
s5 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s5 !== peg$FAILED) {
|
||||
while (s5 !== peg$FAILED) {
|
||||
s4.push(s5);
|
||||
if (peg$r6.test(input.charAt(peg$currPos))) {
|
||||
s5 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s4 = peg$FAILED;
|
||||
}
|
||||
if (s4 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f2(s3, s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
||||
function peg$parsesub_cycle() {
|
||||
var s0, s1, s2, s3, s4, s5, s6, s7;
|
||||
|
||||
@@ -962,7 +895,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -974,12 +907,12 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f3(s4);
|
||||
s0 = peg$f2(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1006,7 +939,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -1018,7 +951,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsepolymeter_steps();
|
||||
@@ -1027,7 +960,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
s8 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f4(s4, s7);
|
||||
s0 = peg$f3(s4, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1053,13 +986,13 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f5(s2);
|
||||
s0 = peg$f4(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1082,7 +1015,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s2 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
@@ -1094,12 +1027,12 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||
}
|
||||
if (s6 !== peg$FAILED) {
|
||||
s7 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f6(s4);
|
||||
s0 = peg$f5(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1119,16 +1052,13 @@ function peg$parse(input, options) {
|
||||
function peg$parseslice() {
|
||||
var s0;
|
||||
|
||||
s0 = peg$parsememory();
|
||||
s0 = peg$parsestep();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parsestep();
|
||||
s0 = peg$parsesub_cycle();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parsesub_cycle();
|
||||
s0 = peg$parsepolymeter();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parsepolymeter();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parseslow_sequence();
|
||||
}
|
||||
s0 = peg$parseslow_sequence();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1171,13 +1101,13 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f7(s2);
|
||||
s0 = peg$f6(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1199,13 +1129,13 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f8(s2);
|
||||
s0 = peg$f7(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1227,7 +1157,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1255,11 +1185,11 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s13 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
if (s13 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f9(s3, s7, s11);
|
||||
s0 = peg$f8(s3, s7, s11);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1293,13 +1223,13 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f10(s2);
|
||||
s0 = peg$f9(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1321,13 +1251,13 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f11(s2);
|
||||
s0 = peg$f10(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1349,7 +1279,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e33); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsenumber();
|
||||
@@ -1357,7 +1287,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f12(s2);
|
||||
s0 = peg$f11(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1375,13 +1305,13 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e34); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parseslice();
|
||||
if (s2 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f13(s2);
|
||||
s0 = peg$f12(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1407,7 +1337,7 @@ function peg$parse(input, options) {
|
||||
s3 = peg$parseslice_op();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f14(s1, s2);
|
||||
s0 = peg$f13(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1432,7 +1362,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f15(s1);
|
||||
s1 = peg$f14(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1481,7 +1411,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f16(s1);
|
||||
s1 = peg$f15(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1530,7 +1460,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f17(s1);
|
||||
s1 = peg$f16(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -1551,7 +1481,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f18(s1, s2);
|
||||
s0 = peg$f17(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1571,7 +1501,7 @@ function peg$parse(input, options) {
|
||||
s2 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f19(s1, s2);
|
||||
s0 = peg$f18(s1, s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1592,7 +1522,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsequote();
|
||||
if (s4 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f20(s3);
|
||||
s0 = peg$f19(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1647,14 +1577,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e35); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e33); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsemini_or_operator();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f21(s3);
|
||||
s0 = peg$f20(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1676,7 +1606,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e36); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e34); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1687,7 +1617,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s4);
|
||||
s0 = peg$f21(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1717,7 +1647,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e37); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e35); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1732,7 +1662,7 @@ function peg$parse(input, options) {
|
||||
s7 = null;
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f23(s3, s5, s7);
|
||||
s0 = peg$f22(s3, s5, s7);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1758,14 +1688,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e38); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e36); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f24(s3);
|
||||
s0 = peg$f23(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1787,14 +1717,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e39); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e37); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f25(s3);
|
||||
s0 = peg$f24(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1816,14 +1746,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e40); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e38); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f26(s3);
|
||||
s0 = peg$f25(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1845,14 +1775,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e41); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e39); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f27(s3);
|
||||
s0 = peg$f26(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1874,7 +1804,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 5;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e42); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e40); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1894,7 +1824,7 @@ function peg$parse(input, options) {
|
||||
s5 = peg$parsequote();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f28(s4);
|
||||
s0 = peg$f27(s4);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -1924,25 +1854,25 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 2;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e43); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e41); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = [];
|
||||
if (peg$r7.test(input.charAt(peg$currPos))) {
|
||||
if (peg$r5.test(input.charAt(peg$currPos))) {
|
||||
s3 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e44); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e42); }
|
||||
}
|
||||
while (s3 !== peg$FAILED) {
|
||||
s2.push(s3);
|
||||
if (peg$r7.test(input.charAt(peg$currPos))) {
|
||||
if (peg$r5.test(input.charAt(peg$currPos))) {
|
||||
s3 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e44); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e42); }
|
||||
}
|
||||
}
|
||||
s1 = [s1, s2];
|
||||
@@ -1964,7 +1894,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 3;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e45); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e43); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
@@ -1973,7 +1903,7 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
@@ -1986,7 +1916,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f29(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -2003,7 +1933,7 @@ function peg$parse(input, options) {
|
||||
s9 = peg$parsemini_or_operator();
|
||||
if (s9 !== peg$FAILED) {
|
||||
peg$savedPos = s7;
|
||||
s7 = peg$f29(s5, s9);
|
||||
s7 = peg$f28(s5, s9);
|
||||
} else {
|
||||
peg$currPos = s7;
|
||||
s7 = peg$FAILED;
|
||||
@@ -2019,11 +1949,11 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s8 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||
}
|
||||
if (s8 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f30(s5, s6);
|
||||
s0 = peg$f29(s5, s6);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2069,7 +1999,7 @@ function peg$parse(input, options) {
|
||||
s4 = peg$parsecomment();
|
||||
}
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f31(s1);
|
||||
s0 = peg$f30(s1);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2084,14 +2014,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s3 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e46); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e44); }
|
||||
}
|
||||
if (s3 !== peg$FAILED) {
|
||||
s4 = peg$parsews();
|
||||
s5 = peg$parsemini_or_operator();
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f32(s1, s5);
|
||||
s0 = peg$f31(s1, s5);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2116,7 +2046,7 @@ function peg$parse(input, options) {
|
||||
s1 = peg$parsemini_or_operator();
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f33(s1);
|
||||
s1 = peg$f32(s1);
|
||||
}
|
||||
s0 = s1;
|
||||
if (s0 === peg$FAILED) {
|
||||
@@ -2149,7 +2079,7 @@ function peg$parse(input, options) {
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f34(s2);
|
||||
s0 = peg$f33(s2);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2167,14 +2097,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e47); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e45); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f35(s3);
|
||||
s0 = peg$f34(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2196,14 +2126,14 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 6;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e48); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e46); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parsews();
|
||||
s3 = peg$parsenumber();
|
||||
if (s3 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f36(s3);
|
||||
s0 = peg$f35(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
@@ -2225,11 +2155,11 @@ function peg$parse(input, options) {
|
||||
peg$currPos += 4;
|
||||
} else {
|
||||
s1 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e49); }
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e47); }
|
||||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s1 = peg$f37();
|
||||
s1 = peg$f36();
|
||||
}
|
||||
s0 = s1;
|
||||
|
||||
@@ -2255,17 +2185,13 @@ function peg$parse(input, options) {
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var MemoryStub = function(source)
|
||||
{
|
||||
this.type_ = "memory";
|
||||
this.source_ = source;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -2291,6 +2217,7 @@ function peg$parse(input, options) {
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
|
||||
peg$result = peg$startRuleFunction();
|
||||
|
||||
|
||||
+11
-16
@@ -19,17 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var MemoryStub = function(source)
|
||||
{
|
||||
this.type_ = "memory";
|
||||
this.source_ = source;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var PatternStub = function(source, alignment)
|
||||
var PatternStub = function(source, alignment, seed)
|
||||
{
|
||||
this.type_ = "pattern";
|
||||
this.arguments_ = { alignment : alignment};
|
||||
this.arguments_ = { alignment: alignment };
|
||||
if (seed !== undefined) {
|
||||
this.arguments_.seed = seed;
|
||||
}
|
||||
this.source_ = source;
|
||||
}
|
||||
|
||||
@@ -55,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
var seed = 0;
|
||||
}
|
||||
|
||||
start = statement
|
||||
@@ -103,11 +100,9 @@ quote = '"' / "'"
|
||||
// ------------------ steps and cycles ---------------------------
|
||||
|
||||
// single step definition (e.g bd)
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "_" / "^"
|
||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
|
||||
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||
|
||||
memory = ws "^" h:[A-Za-z] t:[A-Za-z0-9_]+ { return new MemoryStub(h + t.join("")) }
|
||||
|
||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
|
||||
|
||||
@@ -124,7 +119,7 @@ slow_sequence = ws "<" ws s:sequence ws ">" ws
|
||||
{ s.arguments_.alignment = 'slowcat'; return s; }
|
||||
|
||||
// a slice is either a single step or a sub cycle
|
||||
slice = memory / step / sub_cycle / polymeter / slow_sequence
|
||||
slice = step / sub_cycle / polymeter / slow_sequence
|
||||
|
||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||
// at this point, we assume we can represent them as regular sequence operators
|
||||
@@ -146,7 +141,7 @@ op_fast = "*"a:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||
|
||||
op_degrade = "?"a:number?
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
|
||||
|
||||
op_tail = ":" s:slice
|
||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||
@@ -171,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
|
||||
// a choose is a series of pipe-separated sequence, one of which is
|
||||
// chosen at random, each cycle
|
||||
choose_tail = tail:(pipe @sequence)+
|
||||
{ return { alignment: 'rand', list: tail }; }
|
||||
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
|
||||
|
||||
// if the stack contains only one element, we don't create a stack but return the
|
||||
// underlying element
|
||||
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
||||
|
||||
polymeter_stack = head:sequence tail:stack_tail?
|
||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||
|
||||
+5
-51
@@ -7,23 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
/* var _seedState = 0;
|
||||
const randOffset = 0.0002;
|
||||
|
||||
function _nextSeed() {
|
||||
return _seedState++;
|
||||
} */
|
||||
|
||||
const memory = {};
|
||||
|
||||
export const remember = (name, pat) => {
|
||||
if (pat) {
|
||||
memory[name] = pat;
|
||||
} else {
|
||||
pat = memory[name];
|
||||
}
|
||||
return pat;
|
||||
};
|
||||
const randOffset = 0.0003;
|
||||
|
||||
const applyOptions = (parent, code) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
@@ -55,27 +39,9 @@ const applyOptions = (parent, code) => (pat, i) => {
|
||||
break;
|
||||
}
|
||||
case 'degradeBy': {
|
||||
// TODO: find out what is right here
|
||||
// example:
|
||||
/*
|
||||
stack(
|
||||
s("hh*8").degrade(),
|
||||
s("[ht*8]?")
|
||||
)
|
||||
*/
|
||||
// above example will only be in sync when _degradeBy is used...
|
||||
// it also seems that the nextSeed will create undeterministic behaviour
|
||||
// as it uses a global _seedState. This is probably the reason for
|
||||
// https://github.com/tidalcycles/strudel/issues/245
|
||||
|
||||
// this is how it was:
|
||||
/*
|
||||
return strudel.reify(pat)._degradeByWith(
|
||||
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
||||
op.arguments_.amount ?? 0.5,
|
||||
);
|
||||
*/
|
||||
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
||||
pat = strudel
|
||||
.reify(pat)
|
||||
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
||||
break;
|
||||
}
|
||||
case 'tail': {
|
||||
@@ -125,9 +91,7 @@ export function patternifyAST(ast, code) {
|
||||
return strudel.stack(...aligned);
|
||||
}
|
||||
if (alignment === 'rand') {
|
||||
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
|
||||
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
||||
return strudel.chooseCycles(...children);
|
||||
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||
}
|
||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||
if (!weightedChildren && alignment === 'slowcat') {
|
||||
@@ -149,16 +113,6 @@ export function patternifyAST(ast, code) {
|
||||
case 'element': {
|
||||
return patternifyAST(ast.source_, code);
|
||||
}
|
||||
case 'memory': {
|
||||
const name = ast.source_;
|
||||
if (name in memory) {
|
||||
return new strudel.Pattern((state) => {
|
||||
return memory[name].query(state);
|
||||
});
|
||||
}
|
||||
// TODO - how to default a memory.. via a list?
|
||||
return strudel.pure(undefined);
|
||||
}
|
||||
case 'atom': {
|
||||
if (ast.source_ === '~') {
|
||||
return strudel.silence;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -140,10 +140,22 @@ describe('mini', () => {
|
||||
expect(haps.length < 230).toBe(true);
|
||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||
});
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
it('supports multiple independent uses of the random choice operator ("|")', () => {
|
||||
const numCycles = 1000;
|
||||
const values = mini('[a|b] [a|b]')
|
||||
.queryArc(0, numCycles)
|
||||
.map((e) => e.value);
|
||||
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
|
||||
for (let i = 0; i < values.length; i += 2) {
|
||||
const chunk = values.slice(i, i + 2);
|
||||
observed[chunk.join('')]++;
|
||||
}
|
||||
for (const count of Object.values(observed)) {
|
||||
// Should fall within 99% confidence interval for binomial with p=0.25.
|
||||
expect(215 <= count && count <= 286).toBe(true);
|
||||
}
|
||||
});
|
||||
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||
it('supports the random choice operator ("|") with nesting', () => {
|
||||
const numCycles = 900;
|
||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||
@@ -168,6 +180,8 @@ describe('mini', () => {
|
||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||
// PRNG, this test should succeed
|
||||
expect(chisq <= 15.086).toBe(true);
|
||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||
});*/
|
||||
});
|
||||
it('supports lists', () => {
|
||||
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.6.4",
|
||||
"version": "0.7.0",
|
||||
"description": "React components for strudel",
|
||||
"main": "src/index.js",
|
||||
"publishConfig": {
|
||||
@@ -47,17 +47,17 @@
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
ReactDOM.render(
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { toMidi } from '@strudel.cycles/core';
|
||||
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
|
||||
let loadCache = {};
|
||||
async function loadFont(name) {
|
||||
@@ -8,7 +10,6 @@ async function loadFont(name) {
|
||||
const load = async () => {
|
||||
// TODO: make soundfont source configurable
|
||||
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
|
||||
console.log('load font', name, url);
|
||||
const preset = await fetch(url).then((res) => res.text());
|
||||
let [_, data] = preset.split('={');
|
||||
return eval('{' + data);
|
||||
@@ -17,15 +18,24 @@ async function loadFont(name) {
|
||||
return loadCache[name];
|
||||
}
|
||||
|
||||
export async function getFontBufferSource(name, pitch, ac) {
|
||||
if (typeof pitch === 'string') {
|
||||
pitch = toMidi(pitch);
|
||||
export async function getFontBufferSource(name, value, ac) {
|
||||
let { note = 'c3', freq } = value;
|
||||
let midi;
|
||||
if (freq) {
|
||||
midi = freqToMidi(freq);
|
||||
} else if (typeof note === 'string') {
|
||||
midi = noteToMidi(note);
|
||||
} else if (typeof note === 'number') {
|
||||
midi = note;
|
||||
} else {
|
||||
throw new Error(`unexpected "note" type "${typeof note}"`);
|
||||
}
|
||||
const { buffer, zone } = await getFontPitch(name, pitch, ac);
|
||||
|
||||
const { buffer, zone } = await getFontPitch(name, midi, ac);
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
||||
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
|
||||
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
|
||||
// src detune?
|
||||
src.playbackRate.value = playbackRate;
|
||||
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
|
||||
@@ -114,3 +124,34 @@ async function getBuffer(zone, audioContext) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSoundfonts() {
|
||||
Object.entries(gm).forEach(([name, fonts]) => {
|
||||
registerSound(
|
||||
name,
|
||||
async (time, value, onended) => {
|
||||
const { n = 0 } = value;
|
||||
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
|
||||
const font = fonts[n % fonts.length];
|
||||
const ctx = getAudioContext();
|
||||
const bufferSource = await getFontBufferSource(font, value, ctx);
|
||||
bufferSource.start(time);
|
||||
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
|
||||
bufferSource.connect(envelope);
|
||||
const stop = (releaseTime) => {
|
||||
bufferSource.stop(releaseTime + release);
|
||||
releaseEnvelope(releaseTime);
|
||||
};
|
||||
bufferSource.onended = () => {
|
||||
bufferSource.disconnect();
|
||||
envelope.disconnect();
|
||||
onended();
|
||||
};
|
||||
return { node: envelope, stop };
|
||||
},
|
||||
{ type: 'soundfont', prebake: true, fonts },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
registerSoundfonts();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { getFontBufferSource } from './fontloader.mjs';
|
||||
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
|
||||
import * as soundfontList from './list.mjs';
|
||||
import { startPresetNote } from 'sfumato';
|
||||
import { loadSoundfont } from './sfumato.mjs';
|
||||
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
|
||||
|
||||
+926
-665
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
|
||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||
|
||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
const note = getPlayableNoteValue(h);
|
||||
const preset = sf.presets[n % sf.presets.length];
|
||||
const deadline = ctx.currentTime + t - ct;
|
||||
const args = [ctx, preset, toMidi(note), deadline];
|
||||
const args = [ctx, preset, noteToMidi(note), deadline];
|
||||
const stop = startPresetNote(...args);
|
||||
stop(deadline + h.duration);
|
||||
});
|
||||
@@ -21,5 +21,29 @@ export function loadSoundfont(url) {
|
||||
}
|
||||
const sf = _loadSoundfont(url);
|
||||
soundfontCache.set(url, sf);
|
||||
/*sf.then((font) => {
|
||||
font.presets.forEach((preset) => {
|
||||
console.log('preset', preset.header.name);
|
||||
registerSound(
|
||||
preset.header.name.replaceAll(' ', '_'),
|
||||
(time, value, onended) => {
|
||||
const ctx = getAudioContext();
|
||||
let { note } = value; // freq ?
|
||||
|
||||
const p = font.presets.find((p) => p.header.name === preset.header.name);
|
||||
|
||||
if (!p) {
|
||||
throw new Error('preset not found');
|
||||
}
|
||||
const deadline = time; // - ctx.currentTime;
|
||||
const args = [ctx, p, noteToMidi(note), deadline];
|
||||
const stop = startPresetNote(...args);
|
||||
return { node: undefined, stop };
|
||||
},
|
||||
{ type: 'soundfont' },
|
||||
);
|
||||
});
|
||||
//console.log('f', f);
|
||||
});*/
|
||||
return sf;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/transpiler",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { getAudioContext } from './webaudio.mjs';
|
||||
|
||||
export function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
node.gain.value = value;
|
||||
return node;
|
||||
}
|
||||
|
||||
export const getOscillator = ({ s, freq, t }) => {
|
||||
// make oscillator
|
||||
const o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
o.frequency.value = Number(freq);
|
||||
o.start(t);
|
||||
//o.stop(t + duration + release);
|
||||
const stop = (time) => o.stop(time);
|
||||
return { node: o, stop };
|
||||
};
|
||||
|
||||
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
|
||||
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
|
||||
const gainNode = getAudioContext().createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||
// sustain end
|
||||
return {
|
||||
node: gainNode,
|
||||
stop: (t) => {
|
||||
//if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') {
|
||||
// gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly....
|
||||
// see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735
|
||||
//} else {
|
||||
// firefox: this will glitch when the sustain has not been reached yet at the time of release
|
||||
gainNode.gain.setValueAtTime(sustain * velocity, t);
|
||||
//}
|
||||
gainNode.gain.linearRampToValueAtTime(0, t + release);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().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
|
||||
/* let t = begin;
|
||||
gainNode.gain.setValueAtTime(0, t);
|
||||
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
|
||||
const sustainGain = Math.max(sustain * velocity, 0.001);
|
||||
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
|
||||
if (end - begin < attack + decay) {
|
||||
gainNode.gain.cancelAndHoldAtTime(end);
|
||||
} else {
|
||||
gainNode.gain.setValueAtTime(sustainGain, end);
|
||||
}
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
export const getFilter = (type, frequency, Q) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.Q.value = Q;
|
||||
return filter;
|
||||
};
|
||||
@@ -6,3 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
export * from './webaudio.mjs';
|
||||
export * from './sampler.mjs';
|
||||
export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.0",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -34,7 +34,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*"
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"nanostores": "^0.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2"
|
||||
|
||||
+129
-56
@@ -1,5 +1,6 @@
|
||||
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from './index.mjs';
|
||||
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getEnvelope } from './helpers.mjs';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
@@ -20,7 +21,7 @@ function humanFileSize(bytes, si) {
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export const getSampleBufferSource = async (s, n, note, speed, freq) => {
|
||||
export const getSampleBufferSource = async (s, n, note, speed, freq, bank) => {
|
||||
let transpose = 0;
|
||||
if (freq !== undefined && note !== undefined) {
|
||||
logger('[sampler] hap has note and freq. ignoring note', 'warning');
|
||||
@@ -29,28 +30,11 @@ export const getSampleBufferSource = async (s, n, note, speed, freq) => {
|
||||
transpose = midi - 36; // C3 is middle C
|
||||
|
||||
const ac = getAudioContext();
|
||||
// is sample from loaded samples(..)
|
||||
const samples = getLoadedSamples();
|
||||
if (!samples) {
|
||||
throw new Error('no samples loaded');
|
||||
}
|
||||
const bank = samples?.[s];
|
||||
if (!bank) {
|
||||
throw new Error(
|
||||
`sample not found: "${s}"`,
|
||||
// , try one of ${Object.keys(samples)
|
||||
// .map((s) => `"${s}"`)
|
||||
// .join(', ')}.
|
||||
);
|
||||
}
|
||||
if (typeof bank !== 'object') {
|
||||
throw new Error('wrong format for sample bank:', s);
|
||||
}
|
||||
let sampleUrl;
|
||||
if (Array.isArray(bank)) {
|
||||
sampleUrl = bank[n % bank.length];
|
||||
} else {
|
||||
const midiDiff = (noteA) => toMidi(noteA) - midi;
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
.filter((k) => !k.startsWith('_'))
|
||||
@@ -107,8 +91,6 @@ export const getLoadedBuffer = (url) => {
|
||||
return bufferCache[url];
|
||||
};
|
||||
|
||||
let sampleCache = { current: undefined };
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
* @example
|
||||
@@ -123,7 +105,7 @@ let sampleCache = { current: undefined };
|
||||
*
|
||||
*/
|
||||
|
||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
|
||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||
if (typeof sampleMap === 'string') {
|
||||
if (sampleMap.startsWith('github:')) {
|
||||
let [_, path] = sampleMap.split('github:');
|
||||
@@ -141,43 +123,134 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
|
||||
}
|
||||
return fetch(sampleMap)
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, baseUrl || json._base || base))
|
||||
.then((json) => samples(json, baseUrl || json._base || base, options))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${sampleMap}"`);
|
||||
});
|
||||
}
|
||||
sampleCache.current = {
|
||||
...sampleCache.current,
|
||||
...Object.fromEntries(
|
||||
Object.entries(sampleMap).map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
value = [value];
|
||||
}
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('wrong sample map format for ' + key);
|
||||
}
|
||||
baseUrl = value._base || baseUrl;
|
||||
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
|
||||
if (Array.isArray(value)) {
|
||||
return [key, value.map(replaceUrl)];
|
||||
}
|
||||
// must be object
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(value).map(([note, samples]) => {
|
||||
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
|
||||
}),
|
||||
),
|
||||
];
|
||||
}),
|
||||
),
|
||||
const { prebake, tag } = options;
|
||||
Object.entries(sampleMap).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
value = [value];
|
||||
}
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('wrong sample map format for ' + key);
|
||||
}
|
||||
baseUrl = value._base || baseUrl;
|
||||
const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/');
|
||||
if (Array.isArray(value)) {
|
||||
//return [key, value.map(replaceUrl)];
|
||||
value = value.map(replaceUrl);
|
||||
} else {
|
||||
// must be object
|
||||
value = Object.fromEntries(
|
||||
Object.entries(value).map(([note, samples]) => {
|
||||
return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)];
|
||||
}),
|
||||
);
|
||||
}
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const cutGroups = [];
|
||||
|
||||
export async function onTriggerSample(t, value, onended, bank) {
|
||||
const {
|
||||
s,
|
||||
freq,
|
||||
unit,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
cut,
|
||||
loop,
|
||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
note,
|
||||
speed = 1, // sample playback speed
|
||||
begin = 0,
|
||||
end = 1,
|
||||
} = value;
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
return;
|
||||
}
|
||||
const ac = getAudioContext();
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
|
||||
//const soundfont = getSoundfontKey(s);
|
||||
const time = t + nudge;
|
||||
|
||||
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank);
|
||||
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
|
||||
// console.warn('sample still loading:', s, n);
|
||||
return;
|
||||
}
|
||||
if (!bufferSource) {
|
||||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||
return;
|
||||
}
|
||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||
if (unit === 'c') {
|
||||
// are there other units?
|
||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
|
||||
}
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
bufferSource.start(time, offset);
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
/*if (loop) {
|
||||
// TODO: idea for loopBegin / loopEnd
|
||||
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
|
||||
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
|
||||
// this will simplify perfectly looping things, while still keeping the normalized option
|
||||
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = offset;
|
||||
bufferSource.loopEnd = offset + duration;
|
||||
duration = loop * duration;
|
||||
}*/
|
||||
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
|
||||
bufferSource.connect(envelope);
|
||||
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
||||
envelope.connect(out);
|
||||
bufferSource.onended = function () {
|
||||
bufferSource.disconnect();
|
||||
envelope.disconnect();
|
||||
out.disconnect();
|
||||
onended();
|
||||
};
|
||||
};
|
||||
const stop = (endTime, playWholeBuffer = !clip) => {
|
||||
let releaseTime = endTime;
|
||||
if (playWholeBuffer) {
|
||||
releaseTime = t + (end - begin) * bufferDuration;
|
||||
}
|
||||
bufferSource.stop(releaseTime + release);
|
||||
releaseEnvelope(releaseTime);
|
||||
};
|
||||
const handle = { node: out, bufferSource, stop };
|
||||
|
||||
export const resetLoadedSamples = () => {
|
||||
sampleCache.current = undefined;
|
||||
};
|
||||
// cut groups
|
||||
if (cut !== undefined) {
|
||||
const prev = cutGroups[cut];
|
||||
if (prev) {
|
||||
prev.node.gain.setValueAtTime(1, time);
|
||||
prev.node.gain.linearRampToValueAtTime(0, time + 0.01);
|
||||
}
|
||||
cutGroups[cut] = handle;
|
||||
}
|
||||
|
||||
export const getLoadedSamples = () => sampleCache.current;
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
|
||||
import { registerSound } from './webaudio.mjs';
|
||||
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
|
||||
|
||||
export function registerSynthSounds() {
|
||||
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
|
||||
registerSound(
|
||||
wave,
|
||||
(t, value, onended) => {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
|
||||
let { n, note, freq } = value;
|
||||
// with synths, n and note are the same thing
|
||||
n = note || n || 36;
|
||||
if (typeof n === 'string') {
|
||||
n = noteToMidi(n); // e.g. c3 => 48
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof n === 'number') {
|
||||
freq = midiToFreq(n); // + 48);
|
||||
}
|
||||
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
|
||||
// make oscillator
|
||||
const { node: o, stop } = getOscillator({ t, s: wave, freq });
|
||||
const g = gainNode(0.3);
|
||||
// envelope
|
||||
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
};
|
||||
return {
|
||||
node: o.connect(g).connect(envelope),
|
||||
stop: (releaseTime) => {
|
||||
releaseEnvelope(releaseTime);
|
||||
stop(releaseTime + release);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
+43
-167
@@ -5,15 +5,22 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import { getSampleBufferSource } from './sampler.mjs';
|
||||
const { Pattern } = strudel;
|
||||
const { Pattern, logger } = strudel;
|
||||
import './vowel.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
import { getFilter, gainNode } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
|
||||
// export const getAudioContext = () => Tone.getContext().rawContext;
|
||||
export const soundMap = map();
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
soundMap.setKey(key, { onTrigger, data });
|
||||
}
|
||||
export function getSound(s) {
|
||||
return soundMap.get()[s];
|
||||
}
|
||||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
@@ -38,65 +45,6 @@ export const panic = () => {
|
||||
destination = null;
|
||||
};
|
||||
|
||||
const getFilter = (type, frequency, Q) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.Q.value = Q;
|
||||
return filter;
|
||||
};
|
||||
|
||||
const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().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
|
||||
/* let t = begin;
|
||||
gainNode.gain.setValueAtTime(0, t);
|
||||
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
|
||||
const sustainGain = Math.max(sustain * velocity, 0.001);
|
||||
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
|
||||
if (end - begin < attack + decay) {
|
||||
gainNode.gain.cancelAndHoldAtTime(end);
|
||||
} else {
|
||||
gainNode.gain.setValueAtTime(sustainGain, end);
|
||||
}
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
const getOscillator = ({ s, freq, t, duration, release }) => {
|
||||
// make oscillator
|
||||
const o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
o.frequency.value = Number(freq);
|
||||
o.start(t);
|
||||
o.stop(t + duration + release);
|
||||
return o;
|
||||
};
|
||||
|
||||
const getSoundfontKey = (s) => {
|
||||
if (!globalThis.soundfontList) {
|
||||
// soundfont package not loaded
|
||||
return false;
|
||||
}
|
||||
if (globalThis.soundfontList?.instruments?.includes(s)) {
|
||||
return s;
|
||||
}
|
||||
// check if s is one of the soundfonts, which are loaded into globalThis, to avoid coupling both packages
|
||||
const nameIndex = globalThis.soundfontList?.instrumentNames?.indexOf(s);
|
||||
// convert number nameIndex (0-128) to 3 digit string (001-128)
|
||||
const name = nameIndex < 10 ? `00${nameIndex}` : nameIndex < 100 ? `0${nameIndex}` : nameIndex;
|
||||
if (nameIndex !== -1) {
|
||||
// TODO: indices of instrumentNames do not seem to match instruments
|
||||
return globalThis.soundfontList.instruments.find((instrument) => instrument.startsWith(name));
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (workletsLoading) {
|
||||
@@ -136,13 +84,6 @@ export async function initAudioOnFirstClick() {
|
||||
});
|
||||
}
|
||||
|
||||
function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
node.gain.value = value;
|
||||
return node;
|
||||
}
|
||||
const cutGroups = [];
|
||||
|
||||
let delays = {};
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (!delays[orbit]) {
|
||||
@@ -186,15 +127,11 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure value
|
||||
// destructure
|
||||
let {
|
||||
freq,
|
||||
s,
|
||||
s = 'triangle',
|
||||
bank,
|
||||
sf,
|
||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
note,
|
||||
source,
|
||||
gain = 0.8,
|
||||
// low pass
|
||||
cutoff,
|
||||
@@ -210,110 +147,48 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
crush,
|
||||
shape,
|
||||
pan,
|
||||
speed = 1, // sample playback speed
|
||||
begin = 0,
|
||||
end = 1,
|
||||
vowel,
|
||||
delay = 0,
|
||||
delayfeedback = 0.5,
|
||||
delaytime = 0.25,
|
||||
unit,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
cut,
|
||||
loop,
|
||||
orbit = 1,
|
||||
room,
|
||||
size = 2,
|
||||
} = hap.value;
|
||||
const { velocity = 1 } = hap.context;
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
// the chain will hold all audio nodes that connect to each other
|
||||
const chain = [];
|
||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||
const onended = () => {
|
||||
toDisconnect.forEach((n) => n?.disconnect());
|
||||
};
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
|
||||
// with synths, n and note are the same thing
|
||||
n = note || n || 36;
|
||||
if (typeof n === 'string') {
|
||||
n = toMidi(n); // e.g. c3 => 48
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, hap.value, hapDuration);
|
||||
} else if (getSound(s)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const soundHandle = await onTrigger(t, hap.value, onended);
|
||||
if (soundHandle) {
|
||||
sourceNode = soundHandle.node;
|
||||
soundHandle.stop(t + hapDuration);
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof n === 'number') {
|
||||
freq = fromMidi(n); // + 48);
|
||||
}
|
||||
// make oscillator
|
||||
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
|
||||
chain.push(o);
|
||||
// level down oscillators as they are really loud compared to samples i've tested
|
||||
chain.push(gainNode(0.3));
|
||||
// TODO: make adsr work with samples without pops
|
||||
// envelope
|
||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
|
||||
chain.push(adsr);
|
||||
} else {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
return;
|
||||
}
|
||||
if (!s) {
|
||||
console.warn('no sample specified');
|
||||
return;
|
||||
}
|
||||
const soundfont = getSoundfontKey(s);
|
||||
let bufferSource;
|
||||
|
||||
if (soundfont) {
|
||||
// is soundfont
|
||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
|
||||
} else {
|
||||
// is sample from loaded samples(..)
|
||||
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
|
||||
}
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
|
||||
// console.warn('sample still loading:', s, n);
|
||||
return;
|
||||
}
|
||||
if (!bufferSource) {
|
||||
console.warn('no buffer source');
|
||||
return;
|
||||
}
|
||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||
if (unit === 'c') {
|
||||
// are there other units?
|
||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * cps;
|
||||
}
|
||||
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * duration * bufferSource.playbackRate.value;
|
||||
duration = (end - begin) * duration;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = offset;
|
||||
bufferSource.loopEnd = offset + duration;
|
||||
duration = loop * duration;
|
||||
}
|
||||
t += nudge;
|
||||
|
||||
bufferSource.start(t, offset);
|
||||
if (cut !== undefined) {
|
||||
cutGroups[cut]?.stop(t); // fade out?
|
||||
cutGroups[cut] = bufferSource;
|
||||
}
|
||||
chain.push(bufferSource);
|
||||
bufferSource.stop(t + duration + release);
|
||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
||||
chain.push(adsr);
|
||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||
}
|
||||
if (!sourceNode) {
|
||||
// if onTrigger does not return anything, we will just silently skip
|
||||
// this can be used for things like speed(0) in the sampler
|
||||
return;
|
||||
}
|
||||
if (ac.currentTime > t) {
|
||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||
return;
|
||||
}
|
||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||
chain.push(sourceNode);
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
@@ -357,8 +232,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
// disconnect all nodes when source node has ended:
|
||||
chain[0].onended = () => chain.concat([delaySend, reverbSend]).forEach((n) => n?.disconnect());
|
||||
// toDisconnect = all the node that should be disconnected in onended callback
|
||||
// this is crucial for performance
|
||||
toDisconnect = chain.concat([delaySend, reverbSend]);
|
||||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "WebDirt integration for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { Pattern } = strudel;
|
||||
import * as WebDirt from 'WebDirt';
|
||||
import { getLoadedSamples, loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
//import { loadBuffer, getLoadedBuffer } from '@strudel.cycles/webaudio';
|
||||
|
||||
let webDirt;
|
||||
|
||||
@@ -62,8 +62,9 @@ export function loadWebDirt(config) {
|
||||
* @noAutocomplete
|
||||
*/
|
||||
Pattern.prototype.webdirt = function () {
|
||||
throw new Error('webdirt support has been dropped..');
|
||||
// create a WebDirt object and initialize Web Audio context
|
||||
return this.onTrigger(async (time, e, currentTime) => {
|
||||
/* return this.onTrigger(async (time, e, currentTime) => {
|
||||
if (!webDirt) {
|
||||
throw new Error('WebDirt not initialized!');
|
||||
}
|
||||
@@ -92,5 +93,5 @@ Pattern.prototype.webdirt = function () {
|
||||
webDirt.playSample(msg, deadline);
|
||||
}
|
||||
}
|
||||
});
|
||||
}); */
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
Generated
+1077
-1503
File diff suppressed because it is too large
Load Diff
@@ -848,6 +848,23 @@ exports[`runs examples > example "arpWith" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "arrange" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c ]",
|
||||
"[ 3/8 → 1/2 | note:c ]",
|
||||
"[ 3/4 → 7/8 | note:c ]",
|
||||
"[ 1/1 → 9/8 | note:a ]",
|
||||
"[ 11/8 → 3/2 | note:a ]",
|
||||
"[ 7/4 → 15/8 | note:a ]",
|
||||
"[ 2/1 → 17/8 | note:f ]",
|
||||
"[ 19/8 → 5/2 | note:f ]",
|
||||
"[ 11/4 → 23/8 | note:f ]",
|
||||
"[ 3/1 → 25/8 | note:e ]",
|
||||
"[ 27/8 → 7/2 | note:e ]",
|
||||
"[ 15/4 → 31/8 | note:e ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "attack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:c3 attack:0 ]",
|
||||
|
||||
@@ -63,6 +63,7 @@ exports[`renders tunes > tune: arpoon 1`] = `
|
||||
"[ 0/1 → 1/2 | s:bd bank:RolandTR909 gain:0.5 ]",
|
||||
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 gain:0.5 ]",
|
||||
"[ 1/2 → 2/3 | s:hh bank:RolandTR909 gain:0.5 ]",
|
||||
"[ 5/6 → 1/1 | s:hh bank:RolandTR909 gain:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -8193,11 +8194,11 @@ exports[`renders tunes > tune: orbit 1`] = `
|
||||
|
||||
exports[`renders tunes > tune: outroMusic 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:E3 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:G3 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:B3 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:D4 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 9/2 | note:C5 s:0040_FluidR3_GM_sf2_file attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:E3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:G3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:B3 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 3/1 | note:D4 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ (0/1 → 1/1) ⇝ 9/2 | note:C5 s:gm_epiano1 n:1 attack:0.05 decay:0.1 sustain:0.7 cutoff:1111.7252990603447 gain:0.3 ]",
|
||||
"[ 0/1 → 3/4 | note:C2 s:sawtooth attack:0.05 decay:0.1 sustain:0.7 cutoff:864.536878321087 gain:0.3 ]",
|
||||
"[ 0/1 → 3/4 | s:bd speed:0.9107561463868479 n:3 ]",
|
||||
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd speed:0.9931522866332672 n:3 ]",
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@
|
||||
"isNoteWithOctave",
|
||||
"isNote",
|
||||
"tokenizeNote",
|
||||
"toMidi",
|
||||
"fromMidi",
|
||||
"noteToMidi",
|
||||
"midiToFreq",
|
||||
"freqToMidi",
|
||||
"valueToMidi",
|
||||
"_mod",
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
https://www.1001fonts.com/galactico-font.html
|
||||
|
||||
If the License is “Free” do what ever you want with it, even for commercial use, any “shout-out” or support thrown our way is much appreciated, even donating to buy a cup of coffee helps a lot. If the License is “For Personal Use Only” do what ever you want with it for yourself, but if you plan to make money out of it, meaning for commercial use, buy a license @ www.hellodonmarciano.com
|
||||
|
||||
I’ll appreciate your support when and if possible. Enjoy!
|
||||
|
||||
Galactico is licensed under the 1001Fonts Free For Commercial Use License (FFC)
|
||||
https://www.1001fonts.com/licenses/ffc.html
|
||||
@@ -0,0 +1,7 @@
|
||||
This font was created by Nate Piekos of Blambot Comic Fonts. It's freeware to use as you'd like, but if you redistribute it, you must include this text file with the zip.
|
||||
|
||||
~Nate Piekos
|
||||
|
||||
Blambot Comic Fonts
|
||||
http://www.piekosarts.com/blambotfonts
|
||||
blambot@piekosarts.com
|
||||
Binary file not shown.
@@ -1986,7 +1986,7 @@
|
||||
"Idiophones/Struck%20Idiophones/Tambourine%202/Tamb2_Shake_rr3_Mid.wav",
|
||||
"Idiophones/Struck%20Idiophones/Tambourine%202/Tamb2_Shake_rr4_Mid.wav"
|
||||
],
|
||||
"triangle": [
|
||||
"triangles": [
|
||||
"Idiophones/Struck%20Idiophones/Triangles/Triangle1_HitFM_v1_rr1_Mid.wav",
|
||||
"Idiophones/Struck%20Idiophones/Triangles/Triangle1_HitFM_v1_rr2_Mid.wav",
|
||||
"Idiophones/Struck%20Idiophones/Triangles/Triangle1_HitM_v1_rr2_Mid.wav",
|
||||
|
||||
@@ -73,6 +73,7 @@ export const SIDEBAR: Sidebar = {
|
||||
],
|
||||
Development: [
|
||||
{ text: 'REPL', link: 'technical-manual/repl' },
|
||||
{ text: 'Sounds', link: 'technical-manual/sounds' },
|
||||
{ text: 'Packages', link: 'technical-manual/packages' },
|
||||
{ text: 'Docs', link: 'technical-manual/docs' },
|
||||
{ text: 'Testing', link: 'technical-manual/testing' },
|
||||
|
||||
@@ -18,6 +18,7 @@ if (typeof window !== 'undefined') {
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
import('@strudel.cycles/csound'),
|
||||
import('@strudel.cycles/soundfonts'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ to fit better for the larger context, while mini notation is more practical for
|
||||
|
||||
## Limits of Mini Notation
|
||||
|
||||
While the Mini Notation is a powerful way to write rhythms shortly, it also has its limits. Take this example:
|
||||
While the Mini Notation is a powerful way to write rhythms concisely, it also has its limits. Take this example:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
|
||||
@@ -49,6 +49,10 @@ As a chained function:
|
||||
|
||||
<JsDoc client:idle name="timeCat" h={0} />
|
||||
|
||||
## arrange
|
||||
|
||||
<JsDoc client:idle name="arrange" h={0} />
|
||||
|
||||
## polymeter
|
||||
|
||||
<JsDoc client:idle name="polymeter" h={0} />
|
||||
|
||||
@@ -46,16 +46,16 @@ For drum sounds, strudel uses the comprehensive [tidal-drum-machines](https://gi
|
||||
|
||||
Furthermore, strudel also loads instrument samples from [VCSL](https://github.com/sgossner/VCSL) by default.
|
||||
|
||||
To see which sample names are available, open the `samples` tab in the [REPL](https://strudel.tidalcycles.org/).
|
||||
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.tidalcycles.org/).
|
||||
|
||||
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples itself are not loaded until they are actually played.
|
||||
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
|
||||
This behaviour of loading things only when they are needed is also called `lazy loading`.
|
||||
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
|
||||
[This might be fixed in the future](https://github.com/tidalcycles/strudel/issues/187)
|
||||
|
||||
# Sound Banks
|
||||
|
||||
If we look at the `samples` tab, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
|
||||
If we open the `sounds` tab and then `drum machines`, we can see that the drum samples are all prefixed with drum machine names: `RolandTR808_bd`, `RolandTR808_sd`, `RolandTR808_hh` etc..
|
||||
|
||||
We _could_ use them like this:
|
||||
|
||||
@@ -75,9 +75,9 @@ Also note that some banks won't have samples for all sounds!
|
||||
|
||||
# Selecting Sounds
|
||||
|
||||
If we look again at the `samples` tab, there is also a number behind each name, indicating how many individual samples are available.
|
||||
If we open the `sounds` tab again, followed by tab `drum machines`, there is also a number behind each name, indicating how many individual samples are available.
|
||||
For example `RolandTR909_hh(4)` means there are 4 samples of a TR909 hihat available.
|
||||
By default, `s` will play the first sample, but we can selecting the other ones using `n`, starting from 0:
|
||||
By default, `s` will play the first sample, but we can select the other ones using `n`, starting from 0:
|
||||
|
||||
<MiniRepl client:idle tune={`s("hh*4").bank("RolandTR909").n("<0 1 2 3>")`} />
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Sounds
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
|
||||
# Sounds
|
||||
|
||||
Let's take a closer look about how sounds are implemented in the webaudio output.
|
||||
|
||||
## Registering a sound
|
||||
|
||||
All sounds are registered in the sound map, using the the `registerSound` function:
|
||||
|
||||
```ts
|
||||
function registerSound(
|
||||
name: string, // The name of the sound that should be given to `s`, e.g. `mysaw`
|
||||
// The function called by the scheduler to trigger the sound:
|
||||
(
|
||||
time: number, // The audio context time the sound should start
|
||||
value: object, // The value of the `Hap`
|
||||
onended: () => void // A callback that should be fired when the sound has ended
|
||||
) => {
|
||||
node: AudioNode, // node to connect to rest of the effects chain
|
||||
stop: (time:number) => void // a function that will stop the sound
|
||||
},
|
||||
data: object // meta data, only for ui logic in sounds tab
|
||||
);
|
||||
```
|
||||
|
||||
When `registerSound` is called, it registers `{ onTrigger, data }` under the given `name` in a [nanostore map](https://github.com/nanostores/nanostores#maps).
|
||||
|
||||
### Example
|
||||
|
||||
This might be a bit abstract, so here is a minimal example:
|
||||
|
||||
```js
|
||||
registerSound(
|
||||
'mysaw',
|
||||
(time, value, onended) => {
|
||||
let { freq } = value; // destructure control params
|
||||
const ctx = getAudioContext();
|
||||
// create oscillator
|
||||
const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) });
|
||||
o.start(time);
|
||||
// add gain node to level down osc
|
||||
const g = new GainNode(ctx, { gain: 0.3 });
|
||||
// connect osc to gain
|
||||
const node = o.connect(g);
|
||||
// this function can be called from outside to stop the sound
|
||||
const stop = (time) => o.stop(time);
|
||||
// ended will be fired when stop has been fired
|
||||
o.addEventListener('ended', () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
onended();
|
||||
});
|
||||
return { node, stop };
|
||||
},
|
||||
{ type: 'synth' },
|
||||
);
|
||||
// use the sound
|
||||
freq(220, 440, 330).s('mysaw');
|
||||
```
|
||||
|
||||
You can actually use this code in the [REPL](https://strudel.tidalcycles.org/) and it'll work.
|
||||
After evaluating the code, you should see `mysaw` in listed in the sounds tab.
|
||||
|
||||
## Playing sounds
|
||||
|
||||
Now here is what happens when a sound is played:
|
||||
When the webaudio output plays a `Hap`, it will lookup and call the `onTrigger` function for the given `s`.
|
||||
The returned `node` can then be connected to the rest of the standard effects chain
|
||||
Having the stop function separate allows playing sounds via midi too, where you don't know how long the noteon will last
|
||||
+94
-26
@@ -3,11 +3,12 @@ import { logger } from '@strudel.cycles/core';
|
||||
import { useEvent, cx } from '@strudel.cycles/react';
|
||||
// import { cx } from '@strudel.cycles/react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { loadedSamples } from './Repl';
|
||||
import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { Reference } from './Reference';
|
||||
import { themes } from './themes.mjs';
|
||||
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
|
||||
import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
|
||||
import { useStore } from '@nanostores/react';
|
||||
|
||||
export function Footer({ context }) {
|
||||
const footerContent = useRef();
|
||||
@@ -72,7 +73,7 @@ export function Footer({ context }) {
|
||||
<div className="flex justify-between px-2">
|
||||
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
|
||||
<FooterTab name="intro" label="welcome" />
|
||||
<FooterTab name="samples" />
|
||||
<FooterTab name="sounds" />
|
||||
<FooterTab name="console" />
|
||||
<FooterTab name="reference" />
|
||||
<FooterTab name="settings" />
|
||||
@@ -84,13 +85,10 @@ export function Footer({ context }) {
|
||||
)}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<div
|
||||
className="text-white font-mono text-sm h-[360px] flex-none overflow-auto max-w-full relative"
|
||||
ref={footerContent}
|
||||
>
|
||||
<div className="text-white flex-none h-[360px] overflow-auto max-w-full relative" ref={footerContent}>
|
||||
{activeFooter === 'intro' && <WelcomeTab />}
|
||||
{activeFooter === 'console' && <ConsoleTab log={log} />}
|
||||
{activeFooter === 'samples' && <SamplesTab />}
|
||||
{activeFooter === 'sounds' && <SoundsTab />}
|
||||
{activeFooter === 'reference' && <Reference />}
|
||||
{activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
|
||||
</div>
|
||||
@@ -154,7 +152,7 @@ function WelcomeTab() {
|
||||
<h3>about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="tidalcycles.org/" target="_blank">
|
||||
<a href="https://tidalcycles.org/" target="_blank">
|
||||
tidalcycles
|
||||
</a>
|
||||
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
|
||||
@@ -173,7 +171,7 @@ function WelcomeTab() {
|
||||
|
||||
function ConsoleTab({ log }) {
|
||||
return (
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900">
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
|
||||
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
|
||||
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
|
||||
@@ -193,36 +191,104 @@ function ConsoleTab({ log }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SamplesTab() {
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
|
||||
function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
const { soundsFilter } = useSettings();
|
||||
const soundEntries = useMemo(() => {
|
||||
let filtered = Object.entries(sounds).filter(([key]) => !key.startsWith('_'));
|
||||
if (!sounds) {
|
||||
return [];
|
||||
}
|
||||
if (soundsFilter === 'user') {
|
||||
return filtered.filter(([key, { data }]) => !data.prebake);
|
||||
}
|
||||
if (soundsFilter === 'drums') {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'samples') {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'synths') {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
return filtered;
|
||||
}, [sounds, soundsFilter]);
|
||||
// holds mutable ref to current triggered sound
|
||||
const trigRef = useRef();
|
||||
// stop current sound on mouseup
|
||||
useEvent('mouseup', () => {
|
||||
const t = trigRef.current;
|
||||
trigRef.current = undefined;
|
||||
t?.then((ref) => {
|
||||
ref?.stop(getAudioContext().currentTime + 0.01);
|
||||
});
|
||||
});
|
||||
return (
|
||||
<div id="samples-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
|
||||
<span>{loadedSamples.length} banks loaded:</span>
|
||||
{loadedSamples.map(([name, samples]) => (
|
||||
<span key={name} className="cursor-pointer hover:opacity-50" onClick={() => {}}>
|
||||
{' '}
|
||||
{name}(
|
||||
{Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1}){' '}
|
||||
</span>
|
||||
))}
|
||||
<div id="sounds-tab" className="flex flex-col w-full h-full dark:text-white text-stone-900">
|
||||
<div className="px-2 pb-2 flex-none">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||
items={{
|
||||
samples: 'samples',
|
||||
drums: 'drum-machines',
|
||||
synths: 'Synths',
|
||||
user: 'User',
|
||||
}}
|
||||
></ButtonGroup>
|
||||
</div>
|
||||
<div className="p-2 min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => (
|
||||
<span
|
||||
key={name}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
onMouseDown={async () => {
|
||||
const ctx = getAudioContext();
|
||||
const params = {
|
||||
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
||||
s: name,
|
||||
clip: 1,
|
||||
release: 0.5,
|
||||
};
|
||||
const time = ctx.currentTime + 0.05;
|
||||
const onended = () => trigRef.current?.node?.disconnect();
|
||||
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
||||
trigRef.current.then((ref) => {
|
||||
ref?.node.connect(ctx.destination);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{name}
|
||||
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
|
||||
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
|
||||
</span>
|
||||
))}
|
||||
{!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroup({ value, onChange, items }) {
|
||||
return (
|
||||
<div className="flex grow border border-foreground rounded-md">
|
||||
<div className="flex max-w-lg">
|
||||
{Object.entries(items).map(([key, label], i, arr) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'p-2 grow',
|
||||
i === 0 && 'rounded-l-md',
|
||||
i === arr.length - 1 && 'rounded-r-md',
|
||||
value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
'px-2 border-b h-8',
|
||||
// i === 0 && 'rounded-l-md',
|
||||
// i === arr.length - 1 && 'rounded-r-md',
|
||||
// value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
value === key ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{label.toLowerCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -282,6 +348,8 @@ const fontFamilyOptions = {
|
||||
BigBlueTerminal: 'BigBlueTerminal',
|
||||
x3270: 'x3270',
|
||||
PressStart: 'PressStart2P',
|
||||
galactico: 'galactico',
|
||||
'we-come-in-peace': 'we-come-in-peace',
|
||||
};
|
||||
|
||||
function SettingsTab({ scheduler }) {
|
||||
|
||||
@@ -6,7 +6,7 @@ const visibleFunctions = jsdocJson.docs
|
||||
export function Reference() {
|
||||
return (
|
||||
<div className="flex h-full w-full pt-2 text-foreground">
|
||||
<div className="w-64 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<a key={i} className="cursor-pointer block hover:bg-lineHighlight py-1 px-4" href={`#doc-${i}`}>
|
||||
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
|
||||
|
||||
@@ -6,13 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
|
||||
import {
|
||||
getAudioContext,
|
||||
getLoadedSamples,
|
||||
initAudioOnFirstClick,
|
||||
resetLoadedSamples,
|
||||
webaudioOutput,
|
||||
} from '@strudel.cycles/webaudio';
|
||||
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
@@ -53,7 +47,6 @@ evalScope(
|
||||
...modules,
|
||||
);
|
||||
|
||||
export let loadedSamples = [];
|
||||
const presets = prebake();
|
||||
|
||||
let drawContext, clearCanvas;
|
||||
@@ -62,11 +55,6 @@ if (typeof window !== 'undefined') {
|
||||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
}
|
||||
|
||||
Promise.all([...modules, presets]).then((data) => {
|
||||
// console.log('modules and sample registry loade', data);
|
||||
loadedSamples = Object.entries(getLoadedSamples() || {});
|
||||
});
|
||||
|
||||
const getTime = () => getAudioContext().currentTime;
|
||||
|
||||
async function initCode() {
|
||||
@@ -211,7 +199,7 @@ export function Repl({ embedded = false }) {
|
||||
const { code, name } = getRandomTune();
|
||||
logger(`[repl] ✨ loading random tune "${name}"`);
|
||||
clearCanvas();
|
||||
resetLoadedSamples();
|
||||
resetLoadedSounds();
|
||||
await prebake(); // declare default samples
|
||||
await evaluate(code, false);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
import { samples } from '@strudel.cycles/webaudio';
|
||||
import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core';
|
||||
//import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
|
||||
|
||||
export async function prebake() {
|
||||
// https://archive.org/details/SalamanderGrandPianoV3
|
||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||
return await Promise.all([
|
||||
samples(`./piano.json`, `./piano/`),
|
||||
registerSynthSounds();
|
||||
//registerSoundfonts();
|
||||
await Promise.all([
|
||||
samples(`./piano.json`, `./piano/`, { prebake: true }),
|
||||
// https://github.com/sgossner/VCSL/
|
||||
// https://api.github.com/repositories/126427031/contents/
|
||||
// LICENSE: CC0 general-purpose
|
||||
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/'),
|
||||
samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/'),
|
||||
samples(`./EmuSP12.json`, `./EmuSP12/`),
|
||||
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
|
||||
samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
|
||||
prebake: true,
|
||||
tag: 'drum-machines',
|
||||
}),
|
||||
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }),
|
||||
// samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
]);
|
||||
}
|
||||
|
||||
const maxPan = toMidi('C8');
|
||||
const maxPan = noteToMidi('C8');
|
||||
const panwidth = (pan, width) => pan * width + (1 - width) / 2;
|
||||
|
||||
Pattern.prototype.piano = function () {
|
||||
|
||||
@@ -566,7 +566,7 @@ samples({
|
||||
"C^7 Am7 Dm7 G7".slow(2).voicings('lefthand')
|
||||
.stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major'))
|
||||
.note().slow(4)
|
||||
.s('0040_FluidR3_GM_sf2_file')
|
||||
.s("gm_epiano1:1")
|
||||
.color('steelblue')
|
||||
.stack(
|
||||
"<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2".scale('C3 major')
|
||||
|
||||
@@ -9,6 +9,7 @@ export const defaultSettings = {
|
||||
fontSize: 18,
|
||||
latestCode: '',
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
};
|
||||
|
||||
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
font-family: 'x3270';
|
||||
src: url('/fonts/3270/3270-Regular.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'galactico';
|
||||
src: url('/fonts/galactico/Galactico-Basic.otf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'we-come-in-peace';
|
||||
src: url('/fonts/we-come-in-peace/we-come-in-peace-bb.regular.ttf');
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
@@ -22,7 +30,6 @@
|
||||
--app-height: 100vh;
|
||||
}
|
||||
|
||||
#console-tab,
|
||||
#samples-tab {
|
||||
#console-tab {
|
||||
font-family: BigBlueTerminal, monospace;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user