mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-25 20:48:27 +00:00
Merge remote-tracking branch 'origin/HEAD' into vitest
This commit is contained in:
commit
29009e14f4
@ -38,6 +38,12 @@ Click on the package names to find out more about each one.
|
|||||||
|
|
||||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||||
|
|
||||||
|
<a href="https://github.com/tidalcycles/strudel/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Made with [contrib.rocks](https://contrib.rocks).
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
|
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
|
||||||
|
|||||||
@ -1294,8 +1294,8 @@ export function slowcatPrime(...pats) {
|
|||||||
pats = pats.map(reify);
|
pats = pats.map(reify);
|
||||||
const query = function (state) {
|
const query = function (state) {
|
||||||
const pat_n = Math.floor(state.span.begin) % pats.length;
|
const pat_n = Math.floor(state.span.begin) % pats.length;
|
||||||
const pat = pats[pat_n];
|
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
|
||||||
return pat.query(state);
|
return pat?.query(state) || [];
|
||||||
};
|
};
|
||||||
return new Pattern(query)._splitQueries();
|
return new Pattern(query)._splitQueries();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,8 +53,8 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
note = fromMidi(hap.value);
|
note = fromMidi(hap.value);
|
||||||
} else if (typeof note === 'string' && !isNote(note)) {
|
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||||
throw new Error('not a note: ' + note);
|
throw new Error('not a note: ' + JSON.stringify(note));
|
||||||
}
|
}
|
||||||
return note;
|
return note;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
// a sequence = a serie of elements placed between quotes
|
// a sequence = a serie of elements placed between quotes
|
||||||
// a stack = a serie of vertically aligned slices sharing the same overall length
|
// a stack = a serie of vertically aligned slices sharing the same overall length
|
||||||
// a slice = a serie of horizontally aligned elements
|
// a slice = a serie of horizontally aligned elements
|
||||||
|
// a choose = a serie of elements, one of which is chosen at random
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -81,7 +82,8 @@ DIGIT = [0-9]
|
|||||||
// ------------------ delimiters ---------------------------
|
// ------------------ delimiters ---------------------------
|
||||||
|
|
||||||
ws "whitespace" = [ \n\r\t]*
|
ws "whitespace" = [ \n\r\t]*
|
||||||
comma = ws "," ws;
|
comma = ws "," ws
|
||||||
|
pipe = ws "|" ws
|
||||||
quote = '"' / "'"
|
quote = '"' / "'"
|
||||||
|
|
||||||
// ------------------ steps and cycles ---------------------------
|
// ------------------ steps and cycles ---------------------------
|
||||||
@ -91,7 +93,7 @@ step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
|||||||
step = ws chars:step_char+ ws { return chars.join("") }
|
step = ws chars:step_char+ ws { return chars.join("") }
|
||||||
|
|
||||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||||
sub_cycle = ws "[" ws s:stack ws "]" ws { return s}
|
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s}
|
||||||
|
|
||||||
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
|
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
|
||||||
timeline = ws "<" ws sc:single_cycle ws ">" ws
|
timeline = ws "<" ws sc:single_cycle ws ">" ws
|
||||||
@ -102,7 +104,7 @@ slice = step / sub_cycle / timeline
|
|||||||
|
|
||||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||||
// at this point, we assume we can represent them as regular sequence operators
|
// at this point, we assume we can represent them as regular sequence operators
|
||||||
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate
|
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
|
||||||
|
|
||||||
slice_weight = "@" a:number
|
slice_weight = "@" a:number
|
||||||
{ return { weight: a} }
|
{ return { weight: a} }
|
||||||
@ -122,6 +124,9 @@ slice_fast = "*"a:number
|
|||||||
slice_fixed_step = "%"a:number
|
slice_fixed_step = "%"a:number
|
||||||
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
|
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
|
||||||
|
|
||||||
|
slice_degrade = "?"a:number?
|
||||||
|
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
|
||||||
|
|
||||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||||
slice_with_modifier = s:slice o:slice_modifier?
|
slice_with_modifier = s:slice o:slice_modifier?
|
||||||
{ return new ElementStub(s, o);}
|
{ return new ElementStub(s, o);}
|
||||||
@ -132,14 +137,22 @@ single_cycle = s:(slice_with_modifier)+
|
|||||||
{ return new PatternStub(s,"h"); }
|
{ return new PatternStub(s,"h"); }
|
||||||
|
|
||||||
// a stack is a serie of vertically aligned single cycles, separated by a comma
|
// a stack is a serie of vertically aligned single cycles, separated by a comma
|
||||||
|
stack_tail = tail:(comma @single_cycle)+
|
||||||
|
{ return { alignment: 'v', list: tail }; }
|
||||||
|
|
||||||
|
// a choose is a serie of pipe-separated single cycles, one of which is chosen
|
||||||
|
// at random each time through the pattern
|
||||||
|
choose_tail = tail:(pipe @single_cycle)+
|
||||||
|
{ return { alignment: 'r', list: tail }; }
|
||||||
|
|
||||||
// if the stack contains only one element, we don't create a stack but return the
|
// if the stack contains only one element, we don't create a stack but return the
|
||||||
// underlying element
|
// underlying element
|
||||||
stack = c:single_cycle cs:(comma v:single_cycle { return v})*
|
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
|
||||||
{ if (cs.length == 0 && c instanceof Object) { return c;} else { cs.unshift(c); return new PatternStub(cs,"v");} }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||||
|
|
||||||
// a sequence is a quoted stack
|
// a sequence is a quoted stack
|
||||||
sequence = ws quote s:stack quote
|
sequence = ws quote sc:stack_or_choose quote
|
||||||
{ return s; }
|
{ return sc; }
|
||||||
|
|
||||||
// ------------------ operators ---------------------------
|
// ------------------ operators ---------------------------
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,12 @@ 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;
|
||||||
|
|
||||||
|
var _seedState = 0;
|
||||||
|
|
||||||
|
function _nextSeed() {
|
||||||
|
return _seedState++;
|
||||||
|
}
|
||||||
|
|
||||||
const applyOptions = (parent) => (pat, i) => {
|
const applyOptions = (parent) => (pat, i) => {
|
||||||
const ast = parent.source_[i];
|
const ast = parent.source_[i];
|
||||||
const options = ast.options_;
|
const options = ast.options_;
|
||||||
@ -21,6 +27,8 @@ const applyOptions = (parent) => (pat, i) => {
|
|||||||
return reify(pat).fast(speed);
|
return reify(pat).fast(speed);
|
||||||
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':
|
||||||
|
return reify(pat)._degradeByWith(strudel.rand.early(Math.PI * _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`);
|
||||||
@ -82,6 +90,9 @@ export function patternifyAST(ast) {
|
|||||||
if (alignment === 'v') {
|
if (alignment === 'v') {
|
||||||
return stack(...children);
|
return stack(...children);
|
||||||
}
|
}
|
||||||
|
if (alignment === 'r') {
|
||||||
|
return strudel.chooseInWith(strudel.rand.early(Math.PI * _nextSeed()).segment(1), children);
|
||||||
|
}
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (!weightedChildren && alignment === 't') {
|
if (!weightedChildren && alignment === 't') {
|
||||||
return slowcat(...children);
|
return slowcat(...children);
|
||||||
|
|||||||
@ -50,4 +50,44 @@ describe('mini', () => {
|
|||||||
it('supports euclidean rhythms', () => {
|
it('supports euclidean rhythms', () => {
|
||||||
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||||
});
|
});
|
||||||
|
it('supports the ? operator', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
mini('a?').queryArc(0, 20).map(hap => hap.whole.begin),
|
||||||
|
mini('a').degradeBy(0.5).queryArc(0, 20).map(hap => hap.whole.begin));
|
||||||
|
});
|
||||||
|
// testing things that involve pseudo-randomness, so there's a probability we could fail by chance.
|
||||||
|
// these next few tests work with the current PRNG, and are intended to succeed with p > 0.99 even if the PRNG changes
|
||||||
|
// (as long as the PRNG has a relatively-uniform distribution of values)
|
||||||
|
it('supports degradeBy with default of 50%', () => {
|
||||||
|
const haps = mini('a?').queryArc(0, 1000);
|
||||||
|
assert(459 <= haps.length && haps.length <= 541, 'Number of elements did not fall in 99% confidence interval for binomial with p=0.5');
|
||||||
|
});
|
||||||
|
it('supports degradeBy with an argument', () => {
|
||||||
|
const haps = mini('a?0.8').queryArc(0, 1000);
|
||||||
|
assert(haps.length > 0, 'Should have had at least one element when degradeBy was set at 0.8');
|
||||||
|
assert(haps.length < 230, 'Had too many cycles remaining after degradeBy 0.8');
|
||||||
|
});
|
||||||
|
it('supports the random choice operator ("|") with nesting', () => {
|
||||||
|
const numCycles = 900;
|
||||||
|
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||||
|
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||||
|
// Evaluating this distribution with a chi-squared test.
|
||||||
|
// Note: this just evaluates the overall distribution, not things like correlation/runs of values
|
||||||
|
const observed = haps.reduce((acc, hap) => {
|
||||||
|
acc[hap.value] = (acc[hap.value] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const expected = {
|
||||||
|
a: numCycles / 3, b: numCycles / 6, c: numCycles / 6,
|
||||||
|
d: numCycles / 9, e: numCycles / 9, f: numCycles / 9
|
||||||
|
};
|
||||||
|
let chisq = -numCycles;
|
||||||
|
for (let k in expected) {
|
||||||
|
chisq += observed[k] * observed[k] / expected[k];
|
||||||
|
}
|
||||||
|
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||||
|
// PRNG, this test should succeed
|
||||||
|
assert(chisq <= 15.086,
|
||||||
|
chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Pattern, toMidi } from '@strudel.cycles/core';
|
import { Pattern, toMidi } from '@strudel.cycles/core';
|
||||||
import { samples } from '@strudel.cycles/webaudio';
|
import { samples } from '@strudel.cycles/webaudio';
|
||||||
|
|
||||||
export function prebake() {
|
export function prebake(isMock = false) {
|
||||||
samples(
|
samples(
|
||||||
{
|
{
|
||||||
piano: {
|
piano: {
|
||||||
@ -41,9 +41,11 @@ export function prebake() {
|
|||||||
// 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
|
||||||
'./piano/',
|
'./piano/',
|
||||||
);
|
);
|
||||||
fetch('EmuSP12.json')
|
if (!isMock) {
|
||||||
.then(res => res.json())
|
fetch('EmuSP12.json')
|
||||||
.then(json => samples(json, './EmuSP12/'));
|
.then((res) => res.json())
|
||||||
|
.then((json) => samples(json, './EmuSP12/'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxPan = toMidi('C8');
|
const maxPan = toMidi('C8');
|
||||||
|
|||||||
@ -123,7 +123,7 @@ const uiHelpersMocked = {
|
|||||||
backgroundImage: id,
|
backgroundImage: id,
|
||||||
};
|
};
|
||||||
|
|
||||||
prebake();
|
prebake(true);
|
||||||
|
|
||||||
// TODO: refactor to evalScope
|
// TODO: refactor to evalScope
|
||||||
extend(
|
extend(
|
||||||
|
|||||||
@ -1019,3 +1019,14 @@ x=>x.add(7).color('steelblue')
|
|||||||
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
|
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
|
||||||
.out()
|
.out()
|
||||||
.pianoroll({vertical:1})`;
|
.pianoroll({vertical:1})`;
|
||||||
|
|
||||||
|
export const bossaRandom = `const chords = "<Am7 Am7 Dm7 E7>"
|
||||||
|
const roots = chords.rootNotes(2)
|
||||||
|
|
||||||
|
stack(
|
||||||
|
chords.voicings(['F4', 'A5']).struct(
|
||||||
|
\` x@2 ~ x ~ ~ ~ x |
|
||||||
|
x? ~ ~ x@3 ~ x |
|
||||||
|
x? ~ ~ x ~ x@3\`),
|
||||||
|
roots.struct("x [~ x?0.2] x [~ x?] | x!4 | x@2 ~ ~ ~ x x x").transpose("0 7")
|
||||||
|
).slow(2).pianoroll().note().piano().out();`;
|
||||||
File diff suppressed because one or more lines are too long
@ -192,8 +192,6 @@ Compared to [tidal mini notation](https://tidalcycles.org/docs/patternlib/tutori
|
|||||||
|
|
||||||
- [ ] Tie symbols "\_"
|
- [ ] Tie symbols "\_"
|
||||||
- [ ] feet marking "."
|
- [ ] feet marking "."
|
||||||
- [ ] random choice "|"
|
|
||||||
- [ ] Random removal "?"
|
|
||||||
- [ ] Polymetric sequences "{ ... }"
|
- [ ] Polymetric sequences "{ ... }"
|
||||||
- [ ] Fixed steps using "%"
|
- [ ] Fixed steps using "%"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user