Compare commits

..

5 Commits

Author SHA1 Message Date
Felix Roos 2b8b0154da more complicated test pattern 2024-04-10 00:43:10 +02:00
Felix Roos 2b5c653c56 simplify again 2024-04-10 00:36:47 +02:00
Felix Roos 244d91cc8d take that eslint 2024-04-10 00:32:50 +02:00
Felix Roos 3f368d6438 simplify 2024-04-10 00:22:31 +02:00
Felix Roos 36ee4cfc98 much simpler clock poc 2024-04-09 23:59:00 +02:00
42 changed files with 716 additions and 1555 deletions
+1 -2
View File
@@ -21,5 +21,4 @@ vite.config.js
/src-tauri/target/**/*
reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
packages/node/pattern.mjs
jsdoc-synonyms.js
-1
View File
@@ -11,4 +11,3 @@ pnpm-lock.yaml
pnpm-workspace.yaml
**/dev-dist
website/.astro
packages/node/pattern.mjs
+1 -35
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js';
import { TimeSpan } from './timespan.mjs';
import { removeUndefineds } from './util.mjs';
// Returns the start of the cycle.
Fraction.prototype.sam = function () {
@@ -48,10 +47,6 @@ Fraction.prototype.eq = function (other) {
return this.compare(other) == 0;
};
Fraction.prototype.ne = function (other) {
return this.compare(other) != 0;
};
Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other;
};
@@ -65,22 +60,6 @@ Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other;
};
Fraction.prototype.mulmaybe = function (other) {
return other !== undefined ? this.mul(other) : undefined;
};
Fraction.prototype.divmaybe = function (other) {
return other !== undefined ? this.div(other) : undefined;
};
Fraction.prototype.addmaybe = function (other) {
return other !== undefined ? this.add(other) : undefined;
};
Fraction.prototype.submaybe = function (other) {
return other !== undefined ? this.sub(other) : undefined;
};
Fraction.prototype.show = function (/* excludeWhole = false */) {
// return this.toFraction(excludeWhole);
return this.s * this.n + '/' + this.d;
@@ -106,24 +85,11 @@ const fraction = (n) => {
};
export const gcd = (...fractions) => {
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
};
export const lcm = (...fractions) => {
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce(
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
fraction(1),
);
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
};
fraction._original = Fraction;
+88 -267
View File
@@ -10,29 +10,12 @@ import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
import {
uniqsortr,
removeUndefineds,
flatten,
id,
listRange,
curry,
_mod,
numeralArgs,
parseNumeral,
pairs,
} from './util.mjs';
import { compose, removeUndefineds, flatten, id, listRange, curry, _mod, numeralArgs, parseNumeral } from './util.mjs';
import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs';
let stringParser;
let __tactus = true;
export const calculateTactus = function (x) {
__tactus = x ? true : false;
};
// parser is expected to turn a string into a pattern
// if set, the reify function will parse all strings with it
// intended to use with mini to automatically interpret all strings as mini notation
@@ -48,16 +31,16 @@ export class Pattern {
*/
constructor(query, tactus = undefined) {
this.query = query;
this._Pattern = true; // this property is used to detectinstance of another Pattern
this.tactus = tactus; // in terms of number of steps per cycle
this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
this.__tactus = tactus; // in terms of number of beats per cycle
}
get tactus() {
return this.__tactus;
return this.__tactus ?? Fraction(1);
}
set tactus(tactus) {
this.__tactus = tactus === undefined ? undefined : Fraction(tactus);
this.__tactus = Fraction(tactus);
}
setTactus(tactus) {
@@ -65,17 +48,6 @@ export class Pattern {
return this;
}
withTactus(f) {
if (!__tactus) {
return this;
}
return new Pattern(this.query, this.tactus === undefined ? undefined : f(this.tactus));
}
get hasTactus() {
return this.tactus !== undefined;
}
//////////////////////////////////////////////////////////////////////
// Haskell-style functor, applicative and monadic operations
@@ -158,9 +130,7 @@ export class Pattern {
return span_a.intersection_e(span_b);
};
const result = pat_func.appWhole(whole_func, pat_val);
if (__tactus) {
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
}
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
return result;
}
@@ -272,12 +242,12 @@ export class Pattern {
}
outerBind(func) {
return this.bindWhole((a) => a, func).setTactus(this.tactus);
return this.bindWhole((a) => a, func);
}
outerJoin() {
// Flattens a pattern of patterns into a pattern, where wholes are
// taken from outer haps.
// taken from inner haps.
return this.outerBind(id);
}
@@ -1167,24 +1137,24 @@ function _composeOp(a, b, func) {
export const polyrhythm = stack;
export const pr = stack;
export const pm = s_polymeter;
export const pm = polymeter;
// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
// Pattern.prototype.factories = {
// pure,
// stack,
// slowcat,
// fastcat,
// cat,
// timecat,
// sequence,
// seq,
// polymeter,
// pm,
// polyrhythm,
// pr,
// };
Pattern.prototype.factories = {
pure,
stack,
slowcat,
fastcat,
cat,
timecat,
sequence,
seq,
polymeter,
pm,
polyrhythm,
pr,
};
// the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)
// Elemental patterns
@@ -1196,7 +1166,7 @@ export const pm = s_polymeter;
* @example
* gap(3) // "~@3"
*/
export const gap = (tactus) => new Pattern(() => [], tactus);
export const gap = (tactus) => new Pattern(() => [], Fraction(tactus));
/**
* Does absolutely nothing..
@@ -1265,9 +1235,7 @@ export function stack(...pats) {
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
const result = new Pattern(query);
if (__tactus) {
result.tactus = lcm(...pats.map((pat) => pat.tactus));
}
result.tactus = lcm(...pats.map((pat) => pat.tactus));
return result;
}
@@ -1280,20 +1248,20 @@ function _stackWith(func, pats) {
return pats[0];
}
const [left, ...right] = pats.map((pat) => pat.tactus);
const tactus = __tactus ? left.maximum(...right) : undefined;
const tactus = left.maximum(...right);
return stack(...func(tactus, pats));
}
export function stackLeft(...pats) {
return _stackWith(
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(pat, gap(tactus.sub(pat.tactus))))),
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(pat, gap(tactus.sub(pat.tactus))))),
pats,
);
}
export function stackRight(...pats) {
return _stackWith(
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(gap(tactus.sub(pat.tactus)), pat))),
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(gap(tactus.sub(pat.tactus)), pat))),
pats,
);
}
@@ -1306,7 +1274,7 @@ export function stackCentre(...pats) {
return pat;
}
const g = gap(tactus.sub(pat.tactus).div(2));
return s_cat(g, pat, g);
return timecat(g, pat, g);
}),
pats,
);
@@ -1320,7 +1288,7 @@ export function stackBy(by, ...pats) {
left: stackLeft,
right: stackRight,
expand: stack,
repeat: (...args) => s_polymeterSteps(tactus, ...args),
repeat: (...args) => polymeterSteps(tactus, ...args),
};
return by
.inhabit(lookup)
@@ -1360,7 +1328,7 @@ export function slowcat(...pats) {
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
};
const tactus = __tactus ? lcm(...pats.map((x) => x.tactus)) : undefined;
const tactus = lcm(...pats.map((x) => x.tactus));
return new Pattern(query).splitQueries().setTactus(tactus);
}
@@ -1406,7 +1374,7 @@ export function cat(...pats) {
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
return s_cat(...sections).slow(total);
return timecat(...sections).slow(total);
}
export function fastcat(...pats) {
@@ -1486,7 +1454,7 @@ export const func = curry((a, b) => reify(b).func(a));
* @noAutocomplete
*
*/
export function register(name, func, patternify = true, preserveTactus = false, join = (x) => x.innerJoin()) {
export function register(name, func, patternify = true, preserveTactus = false) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
@@ -1523,7 +1491,7 @@ export function register(name, func, patternify = true, preserveTactus = false,
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
result = join(right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)));
result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
}
}
if (preserveTactus) {
@@ -1567,11 +1535,6 @@ export function register(name, func, patternify = true, preserveTactus = false,
return curry(pfunc, null, arity);
}
// Like register, but defaults to stepJoin
function stepRegister(name, func, patternify = true, preserveTactus = false, join = (x) => x.stepJoin()) {
return register(name, func, patternify, preserveTactus, join);
}
//////////////////////////////////////////////////////////////////////
// Numerical transformations
@@ -1779,9 +1742,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
*/
export const ply = register('ply', function (factor, pat) {
const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
if (__tactus) {
result.tactus = Fraction(factor).mulmaybe(pat.tactus);
}
result.tactus = pat.tactus.mul(factor);
return result;
});
@@ -1796,19 +1757,16 @@ export const ply = register('ply', function (factor, pat) {
* @example
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
*/
export const { fast, density } = register(
['fast', 'density'],
function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor)).setTactus(pat.tactus);
},
true,
true,
);
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
const result = fastQuery.withHapTime((t) => t.div(factor));
result.tactus = factor.mul(pat.tactus);
return result;
});
/**
* Both speeds up the pattern (like 'fast') and the sample playback (like 'speed').
@@ -1976,7 +1934,7 @@ export const zoom = register('zoom', function (s, e, pat) {
return nothing;
}
const d = e.sub(s);
const tactus = __tactus ? pat.tactus.mulmaybe(d) : undefined;
const tactus = pat.tactus.mul(d);
return pat
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
@@ -2018,24 +1976,6 @@ export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate)).setTactus(rate);
});
/**
* The function `swingBy x n` breaks each cycle into `n` slices, and then delays events in the second half of each slice by the amount `x`, which is relative to the size of the (half) slice. So if `x` is 0 it does nothing, `0.5` delays for half the note duration, and 1 will wrap around to doing nothing again. The end result is a shuffle or swing-like rhythm
* @param {number} subdivision
* @param {number} offset
* @example
* s("hh*8").swingBy(1/3, 4)
*/
export const swingBy = register('swingBy', (swing, n, pat) => pat.inside(n, late(seq(0, swing / 2))));
/**
* Shorthand for swingBy with 1/3:
* @param {number} subdivision
* @example
* s("hh*8").swing(4)
* // s("hh*8").swingBy(1/3, 4)
*/
export const swing = register('swing', (n, pat) => pat.swingBy(1 / 3, n));
/**
* Swaps 1s and 0s in a binary pattern.
* @name invert
@@ -2191,7 +2131,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by })));
return stack(left, right).setTactus(__tactus ? lcm(left.tactus, right.tactus) : undefined);
return stack(left, right).setTactus(lcm(left.tactus, right.tactus));
});
/**
@@ -2310,13 +2250,7 @@ export const { iterBack, iterback } = register(
export const { repeatCycles } = register(
'repeatCycles',
function (n, pat) {
return new Pattern(function (state) {
const cycle = state.span.begin.sam();
const source_cycle = cycle.div(n).sam();
const delta = cycle.sub(source_cycle);
state = state.withSpan((span) => span.withTime((spant) => spant.sub(delta)));
return pat.query(state).map((hap) => hap.withSpan((span) => span.withTime((spant) => spant.add(delta))));
}).splitQueries();
return slowcat(...Array(n).fill(pat));
},
true,
true,
@@ -2423,69 +2357,14 @@ Pattern.prototype.tag = function (tag) {
// Tactus-related functions, i.e. ones that do stepwise
// transformations
Pattern.prototype.stepJoin = function () {
const pp = this;
const first_t = s_cat(..._retime(_slices(pp.queryArc(0, 1)))).tactus;
const q = function (state) {
const shifted = pp.early(state.span.begin.sam());
const haps = shifted.query(state.setSpan(new TimeSpan(Fraction(0), Fraction(1))));
const pat = s_cat(..._retime(_slices(haps)));
return pat.query(state);
};
return new Pattern(q, first_t);
};
export function _retime(timedHaps) {
const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0));
const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce(
(a, b) => a.add(b),
Fraction(0),
);
const total_tactus = occupied_perc.eq(0) ? undefined : occupied_tactus.div(occupied_perc);
function adjust(dur, pat) {
if (pat.tactus === undefined) {
return [dur.mulmaybe(total_tactus), pat];
}
return [pat.tactus, pat];
}
return timedHaps.map((x) => adjust(...x));
}
export function _slices(haps) {
// slices evs = map (\s -> ((snd s - fst s), stack $ map value $ fit s evs))
// $ pairs $ sort $ nubOrd $ 0:1:concatMap (\ev -> start (part ev):stop (part ev):[]) evs
const breakpoints = flatten(haps.map((hap) => [hap.part.begin, hap.part.end]));
const unique = uniqsortr([Fraction(0), Fraction(1), ...breakpoints]);
const slicespans = pairs(unique);
return slicespans.map((s) => [
s[1].sub(s[0]),
stack(..._fitslice(new TimeSpan(...s), haps).map((x) => x.value.withHap((h) => h.setContext(h.combineContext(x))))),
]);
}
export function _fitslice(span, haps) {
return removeUndefineds(haps.map((hap) => _match(span, hap)));
}
export function _match(span, hap_p) {
const subspan = span.intersection(hap_p.part);
if (subspan == undefined) {
return undefined;
}
return new Hap(hap_p.whole, subspan, hap_p.value, hap_p.context);
}
/**
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given number of steps per cycle (aka tactus).
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given metrical 'tactus'.
* @example
* s("bd sd cp").steps(4)
* s("bd sd cp").toTactus(4)
* // The same as s("{bd sd cp}%4")
*/
export const steps = register('steps', function (targetTactus, pat) {
if (pat.tactus === undefined) {
return pat;
}
if (pat.tactus.eq(Fraction(0))) {
export const toTactus = register('toTactus', function (targetTactus, pat) {
if (pat.tactus.eq(0)) {
// avoid divide by zero..
return nothing;
}
@@ -2518,14 +2397,14 @@ export function _polymeterListSteps(steps, ...args) {
* Aligns one or more given patterns to the given number of steps per cycle.
* This relies on patterns having coherent number of steps per cycle,
*
* @name s_polymeterSteps
* @name polymeterSteps
* @param {number} steps how many items are placed in one cycle
* @param {any[]} patterns one or more patterns
* @example
* // the same as "{c d, e f g}%4"
* s_polymeterSteps(4, "c d", "e f g")
* polymeterSteps(4, "c d", "e f g")
*/
export function s_polymeterSteps(steps, ...args) {
export function polymeterSteps(steps, ...args) {
if (args.length == 0) {
return silence;
}
@@ -2534,7 +2413,7 @@ export function s_polymeterSteps(steps, ...args) {
return _polymeterListSteps(steps, ...args);
}
return s_polymeter(...args).steps(steps);
return polymeter(...args).toTactus(steps);
}
/**
@@ -2542,25 +2421,19 @@ export function s_polymeterSteps(steps, ...args) {
* @synonyms pm
* @example
* // The same as "{c eb g, c2 g2}"
* s_polymeter("c eb g", "c2 g2")
* polymeter("c eb g", "c2 g2")
*
*/
export function s_polymeter(...args) {
export function polymeter(...args) {
if (Array.isArray(args[0])) {
// Support old behaviour
return _polymeterListSteps(0, ...args);
}
// TODO currently ignoring arguments without tactus...
args = args.filter((arg) => arg.hasTactus);
if (args.length == 0) {
return silence;
}
const tactus = args[0].tactus;
if (tactus.eq(Fraction(0))) {
return nothing;
}
const [head, ...tail] = args;
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
@@ -2570,36 +2443,18 @@ export function s_polymeter(...args) {
/** Sequences patterns like `seq`, but each pattern has a length, relative to the whole.
* This length can either be provided as a [length, pattern] pair, or inferred from
* the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`.
* the pattern's 'tactus', generally inferred by the mininotation.
* @return {Pattern}
* @example
* s_cat([3,"e3"],[1, "g3"]).note()
* timecat([3,"e3"],[1, "g3"]).note()
* // the same as "e3@3 g3".note()
* @example
* s_cat("bd sd cp","hh hh").sound()
* timecat("bd sd cp","hh hh").sound()
* // the same as "bd sd cp hh hh".sound()
*/
export function s_cat(...timepats) {
if (timepats.length === 0) {
return nothing;
}
export function timecat(...timepats) {
const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]);
timepats = timepats.map(findtactus);
if (timepats.find((x) => x[0] === undefined)) {
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
if (times.length === 0) {
return fastcat(...timepats.map((x) => x[1]));
}
if (times.length === timepats.length) {
return nothing;
}
const avg = times.reduce((a, b) => a.add(b), Fraction(0)).div(times.length);
for (let timepat of timepats) {
if (timepat[0] === undefined) {
timepat[0] = avg;
}
}
}
if (timepats.length == 1) {
const result = reify(timepats[0][1]);
result.tactus = timepats[0][0];
@@ -2622,19 +2477,18 @@ export function s_cat(...timepats) {
return result;
}
/** Aliases for `s_cat` */
export const timecat = s_cat;
export const timeCat = s_cat;
/** Deprecated alias for `timecat` */
export const timeCat = timecat;
/**
* *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'.
* Similar to `s_cat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
* Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
*
* @return {Pattern}
* @example
* s_alt(["bd cp", "mt"], "bd").sound()
* stepcat(["bd cp", "mt"], "bd").sound()
*/
export function s_alt(...groups) {
export function stepcat(...groups) {
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
@@ -2643,9 +2497,9 @@ export function s_alt(...groups) {
for (let cycle = 0; cycle < cycles; ++cycle) {
result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length])));
}
result = result.filter((x) => x.hasTactus && x.tactus > 0);
result = result.filter((x) => x.tactus > 0);
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
result = s_cat(...result);
result = timecat(...result);
result.tactus = tactus;
return result;
}
@@ -2653,10 +2507,7 @@ export function s_alt(...groups) {
/**
* *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'.
*/
export const s_add = stepRegister('s_add', function (i, pat) {
if (!pat.hasTactus) {
return nothing;
}
export const stepwax = register('stepwax', function (i, pat) {
if (pat.tactus.lte(0)) {
return nothing;
}
@@ -2684,40 +2535,23 @@ export const s_add = stepRegister('s_add', function (i, pat) {
/**
* *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'.
*/
export const s_sub = stepRegister('s_sub', function (i, pat) {
if (!pat.hasTactus) {
return nothing;
}
export const stepwane = register('stepwane', function (i, pat) {
i = Fraction(i);
if (i.lt(0)) {
return pat.s_add(Fraction(0).sub(pat.tactus.add(i)));
return pat.stepwax(Fraction(0).sub(pat.tactus.add(i)));
}
return pat.s_add(pat.tactus.sub(i));
});
export const s_expand = stepRegister('s_expand', function (factor, pat) {
return pat.withTactus((t) => t.mul(Fraction(factor)));
});
export const s_contract = stepRegister('s_contract', function (factor, pat) {
return pat.withTactus((t) => t.div(Fraction(factor)));
return pat.stepwax(pat.tactus.sub(i));
});
/**
* *EXPERIMENTAL*
*/
Pattern.prototype.s_taperlist = function (amount, times) {
Pattern.prototype.taperlist = function (amount, times) {
const pat = this;
if (!pat.hasTactus) {
return [pat];
}
times = times - 1;
if (times === 0) {
return [pat];
return pat;
}
const list = [];
@@ -2734,33 +2568,23 @@ Pattern.prototype.s_taperlist = function (amount, times) {
}
return list;
};
export const s_taperlist = (amount, times, pat) => pat.s_taperlist(amount, times);
export const taperlist = (amount, times, pat) => pat.taperlist(amount, times);
/**
* *EXPERIMENTAL*
*/
export const s_taper = register(
's_taper',
function (amount, times, pat) {
if (!pat.hasTactus) {
return nothing;
}
const list = pat.s_taperlist(amount, times);
const result = s_cat(...list);
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
return result;
},
true,
false,
(x) => x.stepJoin(),
);
export const steptaper = register('steptaper', function (amount, times, pat) {
const list = pat.taperlist(amount, times);
const result = timecat(...list);
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
return result;
});
/**
* *EXPERIMENTAL*
*/
Pattern.prototype.s_tour = function (...many) {
return s_cat(
Pattern.prototype.steptour = function (...many) {
return stepcat(
...[].concat(
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
this,
@@ -2769,8 +2593,8 @@ Pattern.prototype.s_tour = function (...many) {
);
};
export const s_tour = function (pat, ...many) {
return pat.s_tour(...many);
export const steptour = function (pat, ...many) {
return pat.steptour(...many);
};
//////////////////////////////////////////////////////////////////////
@@ -2797,7 +2621,7 @@ export const chop = register('chop', function (n, pat) {
const func = function (o) {
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
};
return pat.squeezeBind(func).setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
return pat.squeezeBind(func).setTactus(pat.tactus.mul(n));
});
/**
@@ -2812,10 +2636,7 @@ export const striate = register('striate', function (n, pat) {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
const slicePat = slowcat(...slice_objects);
return pat
.set(slicePat)
._fast(n)
.setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
return pat.set(slicePat)._fast(n);
});
/**
-11
View File
@@ -54,12 +54,10 @@ export function repl({
const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
let pPatterns = {};
let anonymousIndex = 0;
let allTransform;
const hush = function () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
return silence;
};
@@ -84,15 +82,6 @@ export function repl({
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
if (id.startsWith('_') || id.endsWith('_')) {
// allows muting a pattern x with x_ or _x
return silence;
}
if (id === '$') {
// allows adding anonymous patterns with $:
id = `$${anonymousIndex}`;
anonymousIndex++;
}
pPatterns[id] = this;
return this;
};
+1 -3
View File
@@ -486,10 +486,8 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* @returns {Pattern}
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
*/
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
export const wrandcat = wchooseCycles;
+22 -69
View File
@@ -21,8 +21,8 @@ import {
cat,
sequence,
palindrome,
s_polymeter,
s_polymeterSteps,
polymeter,
polymeterSteps,
polyrhythm,
silence,
fast,
@@ -47,11 +47,6 @@ import {
time,
run,
pick,
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -608,18 +603,18 @@ describe('Pattern', () => {
);
});
});
describe('s_polymeter()', () => {
describe('polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => {
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
);
});
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
});
@@ -1128,8 +1123,8 @@ describe('Pattern', () => {
it('Is correctly preserved/calculated through transformations', () => {
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(16));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16));
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
@@ -1141,72 +1136,30 @@ describe('Pattern', () => {
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(2)).tactus,
).toStrictEqual(Fraction(6));
expect(
pure((x) => x + 1)
.setTactus(undefined)
.appBoth(pure(1).setTactus(2)).tactus,
).toStrictEqual(Fraction(2));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(undefined)).tactus,
).toStrictEqual(Fraction(3));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
// maybe this should double when they are either all even or all odd
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6));
expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined);
expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2));
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
});
});
describe('s_cat', () => {
it('can cat', () => {
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
});
it('calculates undefined tactuses as the average', () => {
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
});
});
describe('s_taper', () => {
describe('steptaper', () => {
it('can taper', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
expect(sameFirst(sequence(0, 1, 2, 3, 4).steptaper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
});
it('can taper backwards', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
expect(
sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)),
);
});
});
describe('s_add and s_sub', () => {
it('can add from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
describe('wax and wane, left', () => {
it('can wax from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
});
it('can sub to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
it('can wane to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
});
it('can add from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
it('can wax from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
});
it('can sub to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
});
it('can subtract nothing', () => {
expect(sameFirst(pure('a').s_sub(0), pure('a')));
});
it('can subtract nothing, repeatedly', () => {
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
for (var i = 0; i < 100; ++i) {
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
}
it('can wane to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4)));
});
});
});
-37
View File
@@ -61,11 +61,6 @@ export const valueToMidi = (value, fallbackValue) => {
return fallbackValue;
};
// used to schedule external event like midi and osc out
export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
return (targetTimeSeconds - currentTimeSeconds) * 1000;
};
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
@@ -236,14 +231,6 @@ export const splitAt = function (index, value) {
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const pairs = function (xs) {
const result = [];
for (let i = 0; i < xs.length - 1; ++i) {
result.push([xs[i], xs[i + 1]]);
}
return result;
};
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */
@@ -302,30 +289,6 @@ export const sol2note = (n, notation = 'letters') => {
return note + oct;
};
// Remove duplicates from list
export function uniq(a) {
var seen = {};
return a.filter(function (item) {
return seen.hasOwn(item) ? false : (seen[item] = true);
});
}
// Remove duplicates from list, sorting in the process. Mutates argument!
export function uniqsort(a) {
return a.sort().filter(function (item, pos, ary) {
return !pos || item != ary[pos - 1];
});
}
// rational version
export function uniqsortr(a) {
return a
.sort((x, y) => x.compare(y))
.filter(function (item, pos, ary) {
return !pos || item.ne(ary[pos - 1]);
});
}
// code hashing helpers
export function unicodeToBase64(text) {
+2 -4
View File
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
instrument = instrument || 'triangle';
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
return pat.onTrigger((time, hap) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@@ -38,11 +38,9 @@ export const csound = register('csound', (instrument, pat) => {
.join('/');
// TODO: find out how to send a precise ctx based time
// http://www.csounds.com/manual/html/i.html
const timeOffset = targetTime - currentTime; // latency ?
//const timeOffset = time_deprecate - getAudioContext().currentTime
const params = [
`"${instrument}"`, // p1: instrument name
timeOffset, // p2: starting time in arbitrary unit called beats
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
hap.duration + 0, // p3: duration in beats
// instrument specific params:
freq, //.toFixed(precision), // p4: frequency
+3 -3
View File
@@ -1,5 +1,5 @@
import { Invoke } from './utils.mjs';
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
import { Pattern, noteToMidi } from '@strudel/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
@@ -9,8 +9,8 @@ Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs;
const latency = 0.034;
const offset = (targetTime - currentTime + latency) * 1000;
velocity = Math.floor(gain * velocity * 100);
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
const roundedOffset = Math.round(offset);
+3 -3
View File
@@ -1,8 +1,8 @@
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
import { parseNumeral, Pattern } from '@strudel/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
@@ -13,7 +13,7 @@ Pattern.prototype.osc = function () {
const params = [];
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime));
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
Object.keys(controls).forEach((key) => {
const val = controls[key];
+5 -4
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -120,9 +120,10 @@ Pattern.prototype.midi = function (output) {
const device = getDevice(output, WebMidi.outputs);
hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
const latency = 0.034;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
const timeOffsetString = `+${(targetTime - currentTime + latency) * 1000}`;
// destructure value
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
@@ -176,7 +177,7 @@ export async function midin(input) {
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
if (initial || WebMidi.enabled) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
+1 -9
View File
@@ -295,15 +295,7 @@ function peg$parse(input, options) {
var peg$f6 = function(a) { return a };
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 };
var peg$f9 = function(a) { return x => {const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
x.options_['reps'] = reps;
console.log("reps: ", reps)
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
x.options_['weight'] = reps;
console.log("options: ", x.options_);
}
};
var peg$f9 = function(a) { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 };
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
+1 -8
View File
@@ -135,14 +135,7 @@ op_weight = ws ("@" / "_") a:number?
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
op_replicate = ws "!" a:number?
{ return x => {// A bit fiddly, to support both x!4 and x!!! as equivalent..
const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
x.options_['reps'] = reps;
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
x.options_['weight'] = reps;
}
}
{ return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
+14 -7
View File
@@ -27,12 +27,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
pat = strudel.reify(pat)[type](enter(amount));
break;
}
case 'replicate': {
const { amount } = op.arguments_;
pat = strudel.reify(pat);
pat = pat._repeatCycles(amount)._fast(amount);
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
@@ -73,13 +67,26 @@ const applyOptions = (parent, enter) => (pat, i) => {
return pat;
};
function resolveReplications(ast) {
ast.source_ = strudel.flatten(
ast.source_.map((child) => {
const { reps } = child.options_ || {};
if (!reps) {
return [child];
}
delete child.options_.reps;
return Array(reps).fill(child);
}),
);
}
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) {
case 'pattern': {
// resolveReplications(ast);
resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
const with_tactus = children.filter((child) => child.__tactus_source);
-19
View File
@@ -1,19 +0,0 @@
# @strudel/node
This is an experiment to run strudel in node.
## Usage
```sh
# Setup
# - make sure node >= 20 is installed
# - make sure pnpm is installed
cd packages/node
pnpm i
# Usage
pnpm start
```
Then run `sclang` with superdirt in another terminal.
You can now edit and save the file `pattern.mjs` to update your pattern!
-64
View File
@@ -1,64 +0,0 @@
import { createClock, evalScope } from '@strudel/core';
import { evaluate } from '@strudel/transpiler';
import watch from 'node-watch';
import fs from 'node:fs/promises';
import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api';
const audioContext = new AudioContext();
let file = 'pattern.mjs';
let pattern;
async function evaluateFile() {
try {
console.log('// file evaluated:');
const code = await fs.readFile(file, { encoding: 'utf8' });
console.log(code);
const res = await evaluate(code);
pattern = res.pattern;
} catch (err) {
console.error(err);
}
}
// const getTime = () => performance.now() / 1000;
const getTime = () => audioContext.currentTime;
let minLatency = 50;
async function main() {
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
await evaluateFile();
watch(file, { recursive: true }, () => evaluateFile());
let lastEnd;
const clock = createClock(getTime, (phase) => {
if (!lastEnd) {
lastEnd = phase;
return;
}
const haps = pattern.queryArc(lastEnd, phase);
lastEnd = phase;
const cps = 1;
const cycle = Math.floor(phase);
haps
.filter((h) => h.hasOnset())
.forEach((hap) => {
const env = new GainNode(audioContext, { gain: 0 });
const { attack = 0.01, gain = 1 } = hap.value;
env.connect(audioContext.destination);
const now = hap.whole.begin;
const duration = hap.duration;
env.gain
.setValueAtTime(0, now)
.linearRampToValueAtTime(gain * 0.2, now + attack)
.exponentialRampToValueAtTime(0.0001, now + duration);
const frequency = hap.value.freq;
const osc = new OscillatorNode(audioContext, { frequency });
osc.connect(env);
osc.start(now);
osc.stop(now + duration);
});
});
clock.start();
}
main();
-76
View File
@@ -1,76 +0,0 @@
import { createClock, evalScope } from '@strudel/core';
import { evaluate } from '@strudel/transpiler';
import OSC from 'osc-js';
import watch from 'node-watch';
import fs from 'node:fs/promises';
const config = {
receiver: 'udp', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
open: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57121, // @param {number} Port of udp client for messaging
// enabling the following line will receive tidal messages:
// port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
send: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
},
};
const osc = new OSC({ plugin: new OSC.DatagramPlugin(config) });
osc.open(); // start a WebSocket server on port 8080
console.log('osc client running on port', config.open.port);
console.log('osc server running on port', config.send.port);
let file = 'pattern.mjs';
let pattern;
async function evaluateFile() {
try {
console.log('// file evaluated:');
const code = await fs.readFile(file, { encoding: 'utf8' });
console.log(code);
const res = await evaluate(code);
pattern = res.pattern;
} catch (err) {
console.error(err);
}
}
const getTime = () => performance.now() / 1000;
async function main() {
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
await evaluateFile();
watch(file, { recursive: true }, () => evaluateFile());
let lastEnd;
let minLatency = 50;
const clock = createClock(getTime, (phase) => {
if (!lastEnd) {
lastEnd = phase;
return;
}
const haps = pattern.queryArc(lastEnd, phase);
lastEnd = phase;
const cps = 1;
const cycle = Math.floor(phase);
haps
.filter((h) => h.hasOnset())
.forEach((hap) => {
const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
const keyvals = Object.entries(controls).flat();
const ts = Math.floor(performance.timeOrigin + hap.whole.begin * 1000 + minLatency);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
osc.send(bundle);
});
});
clock.start();
}
main();
-37
View File
@@ -1,37 +0,0 @@
{
"name": "@strudel/node",
"version": "1.1.0",
"description": "Strudel running in node",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"main": "index.mjs",
"scripts": {
"osc": "node osc-superdirt.mjs",
"waa": "node node-web-audio-api.mjs"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"node-watch": "^0.7.4",
"node-web-audio-api": "^0.20.0",
"osc-js": "^2.4.0"
}
}
-2
View File
@@ -1,2 +0,0 @@
freq("110 220 [330 440,550,660]")
.attack(.1).gain(.4)
+3 -4
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
import { logger, parseNumeral, Pattern } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
@@ -44,7 +44,7 @@ function connect() {
* @returns Pattern
*/
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
@@ -56,8 +56,7 @@ Pattern.prototype.osc = function () {
const keyvals = Object.entries(controls).flat();
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
const offset = getEventOffsetMs(targetTime, currentTime);
const offset = (time - currentTime) * 1000;
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
const ts = Math.floor(Date.now() + offset);
const message = new OSC.Message('/dirt/play', ...keyvals);
+1 -1
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/>.
*/
import OSC from 'osc-js';
const OSC = require('osc-js');
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.0.9",
"version": "0.0.8",
"description": "",
"keywords": [
"tidalcycles",
+2 -2
View File
@@ -74,6 +74,6 @@ export const dough = async (code) => {
worklet.node.connect(ac.destination);
};
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
}
+55 -60
View File
@@ -188,66 +188,61 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
* .s("piano")
*/
export const scale = register(
'scale',
function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
return (
pat
.fmap((value) => {
const isObject = typeof value === 'object';
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
return (
pat
.fmap((value) => {
const isObject = typeof value === 'object';
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
}
if (isNote(step)) {
// legacy..
return pure(step);
}
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
if (isNote(step)) {
// legacy..
return pure(step);
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
}
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
);
},
true,
true, // preserve tactus
);
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
);
});
-62
View File
@@ -302,33 +302,6 @@ importers:
specifier: ^1.1.0
version: 1.1.0(@vitest/ui@1.1.0)
packages/node:
dependencies:
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/mini':
specifier: workspace:*
version: link:../mini
'@strudel/osc':
specifier: workspace:*
version: link:../osc
'@strudel/tonal':
specifier: workspace:*
version: link:../tonal
'@strudel/transpiler':
specifier: workspace:*
version: link:../transpiler
node-watch:
specifier: ^0.7.4
version: 0.7.4
node-web-audio-api:
specifier: ^0.20.0
version: 0.20.0
osc-js:
specifier: ^2.4.0
version: 2.4.0
packages/osc:
dependencies:
'@strudel/core':
@@ -3494,22 +3467,6 @@ packages:
react: 18.2.0
dev: false
/@napi-rs/cli@2.18.2:
resolution: {integrity: sha512-IXQji3IF5eStxTHe/PxDSRjPrFymYAQ5FbIvqurxzxyWR8nJql9mtvmCP8y2g8tSoW5xhaToLQW0+mO3lUZq4w==}
engines: {node: '>= 10'}
hasBin: true
dev: false
/@napi-rs/triples@1.2.0:
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
dev: false
/@node-rs/helper@1.6.0:
resolution: {integrity: sha512-2OTh/tokcLA1qom1zuCJm2gQzaZljCCbtX1YCrwRVd/toz7KxaDRFeLTAPwhs8m9hWgzrBn5rShRm6IaZofCPw==}
dependencies:
'@napi-rs/triples': 1.2.0
dev: false
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -10760,20 +10717,6 @@ packages:
'@babel/parser': 7.23.6
dev: true
/node-watch@0.7.4:
resolution: {integrity: sha512-RinNxoz4W1cep1b928fuFhvAQ5ag/+1UlMDV7rbyGthBIgsiEouS4kvRayvvboxii4m8eolKOIBo3OjDqbc+uQ==}
engines: {node: '>=6'}
dev: false
/node-web-audio-api@0.20.0:
resolution: {integrity: sha512-DPsRSG3IsI8cLCdejpruir+XgSwUFFJBSfilrtoT+BU/uFcXv/eFyKkulnKIE2642j5b00z9aOHxTHbzdUvTtw==}
engines: {node: '>= 14'}
dependencies:
'@napi-rs/cli': 2.18.2
'@node-rs/helper': 1.6.0
webidl-conversions: 7.0.0
dev: false
/nopt@7.1.0:
resolution: {integrity: sha512-ZFPLe9Iu0tnx7oWhFxAo4s7QTn8+NNDDxYNaKLjE7Dp0tbakQ3M1QhQzsnzXHQBTUO3K9BmwaxnyO8Ayn2I95Q==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -14250,11 +14193,6 @@ packages:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
dev: true
/webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
dev: false
/webmidi@3.1.8:
resolution: {integrity: sha512-PCRic1iTpKxeheb888G0mDe6MBdz/u+s/xfdV6+1ZhZnS6dKtV9gMBth5cpmMip2Livv5lL+ep4/S9DCzHfumg==}
engines: {node: '>=8.5'}
+115 -123
View File
@@ -1,22 +1,22 @@
use rosc::{encoder, OscTime};
use rosc::{OscBundle, OscMessage, OscPacket, OscType};
use rosc::{ encoder, OscTime };
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
use std::net::UdpSocket;
use serde::Deserialize;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;
use tokio::sync::{ mpsc, Mutex };
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
pub struct OscMsg {
pub msg_buf: Vec<u8>,
pub timestamp: u64,
pub msg_buf: Vec<u8>,
pub timestamp: u64,
}
pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
}
const UNIX_OFFSET: u64 = 2_208_988_800; // 70 years in seconds
@@ -25,141 +25,133 @@ const NANOS_PER_SECOND: f64 = 1.0e9;
const SECONDS_PER_NANO: f64 = 1.0 / NANOS_PER_SECOND;
pub fn init(
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>,
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>
) {
tauri::async_runtime::spawn(async move {
async_process_model(async_input_receiver, async_output_transmitter).await
});
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Listen For incoming messages and add to queue
Open OSC Ports
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr).expect("could not connect to OSC address");
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
logger.log(
format!("OSC Message failed to send, the server might no longer be available"),
"error".to_string()
);
}
});
return false;
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open OSC Ports
............................................................*/
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr)
.expect("could not connect to OSC address");
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
logger.log(
format!(
"OSC Message failed to send, the server might no longer be available"
),
"error".to_string(),
);
}
return false;
});
sleep(Duration::from_millis(1));
}
});
sleep(Duration::from_millis(1));
}
});
}
pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>,
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct Param {
name: String,
value: String,
valueisnumber: bool,
name: String,
value: String,
valueisnumber: bool,
}
#[derive(Deserialize)]
pub struct MessageFromJS {
params: Vec<Param>,
timestamp: u64,
target: String,
params: Vec<Param>,
timestamp: u64,
target: String,
}
// Called from JS
#[tauri::command]
pub async fn sendosc(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>,
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}
let duration_since_epoch =
Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
let seconds = u32::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
let timetag = OscTime::from((seconds, fractional));
let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});
let bundle = OscBundle {
content: vec![packet],
timetag,
};
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}
async_proc_input_tx
.send(messages_to_process)
.await
.map_err(|e| e.to_string())
let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
let seconds = u32
::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
let timetag = OscTime::from((seconds, fractional));
let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});
let bundle = OscBundle {
content: vec![packet],
timetag,
};
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
+3 -312
View File
@@ -5972,135 +5972,6 @@ exports[`runs examples > example "s" example index 1 1`] = `
]
`;
exports[`runs examples > example "s_alt" example index 0 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:cp ]",
"[ 2/5 → 3/5 | s:bd ]",
"[ 3/5 → 4/5 | s:mt ]",
"[ 4/5 → 1/1 | s:bd ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:cp ]",
"[ 7/5 → 8/5 | s:bd ]",
"[ 8/5 → 9/5 | s:mt ]",
"[ 9/5 → 2/1 | s:bd ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:cp ]",
"[ 12/5 → 13/5 | s:bd ]",
"[ 13/5 → 14/5 | s:mt ]",
"[ 14/5 → 3/1 | s:bd ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:cp ]",
"[ 17/5 → 18/5 | s:bd ]",
"[ 18/5 → 19/5 | s:mt ]",
"[ 19/5 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "s_cat" example index 0 1`] = `
[
"[ 0/1 → 3/4 | note:e3 ]",
"[ 3/4 → 1/1 | note:g3 ]",
"[ 1/1 → 7/4 | note:e3 ]",
"[ 7/4 → 2/1 | note:g3 ]",
"[ 2/1 → 11/4 | note:e3 ]",
"[ 11/4 → 3/1 | note:g3 ]",
"[ 3/1 → 15/4 | note:e3 ]",
"[ 15/4 → 4/1 | note:g3 ]",
]
`;
exports[`runs examples > example "s_cat" example index 1 1`] = `
[
"[ 0/1 → 1/5 | s:bd ]",
"[ 1/5 → 2/5 | s:sd ]",
"[ 2/5 → 3/5 | s:cp ]",
"[ 3/5 → 4/5 | s:hh ]",
"[ 4/5 → 1/1 | s:hh ]",
"[ 1/1 → 6/5 | s:bd ]",
"[ 6/5 → 7/5 | s:sd ]",
"[ 7/5 → 8/5 | s:cp ]",
"[ 8/5 → 9/5 | s:hh ]",
"[ 9/5 → 2/1 | s:hh ]",
"[ 2/1 → 11/5 | s:bd ]",
"[ 11/5 → 12/5 | s:sd ]",
"[ 12/5 → 13/5 | s:cp ]",
"[ 13/5 → 14/5 | s:hh ]",
"[ 14/5 → 3/1 | s:hh ]",
"[ 3/1 → 16/5 | s:bd ]",
"[ 16/5 → 17/5 | s:sd ]",
"[ 17/5 → 18/5 | s:cp ]",
"[ 18/5 → 19/5 | s:hh ]",
"[ 19/5 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "s_polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/3 | c ]",
"[ 0/1 → 1/3 | c2 ]",
"[ 1/3 → 2/3 | eb ]",
"[ 1/3 → 2/3 | g2 ]",
"[ 2/3 → 1/1 | g ]",
"[ 2/3 → 1/1 | c2 ]",
"[ 1/1 → 4/3 | c ]",
"[ 1/1 → 4/3 | g2 ]",
"[ 4/3 → 5/3 | eb ]",
"[ 4/3 → 5/3 | c2 ]",
"[ 5/3 → 2/1 | g ]",
"[ 5/3 → 2/1 | g2 ]",
"[ 2/1 → 7/3 | c ]",
"[ 2/1 → 7/3 | c2 ]",
"[ 7/3 → 8/3 | eb ]",
"[ 7/3 → 8/3 | g2 ]",
"[ 8/3 → 3/1 | g ]",
"[ 8/3 → 3/1 | c2 ]",
"[ 3/1 → 10/3 | c ]",
"[ 3/1 → 10/3 | g2 ]",
"[ 10/3 → 11/3 | eb ]",
"[ 10/3 → 11/3 | c2 ]",
"[ 11/3 → 4/1 | g ]",
"[ 11/3 → 4/1 | g2 ]",
]
`;
exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | c ]",
"[ 0/1 → 1/4 | e ]",
"[ 1/4 → 1/2 | d ]",
"[ 1/4 → 1/2 | f ]",
"[ 1/2 → 3/4 | c ]",
"[ 1/2 → 3/4 | g ]",
"[ 3/4 → 1/1 | d ]",
"[ 3/4 → 1/1 | e ]",
"[ 1/1 → 5/4 | c ]",
"[ 1/1 → 5/4 | f ]",
"[ 5/4 → 3/2 | d ]",
"[ 5/4 → 3/2 | g ]",
"[ 3/2 → 7/4 | c ]",
"[ 3/2 → 7/4 | e ]",
"[ 7/4 → 2/1 | d ]",
"[ 7/4 → 2/1 | f ]",
"[ 2/1 → 9/4 | c ]",
"[ 2/1 → 9/4 | g ]",
"[ 9/4 → 5/2 | d ]",
"[ 9/4 → 5/2 | e ]",
"[ 5/2 → 11/4 | c ]",
"[ 5/2 → 11/4 | f ]",
"[ 11/4 → 3/1 | d ]",
"[ 11/4 → 3/1 | g ]",
"[ 3/1 → 13/4 | c ]",
"[ 3/1 → 13/4 | e ]",
"[ 13/4 → 7/2 | d ]",
"[ 13/4 → 7/2 | f ]",
"[ 7/2 → 15/4 | c ]",
"[ 7/2 → 15/4 | g ]",
"[ 15/4 → 4/1 | d ]",
"[ 15/4 → 4/1 | e ]",
]
`;
exports[`runs examples > example "samples" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@@ -7241,27 +7112,6 @@ exports[`runs examples > example "stepcat" example index 0 1`] = `
]
`;
exports[`runs examples > example "steps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:sd ]",
"[ 1/2 → 3/4 | s:cp ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:sd ]",
"[ 5/4 → 3/2 | s:cp ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:sd ]",
"[ 2/1 → 9/4 | s:cp ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:sd ]",
"[ 11/4 → 3/1 | s:cp ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:sd ]",
"[ 7/2 → 15/4 | s:cp ]",
"[ 15/4 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "striate" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]",
@@ -7448,112 +7298,6 @@ exports[`runs examples > example "sustain" example index 0 1`] = `
]
`;
exports[`runs examples > example "swing" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 1/24 ⇜ (1/8 → 1/6) | s:hh ]",
"[ (1/6 → 1/4) ⇝ 7/24 | s:hh ]",
"[ 1/4 → 3/8 | s:hh ]",
"[ 7/24 ⇜ (3/8 → 5/12) | s:hh ]",
"[ (5/12 → 1/2) ⇝ 13/24 | s:hh ]",
"[ 1/2 → 5/8 | s:hh ]",
"[ 13/24 ⇜ (5/8 → 2/3) | s:hh ]",
"[ (2/3 → 3/4) ⇝ 19/24 | s:hh ]",
"[ 3/4 → 7/8 | s:hh ]",
"[ 19/24 ⇜ (7/8 → 11/12) | s:hh ]",
"[ (11/12 → 1/1) ⇝ 25/24 | s:hh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 25/24 ⇜ (9/8 → 7/6) | s:hh ]",
"[ (7/6 → 5/4) ⇝ 31/24 | s:hh ]",
"[ 5/4 → 11/8 | s:hh ]",
"[ 31/24 ⇜ (11/8 → 17/12) | s:hh ]",
"[ (17/12 → 3/2) ⇝ 37/24 | s:hh ]",
"[ 3/2 → 13/8 | s:hh ]",
"[ 37/24 ⇜ (13/8 → 5/3) | s:hh ]",
"[ (5/3 → 7/4) ⇝ 43/24 | s:hh ]",
"[ 7/4 → 15/8 | s:hh ]",
"[ 43/24 ⇜ (15/8 → 23/12) | s:hh ]",
"[ (23/12 → 2/1) ⇝ 49/24 | s:hh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 49/24 ⇜ (17/8 → 13/6) | s:hh ]",
"[ (13/6 → 9/4) ⇝ 55/24 | s:hh ]",
"[ 9/4 → 19/8 | s:hh ]",
"[ 55/24 ⇜ (19/8 → 29/12) | s:hh ]",
"[ (29/12 → 5/2) ⇝ 61/24 | s:hh ]",
"[ 5/2 → 21/8 | s:hh ]",
"[ 61/24 ⇜ (21/8 → 8/3) | s:hh ]",
"[ (8/3 → 11/4) ⇝ 67/24 | s:hh ]",
"[ 11/4 → 23/8 | s:hh ]",
"[ 67/24 ⇜ (23/8 → 35/12) | s:hh ]",
"[ (35/12 → 3/1) ⇝ 73/24 | s:hh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 73/24 ⇜ (25/8 → 19/6) | s:hh ]",
"[ (19/6 → 13/4) ⇝ 79/24 | s:hh ]",
"[ 13/4 → 27/8 | s:hh ]",
"[ 79/24 ⇜ (27/8 → 41/12) | s:hh ]",
"[ (41/12 → 7/2) ⇝ 85/24 | s:hh ]",
"[ 7/2 → 29/8 | s:hh ]",
"[ 85/24 ⇜ (29/8 → 11/3) | s:hh ]",
"[ (11/3 → 15/4) ⇝ 91/24 | s:hh ]",
"[ 15/4 → 31/8 | s:hh ]",
"[ 91/24 ⇜ (31/8 → 47/12) | s:hh ]",
"[ (47/12 → 4/1) ⇝ 97/24 | s:hh ]",
]
`;
exports[`runs examples > example "swingBy" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 1/24 ⇜ (1/8 → 1/6) | s:hh ]",
"[ (1/6 → 1/4) ⇝ 7/24 | s:hh ]",
"[ 1/4 → 3/8 | s:hh ]",
"[ 7/24 ⇜ (3/8 → 5/12) | s:hh ]",
"[ (5/12 → 1/2) ⇝ 13/24 | s:hh ]",
"[ 1/2 → 5/8 | s:hh ]",
"[ 13/24 ⇜ (5/8 → 2/3) | s:hh ]",
"[ (2/3 → 3/4) ⇝ 19/24 | s:hh ]",
"[ 3/4 → 7/8 | s:hh ]",
"[ 19/24 ⇜ (7/8 → 11/12) | s:hh ]",
"[ (11/12 → 1/1) ⇝ 25/24 | s:hh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 25/24 ⇜ (9/8 → 7/6) | s:hh ]",
"[ (7/6 → 5/4) ⇝ 31/24 | s:hh ]",
"[ 5/4 → 11/8 | s:hh ]",
"[ 31/24 ⇜ (11/8 → 17/12) | s:hh ]",
"[ (17/12 → 3/2) ⇝ 37/24 | s:hh ]",
"[ 3/2 → 13/8 | s:hh ]",
"[ 37/24 ⇜ (13/8 → 5/3) | s:hh ]",
"[ (5/3 → 7/4) ⇝ 43/24 | s:hh ]",
"[ 7/4 → 15/8 | s:hh ]",
"[ 43/24 ⇜ (15/8 → 23/12) | s:hh ]",
"[ (23/12 → 2/1) ⇝ 49/24 | s:hh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 49/24 ⇜ (17/8 → 13/6) | s:hh ]",
"[ (13/6 → 9/4) ⇝ 55/24 | s:hh ]",
"[ 9/4 → 19/8 | s:hh ]",
"[ 55/24 ⇜ (19/8 → 29/12) | s:hh ]",
"[ (29/12 → 5/2) ⇝ 61/24 | s:hh ]",
"[ 5/2 → 21/8 | s:hh ]",
"[ 61/24 ⇜ (21/8 → 8/3) | s:hh ]",
"[ (8/3 → 11/4) ⇝ 67/24 | s:hh ]",
"[ 11/4 → 23/8 | s:hh ]",
"[ 67/24 ⇜ (23/8 → 35/12) | s:hh ]",
"[ (35/12 → 3/1) ⇝ 73/24 | s:hh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 73/24 ⇜ (25/8 → 19/6) | s:hh ]",
"[ (19/6 → 13/4) ⇝ 79/24 | s:hh ]",
"[ 13/4 → 27/8 | s:hh ]",
"[ 79/24 ⇜ (27/8 → 41/12) | s:hh ]",
"[ (41/12 → 7/2) ⇝ 85/24 | s:hh ]",
"[ 7/2 → 29/8 | s:hh ]",
"[ 85/24 ⇜ (29/8 → 11/3) | s:hh ]",
"[ (11/3 → 15/4) ⇝ 91/24 | s:hh ]",
"[ 15/4 → 31/8 | s:hh ]",
"[ 91/24 ⇜ (31/8 → 47/12) | s:hh ]",
"[ (47/12 → 4/1) ⇝ 97/24 | s:hh ]",
]
`;
exports[`runs examples > example "timecat" example index 0 1`] = `
[
"[ 0/1 → 3/4 | note:e3 ]",
@@ -8135,12 +7879,12 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
"[ 3/8 → 1/2 | s:bd ]",
"[ 1/2 → 5/8 | s:bd ]",
"[ 5/8 → 3/4 | s:bd ]",
"[ 3/4 → 7/8 | s:sd ]",
"[ 3/4 → 7/8 | s:bd ]",
"[ 7/8 → 1/1 | s:bd ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 9/8 → 5/4 | s:bd ]",
"[ 5/4 → 11/8 | s:bd ]",
"[ 11/8 → 3/2 | s:sd ]",
"[ 11/8 → 3/2 | s:bd ]",
"[ 3/2 → 13/8 | s:bd ]",
"[ 13/8 → 7/4 | s:bd ]",
"[ 7/4 → 15/8 | s:bd ]",
@@ -8151,7 +7895,7 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
"[ 19/8 → 5/2 | s:bd ]",
"[ 5/2 → 21/8 | s:bd ]",
"[ 21/8 → 11/4 | s:bd ]",
"[ 11/4 → 23/8 | s:hh ]",
"[ 11/4 → 23/8 | s:bd ]",
"[ 23/8 → 3/1 | s:bd ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 25/8 → 13/4 | s:bd ]",
@@ -8164,59 +7908,6 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
]
`;
exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
[
"[ 0/1 → 1/12 | s:bd ]",
"[ 1/12 → 1/6 | s:bd ]",
"[ 1/6 → 1/4 | s:bd ]",
"[ 1/4 → 1/3 | s:hh ]",
"[ 1/3 → 5/12 | s:hh ]",
"[ 5/12 → 1/2 | s:hh ]",
"[ 1/2 → 7/12 | s:bd ]",
"[ 7/12 → 2/3 | s:bd ]",
"[ 2/3 → 3/4 | s:bd ]",
"[ 3/4 → 5/6 | s:bd ]",
"[ 5/6 → 11/12 | s:bd ]",
"[ 11/12 → 1/1 | s:bd ]",
"[ 1/1 → 13/12 | s:bd ]",
"[ 13/12 → 7/6 | s:bd ]",
"[ 7/6 → 5/4 | s:bd ]",
"[ 5/4 → 4/3 | s:bd ]",
"[ 4/3 → 17/12 | s:bd ]",
"[ 17/12 → 3/2 | s:bd ]",
"[ 3/2 → 19/12 | s:sd ]",
"[ 19/12 → 5/3 | s:sd ]",
"[ 5/3 → 7/4 | s:sd ]",
"[ 7/4 → 11/6 | s:bd ]",
"[ 11/6 → 23/12 | s:bd ]",
"[ 23/12 → 2/1 | s:bd ]",
"[ 2/1 → 25/12 | s:hh ]",
"[ 25/12 → 13/6 | s:hh ]",
"[ 13/6 → 9/4 | s:hh ]",
"[ 9/4 → 7/3 | s:hh ]",
"[ 7/3 → 29/12 | s:hh ]",
"[ 29/12 → 5/2 | s:hh ]",
"[ 5/2 → 31/12 | s:hh ]",
"[ 31/12 → 8/3 | s:hh ]",
"[ 8/3 → 11/4 | s:hh ]",
"[ 11/4 → 17/6 | s:sd ]",
"[ 17/6 → 35/12 | s:sd ]",
"[ 35/12 → 3/1 | s:sd ]",
"[ 3/1 → 37/12 | s:bd ]",
"[ 37/12 → 19/6 | s:bd ]",
"[ 19/6 → 13/4 | s:bd ]",
"[ 13/4 → 10/3 | s:bd ]",
"[ 10/3 → 41/12 | s:bd ]",
"[ 41/12 → 7/2 | s:bd ]",
"[ 7/2 → 43/12 | s:hh ]",
"[ 43/12 → 11/3 | s:hh ]",
"[ 11/3 → 15/4 | s:hh ]",
"[ 15/4 → 23/6 | s:bd ]",
"[ 23/6 → 47/12 | s:bd ]",
"[ 47/12 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "when" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c3 ]",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

+49
View File
@@ -0,0 +1,49 @@
export const laserclock = ({ onTick, audioContext }) => {
const ready = new Promise((resolve) => {
const workletCode = `class LaserClock extends AudioWorkletProcessor {
constructor() {
super();
this.block = 0;
this.tick = 0;
this.started = false;
this.blocksPerCallback = 20;
this.port.onmessage = (e) => {
switch (e.data) {
case "start":
console.log('start!')
this.started = true;
break;
case "stop":
console.log('stop!')
this.started = false;
this.block = 0;
this.tick = 0;
break;
}
};
}
process(inputs, outputs, parameters) {
if(!this.started) {
return true;
}
if(this.block % this.blocksPerCallback === 0) {
this.port.postMessage({tick: this.tick, currentTime});
this.tick++;
}
this.block++;
return true;
}
}
registerProcessor("laserclock-processor", LaserClock);`;
const dataURL = `data:text/javascript;base64,${btoa(workletCode)}`;
audioContext.audioWorklet.addModule(dataURL).then(() => {
const clock = new AudioWorkletNode(audioContext, 'laserclock-processor');
clock.port.onmessage = (e) => onTick(e.data);
clock.connect(audioContext.destination);
resolve(clock);
});
});
const start = () => ready.then((clock) => clock.port.postMessage('start'));
const stop = () => ready.then((clock) => clock.port.postMessage('stop'));
return { ready, start, stop };
};
@@ -146,13 +146,15 @@ Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert.
**Sequenzen verlangsamen mit `/`**
{/* [c2 bb1 f2 eb2] */}
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
<Box>
Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 8s.
Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 4s.
Jede Note ist nun also 2s lang.
Jede Note ist nun also 1s lang.
Schreib noch mehr Töne in die Klammern und achte darauf dass es schneller wird.
@@ -162,9 +164,6 @@ Wenn eine Sequenz unabhängig von ihrem Inhalt immer gleich schnell bleiben soll
**Eins pro Cycle per \< \>**
Im letzten Kapitel haben wir schon gelernt dass `< ... >` (angle brackets) nur ein Element pro Cycle spielt.
Das ist für Melodien auch sehr nützlich:
<MiniRepl client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
<Box>
+29 -64
View File
@@ -78,24 +78,13 @@ Strudel kommt von Haus aus mit einer breiten Auswahl an Drum Sounds:
<Box>
Diese Kombinationen von Buchstaben stehen für verschiedene Teile eines Schlagzeugs:
<img src="/img/drumset.png" />
<a class="text-right text-xs" href="https://de.wikipedia.org/wiki/Schlagzeug#/media/Datei:Drum_set.svg" target="_blank">
original von Pbroks13
</a>
Diese 2-Buchstaben Kombinationen stehen für verschiedene Teile eines Schlagzeugs:
- `bd` = **b**ass **d**rum - Basstrommel
- `sd` = **s**nare **d**rum - Schnarrtrommel
- `rim` = **rim**shot - Rahmenschlag
- `hh` = **h**i**h**at - Hallo Hut
- `oh` = **o**pen **h**ihat - Offener Hallo Hut
- `lt` = **l**ow tom
- `mt` = **m**iddle tom
- `ht` = **h**igh tom
- `rd` = **r**i**d**e cymbal
- `rd` = **cr**ash cymbal
Probier verschiedene Sounds aus!
@@ -142,58 +131,34 @@ Versuch noch mehr Sounds hinzuzfügen!
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd")`} punchcard />
Der Inhalt einer Sequence wird in einen sogenannten Cycle (=Zyklus) zusammengequetscht. Ein Cycle ist standardmäßig 2 Sekunden lang.
**Eins pro Cycle mit `< .. >`**
Hier ist die gleiche Sequence, aber dieses mal umgeben von `< .. >` (angle brackets):
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>")`} punchcard />
Jetzt spielt nur ein Sound pro Cycle. Mit diesen Klammern bleibt das Tempo immer gleich, ganz egal wieviele Elemente enhalten sind!
Das ist jetzt aber etwas langsam, machen wir es schneller mit `*`:
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>*8")`} punchcard />
Die `*8` macht die ganze Sequenz 8 mal so schnell.
<Box>
Warte mal, ist das jetzt nicht das gleiche Ergebnis wie ohne `< ... >*8`? Wofür ist das dann gut?
Korrekt, der echte Vorteil dieser Schreibweise zeigt sich wenn du Elemente entfernst oder hinzufügst. Versuch es mal!
Ändere auch mal die Zahl am Ende um das Tempo zu verändern.
</Box>
Der Inhalt einer Sequence wird in einen sogenannten Cycle (=Zyklus) zusammengequetscht.
**Tempo ändern mit `cpm`**
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
<Box>
cpm = **c**ycles per **m**inute = Cycles pro Minute
Das Tempo is standardmäßig is 30 Cycles pro Minute = 120/4 = 1 Cycle alle 2 Sekunden
Das Tempo ist standardmäßig auf 60cpm eingestellt, also 1 Cycle pro Sekunde.
In taditioneller Musik-Terminologie könnte man sagen, das Pattern oben besteht aus 8tel Noten auf 90bpm im 4/4 Takt.
Kein Sorge wenn dir diese Begriffe nichts sagen, das ist nicht notwendig um mit Strudel Musik zu machen.
`cpm` ist angelehnt an `bpm` (=beats per minute).
</Box>
Wir werden später noch mehr Möglichkeiten kennen lernen das Tempo zu verändern.
**Pausen mit '-' oder '~'**
**Pausen mit '~'**
<MiniRepl client:visible tune={`sound("bd hh - rim")`} punchcard />
<MiniRepl client:visible tune={`sound("bd hh ~ rim")`} punchcard />
<Box>
Du siehst wahrscheinlich auch Patterns von anderen Leuten mit '~' als Pausensymbol.
Besonders für deutsche Tastaturen ist `-` eine Alternative zum schwer tippbaren `~`.
Tilde tippen:
- Windows / Linux: `Alt Gr` + `~`
- Mac: `option` + `N`
</Box>
@@ -254,7 +219,7 @@ Du kannst so tief verschachteln wie du willst!
Du kannst so viele Kommas benutzen wie du möchtest:
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, - casio")`} punchcard />
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, ~ casio")`} punchcard />
Kommas können auch in Unter-Sequenzen verwendet werden:
@@ -272,9 +237,9 @@ Es kommt öfter vor, dass man die gleiche Idee auf verschiedene Arten ausdrücke
<MiniRepl
client:visible
tune={`sound(\`bd*2, - cp,
- - - oh, hh*4,
[- casio]*2\`)`}
tune={`sound(\`bd*2, ~ cp,
~ ~ ~ oh, hh*4,
[~ casio]*2\`)`}
punchcard
/>
@@ -304,7 +269,7 @@ Das haben wir bisher gelernt:
| --------------------- | ----------- | --------------------------------------------------------------------- |
| Sequenz | Leerzeichen | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
| Sound Nummer | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Pausen | - | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
| Pausen | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Unter-Sequenzen | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Unter-Unter-Sequenzen | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
| Schneller | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
@@ -328,7 +293,7 @@ Die folgenden Funktionen haben wir bereits gesehen:
**Klassischer House**
<MiniRepl client:visible tune={`sound("bd*2, - cp, [- hh]*2").bank("RolandTR909")`} punchcard />
<MiniRepl client:visible tune={`sound("bd*2, ~ cp, [~ hh]*2").bank("RolandTR909")`} punchcard />
<Box>
@@ -345,7 +310,7 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
.bank("RolandCompurhythm1000")`}
punchcard
/>
@@ -355,10 +320,10 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound(\`
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
\`).cpm(90/4)`}
punchcard
/>
@@ -368,10 +333,10 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound(\`
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
[- - - - ] [cp - - - ] [- - - - ] [- cp - - ],
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
\`).bank("RolandTR808").cpm(88/4)`}
punchcard
/>
@@ -381,9 +346,9 @@ We Will Rock you
<MiniRepl
client:visible
tune={`s(\`jazz*2,
insect [crow metal] - -,
- space:4 - space:1,
- wind\`)
insect [crow metal] ~ ~,
~ space:4 ~ space:1,
~ wind\`)
.cpm(100/2)`}
punchcard
/>
+92
View File
@@ -0,0 +1,92 @@
<body>
<button id="playButton">play</button>
<button id="stopButton">stop</button>
<script>
import { laserclock } from '../laserclock.mjs';
import { evaluate } from '@strudel/transpiler';
import { evalScope } from '@strudel/core';
import { superdough, getAudioContext, registerSynthSounds, initAudioOnFirstClick } from '@strudel/webaudio';
const init = async () => {
initAudioOnFirstClick();
await evalScope(
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
);
};
init().then(async () => {
const ctx = getAudioContext();
const code = `
// "tupper class" @by eddyflux
samples('github:yaxu/clean-breaks')
samples('github:eddyflux/crate')
samples('github:eddyflux/wax')
let cps = 68/60/4;
// setcps(cps) // currently not supported by lasterclock.. still sounds funky :D
stack(
s("loop4/4").fit()
.layer(
x=>x
.chop("64 [32 64]")
.juxBy(.6, rev)
.hpf(500)
.dec(sine.range(.05,.18).slow(8))
.room(.5).sometimes(mul(speed("2 | -2")))
.shape(.6).postgain(sine.range(0,.8).slow(16))
//.hush()
,
x=>x.chop(64).lpf(400)
.shape(.6).cut(4)
.dec(.2).postgain(.9)
.mask("<0!4 1!16>")
)
//.hush()
//.hpf(500)
,
s("riffin").fit().shape(.2).chop(32).dec(.1).postgain(.6)
.sometimesBy("0 [0 .5] 0 0", x=>x.set.squeeze(gain(".9 .6 .2 .1")).room(.3))
.sometimesBy("0 [0 0] 0 .5", x=>x.mul(speed("-2")))
.hurry("<.5!4 1!16>")
//.hush()
,
s("useme/2").fit()
.chop("<16 32>/32")//.rarely(ply("2"))
.sometimesBy("0 [.2 0]",ply("3"))
.dec(.19).room(.4).cut(9)
.hpf(800)
.mask("[1!4 0 1 1 0]*2")
//.hush()
,
s("[bd rim:1 [~ bd] rim:2]*2").bank('crate')
)
.reset("<x@3 x*[4 [8 [16 32]]] x@16>")
.late("[0 .002]*16").fast(cps)
`;
let { pattern } = await evaluate(code);
let last;
let minLatency = 0.1;
const { start, stop } = laserclock({
audioContext: ctx,
onTick: (data) => {
const { currentTime } = data;
if (last) {
const haps = pattern.queryArc(last, currentTime).filter((h) => h.hasOnset());
haps.forEach((hap) => {
const start = hap.whole.begin + minLatency;
superdough(hap.value, `=${start}`, hap.duration);
});
}
last = currentTime;
},
} as any);
registerSynthSounds();
document.getElementById('playButton')?.addEventListener('click', () => start());
document.getElementById('stopButton')?.addEventListener('click', () => stop());
});
</script>
</body>
+132 -112
View File
@@ -24,31 +24,20 @@ Here, we are using the `s` function to play back different default samples (`bd`
For drum sounds, strudel uses the comprehensive [tidal-drum-machines](https://github.com/ritchse/tidal-drum-machines) library, with the following naming convention:
| Drum | Abbreviation |
| -------------------- | ------------ |
| Bass drum, Kick drum | bd |
| Snare drum | sd |
| Rimshot | rim |
| Clap | cp |
| Closed hi-hat | hh |
| Open hi-hat | oh |
| Crash | cr |
| Ride | rd |
| High tom | ht |
| Medium tom | mt |
| Low tom | lt |
<img src="/img/drumset.png" />
<a class="text-right text-xs" href="https://de.wikipedia.org/wiki/Schlagzeug#/media/Datei:Drum_set.svg" target="_blank">
original von Pbroks13
</a>
More percussive sounds:
| Source | Abbreviation |
| Drum | Abbreviation |
| ----------------------------------- | ------------ |
| Bass drum, Kick drum | bd |
| Snare drum | sd |
| Rimshot | rim |
| Clap | cp |
| Closed hi-hat | hh |
| Open hi-hat | oh |
| Crash | cr |
| Ride | rd |
| Shakers (and maracas, cabasas, etc) | sh |
| High tom | ht |
| Medium tom | mt |
| Low tom | lt |
| Cowbell | cb |
| Tambourine | tb |
| Other percussions | perc |
@@ -74,11 +63,11 @@ We _could_ use them like this:
... but thats obviously a bit much to write. Using the `bank` function, we can shorten this to:
<MiniRepl client:idle tune={`s("bd sd,hh*16").bank("RolandTR808")`} />
<MiniRepl client:idle tune={`s("bd sd bd sd,hh*16").bank("RolandTR808")`} />
You could even pattern the bank to switch between different drum machines:
<MiniRepl client:idle tune={`s("bd sd,hh*16").bank("<RolandTR808 RolandTR909>")`} />
<MiniRepl client:idle tune={`s("bd sd bd sd,hh*16").bank("RolandTR808 RolandTR909")`} />
Behind the scenes, `bank` will just prepend the drum machine name to the sample name with `_` to get the full name.
This of course only works because the name after `_` (`bd`, `sd` etc..) is standardized.
@@ -108,128 +97,159 @@ Selecting sounds also works inside the mini notation, using "`:`" like this:
# Loading Custom Samples
You can load a non-standard sample map using the `samples` function.
You can load your own sample map using the `samples` function.
In this example we create a map using sounds from the default sample map:
## Loading samples from file URLs
<MiniRepl
client:idle
tune={`samples({
bd: 'bd/BT0AADA.wav',
sd: 'sd/rytm-01-classic.wav',
hh: 'hh27/000_hh27closedhh.wav',
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
s("bd sd bd sd,hh*16")`}
/>
In this example we assign names `bassdrum`, `hihat` and `snaredrum` to specific audio files on a server:
When you load your own samples, you can choose the names that you will then refer to in your pattern string inside the `s` function.
Compare with this example which uses the same samples, but with different names.
<MiniRepl
client:idle
tune={`samples({
bassdrum: 'bd/BT0AADA.wav',
snaredrum: 'sd/rytm-01-classic.wav',
hihat: 'hh27/000_hh27closedhh.wav',
snaredrum: ['sd/rytm-01-classic.wav', 'sd/rytm-00-hard.wav'],
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
s("bassdrum snaredrum:0 bassdrum snaredrum:1, hihat*16")`}
s("bassdrum snaredrum bassdrum snaredrum, hihat*16")`}
/>
You can freely choose any combination of letters for each sample name. It is even possible to override the default sounds.
The names you pick will be made available in the `s` function.
Make sure that the URL and each sample path form a correct URL!
Here we have changed the "map" to include longer sample names.
In the above example, `bassdrum` will load:
## The `samples` function
```
https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/bd/BT0AADA.wav
|----------------------base path --------------------------------|--sample path-|
```
The `samples` function has two arguments:
Note that we can either load a single file, like for `bassdrum` and `hihat`, or a list of files like for `snaredrum`!
As soon as you run the code, your chosen sample names will be listed in `sounds` -> `user`.
- A [JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) that maps sound names to audio file paths.
- A base URL that comes before each path describing where the sample folder can be found online.
- Make sure your base URL ends with a slash, while your sample paths do **not** begin with one!
## Loading Samples from a strudel.json file
To see how this looks in practice, compare the [DirtSamples GitHub repo](https://github.com/tidalcycles/Dirt-Samples) with the previous sample map example.
The above way to load samples might be tedious to write out / copy paste each time you write a new pattern.
To avoid that, you can simply pass a URL to a `strudel.json` file somewhere on the internet:
Because GitHub is a popular place for uploading open source samples, it has its own shortcut:
<MiniRepl
client:idle
tune={`samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json')
tune={`samples({
bd: 'bd/BT0AADA.wav',
sd: 'sd/rytm-01-classic.wav',
hh: 'hh27/000_hh27closedhh.wav',
}, 'github:tidalcycles/dirt-samples');
s("bd sd bd sd,hh*16")`}
/>
The file is expected to define a sample map using JSON, in the same format as described above.
Additionally, the base path can be defined with the `_base` key.
The last section could be written as:
The format is `github:user/repo/branch/`.
```json
{
"_base": "https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/",
"bassdrum": "bd/BT0AADA.wav",
"snaredrum": "sd/rytm-01-classic.wav",
"hihat": "hh27/000_hh27closedhh.wav"
}
```
## Github Shortcut
Because loading samples from github is common, there is a shortcut:
Let's see another example, this time based on the following GitHub repo: https://github.com/jarmitage/jarmitage.github.io.
We can see there are some guitar samples inside the `/samples` folder, so let's try to load them:
<MiniRepl
client:idle
tune={`samples('github:tidalcycles/dirt-samples')
s("bd sd bd sd,hh*16")`}
tune={`samples({
g0: 'samples/guitar/guitar_0.wav',
g1: 'samples/guitar/guitar_1.wav',
g2: 'samples/guitar/guitar_2.wav',
g3: 'samples/guitar/guitar_3.wav',
g4: 'samples/guitar/guitar_4.wav'
}, 'github:jarmitage/jarmitage.github.io/master/');
s("<g0 g1 g2 g3 g4>/2")`}
/>
The format is `samples('github:<user>/<repo>/<branch>')`. If you omit `branch` (like above), the `main` branch will be used.
It assumes a `strudel.json` file to be present at the root of the repository:
## Multiple Samples per Sound
```
https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json
```
## From Disk via "Import Sounds"
If you don't want to upload your samples to the internet, you can also load them from your local disk.
Go to the `sounds` tab in the REPL and press "import sounds".
Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
Example:
```
└─ samples
├─ swoop
│ ├─ swoopshort.wav
│ ├─ swooplong.wav
│ └─ swooptight.wav
└─ smash
├─ smashhigh.wav
├─ smashlow.wav
└─ smashmiddle.wav
```
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files.
If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)`
The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`.
## From Disk via @strudel/sampler
Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server.
The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
```sh
cd samples
npx @strudel/sampler
```
Then you can load it via:
It is also possible, to declare multiple files for one sound, using the array notation:
<MiniRepl
client:idle
tune={`samples('http://localhost:5432/');
n("<0 1 2>").s("swoop smash")`}
tune={`samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/dirt-samples');
s("bd:0 bd:1,~ <sd:0 sd:1> ~ sd:0,[hh:0 hh:1]*4")`}
/>
The handy thing about `@strudel/sampler` is that it auto-generates the `strudel.json` file based on your folder structure.
You can see what it generated by going to `http://localhost:5432` with your browser.
The `:0` `:1` etc. are the indices of the array.
The sample number can also be set using `n`:
Note: You need [NodeJS](https://nodejs.org/) installed on your system for this to work.
<MiniRepl
client:idle
tune={`samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/dirt-samples');
s("bd bd,~ sd ~ sd,hh*8").n("<0 1>")`}
/>
## Specifying Pitch
In that case, we might load our guitar sample map a different way:
To make sure your samples are in tune when playing them with `note`, you can specify a base pitch like this:
<MiniRepl
client:idle
tune={`samples({
guitar: [
'samples/guitar/guitar_0.wav',
'samples/guitar/guitar_1.wav',
'samples/guitar/guitar_2.wav',
'samples/guitar/guitar_3.wav',
'samples/guitar/guitar_4.wav'
]
}, 'github:jarmitage/jarmitage.github.io/master/');
s("<guitar:0 guitar:1 guitar:2 guitar:3 guitar:4>*2")`}
/>
And as above, we can choose the sample number using `n` for even more flexibility:
<MiniRepl
client:idle
tune={`samples({
guitar: [
'samples/guitar/guitar_0.wav',
'samples/guitar/guitar_1.wav',
'samples/guitar/guitar_2.wav',
'samples/guitar/guitar_3.wav',
'samples/guitar/guitar_4.wav'
]
}, 'github:jarmitage/jarmitage.github.io/master/');
n("<0 1 2 3 4>*2").s("guitar")`}
/>
## Pitched Sounds
For pitched sounds, you can use `note`, just like with synths:
<MiniRepl
client:idle
tune={`samples({
'gtr': 'gtr/0001_cleanC.wav',
}, 'github:tidalcycles/dirt-samples');
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').gain(.5)`}
/>
Here, the guitar samples will overlap, because they always play till the end.
If we want them to behave more like a synth, we can add `clip(1)`:
<MiniRepl
client:idle
tune={`samples({
'gtr': 'gtr/0001_cleanC.wav',
}, 'github:tidalcycles/dirt-samples');
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').clip(1)
.gain(.5)`}
/>
## Base Pitch
If we have 2 samples with different base pitches, we can make them in tune by specifying the pitch like this:
<MiniRepl
client:idle
@@ -241,6 +261,8 @@ note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s("gtr,moog").clip(1)
.gain(.5)`}
/>
If a sample has no pitch set, `c3` is the default.
We can also declare different samples for different regions of the keyboard:
<MiniRepl
@@ -258,8 +280,6 @@ note("g2!2 <bb2 c3>!2, <c4@3 [<eb4 bb3> g4 f4]>")
The sampler will always pick the closest matching sample for the current note!
Note that this notation for pitched sounds also works inside a `strudel.json` file.
## Shabda
If you don't want to select samples by hand, there is also the wonderful tool called [shabda](https://shabda.ndre.gr/).
@@ -106,12 +106,4 @@ Some of these have equivalent operators in the Mini Notation:
<JsDoc client:idle name="ribbon" h={0} />
## swingBy
<JsDoc client:idle name="swingBy" h={0} />
## swing
<JsDoc client:idle name="swing" h={0} />
Apart from modifying time, there are ways to [Control Parameters](/functions/value-modifiers/).
@@ -23,7 +23,7 @@ following example:
```js
'0 1 2'.add('10 20');
('10 [11 21] 22');
('10 [11 21] 20');
```
They are similar to the previous example in that the number `1` is split in two, with its two halves added to `10` and `20` respectively. However, the `11` 'remembers' that it is a fragment of that original `1` event, and so is treated as having a duration of a third of a cycle, despite only being active for a sixth of a cycle. Likewise, the `21` is also a fragment of that original `1` event, but a fragment of its second half. Because the start of its event is missing, it wouldn't actually trigger a sound (unless it underwent further pattern transformations/combinations).
@@ -41,7 +41,7 @@ This makes way for other ways to align the pattern, and several are already defi
- `mix` - structures from both patterns are combined, so that the new events are not fragments but are created at intersections of events from both sides.
- `squeeze` - cycles from the pattern on the right are squeezed into events on the left. So that e.g. `"0 1 2".add.squeeze("10 20")` is equivalent to `"[10 20] [11 21] [12 22]"`.
- `squeezeout` - as with `squeeze`, but cycles from the left are squeezed into events on the right. So, `"0 1 2".add.squeezeout("10 20")` is equivalent to `[10 11 12] [20 21 22]`.
- `reset` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.reset("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'reset' cycles on the left.
- `reset` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'trigger' cycles on the left.
- `restart` is similar to `reset`, but the pattern is 'restarted' from its very first cycle, rather than from the current cycle. `reset` and `restart` therefore only give different results where the leftmost pattern differs from one cycle to the next.
We will save going deeper into the background, design and practicalities of these alignment functions for future publications. However in the next section, we take them as a case study for looking at the different design affordances offered by Haskell to Tidal, and JavaScript to Strudel.
+41 -42
View File
@@ -83,15 +83,15 @@ Let's combine all of the above into a little tune:
<MiniRepl
client:visible
tune={`stack(
stack(
sound("hh*8").gain("[.25 1]*4"),
sound("bd*4,[~ sd:1]*2")
),
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth").lpf("200 1000 200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
.sound("sawtooth").vowel("<a e i o>")
)`}
stack(
sound("hh*8").gain("[.25 1]*4"),
sound("bd*4,[~ sd:1]*2")
),
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth").lpf("200 1000 200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
.sound("sawtooth").vowel("<a e i o>")
) `}
/>
<Box>
@@ -152,9 +152,9 @@ Can you guess what they do?
<MiniRepl
client:visible
tune={`stack(
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted"),
sound("<bd rim>").bank("RolandTR707")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted"),
sound("<bd rim>").bank("RolandTR707")
).delay(".5")`}
/>
@@ -168,7 +168,7 @@ What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third num
</Box>
<QA q="Click to see solution" client:visible>
<QA q="Lösung anzeigen" client:visible>
`delay("a:b:c")`:
@@ -200,12 +200,12 @@ Add a delay too!
<MiniRepl
client:visible
tune={`stack(
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
room(2).gain(.5)
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.5)
)`}
/>
@@ -214,15 +214,15 @@ Let's add a bass to make this complete:
<MiniRepl
client:visible
tune={`stack(
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.4),
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
.scale("D2:minor")
.sound("sawtooth,triangle").lpf(800)
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5),
n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.4),
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
.scale("D2:minor")
.sound("sawtooth,triangle").lpf(800)
)`}
/>
@@ -237,7 +237,7 @@ Try adding `.hush()` at the end of one of the patterns in the stack...
<MiniRepl
client:visible
tune={`sound("numbers:1 numbers:2 numbers:3 numbers:4")
.pan("0 0.3 .6 1")`}
.pan("0 0.3 .6 1")`}
/>
**speed**
@@ -295,8 +295,8 @@ We can change the automation speed with slow / fast:
<MiniRepl
client:visible
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
.sound("sawtooth")
.lpf(sine.range(100, 2000).slow(4))`}
.sound("sawtooth")
.lpf(sine.range(100, 2000).slow(4))`}
/>
<Box>
@@ -307,16 +307,15 @@ The whole automation will now take 8 cycles to repeat.
## Recap
| name | example |
| ------- | ---------------------------------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| signals | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain (saw)`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
| name | example |
| ----- | ---------------------------------------------------------------------------------------- |
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
+8 -16
View File
@@ -147,43 +147,35 @@ We will see more ways to combine patterns later..
**Divide sequences with `/` to slow them down**
{/* [c2 bb1 f2 eb2] */}
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
<Box>
The `/4` plays the sequence in brackets over 4 cycles (=8s).
The `/4` plays the sequence in brackets over 4 cycles (=4s).
So each of the 4 notes is 2s long.
So each of the 4 notes is 1s long.
Try adding more notes inside the brackets and notice how it gets faster.
</Box>
**Play one per cycle with `< ... >`**
Because it is so common to just play one thing per cycle, you can..
In the last section, we learned that `< ... >` (angle brackets) can be used to play only one thing per cycle,
which is useful for longer melodies too:
**Play one per cycle with \< \>**
<MiniRepl client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
<Box>
Try adding more notes inside the brackets and notice how the tempo stays the same.
The angle brackets are actually just a shortcut:
`<a b c>` = `[a b c]/3`
`<a b c d>` = `[a b c d]/4`
...
Try adding more notes inside the brackets and notice how it does **not** get faster.
</Box>
**Play one sequence per cycle**
We can combine the 2 types of brackets in all sorts of different ways.
Here is an example of a repetitive bassline:
{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4> */}
<MiniRepl
client:visible
+32 -69
View File
@@ -78,22 +78,11 @@ By default, Strudel comes with a wide selection of drum sounds:
These letter combinations stand for different parts of a drum set:
<img src="/img/drumset.png" />
<a class="text-right text-xs" href="https://de.wikipedia.org/wiki/Schlagzeug#/media/Datei:Drum_set.svg" target="_blank">
original image by Pbroks13
</a>
- `bd` = **b**ass **d**rum
- `sd` = **s**nare **d**rum
- `rim` = **rim**shot
- `hh` = **h**i**h**at
- `oh` = **o**pen **h**ihat
- `lt` = **l**ow tom
- `mt` = **m**iddle tom
- `ht` = **h**igh tom
- `rd` = **r**i**d**e cymbal
- `cr` = **cr**ash cymbal
Try out different drum sounds!
@@ -140,54 +129,29 @@ Try adding more sounds to the sequence!
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd")`} punchcard />
The content of a sequence will be squished into what's called a cycle. A cycle is 2s long by default.
The content of a sequence will be squished into what's called a cycle.
**One per cycle with `< .. >`**
**One way to change the tempo is using `cpm`**
Here is the same sequence, but this time sourrounded with `< .. >` (angle brackets):
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>")`} punchcard />
This will play only one sound per cycle. With these brackets, the tempo doesn't change when we add or remove elements!
Because this is now very slow, we can speed it up again like this:
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>*8")`} punchcard />
Here, the `*8` means we make the whole thing 8 times faster.
<Box>
Wait a minute, isn't this the same as without `< ... >*8`? Why do we need it then?
That's true, the special thing about this notation is that the tempo won't change when you add or remove elements, try it!
Try also changing the number at the end to change the tempo!
</Box>
**changing the tempo with cpm**
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
<Box>
cpm = cycles per minute
By default, the tempo is 30 cycles per minute = 120/4 = 1 cycle every 2 seconds
In western music terms, you could say the above are 8ths notes at 90bpm in 4/4 time.
But don't worry if you don't know these terms, as they are not required to make music with Strudel.
By default, the tempo is 30 cycles per minute = 1 half cycle per second.
</Box>
**Add a rests in a sequence with '-' or '~'**
We will look at other ways to change the tempo later!
<MiniRepl client:visible tune={`sound("bd hh - rim - bd hh rim")`} punchcard />
**Add a rests in a sequence with '~'**
<MiniRepl client:visible tune={`sound("bd hh ~ rim ~ bd hh rim")`} punchcard />
**Sub-Sequences with [brackets]**
<MiniRepl client:visible tune={`sound("bd [hh hh] sd [hh bd] bd - [hh sd] cp")`} punchcard />
<MiniRepl client:visible tune={`sound("bd [hh hh] sd [hh bd] bd ~ [hh sd] cp")`} punchcard />
<Box>
@@ -199,7 +163,7 @@ Similar to the whole sequence, the content of a sub-sequence will be squished to
**Multiplication: Speed things up**
<MiniRepl client:visible tune={`sound("bd hh*2 rim hh*3 bd [- hh*2] rim hh*2")`} punchcard />
<MiniRepl client:visible tune={`sound("bd hh*2 rim hh*3 bd [~ hh*2] rim hh*2")`} punchcard />
**Multiplication: Speed up subsequences**
@@ -231,7 +195,7 @@ You can go as deep as you want!
You can use as many commas as you want:
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, - casio")`} punchcard />
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, ~ casio")`} punchcard />
Commas can also be used inside sub-sequences:
@@ -249,9 +213,9 @@ It is quite common that there are many ways to express the same idea.
<MiniRepl
client:visible
tune={`sound(\`bd*2, - cp,
- - - oh, hh*4,
[- casio]*2\`)`}
tune={`sound(\`bd*2, ~ cp,
~ ~ ~ oh, hh*4,
[~ casio]*2\`)`}
punchcard
/>
@@ -268,14 +232,13 @@ This is shorter and more readable than:
## Recap
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
This is what we've learned so far:
This is what we've leared so far:
| Concept | Syntax | Example |
| ----------------- | -------- | ----------------------------------------------------------------------- |
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
| Rests | - or ~ | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`sound("<bd hh rim oh bd rim>")`} /> |
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> |
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
@@ -285,9 +248,9 @@ The Mini-Notation is usually used inside some function. These are the functions
| Name | Description | Example |
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd")`} /> |
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").bank("RolandTR909")`} /> |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").cpm(45)`} /> |
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd")`} /> |
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").bank("RolandTR909")`} /> |
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> |
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2 0 6 3 2").sound("jazz")`} /> |
## Examples
@@ -303,7 +266,7 @@ The Mini-Notation is usually used inside some function. These are the functions
**Classic house**
<MiniRepl client:visible tune={`sound("bd*4, [- cp]*2, [- hh]*4").bank("RolandTR909")`} punchcard />
<MiniRepl client:visible tune={`sound("bd*4, [~ cp]*2, [~ hh]*4").bank("RolandTR909")`} punchcard />
<Box>
@@ -320,7 +283,7 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
.bank("RolandCompurhythm1000").cpm(120/2)`}
punchcard
/>
@@ -330,10 +293,10 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound(\`
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
\`).cpm(90/4)`}
punchcard
/>
@@ -343,10 +306,10 @@ We Will Rock you
<MiniRepl
client:visible
tune={`sound(\`
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
[- - - - ] [cp - - - ] [- - - - ] [~ cp - - ],
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
\`).bank("RolandTR808").cpm(88/4)`}
punchcard
/>
@@ -356,9 +319,9 @@ We Will Rock you
<MiniRepl
client:visible
tune={`s(\`jazz*2,
insect [crow metal] - -,
- space:4 - space:1,
- wind\`)
insect [crow metal] ~ ~,
~ space:4 ~ space:1,
~ wind\`)
.cpm(100/2)`}
punchcard
/>
@@ -155,13 +155,12 @@ In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying.
</Box>
off is also useful for modifying other sounds, and can even be nested:
off is also useful for sounds:
<MiniRepl
client:visible
tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1")
.off(2/16, x=>x.speed(1.5).gain(.25)
.off(3/16, y=>y.vowel("<a e i o>*8")))`}
.off(1/16, x=>x.speed(1.5).gain(.25))`}
/>
| name | description | example |
+1 -6
View File
@@ -36,12 +36,7 @@ export function WelcomeTab({ context }) {
<a href="https://tidalcycles.org/" target="_blank">
tidalcycles
</a>
, which is a popular live coding language for music, written in Haskell. Strudel is free/open source software:
you can redistribute and/or modify it under the terms of the{' '}
<a href="https://github.com/tidalcycles/strudel/blob/main/LICENSE" target="_blank">
GNU Affero General Public License
</a>
. You can find the source code at{' '}
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
<a href="https://github.com/tidalcycles/strudel" target="_blank">
github
</a>
+2 -2
View File
@@ -26,13 +26,13 @@ export async function initCode() {
// load code from url hash (either short hash from database or decode long hash)
try {
const initialUrl = window.location.href;
const hash = initialUrl.split('?')[1]?.split('#')?.[0]?.split('&')[0];
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
const codeParam = window.location.href.split('#')[1] || '';
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
if (codeParam) {
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
return hash2code(codeParam);
} else if (hash) {
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
return supabase
.from('code_v1')
.select('code')