Merge remote-tracking branch 'origin/main' into general-purpose-scheduler

This commit is contained in:
Felix Roos 2022-11-06 11:06:32 +01:00
commit 0485632e22
53 changed files with 24365 additions and 8987 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'],
@ -100,6 +95,18 @@ const generic_params = [
'attack', 'attack',
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.', 'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
], ],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
*
* @name bank
* @param {string | Pattern} bank the name of the bank
* @example
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['f', 'bank', 'selects sound bank to use'],
// TODO: find out how this works? // TODO: find out how this works?
/* /*
* Envelope decay time = the time it takes after the attack time to reach the sustain level. * Envelope decay time = the time it takes after the attack time to reach the sustain level.

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';
@ -17,6 +17,7 @@ export * from './util.mjs';
export * from './speak.mjs'; export * from './speak.mjs';
export * from './clockworker.mjs'; export * from './clockworker.mjs';
export * from './scheduler.mjs'; export * from './scheduler.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)));
@ -63,6 +65,17 @@ export class Pattern {
return new Pattern((state) => this.query(state.withSpan(func))); return new Pattern((state) => this.query(state.withSpan(func)));
} }
withQuerySpanMaybe(func) {
const pat = this;
return new Pattern((state) => {
const newState = state.withSpan(func);
if (!newState.span) {
return [];
}
return pat.query(newState);
});
}
/** /**
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the * As with {@link Pattern#withQuerySpan}, but the function is applied to both the
* begin and end time of the query timespan. * begin and end time of the query timespan.
@ -435,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;
});
} }
/** /**
@ -744,12 +731,17 @@ export class Pattern {
// // there is no gap.. so maybe revert to _fast? // // there is no gap.. so maybe revert to _fast?
// return this._fast(factor) // return this._fast(factor)
// } // }
// A bit fiddly, to drop zero-width queries at the start of the next cycle
const qf = function (span) { const qf = function (span) {
const cycle = span.begin.sam(); const cycle = span.begin.sam();
const begin = cycle.add(span.begin.sub(cycle).mul(factor).min(1)); const bpos = span.begin.sub(cycle).mul(factor).min(1);
const end = cycle.add(span.end.sub(cycle).mul(factor).min(1)); const epos = span.end.sub(cycle).mul(factor).min(1);
return new TimeSpan(begin, end); if (bpos >= 1) {
return undefined;
}
return new TimeSpan(cycle.add(bpos), cycle.add(epos));
}; };
// Also fiddly, to maintain the right 'whole' relative to the part
const ef = function (hap) { const ef = function (hap) {
const begin = hap.part.begin; const begin = hap.part.begin;
const end = hap.part.end; const end = hap.part.end;
@ -765,7 +757,7 @@ export class Pattern {
); );
return new Hap(newWhole, newPart, hap.value, hap.context); return new Hap(newWhole, newPart, hap.value, hap.context);
}; };
return this.withQuerySpan(qf)._withHap(ef)._splitQueries(); return this.withQuerySpanMaybe(qf)._withHap(ef)._splitQueries();
} }
// Compress each cycle into the given timespan, leaving a gap // Compress each cycle into the given timespan, leaving a gap
@ -1370,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],
@ -1396,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.
@ -1406,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.
@ -1415,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],

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);
@ -137,6 +140,9 @@ describe('Pattern', () => {
it('Can make a pattern', () => { it('Can make a 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', () => {
expect(pure('hello').queryArc(0, 0).length).toBe(1);
});
}); });
describe('fmap()', () => { describe('fmap()', () => {
it('Can add things', () => { it('Can add things', () => {
@ -191,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()', () => {
@ -373,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()', () => {
@ -430,6 +440,16 @@ describe('Pattern', () => {
// mini('[c3 g3]/2 eb3') always plays [c3 eb3] // mini('[c3 g3]/2 eb3') always plays [c3 eb3]
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3] // mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
}); });
it('Supports zero-length queries', () => {
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
});
});
describe('slow()', () => {
it('Supports zero-length queries', () => {
expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
steady('a')._setContext({}).queryArc(0, 0),
);
});
}); });
describe('inside', () => { describe('inside', () => {
it('can rev inside a cycle', () => { it('can rev inside a cycle', () => {
@ -799,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', () => {
@ -855,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', () => {
@ -92,16 +105,16 @@ describe('getFrequency', () => {
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220); expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
}); });
it('should return frequencies unchanged', () => { it('should return frequencies unchanged', () => {
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440); expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
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

@ -18,6 +18,11 @@ export class TimeSpan {
const end = this.end; const end = this.end;
const end_sam = end.sam(); const end_sam = end.sam();
// Support zero-width timespans
if (begin.equals(end)) {
return([new TimeSpan(begin, end)]);
}
while (end.gt(begin)) { while (end.gt(begin)) {
// If begin and end are in the same cycle, we're done. // If begin and end are in the same cycle, we're done.
if (begin.sam().equals(end_sam)) { if (begin.sam().equals(end_sam)) {

View File

@ -133,3 +133,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

File diff suppressed because it is too large Load Diff

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,29 @@ 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`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = sampleMap.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
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

@ -100,12 +100,8 @@ const getSoundfontKey = (s) => {
const getSampleBufferSource = async (s, n, note, speed) => { const getSampleBufferSource = async (s, n, note, speed) => {
let transpose = 0; let transpose = 0;
let midi; let midi = typeof note === 'string' ? toMidi(note) : note || 36;
transpose = midi - 36; // C3 is middle C
if (note !== undefined) {
midi = typeof note === 'string' ? toMidi(note) : note;
transpose = midi - 36; // C3 is middle C
}
const ac = getAudioContext(); const ac = getAudioContext();
// is sample from loaded samples(..) // is sample from loaded samples(..)
@ -128,9 +124,6 @@ const getSampleBufferSource = async (s, n, note, speed) => {
if (Array.isArray(bank)) { if (Array.isArray(bank)) {
sampleUrl = bank[n % bank.length]; sampleUrl = bank[n % bank.length];
} else { } else {
if (!note) {
throw new Error('no note(...) set for sound', s);
}
const midiDiff = (noteA) => toMidi(noteA) - midi; const midiDiff = (noteA) => toMidi(noteA) - midi;
// object format will expect keys as notes // object format will expect keys as notes
const closest = Object.keys(bank) const closest = Object.keys(bank)
@ -253,6 +246,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
let { let {
freq, freq,
s, s,
bank,
sf, sf,
clip = 0, // if 1, samples will be cut off when the hap ends clip = 0, // if 1, samples will be cut off when the hap ends
n = 0, n = 0,
@ -288,6 +282,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
gain *= velocity; // legacy fix for velocity gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other // the chain will hold all audio nodes that connect to each other
const chain = []; const chain = [];
if (bank && s) {
s = `${bank}_${s}`;
}
if (typeof s === 'string') { if (typeof s === 'string') {
[s, n] = splitSN(s, n); [s, n] = splitSN(s, n);
} }

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

@ -2,6 +2,7 @@
"name": "@strudel.cycles/repl", "name": "@strudel.cycles/repl",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"start": "vite", "start": "vite",
@ -12,7 +13,8 @@
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt", "add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
"predeploy": "npm run build", "predeploy": "npm run build",
"deploy": "gh-pages -d ../docs", "deploy": "gh-pages -d ../docs",
"static": "npx serve ../docs" "static": "npx serve ../docs",
"dbdump": "node src/test/dbdump.js > src/test/dbdump.json"
}, },
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^1.35.3", "@supabase/supabase-js": "^1.35.3",
@ -27,6 +29,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"
} }
} }

33
repl/public/piano.json Normal file
View File

@ -0,0 +1,33 @@
{
"piano": {
"A0": "A0v8.mp3",
"C1": "C1v8.mp3",
"Ds1": "Ds1v8.mp3",
"Fs1": "Fs1v8.mp3",
"A1": "A1v8.mp3",
"C2": "C2v8.mp3",
"Ds2": "Ds2v8.mp3",
"Fs2": "Fs2v8.mp3",
"A2": "A2v8.mp3",
"C3": "C3v8.mp3",
"Ds3": "Ds3v8.mp3",
"Fs3": "Fs3v8.mp3",
"A3": "A3v8.mp3",
"C4": "C4v8.mp3",
"Ds4": "Ds4v8.mp3",
"Fs4": "Fs4v8.mp3",
"A4": "A4v8.mp3",
"C5": "C5v8.mp3",
"Fs5": "Fs5v8.mp3",
"A5": "A5v8.mp3",
"C6": "C6v8.mp3",
"Ds6": "Ds6v8.mp3",
"Fs6": "Fs6v8.mp3",
"A6": "A6v8.mp3",
"C7": "C7v8.mp3",
"Ds7": "Ds7v8.mp3",
"Fs7": "Fs7v8.mp3",
"A7": "A7v8.mp3",
"C8": "C8v8.mp3"
}
}

File diff suppressed because it is too large Load Diff

2164
repl/public/vcsl.json Normal file

File diff suppressed because it is too large Load Diff

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

@ -2,49 +2,16 @@ import { Pattern, toMidi } from '@strudel.cycles/core';
import { samples } from '@strudel.cycles/webaudio'; import { samples } from '@strudel.cycles/webaudio';
export async function prebake({ isMock = false, baseDir = '.' } = {}) { export async function prebake({ isMock = false, baseDir = '.' } = {}) {
samples( if (!isMock) {
{
piano: {
A0: 'A0v8.mp3',
C1: 'C1v8.mp3',
Ds1: 'Ds1v8.mp3',
Fs1: 'Fs1v8.mp3',
A1: 'A1v8.mp3',
C2: 'C2v8.mp3',
Ds2: 'Ds2v8.mp3',
Fs2: 'Fs2v8.mp3',
A2: 'A2v8.mp3',
C3: 'C3v8.mp3',
Ds3: 'Ds3v8.mp3',
Fs3: 'Fs3v8.mp3',
A3: 'A3v8.mp3',
C4: 'C4v8.mp3',
Ds4: 'Ds4v8.mp3',
Fs4: 'Fs4v8.mp3',
A4: 'A4v8.mp3',
C5: 'C5v8.mp3',
Ds4: 'Ds4v8.mp3',
Fs5: 'Fs5v8.mp3',
A5: 'A5v8.mp3',
C6: 'C6v8.mp3',
Ds6: 'Ds6v8.mp3',
Fs6: 'Fs6v8.mp3',
A6: 'A6v8.mp3',
C7: 'C7v8.mp3',
Ds7: 'Ds7v8.mp3',
Fs7: 'Fs7v8.mp3',
A7: 'A7v8.mp3',
C8: 'C8v8.mp3',
},
},
// 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
`${baseDir}/piano/`, samples('piano.json', `${baseDir}/piano/`);
); // https://github.com/sgossner/VCSL/
if (!isMock) { // https://api.github.com/repositories/126427031/contents/
await fetch('EmuSP12.json') // LICENSE: CC0 general-purpose
.then((res) => res.json()) samples('vcsl.json', 'github:sgossner/VCSL/master/');
.then((json) => samples(json, `${baseDir}/EmuSP12/`)); samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/');
samples('EmuSP12.json', `${baseDir}/EmuSP12/`);
} }
} }

View File

@ -65,6 +65,7 @@ const toneHelpersMocked = {
Chorus: MockedNode, Chorus: MockedNode,
Freeverb: MockedNode, Freeverb: MockedNode,
Gain: MockedNode, Gain: MockedNode,
Reverb: MockedNode,
vol: mockNode, vol: mockNode,
out: id, out: id,
osc: id, osc: id,
@ -83,7 +84,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;
}; };
@ -114,17 +117,40 @@ strudel.Pattern.prototype.adsr = function () {
strudel.Pattern.prototype.out = function () { strudel.Pattern.prototype.out = function () {
return this; return this;
}; };
strudel.Pattern.prototype.soundfont = function () {
return this;
};
// tune mock // tune mock
strudel.Pattern.prototype.tune = function () { strudel.Pattern.prototype.tune = function () {
return this; return this;
}; };
strudel.Pattern.prototype.midi = function () {
return this;
};
const uiHelpersMocked = { const uiHelpersMocked = {
backgroundImage: id, backgroundImage: id,
}; };
prebake({ isMock: true }); prebake({ isMock: true });
const canvasCtx = {
clearRect: () => {},
fillText: () => {},
fillRect: () => {},
canvas: {
width: 100,
height: 100,
},
};
const audioCtx = {
currentTime: 1,
};
const getDrawContext = () => canvasCtx;
const getAudioContext = () => audioCtx;
const loadSoundfont = () => {};
// TODO: refactor to evalScope // TODO: refactor to evalScope
evalScope( evalScope(
// Tone, // Tone,
@ -144,6 +170,10 @@ evalScope(
// gist, // gist,
// euclid, // euclid,
mini, mini,
getDrawContext,
getAudioContext,
loadSoundfont,
Clock: {}, // whatever
// Tone, // Tone,
}, },
); );
@ -195,3 +225,6 @@ export const testCycles = {
hyperpop: 10, hyperpop: 10,
festivalOfFingers3: 16, festivalOfFingers3: 16,
}; };
// fixed: https://strudel.tidalcycles.org/?DBp75NUfSxIn (missing .note())
// bug: https://strudel.tidalcycles.org/?xHaKTd1kTpCn + https://strudel.tidalcycles.org/?o5LLePbx8kiQ

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

10
repl/src/test/dbdump.js Normal file
View File

@ -0,0 +1,10 @@
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://pidxdsxphlhzjnzmifth.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
);
const { data } = await supabase.from('code');
console.log(JSON.stringify(data));

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
import { queryCode } from '../runtime.mjs';
import { describe, it } from 'vitest';
import data from './dbdump.json';
describe('renders shared tunes', async () => {
data.forEach(({ id, code, hash }) => {
const url = `https://strudel.tidalcycles.org/?${hash}`;
it(`shared tune ${id} ${url}`, async ({ expect }) => {
if (code.includes('import(')) {
console.log('skip', url);
return;
}
const haps = await queryCode(code, 1);
expect(haps).toMatchSnapshot();
});
});
});

View File

@ -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/>. 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 const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]] export const tetrisMini = `note(\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]] [a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5] [b4 [~ c5] d5 e5]
[c5 a4 a4 ~] [c5 a4 a4 ~]
@ -19,7 +19,7 @@ export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
[[d2 d3]*4] [[d2 d3]*4]
[[c2 c3]*4] [[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2] [[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16) [[a1 a2]*4]\`).slow(16)
`; `;
export const swimming = `stack( export const swimming = `stack(
@ -80,7 +80,7 @@ export const swimming = `stack(
"[F2 A2 Bb2 B2]", "[F2 A2 Bb2 B2]",
"[G2 C2 F2 F2]" "[G2 C2 F2 F2]"
) )
).slow(51); ).note().slow(51);
`; `;
export const giantSteps = `stack( export const giantSteps = `stack(
@ -105,7 +105,7 @@ export const giantSteps = `stack(
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]" "[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
) )
).slow(20)`; ).slow(20).note()`;
export const giantStepsReggae = `stack( export const giantStepsReggae = `stack(
// melody // melody
@ -132,7 +132,7 @@ export const giantStepsReggae = `stack(
"[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]" "[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]"
) )
.struct("x ~".fast(4*8)) .struct("x ~".fast(4*8))
).slow(25)`; ).slow(25).note()`;
export const zeldasRescue = `stack( export const zeldasRescue = `stack(
// melody // melody
@ -511,7 +511,7 @@ stack(
f3!2 e3!2 ab3!2 ~!2 f3!2 e3!2 ab3!2 ~!2
>\` >\`
.legato(.5) .legato(.5)
).fast(2) // .note().piano()`; ).fast(2).note()`;
/* /*
// TODO: does not work on linux (at least for me..) // TODO: does not work on linux (at least for me..)
@ -554,7 +554,6 @@ stack(
"<D2 A2 G2 F2>".euclidLegato(6,8,1).note().s('bass').clip(1).gain(.8) "<D2 A2 G2 F2>".euclidLegato(6,8,1).note().s('bass').clip(1).gain(.8)
) )
.slow(6) .slow(6)
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'}) .pianoroll({minMidi:20,maxMidi:120,background:'transparent'})
`; `;
@ -701,7 +700,6 @@ stack(
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes .echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
//.hush() //.hush()
) )
.slow(3/2)`; .slow(3/2)`;
export const swimmingWithSoundfonts = `stack( export const swimmingWithSoundfonts = `stack(
@ -814,7 +812,6 @@ x=>x.add(7).color('steelblue')
//.hcutoff(400) //.hcutoff(400)
.clip(1) .clip(1)
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2")) .stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
.pianoroll({vertical:1})`; .pianoroll({vertical:1})`;
export const bossaRandom = `const chords = "<Am7 Am7 Dm7 E7>" export const bossaRandom = `const chords = "<Am7 Am7 Dm7 E7>"
@ -857,3 +854,130 @@ export const orbit = `stack(
.delayfeedback(.7) .delayfeedback(.7)
.orbit(2) .orbit(2)
).sometimes(x=>x.speed("-1"))`; ).sometimes(x=>x.speed("-1"))`;
export const belldub = `samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}})
// "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org
stack(
// bass
note("[0 ~] [2 [0 2]] [4 4*2] [[4 ~] [2 ~] 0@2]".scale('g1 dorian').superimpose(x=>x.add(.02)))
.s('sawtooth').cutoff(200).resonance(20).gain(.15).shape(.6).release(.05),
// perc
s("[~ hh]*4").room("0 0.5".fast(2)).end(perlin.range(0.02,1)),
s("mt lt ht").struct("x(3,8)").fast(2).gain(.5).room(.5).sometimes(x=>x.speed(".5")),
s("misc:2").speed(1).delay(.5).delaytime(1/3).gain(.4),
// chords
note("[~ Gm7] ~ [~ Dm7] ~".voicings().superimpose(x=>x.add(.1)))
.s('sawtooth').gain(.5)
.cutoff(perlin.range(400,3000).slow(8))
.decay(perlin.range(0.05,.2)).sustain(0)
.delay(.9).room(1),
// blips
note(
"0 5 4 2".iter(4)
.off(1/3, add(7))
.scale('g4 dorian')
).s('square').cutoff(2000).decay(.03).sustain(0)
.degradeBy(.2)
.orbit(2).delay(.2).delaytime(".33 | .6 | .166 | .25")
.room(1).gain(.5).mask("<0 1>/8"),
// bell
note(rand.range(0,12).struct("x(5,8)").scale('g2 minor pentatonic')).s('bell').begin(.05)
.delay(.2).degradeBy(.4).gain(.4)
.mask("<1 0>/8")
).slow(5)`;
export const dinofunk = `samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3',
dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}})
stack(
s('bass').loopAt(8,1).clip(1),
s("bd*2, ~ sd,hh*4"),
note("Abm7".voicings(['c3','a4']).struct("x(3,8,1)".slow(2))),
"0 1 2 3".scale('ab4 minor pentatonic')
.superimpose(x=>x.add(.1))
.sometimes(x=>x.add(12))
.note().s('sawtooth')
.cutoff(sine.range(400,2000).slow(16)).gain(.8)
.decay(perlin.range(.05,.2)).sustain(0)
.delay(sine.range(0,.5).slow(32))
.degradeBy(.4).room(1),
note("<b4 eb4>").s('dino').delay(.8).slow(8).room(.5)
)`;
export const sampleDemo = `stack(
// percussion
s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3")
.sometimes(x=>x.speed(2)),
// melody
note("<0 4 1 3 2>".off(".25 | .125",add(2)).scale('D3 hirajoshi'))
.s("clavisynth").gain(.2).delay(.25).jux(rev)
.degradeBy(sine.range(0,.5).slow(32)),
// bass
note("<0@3 <2(3,8) 3(3,8)>>".scale('D1 hirajoshi'))
.s('psaltery_pluck').gain(.6).clip(1)
.release(.1).room(.5)
)`;
export const holyflute = `"c3 eb3(3,8) c4/2 g3*2"
.superimpose(
x=>x.slow(2).add(12),
x=>x.slow(4).sub(5)
).add("<0 1>/16")
.note().s('ocarina_vib').clip(1)
.release(.1).room(1).gain(.2)
.color("salmon | orange | darkseagreen")
.pianoroll({fold:0,autorange:0,vertical:0,cycles:12,smear:0,minMidi:40})
`;
export const flatrave = `stack(
s("bd*2,~ [cp,sd]").bank('RolandTR909'),
s("hh:1*4").sometimes(fast("2"))
.rarely(x=>x.speed(".5").delay(.5))
.end(perlin.range(0.02,.05).slow(8))
.bank('RolandTR909').room(.5)
.gain("0.4,0.4(5,8)"),
note("<0 2 5 3>".scale('G1 minor')).struct("x(5,8)")
.s('sawtooth').decay(.1).sustain(0),
note("<G4 A4 Bb4 A4>,Bb3,D3").struct("~ x*2").s('square').clip(1)
.cutoff(sine.range(500,4000).slow(16)).resonance(10)
.decay(sine.slow(15).range(.05,.2)).sustain(0)
.room(.5).gain(.3).delay(.2).mask("<0 1@3>/8"),
"0 5 3 2".sometimes(slow(2)).off(1/8,add(5)).scale('G4 minor').note()
.decay(.05).sustain(0).delay(.2).degradeBy(.5).mask("<0 1>/16")
)`;
export const amensister = `samples('github:tidalcycles/Dirt-Samples/master')
stack(
// amen
n("0 1 2 3 4 5 6 7")
.sometimes(x=>x.ply(2))
.rarely(x=>x.speed("2 | -2"))
.sometimesBy(.4, x=>x.delay(".5"))
.s("amencutup")
.slow(2)
.room(.5)
,
// bass
sine.add(saw.slow(4)).range(0,7).segment(8)
.superimpose(x=>x.add(.1))
.scale('G0 minor').note()
.s("sawtooth").decay(.1).sustain(0)
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
.degradeBy("0 0.1 .5 .1")
.rarely(add(note("12")))
,
// chord
note("Bb3,D4".superimpose(x=>x.add(.2)))
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
,
// alien
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
,
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
).reset("<x@7 x(5,8)>")`;

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();
});
});
});
});

View File

@ -11,5 +11,13 @@ export default defineConfig({
reporters: 'verbose', reporters: 'verbose',
isolate: false, isolate: false,
silent: true, silent: true,
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.*',
'**/shared.test.mjs',
],
}, },
}); });