Merge remote-tracking branch 'origin/main' into some-tunes

This commit is contained in:
Felix Roos 2022-11-06 00:38:05 +01:00
commit da8e00eb4e
41 changed files with 10570 additions and 7255 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [16, 17] node-version: [18]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

12401
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
"private": true, "private": true,
"description": "Port of tidalcycles to javascript", "description": "Port of tidalcycles to javascript",
"scripts": { "scripts": {
"pretest": "cd tutorial && npm run jsdoc-json",
"test": "vitest run --version", "test": "vitest run --version",
"test-ui": "vitest --ui", "test-ui": "vitest --ui",
"test-coverage": "vitest --coverage", "test-coverage": "vitest --coverage",
@ -43,11 +44,10 @@
"c8": "^7.12.0", "c8": "^7.12.0",
"events": "^3.3.0", "events": "^3.3.0",
"gh-pages": "^4.0.0", "gh-pages": "^4.0.0",
"happy-dom": "^6.0.4",
"jsdoc": "^3.6.10", "jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1", "jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0", "lerna": "^6.0.0",
"rollup-plugin-visualizer": "^5.8.1", "rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.21.1" "vitest": "^0.21.1"
} }

View File

@ -40,11 +40,6 @@ const generic_params = [
* @example * @example
* n("0 1 2 3").s('east').osc() * n("0 1 2 3").s('east').osc()
*/ */
// TODO: nOut does not work
// TODO: notes don't work as expected
// current "workaround" for notes:
// s('superpiano').n("<c0 d0 e0 g0>"._asNumber()).osc()
// -> .n or .osc (or .superdirt) would need to convert note strings to numbers
// also see https://github.com/tidalcycles/strudel/pull/63 // also see https://github.com/tidalcycles/strudel/pull/63
['f', 'n', 'The note or sample number to choose for a synth or sampleset'], ['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
['f', 'note', 'The note or pitch to play a sound or synth with'], ['f', 'note', 'The note or pitch to play a sound or synth with'],
@ -110,11 +105,7 @@ const generic_params = [
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd") * s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
* *
*/ */
[ ['f', 'bank', 'selects sound bank to use'],
'f',
'bank',
'selects sound bank to use',
],
// TODO: find out how this works? // TODO: find out how this works?
/* /*

View File

@ -21,6 +21,7 @@ import Fraction, { gcd } from './fraction.mjs';
* @example * @example
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123 * const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
* console.log(line); * console.log(line);
* silence;
*/ */
function drawLine(pat, chars = 60) { function drawLine(pat, chars = 60) {
let cycle = 0; let cycle = 0;

View File

@ -0,0 +1,44 @@
<input
type="text"
id="text"
value="seq('c3','eb3','g3').note().s('sawtooth').out()"
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
spellcheck="false"
/>
<button id="start">play</button>
<div id="output"></div>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest');
const controls = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest/controls.mjs');
const { getAudioContext, Scheduler } = await import('https://cdn.skypack.dev/@strudel.cycles/webaudio@latest');
let scheduler;
const audioContext = getAudioContext();
const latency = 0.2;
Object.assign(window, strudel);
Object.assign(window, controls.default);
scheduler = new Scheduler({
audioContext,
interval: 0.1,
latency,
onEvent: (hap) => {
if (!hap.context.onTrigger) {
console.warn('no output chosen. use one of .out() .webdirt() .osc()');
}
},
});
let started;
document.getElementById('start').addEventListener('click', async () => {
const code = document.getElementById('text').value;
const pattern = eval(code);
const events = pattern._firstCycleValues;
console.log(code, '->', events);
scheduler.setPattern(pattern);
if (!started) {
scheduler.start();
started = true;
}
});
</script>

View File

@ -4,10 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export * from './controls.mjs'; import controls from './controls.mjs';
export * from './euclid.mjs'; export * from './euclid.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
export { Fraction }; export { Fraction, controls };
export * from './hap.mjs'; export * from './hap.mjs';
export * from './pattern.mjs'; export * from './pattern.mjs';
export * from './signal.mjs'; export * from './signal.mjs';
@ -15,6 +15,7 @@ export * from './state.mjs';
export * from './timespan.mjs'; export * from './timespan.mjs';
export * from './util.mjs'; export * from './util.mjs';
export * from './speak.mjs'; export * from './speak.mjs';
export { default as drawLine } from './drawLine.mjs';
export { default as gist } from './gist.js'; export { default as gist } from './gist.js';
// below won't work with runtime.mjs (json import fails) // below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json'; /* import * as p from './package.json';

View File

@ -10,7 +10,7 @@ import Hap from './hap.mjs';
import State from './state.mjs'; import State from './state.mjs';
import { unionWithObj } from './value.mjs'; import { unionWithObj } from './value.mjs';
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs'; import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
/** @class Class representing a pattern. */ /** @class Class representing a pattern. */
@ -32,8 +32,10 @@ export class Pattern {
* @param {Fraction | number} end to time * @param {Fraction | number} end to time
* @returns Hap[] * @returns Hap[]
* @example * @example
* const pattern = sequence('a', ['b', 'c']); * const pattern = sequence('a', ['b', 'c'])
* const haps = pattern.queryArc(0, 1); * const haps = pattern.queryArc(0, 1)
* console.log(haps)
* silence
*/ */
queryArc(begin, end) { queryArc(begin, end) {
return this.query(new State(new TimeSpan(begin, end))); return this.query(new State(new TimeSpan(begin, end)));
@ -68,7 +70,7 @@ export class Pattern {
return new Pattern((state) => { return new Pattern((state) => {
const newState = state.withSpan(func); const newState = state.withSpan(func);
if (!newState.span) { if (!newState.span) {
return []; return [];
} }
return pat.query(newState); return pat.query(newState);
}); });
@ -446,34 +448,8 @@ export class Pattern {
return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._TrigzeroJoin(); return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._TrigzeroJoin();
} }
_asNumber(dropfails = false, softfail = false) { _asNumber() {
return this._withHap((hap) => { return this.fmap(parseNumeral);
const asNumber = Number(hap.value);
if (!isNaN(asNumber)) {
return hap.withValue(() => asNumber);
}
const specialValue = {
e: Math.E,
pi: Math.PI,
}[hap.value];
if (typeof specialValue !== 'undefined') {
return hap.withValue(() => specialValue);
}
if (isNote(hap.value)) {
// set context type to midi to let the player know its meant as midi number and not as frequency
return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
}
if (dropfails) {
// return 'nothing'
return undefined;
}
if (softfail) {
// return original hap
return hap;
}
throw new Error('cannot parse as number: "' + hap.value + '"');
return hap;
});
} }
/** /**
@ -761,7 +737,7 @@ export class Pattern {
const bpos = span.begin.sub(cycle).mul(factor).min(1); const bpos = span.begin.sub(cycle).mul(factor).min(1);
const epos = span.end.sub(cycle).mul(factor).min(1); const epos = span.end.sub(cycle).mul(factor).min(1);
if (bpos >= 1) { if (bpos >= 1) {
return undefined; return undefined;
} }
return new TimeSpan(cycle.add(bpos), cycle.add(epos)); return new TimeSpan(cycle.add(bpos), cycle.add(epos));
}; };
@ -1386,9 +1362,6 @@ function _composeOp(a, b, func) {
// Make composers // Make composers
(function () { (function () {
const num = (pat) => pat._asNumber();
const numOrString = (pat) => pat._asNumber(false, true);
// pattern composers // pattern composers
const composers = { const composers = {
set: [(a, b) => b], set: [(a, b) => b],
@ -1412,7 +1385,7 @@ function _composeOp(a, b, func) {
* // Behind the scenes, the notes are converted to midi numbers: * // Behind the scenes, the notes are converted to midi numbers:
* // "48 52 55".add("<0 5 7 0>").note() * // "48 52 55".add("<0 5 7 0>").note()
*/ */
add: [(a, b) => a + b, numOrString], // support string concatenation add: [numeralArgs((a, b) => a + b)], // support string concatenation
/** /**
* *
* Like add, but the given numbers are subtracted. * Like add, but the given numbers are subtracted.
@ -1422,7 +1395,7 @@ function _composeOp(a, b, func) {
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note() * "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
* // See add for more information. * // See add for more information.
*/ */
sub: [(a, b) => a - b, num], sub: [numeralArgs((a, b) => a - b)],
/** /**
* *
* Multiplies each number by the given factor. * Multiplies each number by the given factor.
@ -1431,21 +1404,21 @@ function _composeOp(a, b, func) {
* @example * @example
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq() * "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
*/ */
mul: [(a, b) => a * b, num], mul: [numeralArgs((a, b) => a * b)],
/** /**
* *
* Divides each number by the given factor. * Divides each number by the given factor.
* @name div * @name div
* @memberof Pattern * @memberof Pattern
*/ */
div: [(a, b) => a / b, num], div: [numeralArgs((a, b) => a / b)],
mod: [mod, num], mod: [numeralArgs(mod)],
pow: [Math.pow, num], pow: [numeralArgs(Math.pow)],
_and: [(a, b) => a & b, num], _and: [numeralArgs((a, b) => a & b)],
_or: [(a, b) => a | b, num], _or: [numeralArgs((a, b) => a | b)],
_xor: [(a, b) => a ^ b, num], _xor: [numeralArgs((a, b) => a ^ b)],
_lshift: [(a, b) => a << b, num], _lshift: [numeralArgs((a, b) => a << b)],
_rshift: [(a, b) => a >> b, num], _rshift: [numeralArgs((a, b) => a >> b)],
// TODO - force numerical comparison if both look like numbers? // TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b], lt: [(a, b) => a < b],
@ -1572,7 +1545,7 @@ export function pure(value) {
export function isPattern(thing) { export function isPattern(thing) {
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled // thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
const is = thing instanceof Pattern || thing._Pattern; const is = thing instanceof Pattern || thing?._Pattern;
if (!thing instanceof Pattern) { if (!thing instanceof Pattern) {
console.warn( console.warn(
`Found Pattern that fails "instanceof Pattern" check. `Found Pattern that fails "instanceof Pattern" check.

View File

@ -238,6 +238,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin(); export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
// this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => { export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor); const pata = pat.fmap(Math.floor);
const patb = pat.fmap((t) => Math.floor(t) + 1); const patb = pat.fmap((t) => Math.floor(t) + 1);
@ -255,7 +256,7 @@ export const perlinWith = (pat) => {
* s("bd sd,hh*4").cutoff(perlin.range(500,2000)) * s("bd sd,hh*4").cutoff(perlin.range(500,2000))
* *
*/ */
export const perlin = perlinWith(time); export const perlin = perlinWith(time.fmap((v) => Number(v)));
Pattern.prototype._degradeByWith = function (withPat, x) { Pattern.prototype._degradeByWith = function (withPat, x) {
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x)); return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));

View File

@ -47,6 +47,9 @@ import {
import { steady } from '../signal.mjs'; import { steady } from '../signal.mjs';
import controls from '../controls.mjs';
const { n } = controls;
const st = (begin, end) => new State(ts(begin, end)); const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context); const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
@ -138,7 +141,7 @@ describe('Pattern', () => {
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3); expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
}); });
it('Supports zero-width queries', () => { it('Supports zero-width queries', () => {
expect(pure('hello').queryArc(0,0).length).toBe(1); expect(pure('hello').queryArc(0, 0).length).toBe(1);
}); });
}); });
describe('fmap()', () => { describe('fmap()', () => {
@ -194,6 +197,9 @@ describe('Pattern', () => {
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]), sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
); );
}); });
it('can add object patterns', () => {
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
});
}); });
describe('keep()', () => { describe('keep()', () => {
it('can structure In()', () => { it('can structure In()', () => {
@ -376,9 +382,10 @@ describe('Pattern', () => {
); );
}); });
it('copes with breaking up events across cycles', () => { it('copes with breaking up events across cycles', () => {
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual( expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual([
[hap(ts(0, 1), ts(0, 0.5), 'a'), hap(ts(0.5, 1.5), ts(1, 1.5), 'a')] hap(ts(0, 1), ts(0, 0.5), 'a'),
); hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
]);
}); });
}); });
describe('_compressSpan()', () => { describe('_compressSpan()', () => {
@ -434,14 +441,14 @@ describe('Pattern', () => {
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3] // mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
}); });
it('Supports zero-length queries', () => { it('Supports zero-length queries', () => {
expect(steady('a')._slow(1).queryArc(0,0) expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
).toStrictEqual(steady('a').queryArc(0,0))
}); });
}); });
describe('slow()', () => { describe('slow()', () => {
it('Supports zero-length queries', () => { it('Supports zero-length queries', () => {
expect(steady('a').slow(1)._setContext({}).queryArc(0,0) expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
).toStrictEqual(steady('a')._setContext({}).queryArc(0,0)) steady('a')._setContext({}).queryArc(0, 0),
);
}); });
}); });
describe('inside', () => { describe('inside', () => {
@ -812,10 +819,11 @@ describe('Pattern', () => {
}); });
it('Squeezes to the correct cycle', () => { it('Squeezes to the correct cycle', () => {
expect( expect(
pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value) pure(time.struct(true))
).toStrictEqual( ._squeezeJoin()
[Fraction(3.5)] .queryArc(3, 4)
) .map((x) => x.value),
).toStrictEqual([Fraction(3.5)]);
}); });
}); });
describe('ply', () => { describe('ply', () => {
@ -868,9 +876,7 @@ describe('Pattern', () => {
}); });
describe('range', () => { describe('range', () => {
it('Can be patterned', () => { it('Can be patterned', () => {
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual( expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
sequence(0, 0.5).firstCycle(),
);
}); });
}); });
describe('range2', () => { describe('range2', () => {

View File

@ -5,7 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { pure } from '../pattern.mjs'; import { pure } from '../pattern.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs'; import {
isNote,
tokenizeNote,
toMidi,
fromMidi,
mod,
compose,
getFrequency,
getPlayableNoteValue,
parseNumeral,
parseFractional,
numeralArgs,
fractionalArgs,
} from '../util.mjs';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
describe('isNote', () => { describe('isNote', () => {
@ -96,12 +109,12 @@ describe('getFrequency', () => {
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432); expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
}); });
it('should turn object with a "freq" property into frequency', () => { it('should turn object with a "freq" property into frequency', () => {
expect(getFrequency(happify({freq: 220}))).toEqual(220) expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
expect(getFrequency(happify({freq: 440}))).toEqual(440) expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
}); });
it('should throw an error when given a non-note', () => { it('should throw an error when given a non-note', () => {
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`) expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`) expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
}); });
}); });
@ -140,22 +153,72 @@ describe('compose', () => {
describe('getPlayableNoteValue', () => { describe('getPlayableNoteValue', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context); const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
it('should return object "note" property', () => { it('should return object "note" property', () => {
expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4') expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
}); });
it('should return object "n" property', () => { it('should return object "n" property', () => {
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4') expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
}); });
it('should return object "value" property', () => { it('should return object "value" property', () => {
expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4') expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
}); });
it('should turn midi into frequency', () => { it('should turn midi into frequency', () => {
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220) expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
}) });
it('should return frequency value', () => { it('should return frequency value', () => {
expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220) expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
}) });
it('should throw an error if value is not an object, number, or string', () => { it('should throw an error if value is not an object, number, or string', () => {
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`) expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`) expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
}) });
});
describe('parseNumeral', () => {
it('should parse numbers as is', () => {
expect(parseNumeral(4)).toBe(4);
expect(parseNumeral(0)).toBe(0);
expect(parseNumeral(20)).toBe(20);
expect(parseNumeral('20')).toBe(20);
expect(parseNumeral(1.5)).toBe(1.5);
});
it('should parse notes', () => {
expect(parseNumeral('c4')).toBe(60);
expect(parseNumeral('c#4')).toBe(61);
expect(parseNumeral('db4')).toBe(61);
});
it('should throw an error for unknown strings', () => {
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
});
});
describe('parseFractional', () => {
it('should parse numbers as is', () => {
expect(parseFractional(4)).toBe(4);
expect(parseFractional(0)).toBe(0);
expect(parseFractional(20)).toBe(20);
expect(parseFractional('20')).toBe(20);
expect(parseFractional(1.5)).toBe(1.5);
});
it('should parse fractional shorthands values', () => {
expect(parseFractional('w')).toBe(1);
expect(parseFractional('h')).toBe(0.5);
expect(parseFractional('q')).toBe(0.25);
expect(parseFractional('e')).toBe(0.125);
});
it('should throw an error for unknown strings', () => {
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
});
});
describe('numeralArgs', () => {
it('should convert function arguments to numbers', () => {
const add = numeralArgs((a, b) => a + b);
expect(add('c4', 2)).toBe(62);
});
});
describe('fractionalArgs', () => {
it('should convert function arguments to numbers', () => {
const add = fractionalArgs((a, b) => a + b);
expect(add('q', 2)).toBe(2.25);
});
}); });

View File

@ -130,3 +130,46 @@ export function curry(func, overload) {
} }
return fn; return fn;
} }
export function parseNumeral(numOrString) {
const asNumber = Number(numOrString);
if (!isNaN(asNumber)) {
return asNumber;
}
if (isNote(numOrString)) {
return toMidi(numOrString);
}
throw new Error(`cannot parse as numeral: "${numOrString}"`);
}
export function mapArgs(fn, mapFn) {
return (...args) => fn(...args.map(mapFn));
}
export function numeralArgs(fn) {
return mapArgs(fn, parseNumeral);
}
export function parseFractional(numOrString) {
const asNumber = Number(numOrString);
if (!isNaN(asNumber)) {
return asNumber;
}
const specialValue = {
pi: Math.PI,
w: 1,
h: 0.5,
q: 0.25,
e: 0.125,
s: 0.0625,
t: 1 / 3,
f: 0.2,
x: 1 / 6,
}[numOrString];
if (typeof specialValue !== 'undefined') {
return specialValue;
}
throw new Error(`cannot parse as fractional: "${numOrString}"`);
}
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);

View File

@ -10,15 +10,22 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script> <script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
<strudel-repl> <strudel-repl>
<!-- <!--
"a4 [a3 c3] a3 c3".color('#F9D649') note(`[[e5 [b4 c5] d5 [c5 b4]]
.sub("<7 12 5 12>".slow(2)) [a4 [a4 c5] e5 [d5 c5]]
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928")) [b4 [~ c5] d5 e5]
.off(1/8,x=>x.add(12).color('#215CB6')) [c5 a4 a4 ~]
.slow(2) [[~ d5] [~ f5] a5 [g5 f5]]
.legato(sine.range(0.3, 2).slow(28)) [e5 [~ c5] e5 [d5 c5]]
.wave("sawtooth square".fast(2)) [b4 [b4 c5] d5 e5]
.filter('lowpass', cosine.range(500,4000).slow(16)) [c5 a4 a4 ~]],
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'}) [[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
--> -->
</strudel-repl> </strudel-repl>
``` ```

View File

@ -2,14 +2,21 @@
<!-- <script src="./embed.js"></script> --> <!-- <script src="./embed.js"></script> -->
<strudel-repl> <strudel-repl>
<!-- <!--
"a4 [a3 c3] a3 c3".color('#F9D649') note(`[[e5 [b4 c5] d5 [c5 b4]]
.sub("<7 12 5 12>".slow(2)) [a4 [a4 c5] e5 [d5 c5]]
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928")) [b4 [~ c5] d5 e5]
.off(1/8,x=>x.add(12).color('#215CB6')) [c5 a4 a4 ~]
.slow(2) [[~ d5] [~ f5] a5 [g5 f5]]
.legato(sine.range(0.3, 2).slow(28)) [e5 [~ c5] e5 [d5 c5]]
.wave("sawtooth square".fast(2)) [b4 [b4 c5] d5 e5]
.filter('lowpass', cosine.range(500,4000).slow(16)) [c5 a4 a4 ~]],
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'}) [[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
--> -->
</strudel-repl> </strudel-repl>

View File

@ -30,10 +30,10 @@
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.2.0", "@strudel.cycles/core": "^0.2.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"shift-ast": "^6.1.0", "shift-ast": "^7.0.0",
"shift-codegen": "^7.0.3", "shift-codegen": "^8.1.0",
"shift-parser": "^7.0.3", "shift-parser": "^8.0.0",
"shift-spec": "^2018.0.2", "shift-spec": "^2019.0.0",
"shift-traverser": "^1.0.0" "shift-traverser": "^1.0.0"
} }
} }

View File

@ -5,12 +5,11 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { isNote } from 'tone'; import { isNote } from 'tone';
import _WebMidi from 'webmidi'; import * as _WebMidi from 'webmidi';
import { Pattern, isPattern } from '@strudel.cycles/core'; import { Pattern, isPattern } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone'; import { Tone } from '@strudel.cycles/tone';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const WebMidi = _WebMidi; export const { WebMidi } = _WebMidi;
export function enableWebMidi() { export function enableWebMidi() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -23,6 +23,6 @@
"dependencies": { "dependencies": {
"@strudel.cycles/tone": "^0.2.0", "@strudel.cycles/tone": "^0.2.0",
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^2.5.2" "webmidi": "^3.0.21"
} }
} }

View File

@ -33,3 +33,12 @@ yields:
## Mini Notation API ## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/) See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Building the Parser
The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) using [peggy](https://peggyjs.org/).
To generate the parser, run
```js
npm run build:parser
```

View File

@ -6,7 +6,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 krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core';
import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs'; // import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel; const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
@ -29,7 +29,10 @@ const applyOptions = (parent) => (pat, i) => {
case 'bjorklund': case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation); return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
case 'degradeBy': case 'degradeBy':
return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount); return reify(pat)._degradeByWith(
strudel.rand.early(randOffset * _nextSeed()).segment(1),
operator.arguments_.amount,
);
// TODO: case 'fixed-step': "%" // TODO: case 'fixed-step': "%"
} }
console.warn(`operator "${operator.type_}" not implemented`); console.warn(`operator "${operator.type_}" not implemented`);
@ -112,9 +115,9 @@ export function patternifyAST(ast) {
return silence; return silence;
} }
if (typeof ast.source_ !== 'object') { if (typeof ast.source_ !== 'object') {
if (!addMiniLocations) { /* if (!addMiniLocations) {
return ast.source_; return ast.source_;
} } */
if (!ast.location_) { if (!ast.location_) {
console.warn('no location for', ast); console.warn('no location for', ast);
return ast.source_; return ast.source_;

View File

@ -35,3 +35,5 @@ s("<bd sd> hh").osc()
``` ```
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)... or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)

View File

@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import OSC from 'osc-js'; import OSC from 'osc-js';
import { Pattern } from '@strudel.cycles/core'; import { parseNumeral, Pattern } from '@strudel.cycles/core';
const comm = new OSC(); const comm = new OSC();
comm.open(); comm.open();
@ -31,6 +31,10 @@ Pattern.prototype.osc = function () {
startedAt = Date.now() - currentTime * 1000; startedAt = Date.now() - currentTime * 1000;
} }
const controls = Object.assign({}, { cps, cycle, delta }, hap.value); const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));
controls.note && (controls.note = parseNumeral(controls.note));
const keyvals = Object.entries(controls).flat(); const keyvals = Object.entries(controls).flat();
const ts = Math.floor(startedAt + (time + latency) * 1000); const ts = Math.floor(startedAt + (time + latency) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals); const message = new OSC.Message('/dirt/play', ...keyvals);

View File

@ -30,6 +30,6 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"osc-js": "^2.3.2" "osc-js": "^2.4.0"
} }
} }

View File

@ -1,4 +1,44 @@
# @strudel.cycles/react # @strudel.cycles/react
This package contains react hooks and components for strudel. This package contains react hooks and components for strudel. It is used internally by the Strudel REPL.
Example coming soon
## Install
```js
npm i @strudel.cycles/react
```
## Usage
Here is a minimal example of how to set up a MiniRepl:
```jsx
import { evalScope } from '@strudel.cycles/eval';
import { MiniRepl } from '@strudel.cycles/react';
import controls from '@strudel.cycles/core/controls.mjs';
import { prebake } from '../repl/src/prebake.mjs';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
/* probably import other strudel packages */
);
prebake();
export function Repl({ tune }) {
return <MiniRepl tune={tune} hideOutsideView={true} />;
}
```
## Development
If you change something in here and want to see the changes in the repl, make sure to run `npm run build` inside this folder!
```js
npm run dev # dev server
npm run build # build package
```

File diff suppressed because one or more lines are too long

View File

@ -1,582 +1,367 @@
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react'; import d, { useCallback as C, useState as b, useEffect as S, useMemo as L, useRef as X, useLayoutEffect as Y } from "react";
import _CodeMirror from '@uiw/react-codemirror'; import Z from "@uiw/react-codemirror";
import { Decoration, EditorView } from '@codemirror/view'; import { Decoration as x, EditorView as j } from "@codemirror/view";
import { StateEffect, StateField } from '@codemirror/state'; import { StateEffect as K, StateField as Q } from "@codemirror/state";
import { javascript } from '@codemirror/lang-javascript'; import { javascript as ee } from "@codemirror/lang-javascript";
import { tags } from '@lezer/highlight'; import { tags as i } from "@lezer/highlight";
import { createTheme } from '@uiw/codemirror-themes'; import { createTheme as te } from "@uiw/codemirror-themes";
import { useInView } from 'react-hook-inview'; import { useInView as oe } from "react-hook-inview";
import { evaluate } from '@strudel.cycles/eval'; import { evaluate as ne } from "@strudel.cycles/eval";
import { Tone } from '@strudel.cycles/tone'; import { Tone as h } from "@strudel.cycles/tone";
import { TimeSpan, State } from '@strudel.cycles/core'; import { TimeSpan as re, State as ae } from "@strudel.cycles/core";
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio'; import { webaudioOutputTrigger as se } from "@strudel.cycles/webaudio";
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi'; import { WebMidi as k, enableWebMidi as ce } from "@strudel.cycles/midi";
const ie = te({
var strudelTheme = createTheme({ theme: "dark",
theme: 'dark',
settings: { settings: {
background: '#222', background: "#222",
foreground: '#75baff', // whats that? foreground: "#75baff",
caret: '#ffcc00', caret: "#ffcc00",
selection: 'rgba(128, 203, 196, 0.5)', selection: "rgba(128, 203, 196, 0.5)",
selectionMatch: '#036dd626', selectionMatch: "#036dd626",
lineHighlight: '#8a91991a', lineHighlight: "#8a91991a",
gutterBackground: 'transparent', gutterBackground: "transparent",
// gutterForeground: '#8a919966', gutterForeground: "#676e95"
gutterForeground: '#676e95',
}, },
styles: [ styles: [
{ tag: tags.keyword, color: '#c792ea' }, { tag: i.keyword, color: "#c792ea" },
{ tag: tags.operator, color: '#89ddff' }, { tag: i.operator, color: "#89ddff" },
{ tag: tags.special(tags.variableName), color: '#eeffff' }, { tag: i.special(i.variableName), color: "#eeffff" },
{ tag: tags.typeName, color: '#f07178' }, { tag: i.typeName, color: "#f07178" },
{ tag: tags.atom, color: '#f78c6c' }, { tag: i.atom, color: "#f78c6c" },
{ tag: tags.number, color: '#ff5370' }, { tag: i.number, color: "#ff5370" },
{ tag: tags.definition(tags.variableName), color: '#82aaff' }, { tag: i.definition(i.variableName), color: "#82aaff" },
{ tag: tags.string, color: '#c3e88d' }, { tag: i.string, color: "#c3e88d" },
{ tag: tags.special(tags.string), color: '#f07178' }, { tag: i.special(i.string), color: "#f07178" },
{ tag: tags.comment, color: '#7d8799' }, { tag: i.comment, color: "#7d8799" },
{ tag: tags.variableName, color: '#f07178' }, { tag: i.variableName, color: "#f07178" },
{ tag: tags.tagName, color: '#ff5370' }, { tag: i.tagName, color: "#ff5370" },
{ tag: tags.bracket, color: '#a2a1a4' }, { tag: i.bracket, color: "#a2a1a4" },
{ tag: tags.meta, color: '#ffcb6b' }, { tag: i.meta, color: "#ffcb6b" },
{ tag: tags.attributeName, color: '#c792ea' }, { tag: i.attributeName, color: "#c792ea" },
{ tag: tags.propertyName, color: '#c792ea' }, { tag: i.propertyName, color: "#c792ea" },
{ tag: tags.className, color: '#decb6b' }, { tag: i.className, color: "#decb6b" },
{ tag: tags.invalid, color: '#ffffff' }, { tag: i.invalid, color: "#ffffff" }
], ]
}); });
const P = K.define(), le = Q.define({
var style = '';
const setFlash = StateEffect.define();
const flashField = StateField.define({
create() { create() {
return Decoration.none; return x.none;
}, },
update(flash2, tr) { update(e, n) {
try { try {
for (let e of tr.effects) { for (let r of n.effects)
if (e.is(setFlash)) { if (r.is(P))
if (e.value) { if (r.value) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } }); const c = x.mark({ attributes: { style: "background-color: #FFCA2880" } });
flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]); e = x.set([c.range(0, n.newDoc.length)]);
} else { } else
flash2 = Decoration.set([]); e = x.set([]);
} return e;
} } catch (r) {
} return console.warn("flash error", r), e;
return flash2;
} catch (err) {
console.warn("flash error", err);
return flash2;
} }
}, },
provide: (f) => EditorView.decorations.from(f) provide: (e) => j.decorations.from(e)
}); }), de = (e) => {
const flash = (view) => { e.dispatch({ effects: P.of(!0) }), setTimeout(() => {
view.dispatch({ effects: setFlash.of(true) }); e.dispatch({ effects: P.of(!1) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, 200); }, 200);
}; }, B = K.define(), ue = Q.define({
const setHighlights = StateEffect.define();
const highlightField = StateField.define({
create() { create() {
return Decoration.none; return x.none;
}, },
update(highlights, tr) { update(e, n) {
try { try {
for (let e of tr.effects) { for (let r of n.effects)
if (e.is(setHighlights)) { if (r.is(B)) {
const marks = e.value.map( const c = r.value.map(
(hap) => (hap.context.locations || []).map(({ start, end }) => { (l) => (l.context.locations || []).map(({ start: a, end: m }) => {
const color = hap.context.color || "#FFCA28"; const t = l.context.color || "#FFCA28";
let from = tr.newDoc.line(start.line).from + start.column; let u = n.newDoc.line(a.line).from + a.column, o = n.newDoc.line(m.line).from + m.column;
let to = tr.newDoc.line(end.line).from + end.column; const y = n.newDoc.length;
const l = tr.newDoc.length; return u > y || o > y ? void 0 : x.mark({ attributes: { style: `outline: 1.5px solid ${t};` } }).range(u, o);
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
}) })
).flat().filter(Boolean) || []; ).flat().filter(Boolean) || [];
highlights = Decoration.set(marks, true); e = x.set(c, !0);
} }
} return e;
return highlights; } catch {
} catch (err) { return x.set([]);
return Decoration.set([]);
} }
}, },
provide: (f) => EditorView.decorations.from(f) provide: (e) => j.decorations.from(e)
}); }), fe = [ee(), ie, ue, le];
const extensions = [javascript(), strudelTheme, highlightField, flashField]; function me({ value: e, onChange: n, onViewChanged: r, onSelectionChange: c, options: l, editorDidMount: a }) {
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) { const m = C(
const handleOnChange = useCallback( (o) => {
(value2) => { n?.(o);
onChange?.(value2);
}, },
[onChange] [n]
); ), t = C(
const handleOnCreateEditor = useCallback( (o) => {
(view) => { r?.(o);
onViewChanged?.(view);
}, },
[onViewChanged] [r]
); ), u = C(
const handleOnUpdate = useCallback( (o) => {
(viewUpdate) => { o.selectionSet && c && c?.(o.state.selection);
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
}, },
[onSelectionChange] [c]
); );
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, { return /* @__PURE__ */ d.createElement(d.Fragment, null, /* @__PURE__ */ d.createElement(Z, {
value, value: e,
onChange: handleOnChange, onChange: m,
onCreateEditor: handleOnCreateEditor, onCreateEditor: t,
onUpdate: handleOnUpdate, onUpdate: u,
extensions extensions: fe
})); }));
} }
function ge(e) {
/* const { onEvent: n, onQuery: r, onSchedule: c, ready: l = !0, onDraw: a } = e, [m, t] = b(!1), u = 1, o = () => Math.floor(h.getTransport().seconds / u), y = (v = o()) => {
useCycle.mjs - <short description TODO> const O = new re(v, v + 1), M = r?.(new ae(O)) || [];
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.mjs> c?.(M, v);
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/>. const _ = O.begin.valueOf();
*/ h.getTransport().cancel(_);
const R = (v + 1) * u - 0.5, E = Math.max(h.getTransport().seconds, R) + 0.1;
/* export declare interface UseCycleProps { h.getTransport().schedule(() => {
onEvent: ToneEventCallback<any>; y(v + 1);
onQuery?: (state: State) => Hap[]; }, E), M?.filter((g) => g.part.begin.equals(g.whole?.begin)).forEach((g) => {
onSchedule?: (events: Hap[], cycle: number) => void; h.getTransport().schedule((D) => {
onDraw?: ToneEventCallback<any>; n(D, g, h.getContext().currentTime), h.Draw.schedule(() => {
ready?: boolean; // if false, query will not be called on change props a?.(D, g);
} */ }, D);
}, g.part.begin.valueOf());
// function useCycle(props: UseCycleProps) { });
function useCycle(props) {
// onX must use useCallback!
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event, Tone.getContext().currentTime);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, event);
}, time);
}, event.part.begin.valueOf());
});
}; };
S(() => {
useEffect(() => { l && y();
ready && query(); }, [n, c, r, a, l]);
}, [onEvent, onSchedule, onQuery, onDraw, ready]); const w = async () => {
t(!0), await h.start(), h.getTransport().start("+0.1");
const start = async () => { }, p = () => {
setStarted(true); h.getTransport().pause(), t(!1);
await Tone.start();
Tone.getTransport().start('+0.1');
}; };
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return { return {
start, start: w,
stop, stop: p,
onEvent, onEvent: n,
started, started: m,
setStarted, setStarted: t,
toggle, toggle: () => m ? p() : w(),
query, query: y,
activeCycle, activeCycle: o
}; };
} }
function pe(e) {
/* return S(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), C((n) => window.postMessage(n, "*"), []);
usePostMessage.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function usePostMessage(listener) {
useEffect(() => {
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}, [listener]);
return useCallback((data) => window.postMessage(data, '*'), []);
} }
let he = () => Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
const be = (e) => encodeURIComponent(btoa(e));
function ye({ tune: e, autolink: n = !0, onEvent: r, onDraw: c }) {
const l = L(() => he(), []), [a, m] = b(e), [t, u] = b(), [o, y] = b(""), [w, p] = b(), [q, v] = b(!1), [O, M] = b(""), [_, R] = b(), E = L(() => a !== t || w, [a, t, w]), g = C((f) => y((s) => s + `${s ? `
/* ` : ""}${f}`), []), D = L(() => {
useRepl.mjs - <short description TODO> if (t && !t.includes("strudel disable-highlighting"))
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.mjs> return (f, s) => c?.(f, s, t);
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/>. }, [t, c]), z = L(() => t && t.includes("strudel hide-header"), [t]), N = L(() => t && t.includes("strudel hide-console"), [t]), F = ge({
*/ onDraw: D,
onEvent: C(
let s4 = () => { (f, s, J) => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
const generateHash = (code) => encodeURIComponent(btoa(code));
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState('');
const [error, setError] = useState();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState();
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
const onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return (time, event) => onDrawProp?.(time, event, activeCode);
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try { try {
onEvent?.(event); r?.(s), s.context.logs?.length && s.context.logs.forEach(g);
if (event.context.logs?.length) { const { onTrigger: A = se } = s.context;
event.context.logs.forEach(pushLog); A(f, s, J, 1);
} } catch (A) {
const { onTrigger = webaudioOutputTrigger } = event.context; console.warn(A), A.message = "unplayable event: " + A?.message, g(A.message);
onTrigger(time, event, currentTime, 1 /* cps */);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
} }
}, },
[onEvent, pushLog], [r, g]
), ),
onQuery: useCallback( onQuery: C(
(state) => { (f) => {
try { try {
return pattern?.query(state) || []; return _?.query(f) || [];
} catch (err) { } catch (s) {
console.warn(err); return console.warn(s), s.message = "query error: " + s.message, p(s), [];
err.message = 'query error: ' + err.message;
setError(err);
return [];
} }
}, },
[pattern], [_]
), ),
onSchedule: useCallback((_events, cycle) => logCycle(_events), []), onSchedule: C((f, s) => G(f), []),
ready: !!pattern && !!activeCode, ready: !!_ && !!t
}); }), V = pe(({ data: { from: f, type: s } }) => {
s === "start" && f !== l && (F.setStarted(!1), u(void 0));
const broadcast = usePostMessage(({ data: { from, type } }) => { }), I = C(
if (type === 'start' && from !== id) { async (f = a) => {
// console.log('message', from, type); if (t && !E) {
cycle.setStarted(false); p(void 0), F.start();
setActiveCode(undefined);
}
});
const activateCode = useCallback(
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
return; return;
} }
try { try {
setPending(true); v(!0);
const parsed = await evaluate(_code); const s = await ne(f);
cycle.start(); F.start(), V({ type: "start", from: l }), R(() => s.pattern), n && (window.location.hash = "#" + encodeURIComponent(btoa(a))), M(be(a)), p(void 0), u(f), v(!1);
broadcast({ type: 'start', from: id }); } catch (s) {
setPattern(() => parsed.pattern); s.message = "evaluation error: " + s.message, console.warn(s), p(s);
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
setHash(generateHash(code));
setError(undefined);
setActiveCode(_code);
setPending(false);
} catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
} }
}, },
[activeCode, dirty, code, cycle, autolink, id, broadcast], [t, E, a, F, n, l, V]
); ), G = (f, s) => {
// logs events of cycle f.length;
const logCycle = (_events, cycle) => {
if (_events.length) ;
}; };
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return { return {
hideHeader, hideHeader: z,
hideConsole, hideConsole: N,
pending, pending: q,
code, code: a,
setCode, setCode: m,
pattern, pattern: _,
error, error: w,
cycle, cycle: F,
setPattern, setPattern: R,
dirty, dirty: E,
log, log: o,
togglePlay, togglePlay: () => {
setActiveCode, F.started ? F.stop() : I();
activateCode, },
activeCode, setActiveCode: u,
pushLog, activateCode: I,
hash, activeCode: t,
pushLog: g,
hash: O
}; };
} }
function W(...e) {
/* return e.filter(Boolean).join(" ");
cx.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
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/>.
*/
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
} }
let H = [], U;
let highlights = []; // actively highlighted events function we({ view: e, pattern: n, active: r }) {
let lastEnd; S(() => {
if (e)
function useHighlighting({ view, pattern, active }) { if (n && r) {
useEffect(() => { let l = function() {
if (view) {
if (pattern && active) {
let frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
try { try {
const audioTime = Tone.getTransport().seconds; const a = h.getTransport().seconds, t = [Math.max(U || a, a - 1 / 10), a + 1 / 60];
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away U = a + 1 / 60, H = H.filter((o) => o.whole.end > a);
// see https://github.com/tidalcycles/strudel/issues/108 const u = n.queryArc(...t).filter((o) => o.hasOnset());
const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10); H = H.concat(u), e.dispatch({ effects: B.of(H) });
const span = [begin, audioTime + 1 / 60]; } catch {
lastEnd = audioTime + 1 / 60; e.dispatch({ effects: B.of([]) });
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
highlights = highlights.concat(haps); // add potential new onsets
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
} catch (err) {
// console.log('error in updateHighlights', err);
view.dispatch({ effects: setHighlights.of([]) });
} }
frame = requestAnimationFrame(updateHighlights); c = requestAnimationFrame(l);
} }, c = requestAnimationFrame(l);
return () => { return () => {
cancelAnimationFrame(frame); cancelAnimationFrame(c);
}; };
} else { } else
highlights = []; H = [], e.dispatch({ effects: B.of([]) });
view.dispatch({ effects: setHighlights.of([]) }); }, [n, r, e]);
}
}
}, [pattern, active, view]);
} }
const ve = "_container_3i85k_1", Ee = "_header_3i85k_5", ke = "_buttons_3i85k_9", Ce = "_button_3i85k_9", Me = "_buttonDisabled_3i85k_17", _e = "_error_3i85k_21", Ne = "_body_3i85k_25", T = {
var tailwind = ''; container: ve,
header: Ee,
const container = "_container_3i85k_1"; buttons: ke,
const header = "_header_3i85k_5"; button: Ce,
const buttons = "_buttons_3i85k_9"; buttonDisabled: Me,
const button = "_button_3i85k_9"; error: _e,
const buttonDisabled = "_buttonDisabled_3i85k_17"; body: Ne
const error = "_error_3i85k_21";
const body = "_body_3i85k_25";
var styles = {
container: container,
header: header,
buttons: buttons,
button: button,
buttonDisabled: buttonDisabled,
error: error,
body: body
}; };
function $({ type: e }) {
function Icon({ type }) { return /* @__PURE__ */ d.createElement("svg", {
return /* @__PURE__ */ React.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
className: "sc-h-5 sc-w-5", className: "sc-h-5 sc-w-5",
viewBox: "0 0 20 20", viewBox: "0 0 20 20",
fill: "currentColor" fill: "currentColor"
}, { }, {
refresh: /* @__PURE__ */ React.createElement("path", { refresh: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
clipRule: "evenodd" clipRule: "evenodd"
}), }),
play: /* @__PURE__ */ React.createElement("path", { play: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
clipRule: "evenodd" clipRule: "evenodd"
}), }),
pause: /* @__PURE__ */ React.createElement("path", { pause: /* @__PURE__ */ d.createElement("path", {
fillRule: "evenodd", fillRule: "evenodd",
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
clipRule: "evenodd" clipRule: "evenodd"
}) })
}[type]); }[e]);
} }
function Ve({ tune: e, hideOutsideView: n = !1, init: r, onEvent: c, enableKeyboard: l }) {
function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) { const { code: a, setCode: m, pattern: t, activeCode: u, activateCode: o, evaluateOnly: y, error: w, cycle: p, dirty: q, togglePlay: v, stop: O } = ye({
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({ tune: e,
tune, autolink: !1,
autolink: false, onEvent: c
onEvent
}); });
useEffect(() => { S(() => {
init && evaluateOnly(); r && y();
}, [tune, init]); }, [e, r]);
const [view, setView] = useState(); const [M, _] = b(), [R, E] = oe({
const [ref, isVisible] = useInView({
threshold: 0.01 threshold: 0.01
}); }), g = X(), D = L(() => ((E || !n) && (g.current = !0), E || g.current), [E, n]);
const wasVisible = useRef(); return we({ view: M, pattern: t, active: p.started && !u?.includes("strudel disable-highlighting") }), Y(() => {
const show = useMemo(() => { if (l) {
if (isVisible || !hideOutsideView) { const z = async (N) => {
wasVisible.current = true; (N.ctrlKey || N.altKey) && (N.code === "Enter" ? (N.preventDefault(), de(M), await o()) : N.code === "Period" && (p.stop(), N.preventDefault()));
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === "Enter") {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === "Period") {
cycle.stop();
e.preventDefault();
}
}
}; };
window.addEventListener("keydown", handleKeyPress, true); return window.addEventListener("keydown", z, !0), () => window.removeEventListener("keydown", z, !0);
return () => window.removeEventListener("keydown", handleKeyPress, true);
} }
}, [enableKeyboard, pattern, code, activateCode, cycle, view]); }, [l, t, a, o, p, M]), /* @__PURE__ */ d.createElement("div", {
return /* @__PURE__ */ React.createElement("div", { className: T.container,
className: styles.container, ref: R
ref }, /* @__PURE__ */ d.createElement("div", {
}, /* @__PURE__ */ React.createElement("div", { className: T.header
className: styles.header }, /* @__PURE__ */ d.createElement("div", {
}, /* @__PURE__ */ React.createElement("div", { className: T.buttons
className: styles.buttons }, /* @__PURE__ */ d.createElement("button", {
}, /* @__PURE__ */ React.createElement("button", { className: W(T.button, p.started ? "sc-animate-pulse" : ""),
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""), onClick: () => v()
onClick: () => togglePlay() }, /* @__PURE__ */ d.createElement($, {
}, /* @__PURE__ */ React.createElement(Icon, { type: p.started ? "pause" : "play"
type: cycle.started ? "pause" : "play" })), /* @__PURE__ */ d.createElement("button", {
})), /* @__PURE__ */ React.createElement("button", { className: W(q ? T.button : T.buttonDisabled),
className: cx(dirty ? styles.button : styles.buttonDisabled), onClick: () => o()
onClick: () => activateCode() }, /* @__PURE__ */ d.createElement($, {
}, /* @__PURE__ */ React.createElement(Icon, {
type: "refresh" type: "refresh"
}))), error && /* @__PURE__ */ React.createElement("div", { }))), w && /* @__PURE__ */ d.createElement("div", {
className: styles.error className: T.error
}, error.message)), /* @__PURE__ */ React.createElement("div", { }, w.message)), /* @__PURE__ */ d.createElement("div", {
className: styles.body className: T.body
}, show && /* @__PURE__ */ React.createElement(CodeMirror, { }, D && /* @__PURE__ */ d.createElement(me, {
value: code, value: a,
onChange: setCode, onChange: m,
onViewChanged: setView onViewChanged: _
}))); })));
} }
function Ie(e) {
/* const { ready: n, connected: r, disconnected: c } = e, [l, a] = b(!0), [m, t] = b(k?.outputs || []);
useWebMidi.js - <short description TODO> return S(() => {
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js> ce().then(() => {
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/>. k.addListener("connected", (o) => {
*/ t([...k.outputs]), r?.(k, o);
}), k.addListener("disconnected", (o) => {
function useWebMidi(props) { t([...k.outputs]), c?.(k, o);
const { ready, connected, disconnected } = props; }), n?.(k), a(!1);
const [loading, setLoading] = useState(true); }).catch((o) => {
const [outputs, setOutputs] = useState(WebMidi?.outputs || []); if (o) {
useEffect(() => { console.error(o), console.warn("Web Midi could not be enabled..");
enableWebMidi() return;
.then(() => { }
// Reacting when a new device becomes available });
WebMidi.addListener('connected', (e) => { }, [n, r, c, m]), { loading: l, outputs: m, outputByName: (o) => k.getOutputByName(o) };
setOutputs([...WebMidi.outputs]);
connected?.(WebMidi, e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
setOutputs([...WebMidi.outputs]);
disconnected?.(WebMidi, e);
});
ready?.(WebMidi);
setLoading(false);
})
.catch((err) => {
if (err) {
console.error(err);
//throw new Error("Web Midi could not be enabled...");
console.warn('Web Midi could not be enabled..');
return;
}
});
}, [ready, connected, disconnected, outputs]);
const outputByName = (name) => WebMidi.getOutputByName(name);
return { loading, outputs, outputByName };
} }
export {
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi }; me as CodeMirror,
Ve as MiniRepl,
W as cx,
de as flash,
ge as useCycle,
we as useHighlighting,
pe as usePostMessage,
ye as useRepl,
Ie as useWebMidi
};

View File

@ -1 +1 @@
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto} .cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}

View File

@ -37,12 +37,12 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.0.2", "@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.2.0", "@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/eval": "^0.2.0", "@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/tone": "^0.2.0", "@strudel.cycles/tone": "^0.2.0",
"@uiw/codemirror-themes": "^4.11.4", "@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.11.4", "@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0" "react-hook-inview": "^4.5.0"
}, },
"peerDependencies": { "peerDependencies": {
@ -52,12 +52,12 @@
"devDependencies": { "devDependencies": {
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@vitejs/plugin-react": "^1.3.0", "@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"postcss": "^8.4.13", "postcss": "^8.4.18",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"vite": "^2.9.9" "vite": "^3.2.2"
} }
} }

View File

@ -2,8 +2,10 @@ import React from 'react';
import { MiniRepl } from './components/MiniRepl'; import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css'; import 'tailwindcss/tailwind.css';
import { evalScope } from '@strudel.cycles/eval'; import { evalScope } from '@strudel.cycles/eval';
import { controls } from '@strudel.cycles/core';
evalScope( evalScope(
controls,
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tone'), import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
@ -16,7 +18,7 @@ evalScope(
function App() { function App() {
return ( return (
<div> <div>
<MiniRepl tune={`"c3"`} /> <MiniRepl tune={`note("c3")`} />
</div> </div>
); );
} }

View File

@ -27,6 +27,6 @@
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.2.0", "@strudel.cycles/core": "^0.2.0",
"@tonaljs/tonal": "^4.6.5", "@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15" "webmidi": "^3.0.21"
} }
} }

View File

@ -23,7 +23,6 @@
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.2.0", "@strudel.cycles/core": "^0.2.0",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1", "chord-voicings": "^0.0.1",
"tone": "^14.7.77" "tone": "^14.7.77"
} }

View File

@ -30,8 +30,6 @@ const {
getDestination, getDestination,
Players, Players,
} = Tone; } = Tone;
import * as tonePiano from '@tonejs/piano';
const { Piano } = tonePiano;
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs'; import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
// "balanced" | "interactive" | "playback"; // "balanced" | "interactive" | "playback";
@ -61,10 +59,6 @@ Pattern.prototype.tone = function (instrument) {
instrument.triggerAttack(note, time); instrument.triggerAttack(note, time);
} else if (instrument instanceof NoiseSynth) { } else if (instrument instanceof NoiseSynth) {
instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value instrument.triggerAttackRelease(hap.duration.valueOf(), time); // noise has no value
} else if (instrument instanceof Piano) {
note = getPlayableNoteValue(hap);
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + hap.duration.valueOf(), velocity });
} else if (instrument instanceof Sampler) { } else if (instrument instanceof Sampler) {
note = getPlayableNoteValue(hap); note = getPlayableNoteValue(hap);
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity); instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
@ -110,11 +104,6 @@ export const players = (options, baseUrl = '') => {
}); });
}; };
export const synth = (options) => new Synth(options); export const synth = (options) => new Synth(options);
export const piano = async (options = { velocities: 1 }) => {
const p = new Piano(options);
await p.load();
return p;
};
// effect helpers // effect helpers
export const vol = (v) => new Gain(v); export const vol = (v) => new Gain(v);

View File

@ -1,7 +1,7 @@
if (typeof AudioContext !== 'undefined') { if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.impulseResponse = function (duration) { AudioContext.prototype.impulseResponse = function (duration, channels = 1) {
const length = this.sampleRate * duration; const length = this.sampleRate * duration;
const impulse = this.createBuffer(2, length, this.sampleRate); const impulse = this.createBuffer(channels, length, this.sampleRate);
const IR = impulse.getChannelData(0); const IR = impulse.getChannelData(0);
for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration); for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration);
return impulse; return impulse;

View File

@ -102,7 +102,21 @@ export const loadGithubSamples = async (path, nameFn) => {
* *
*/ */
export const samples = (sampleMap, baseUrl = sampleMap._base || '') => { export const samples = async (sampleMap, baseUrl = sampleMap._base || '') => {
if (typeof sampleMap === 'string') {
if (sampleMap.startsWith('github:')) {
const [_, path] = sampleMap.split('github:');
sampleMap = `https://raw.githubusercontent.com/${path}/strudel.json`;
}
const base = sampleMap.split('/').slice(0, -1).join('/');
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
}
sampleCache.current = { sampleCache.current = {
...sampleCache.current, ...sampleCache.current,
...Object.fromEntries( ...Object.fromEntries(

View File

@ -22,8 +22,9 @@ class CoarseProcessor extends AudioWorkletProcessor {
this.notStarted = false; this.notStarted = false;
output[0][0] = input[0][0]; output[0][0] = input[0][0];
for (let n = 1; n < blockSize; n++) { for (let n = 1; n < blockSize; n++) {
if (n % coarse == 0) output[0][n] = input[0][n]; for (let o = 0; o < output.length; o++) {
else output[0][n] = output[0][n - 1]; output[o][n] = n % coarse == 0 ? input[0][n] : output[o][n - 1];
}
} }
} }
return this.notStarted || hasInput; return this.notStarted || hasInput;
@ -52,11 +53,19 @@ class CrushProcessor extends AudioWorkletProcessor {
this.notStarted = false; this.notStarted = false;
if (crush.length === 1) { if (crush.length === 1) {
const x = Math.pow(2, crush[0] - 1); const x = Math.pow(2, crush[0] - 1);
for (let n = 0; n < blockSize; n++) output[0][n] = Math.round(input[0][n] * x) / x; for (let n = 0; n < blockSize; n++) {
const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
} else { } else {
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
let x = Math.pow(2, crush[n] - 1); let x = Math.pow(2, crush[n] - 1);
output[0][n] = Math.round(input[0][n] * x) / x; const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
} }
} }
} }
@ -86,7 +95,10 @@ class ShapeProcessor extends AudioWorkletProcessor {
if (hasInput) { if (hasInput) {
this.notStarted = false; this.notStarted = false;
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
output[0][n] = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n])); const value = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n]));
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
} }
} }
return this.notStarted || hasInput; return this.notStarted || hasInput;

865
repl/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,6 @@
"postcss": "^8.4.13", "postcss": "^8.4.13",
"rollup-plugin-visualizer": "^5.8.1", "rollup-plugin-visualizer": "^5.8.1",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"vite": "^2.9.9" "vite": "^3.2.2"
} }
} }

View File

@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import controls from '@strudel.cycles/core/controls.mjs';
import { evalScope, evaluate } from '@strudel.cycles/eval'; import { evalScope, evaluate } from '@strudel.cycles/eval';
import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react'; import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react';
import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone'; import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
@ -15,6 +14,7 @@ import * as tunes from './tunes.mjs';
import { prebake } from './prebake.mjs'; import { prebake } from './prebake.mjs';
import * as WebDirt from 'WebDirt'; import * as WebDirt from 'WebDirt';
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio'; import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
import { controls } from '@strudel.cycles/core';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@ -26,7 +26,7 @@ const supabase = createClient(
evalScope( evalScope(
Tone, Tone,
controls, controls, // sadly, this cannot be exported from core direclty
{ WebDirt }, { WebDirt },
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tone'), import('@strudel.cycles/tone'),

View File

@ -1,23 +1,17 @@
import { Pattern, toMidi } from '@strudel.cycles/core'; import { Pattern, toMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio'; import { samples } from '@strudel.cycles/webaudio';
const loadSamples = async (url, baseDir = '') => {
await fetch(url)
.then((res) => res.json())
.then((json) => samples(json, baseDir));
};
export async function prebake({ isMock = false, baseDir = '.' } = {}) { export async function prebake({ isMock = false, baseDir = '.' } = {}) {
if (!isMock) { if (!isMock) {
// https://archive.org/details/SalamanderGrandPianoV3 // https://archive.org/details/SalamanderGrandPianoV3
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
loadSamples('piano.json', `${baseDir}/piano/`); samples('piano.json', `${baseDir}/piano/`);
// https://github.com/sgossner/VCSL/ // https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/ // https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose // LICENSE: CC0 general-purpose
loadSamples('vcsl.json', 'github:sgossner/VCSL/master/'); samples('vcsl.json', 'github:sgossner/VCSL/master/');
loadSamples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'); samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/');
loadSamples('EmuSP12.json', `${baseDir}/EmuSP12/`); samples('EmuSP12.json', `${baseDir}/EmuSP12/`);
} }
} }

View File

@ -83,7 +83,9 @@ const toneHelpersMocked = {
highpass: mockNode, highpass: mockNode,
}; };
// tone mock strudel.Pattern.prototype.osc = function () {
return this;
};
strudel.Pattern.prototype.tone = function () { strudel.Pattern.prototype.tone = function () {
return this; return this;
}; };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
import { queryCode } from '../../repl/src/runtime.mjs';
import { describe, it } from 'vitest';
import doc from '../../doc.json';
describe('runs examples', () => {
const { docs } = doc;
docs.forEach(async (doc) => {
doc.examples?.forEach((example, i) => {
it(`example "${doc.name}" example index ${i}`, async ({ expect }) => {
const haps = await queryCode(example, 4);
expect(haps).toMatchSnapshot();
});
});
});
});