mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-13 06:38:29 +00:00
Merge remote-tracking branch 'origin/main' into general-purpose-scheduler
This commit is contained in:
commit
0485632e22
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16, 17]
|
||||
node-version: [18]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
12401
package-lock.json
generated
12401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
"scripts": {
|
||||
"pretest": "cd tutorial && npm run jsdoc-json",
|
||||
"test": "vitest run --version",
|
||||
"test-ui": "vitest --ui",
|
||||
"test-coverage": "vitest --coverage",
|
||||
@ -43,11 +44,10 @@
|
||||
"c8": "^7.12.0",
|
||||
"events": "^3.3.0",
|
||||
"gh-pages": "^4.0.0",
|
||||
"happy-dom": "^6.0.4",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"lerna": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"vitest": "^0.21.1"
|
||||
}
|
||||
|
||||
@ -40,11 +40,6 @@ const generic_params = [
|
||||
* @example
|
||||
* 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
|
||||
['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'],
|
||||
@ -100,6 +95,18 @@ const generic_params = [
|
||||
'attack',
|
||||
'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?
|
||||
/*
|
||||
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
|
||||
|
||||
@ -21,6 +21,7 @@ import Fraction, { gcd } from './fraction.mjs';
|
||||
* @example
|
||||
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
|
||||
* console.log(line);
|
||||
* silence;
|
||||
*/
|
||||
function drawLine(pat, chars = 60) {
|
||||
let cycle = 0;
|
||||
|
||||
44
packages/core/examples/scheduled.html
Normal file
44
packages/core/examples/scheduled.html
Normal 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>
|
||||
@ -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/>.
|
||||
*/
|
||||
|
||||
export * from './controls.mjs';
|
||||
import controls from './controls.mjs';
|
||||
export * from './euclid.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
export { Fraction };
|
||||
export { Fraction, controls };
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
@ -17,6 +17,7 @@ export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './clockworker.mjs';
|
||||
export * from './scheduler.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
|
||||
@ -10,7 +10,7 @@ import Hap from './hap.mjs';
|
||||
import State from './state.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';
|
||||
|
||||
/** @class Class representing a pattern. */
|
||||
@ -32,8 +32,10 @@ export class Pattern {
|
||||
* @param {Fraction | number} end to time
|
||||
* @returns Hap[]
|
||||
* @example
|
||||
* const pattern = sequence('a', ['b', 'c']);
|
||||
* const haps = pattern.queryArc(0, 1);
|
||||
* const pattern = sequence('a', ['b', 'c'])
|
||||
* const haps = pattern.queryArc(0, 1)
|
||||
* console.log(haps)
|
||||
* silence
|
||||
*/
|
||||
queryArc(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)));
|
||||
}
|
||||
|
||||
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
|
||||
* 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();
|
||||
}
|
||||
|
||||
_asNumber(dropfails = false, softfail = false) {
|
||||
return this._withHap((hap) => {
|
||||
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;
|
||||
});
|
||||
_asNumber() {
|
||||
return this.fmap(parseNumeral);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -744,12 +731,17 @@ export class Pattern {
|
||||
// // there is no gap.. so maybe revert to _fast?
|
||||
// return this._fast(factor)
|
||||
// }
|
||||
// A bit fiddly, to drop zero-width queries at the start of the next cycle
|
||||
const qf = function (span) {
|
||||
const cycle = span.begin.sam();
|
||||
const begin = cycle.add(span.begin.sub(cycle).mul(factor).min(1));
|
||||
const end = cycle.add(span.end.sub(cycle).mul(factor).min(1));
|
||||
return new TimeSpan(begin, end);
|
||||
const bpos = span.begin.sub(cycle).mul(factor).min(1);
|
||||
const epos = span.end.sub(cycle).mul(factor).min(1);
|
||||
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 begin = hap.part.begin;
|
||||
const end = hap.part.end;
|
||||
@ -765,7 +757,7 @@ export class Pattern {
|
||||
);
|
||||
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
|
||||
@ -1370,9 +1362,6 @@ function _composeOp(a, b, func) {
|
||||
|
||||
// Make composers
|
||||
(function () {
|
||||
const num = (pat) => pat._asNumber();
|
||||
const numOrString = (pat) => pat._asNumber(false, true);
|
||||
|
||||
// pattern composers
|
||||
const composers = {
|
||||
set: [(a, b) => b],
|
||||
@ -1396,7 +1385,7 @@ function _composeOp(a, b, func) {
|
||||
* // Behind the scenes, the notes are converted to midi numbers:
|
||||
* // "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.
|
||||
@ -1406,7 +1395,7 @@ function _composeOp(a, b, func) {
|
||||
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
|
||||
* // See add for more information.
|
||||
*/
|
||||
sub: [(a, b) => a - b, num],
|
||||
sub: [numeralArgs((a, b) => a - b)],
|
||||
/**
|
||||
*
|
||||
* Multiplies each number by the given factor.
|
||||
@ -1415,21 +1404,21 @@ function _composeOp(a, b, func) {
|
||||
* @example
|
||||
* "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.
|
||||
* @name div
|
||||
* @memberof Pattern
|
||||
*/
|
||||
div: [(a, b) => a / b, num],
|
||||
mod: [mod, num],
|
||||
pow: [Math.pow, num],
|
||||
_and: [(a, b) => a & b, num],
|
||||
_or: [(a, b) => a | b, num],
|
||||
_xor: [(a, b) => a ^ b, num],
|
||||
_lshift: [(a, b) => a << b, num],
|
||||
_rshift: [(a, b) => a >> b, num],
|
||||
div: [numeralArgs((a, b) => a / b)],
|
||||
mod: [numeralArgs(mod)],
|
||||
pow: [numeralArgs(Math.pow)],
|
||||
_and: [numeralArgs((a, b) => a & b)],
|
||||
_or: [numeralArgs((a, b) => a | b)],
|
||||
_xor: [numeralArgs((a, b) => a ^ b)],
|
||||
_lshift: [numeralArgs((a, b) => a << b)],
|
||||
_rshift: [numeralArgs((a, b) => a >> b)],
|
||||
|
||||
// TODO - force numerical comparison if both look like numbers?
|
||||
lt: [(a, b) => a < b],
|
||||
|
||||
@ -238,6 +238,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||
|
||||
// this function expects pat to be a pattern of floats...
|
||||
export const perlinWith = (pat) => {
|
||||
const pata = pat.fmap(Math.floor);
|
||||
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))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time);
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
|
||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
||||
|
||||
@ -47,6 +47,9 @@ import {
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import controls from '../controls.mjs';
|
||||
|
||||
const { n } = controls;
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||
@ -137,6 +140,9 @@ describe('Pattern', () => {
|
||||
it('Can make a pattern', () => {
|
||||
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()', () => {
|
||||
it('Can add things', () => {
|
||||
@ -191,6 +197,9 @@ describe('Pattern', () => {
|
||||
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()', () => {
|
||||
it('can structure In()', () => {
|
||||
@ -373,9 +382,10 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('copes with breaking up events across cycles', () => {
|
||||
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')]
|
||||
);
|
||||
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'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', () => {
|
||||
@ -430,6 +440,16 @@ describe('Pattern', () => {
|
||||
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
|
||||
// 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', () => {
|
||||
it('can rev inside a cycle', () => {
|
||||
@ -799,10 +819,11 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('Squeezes to the correct cycle', () => {
|
||||
expect(
|
||||
pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value)
|
||||
).toStrictEqual(
|
||||
[Fraction(3.5)]
|
||||
)
|
||||
pure(time.struct(true))
|
||||
._squeezeJoin()
|
||||
.queryArc(3, 4)
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual([Fraction(3.5)]);
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
@ -855,9 +876,7 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('range', () => {
|
||||
it('Can be patterned', () => {
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
|
||||
sequence(0, 0.5).firstCycle(),
|
||||
);
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('range2', () => {
|
||||
|
||||
@ -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 { 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';
|
||||
|
||||
describe('isNote', () => {
|
||||
@ -92,16 +105,16 @@ describe('getFrequency', () => {
|
||||
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
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);
|
||||
});
|
||||
it('should turn object with a "freq" property into frequency', () => {
|
||||
expect(getFrequency(happify({freq: 220}))).toEqual(220)
|
||||
expect(getFrequency(happify({freq: 440}))).toEqual(440)
|
||||
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
|
||||
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
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('Q'))).toThrowError(`not a note or frequency: Q`);
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -140,22 +153,72 @@ describe('compose', () => {
|
||||
describe('getPlayableNoteValue', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
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', () => {
|
||||
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
|
||||
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
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', () => {
|
||||
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
|
||||
})
|
||||
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
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', () => {
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
|
||||
})
|
||||
});
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,6 +18,11 @@ export class TimeSpan {
|
||||
const end = this.end;
|
||||
const end_sam = end.sam();
|
||||
|
||||
// Support zero-width timespans
|
||||
if (begin.equals(end)) {
|
||||
return([new TimeSpan(begin, end)]);
|
||||
}
|
||||
|
||||
while (end.gt(begin)) {
|
||||
// If begin and end are in the same cycle, we're done.
|
||||
if (begin.sam().equals(end_sam)) {
|
||||
|
||||
@ -133,3 +133,46 @@ export function curry(func, overload) {
|
||||
}
|
||||
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);
|
||||
|
||||
@ -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>
|
||||
<strudel-repl>
|
||||
<!--
|
||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||
[a4 [a4 c5] e5 [d5 c5]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||
[e5 [~ c5] e5 [d5 c5]]
|
||||
[b4 [b4 c5] d5 e5]
|
||||
[c5 a4 a4 ~]],
|
||||
[[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>
|
||||
```
|
||||
|
||||
@ -2,14 +2,21 @@
|
||||
<!-- <script src="./embed.js"></script> -->
|
||||
<strudel-repl>
|
||||
<!--
|
||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||
[a4 [a4 c5] e5 [d5 c5]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||
[e5 [~ c5] e5 [d5 c5]]
|
||||
[b4 [b4 c5] d5 e5]
|
||||
[c5 a4 a4 ~]],
|
||||
[[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>
|
||||
|
||||
@ -30,10 +30,10 @@
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
"shift-parser": "^7.0.3",
|
||||
"shift-spec": "^2018.0.2",
|
||||
"shift-ast": "^7.0.0",
|
||||
"shift-codegen": "^8.1.0",
|
||||
"shift-parser": "^8.0.0",
|
||||
"shift-spec": "^2019.0.0",
|
||||
"shift-traverser": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,12 +5,11 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { isNote } from 'tone';
|
||||
import _WebMidi from 'webmidi';
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
|
||||
// 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() {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@ -23,6 +23,6 @@
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
"webmidi": "^3.0.21"
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,3 +33,12 @@ yields:
|
||||
## Mini Notation API
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
@ -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 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;
|
||||
|
||||
@ -29,7 +29,10 @@ const applyOptions = (parent) => (pat, i) => {
|
||||
case 'bjorklund':
|
||||
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
|
||||
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': "%"
|
||||
}
|
||||
console.warn(`operator "${operator.type_}" not implemented`);
|
||||
@ -112,9 +115,9 @@ export function patternifyAST(ast) {
|
||||
return silence;
|
||||
}
|
||||
if (typeof ast.source_ !== 'object') {
|
||||
if (!addMiniLocations) {
|
||||
/* if (!addMiniLocations) {
|
||||
return ast.source_;
|
||||
}
|
||||
} */
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
|
||||
@ -35,3 +35,5 @@ s("<bd sd> hh").osc()
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
@ -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 { Pattern } from '@strudel.cycles/core';
|
||||
import { parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||
|
||||
const comm = new OSC();
|
||||
comm.open();
|
||||
@ -31,6 +31,10 @@ Pattern.prototype.osc = function () {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
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 ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
|
||||
@ -30,6 +30,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.2"
|
||||
"osc-js": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,44 @@
|
||||
# @strudel.cycles/react
|
||||
|
||||
This package contains react hooks and components for strudel.
|
||||
Example coming soon
|
||||
This package contains react hooks and components for strudel. It is used internally by the Strudel REPL.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
4
packages/react/dist/index.cjs.js
vendored
4
packages/react/dist/index.cjs.js
vendored
File diff suppressed because one or more lines are too long
1008
packages/react/dist/index.es.js
vendored
1008
packages/react/dist/index.es.js
vendored
File diff suppressed because it is too large
Load Diff
2
packages/react/dist/style.css
vendored
2
packages/react/dist/style.css
vendored
@ -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}
|
||||
|
||||
@ -37,12 +37,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/eval": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"@uiw/codemirror-themes": "^4.11.4",
|
||||
"@uiw/react-codemirror": "^4.11.4",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
"@uiw/react-codemirror": "^4.12.4",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -52,12 +52,12 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^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",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^2.9.9"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ import React from 'react';
|
||||
import { MiniRepl } from './components/MiniRepl';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import { evalScope } from '@strudel.cycles/eval';
|
||||
import { controls } from '@strudel.cycles/core';
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
@ -16,7 +18,7 @@ evalScope(
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<MiniRepl tune={`"c3"`} />
|
||||
<MiniRepl tune={`note("c3")`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,6 +27,6 @@
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
"webmidi": "^3.0.21"
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
}
|
||||
|
||||
@ -30,8 +30,6 @@ const {
|
||||
getDestination,
|
||||
Players,
|
||||
} = Tone;
|
||||
import * as tonePiano from '@tonejs/piano';
|
||||
const { Piano } = tonePiano;
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
|
||||
// "balanced" | "interactive" | "playback";
|
||||
@ -61,10 +59,6 @@ Pattern.prototype.tone = function (instrument) {
|
||||
instrument.triggerAttack(note, time);
|
||||
} else if (instrument instanceof NoiseSynth) {
|
||||
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) {
|
||||
note = getPlayableNoteValue(hap);
|
||||
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 piano = async (options = { velocities: 1 }) => {
|
||||
const p = new Piano(options);
|
||||
await p.load();
|
||||
return p;
|
||||
};
|
||||
|
||||
// effect helpers
|
||||
export const vol = (v) => new Gain(v);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
if (typeof AudioContext !== 'undefined') {
|
||||
AudioContext.prototype.impulseResponse = function (duration) {
|
||||
AudioContext.prototype.impulseResponse = function (duration, channels = 1) {
|
||||
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);
|
||||
for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration);
|
||||
return impulse;
|
||||
|
||||
@ -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,
|
||||
...Object.fromEntries(
|
||||
|
||||
@ -100,12 +100,8 @@ const getSoundfontKey = (s) => {
|
||||
|
||||
const getSampleBufferSource = async (s, n, note, speed) => {
|
||||
let transpose = 0;
|
||||
let midi;
|
||||
|
||||
if (note !== undefined) {
|
||||
midi = typeof note === 'string' ? toMidi(note) : note;
|
||||
transpose = midi - 36; // C3 is middle C
|
||||
}
|
||||
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
|
||||
transpose = midi - 36; // C3 is middle C
|
||||
|
||||
const ac = getAudioContext();
|
||||
// is sample from loaded samples(..)
|
||||
@ -128,9 +124,6 @@ const getSampleBufferSource = async (s, n, note, speed) => {
|
||||
if (Array.isArray(bank)) {
|
||||
sampleUrl = bank[n % bank.length];
|
||||
} else {
|
||||
if (!note) {
|
||||
throw new Error('no note(...) set for sound', s);
|
||||
}
|
||||
const midiDiff = (noteA) => toMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
const closest = Object.keys(bank)
|
||||
@ -253,6 +246,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
let {
|
||||
freq,
|
||||
s,
|
||||
bank,
|
||||
sf,
|
||||
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
@ -288,6 +282,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
// the chain will hold all audio nodes that connect to each other
|
||||
const chain = [];
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
if (typeof s === 'string') {
|
||||
[s, n] = splitSN(s, n);
|
||||
}
|
||||
|
||||
@ -22,8 +22,9 @@ class CoarseProcessor extends AudioWorkletProcessor {
|
||||
this.notStarted = false;
|
||||
output[0][0] = input[0][0];
|
||||
for (let n = 1; n < blockSize; n++) {
|
||||
if (n % coarse == 0) output[0][n] = input[0][n];
|
||||
else output[0][n] = output[0][n - 1];
|
||||
for (let o = 0; o < output.length; o++) {
|
||||
output[o][n] = n % coarse == 0 ? input[0][n] : output[o][n - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.notStarted || hasInput;
|
||||
@ -52,11 +53,19 @@ class CrushProcessor extends AudioWorkletProcessor {
|
||||
this.notStarted = false;
|
||||
if (crush.length === 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 {
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
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) {
|
||||
this.notStarted = false;
|
||||
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;
|
||||
|
||||
865
repl/package-lock.json
generated
865
repl/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
||||
"name": "@strudel.cycles/repl",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"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",
|
||||
"predeploy": "npm run build",
|
||||
"deploy": "gh-pages -d ../docs",
|
||||
"static": "npx serve ../docs"
|
||||
"static": "npx serve ../docs",
|
||||
"dbdump": "node src/test/dbdump.js > src/test/dbdump.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
@ -27,6 +29,6 @@
|
||||
"postcss": "^8.4.13",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^2.9.9"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
33
repl/public/piano.json
Normal file
33
repl/public/piano.json
Normal 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"
|
||||
}
|
||||
}
|
||||
2658
repl/public/tidal-drum-machines.json
Normal file
2658
repl/public/tidal-drum-machines.json
Normal file
File diff suppressed because it is too large
Load Diff
2164
repl/public/vcsl.json
Normal file
2164
repl/public/vcsl.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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/>.
|
||||
*/
|
||||
|
||||
import controls from '@strudel.cycles/core/controls.mjs';
|
||||
import { evalScope, evaluate } from '@strudel.cycles/eval';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react';
|
||||
import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
|
||||
@ -15,6 +14,7 @@ import * as tunes from './tunes.mjs';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import * as WebDirt from 'WebDirt';
|
||||
import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { controls } from '@strudel.cycles/core';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
@ -26,7 +26,7 @@ const supabase = createClient(
|
||||
|
||||
evalScope(
|
||||
Tone,
|
||||
controls,
|
||||
controls, // sadly, this cannot be exported from core direclty
|
||||
{ WebDirt },
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
|
||||
@ -2,49 +2,16 @@ import { Pattern, toMidi } from '@strudel.cycles/core';
|
||||
import { samples } from '@strudel.cycles/webaudio';
|
||||
|
||||
export async function prebake({ isMock = false, baseDir = '.' } = {}) {
|
||||
samples(
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
if (!isMock) {
|
||||
// https://archive.org/details/SalamanderGrandPianoV3
|
||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||
`${baseDir}/piano/`,
|
||||
);
|
||||
if (!isMock) {
|
||||
await fetch('EmuSP12.json')
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, `${baseDir}/EmuSP12/`));
|
||||
samples('piano.json', `${baseDir}/piano/`);
|
||||
// https://github.com/sgossner/VCSL/
|
||||
// https://api.github.com/repositories/126427031/contents/
|
||||
// LICENSE: CC0 general-purpose
|
||||
samples('vcsl.json', 'github:sgossner/VCSL/master/');
|
||||
samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/');
|
||||
samples('EmuSP12.json', `${baseDir}/EmuSP12/`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,7 @@ const toneHelpersMocked = {
|
||||
Chorus: MockedNode,
|
||||
Freeverb: MockedNode,
|
||||
Gain: MockedNode,
|
||||
Reverb: MockedNode,
|
||||
vol: mockNode,
|
||||
out: id,
|
||||
osc: id,
|
||||
@ -83,7 +84,9 @@ const toneHelpersMocked = {
|
||||
highpass: mockNode,
|
||||
};
|
||||
|
||||
// tone mock
|
||||
strudel.Pattern.prototype.osc = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype.tone = function () {
|
||||
return this;
|
||||
};
|
||||
@ -114,17 +117,40 @@ strudel.Pattern.prototype.adsr = function () {
|
||||
strudel.Pattern.prototype.out = function () {
|
||||
return this;
|
||||
};
|
||||
strudel.Pattern.prototype.soundfont = function () {
|
||||
return this;
|
||||
};
|
||||
// tune mock
|
||||
strudel.Pattern.prototype.tune = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
strudel.Pattern.prototype.midi = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
const uiHelpersMocked = {
|
||||
backgroundImage: id,
|
||||
};
|
||||
|
||||
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
|
||||
evalScope(
|
||||
// Tone,
|
||||
@ -144,6 +170,10 @@ evalScope(
|
||||
// gist,
|
||||
// euclid,
|
||||
mini,
|
||||
getDrawContext,
|
||||
getAudioContext,
|
||||
loadSoundfont,
|
||||
Clock: {}, // whatever
|
||||
// Tone,
|
||||
},
|
||||
);
|
||||
@ -195,3 +225,6 @@ export const testCycles = {
|
||||
hyperpop: 10,
|
||||
festivalOfFingers3: 16,
|
||||
};
|
||||
|
||||
// fixed: https://strudel.tidalcycles.org/?DBp75NUfSxIn (missing .note())
|
||||
// bug: https://strudel.tidalcycles.org/?xHaKTd1kTpCn + https://strudel.tidalcycles.org/?o5LLePbx8kiQ
|
||||
|
||||
6928
repl/src/test/__snapshots__/shared.test.mjs.snap
Normal file
6928
repl/src/test/__snapshots__/shared.test.mjs.snap
Normal file
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
10
repl/src/test/dbdump.js
Normal 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));
|
||||
1
repl/src/test/dbdump.json
Normal file
1
repl/src/test/dbdump.json
Normal file
File diff suppressed because one or more lines are too long
17
repl/src/test/shared.test.mjs
Normal file
17
repl/src/test/shared.test.mjs
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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/>.
|
||||
*/
|
||||
|
||||
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]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
@ -19,7 +19,7 @@ export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
|
||||
[[d2 d3]*4]
|
||||
[[c2 c3]*4]
|
||||
[[b1 b2]*2 [e2 e3]*2]
|
||||
[[a1 a2]*4]\`.slow(16)
|
||||
[[a1 a2]*4]\`).slow(16)
|
||||
`;
|
||||
|
||||
export const swimming = `stack(
|
||||
@ -80,7 +80,7 @@ export const swimming = `stack(
|
||||
"[F2 A2 Bb2 B2]",
|
||||
"[G2 C2 F2 F2]"
|
||||
)
|
||||
).slow(51);
|
||||
).note().slow(51);
|
||||
`;
|
||||
|
||||
export const giantSteps = `stack(
|
||||
@ -105,7 +105,7 @@ export const giantSteps = `stack(
|
||||
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
|
||||
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
|
||||
)
|
||||
).slow(20)`;
|
||||
).slow(20).note()`;
|
||||
|
||||
export const giantStepsReggae = `stack(
|
||||
// melody
|
||||
@ -132,7 +132,7 @@ export const giantStepsReggae = `stack(
|
||||
"[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]"
|
||||
)
|
||||
.struct("x ~".fast(4*8))
|
||||
).slow(25)`;
|
||||
).slow(25).note()`;
|
||||
|
||||
export const zeldasRescue = `stack(
|
||||
// melody
|
||||
@ -511,7 +511,7 @@ stack(
|
||||
f3!2 e3!2 ab3!2 ~!2
|
||||
>\`
|
||||
.legato(.5)
|
||||
).fast(2) // .note().piano()`;
|
||||
).fast(2).note()`;
|
||||
|
||||
/*
|
||||
// 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)
|
||||
)
|
||||
.slow(6)
|
||||
|
||||
.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
|
||||
//.hush()
|
||||
)
|
||||
|
||||
.slow(3/2)`;
|
||||
|
||||
export const swimmingWithSoundfonts = `stack(
|
||||
@ -814,7 +812,6 @@ x=>x.add(7).color('steelblue')
|
||||
//.hcutoff(400)
|
||||
.clip(1)
|
||||
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
|
||||
|
||||
.pianoroll({vertical:1})`;
|
||||
|
||||
export const bossaRandom = `const chords = "<Am7 Am7 Dm7 E7>"
|
||||
@ -857,3 +854,130 @@ export const orbit = `stack(
|
||||
.delayfeedback(.7)
|
||||
.orbit(2)
|
||||
).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)>")`;
|
||||
|
||||
3197
tutorial/test/__snapshots__/examples.test.mjs.snap
Normal file
3197
tutorial/test/__snapshots__/examples.test.mjs.snap
Normal file
File diff suppressed because it is too large
Load Diff
15
tutorial/test/examples.test.mjs
Normal file
15
tutorial/test/examples.test.mjs
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -11,5 +11,13 @@ export default defineConfig({
|
||||
reporters: 'verbose',
|
||||
isolate: false,
|
||||
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',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user