Compare commits

...

53 Commits

Author SHA1 Message Date
Felix Roos 35b9c82bdd add node-web-audio-api variant 2024-05-03 13:05:02 +02:00
Felix Roos 0fcfc308fd make computer less angry 2024-05-03 10:34:35 +02:00
Felix Roos 931016143c node + superdirt experiment 2024-05-03 10:21:56 +02:00
Felix Roos 8e2d883e86 Merge pull request #1086 from tidalcycles/fix-dough-csound-timing
fix: csound + dough timing
2024-05-03 08:46:51 +02:00
Felix Roos 8db443ba26 fix: csound + dough timing 2024-05-03 00:42:13 +02:00
Alex McLean 34e9d73908 Improve performance of ! (replicate) (#1084)
* Implement ! with weighted repeatCycles
* reimplement repeatCycles without array filling
2024-05-02 23:37:32 +01:00
Felix Roos deed379dba Merge pull request #1083 from tidalcycles/fix-urls-with-extras
fix: url parsing with extra params
2024-05-02 23:40:22 +02:00
Felix Roos 5b4322c964 fix: url parsing with extra params 2024-05-02 22:39:39 +02:00
Alex McLean 91bb0a1d36 Add calculateTactus() for turning off calculation of tactus in (#1081)
core functions.
Also while I'm here stop fast/slow from manipulating tactus.
2024-04-29 11:36:10 +01:00
Jade (Rose) Rowland 47b65846e9 Merge pull request #1076 from diegodorado/patch-1
fix docs on alignment.mdx
2024-04-28 14:56:21 -04:00
Jade (Rose) Rowland 55eafaf4d8 Merge pull request #1073 from cleary/patch-5
add signals to recap in first-effects.mdx
2024-04-28 14:49:17 -04:00
Jade (Rose) Rowland 2e3869c0e0 Merge pull request #1072 from cleary/patch-4
fix translation issue in first-effects.mdx
2024-04-28 14:44:56 -04:00
Jade (Rose) Rowland 4015a11bd2 Merge pull request #1077 from daslyfe/fix_formatting_test
fix failing format test
2024-04-28 14:44:29 -04:00
Jade (Rose) Rowland 15f16006d0 ran prettier again? 2024-04-28 14:43:35 -04:00
Jade (Rose) Rowland ce79a44848 fix failing format test 2024-04-28 14:36:52 -04:00
Diego Dorado d253e35b6c fix docs on alignment.mdx 2024-04-28 10:53:53 -03:00
Jade (Rose) Rowland 19ef941c9d Merge pull request #1075 from cleary/patch-7
add nesting to `off` example variation in pattern-effects.mdx
2024-04-27 16:50:17 -04:00
Jade (Rose) Rowland ac7658d0a0 Merge pull request #1070 from cleary/patch-3
add `<...>` to first-sounds.mdx recap
2024-04-27 16:48:58 -04:00
Jade (Rose) Rowland 635a6c62b0 Merge pull request #1069 from cleary/patch-2
fix first sounds typo
2024-04-27 16:48:07 -04:00
Jade (Rose) Rowland acc100da5c Merge pull request #1068 from cleary/patch-1
fix cr typo on first-sounds.mdx
2024-04-27 16:47:44 -04:00
Bernard Gray 69f8498d4b Use "Alternate" for <...>, consistent with recap page in first-sounds.mdx 2024-04-27 14:20:26 +10:00
Bernard Gray 975eb684b9 add nesting to off example variation in pattern-effects.mdx
I was immediately curious whether `off` could be nested - it turns out it can, so I added an example of it in action.
This example should also add clarity to the flexibility of pattern naming inside `off` arguments as well
2024-04-27 11:35:08 +10:00
Bernard Gray 5c4ad57309 add signals to recap in first-effects.mdx 2024-04-27 10:47:30 +10:00
Bernard Gray 0be5f77247 fix translation issue in first-effects.mdx 2024-04-27 10:15:55 +10:00
Alex McLean bbf8577f85 More tactus tidying (#1071)
* preserve tactus for register (only non-formatting change is adding args to register)
* support undefined tactus values
2024-04-26 14:12:30 +01:00
Bernard Gray d8eb186ee4 add <...> to recap 2024-04-25 06:35:34 +10:00
Bernard Gray 54829bd28c fix first sounds typo 2024-04-25 06:24:56 +10:00
Bernard Gray 5c01760b28 fix cr typo on first-sounds.mdx 2024-04-25 06:01:49 +10:00
Alex McLean a189626e8b Fix stepjoin (#1067)
* sort fractions properly - fixes #1066
2024-04-23 22:37:21 +01:00
Alex McLean 6a1fa7e67f clarify license (#1064) 2024-04-23 22:32:00 +01:00
Alex McLean 267f58b7be Tactus tweaks - fixes for maintaining tactus and highlight locations (#1065)
* outer bind/join should keep outer tactus
* keep locations of outer pattern in stepJoin
2024-04-23 14:14:30 +01:00
Jade (Rose) Rowland 9348a8015a Merge pull request #1062 from daslyfe/osc_time_fix
fix OSC timing for recent scheduler updates
2024-04-21 17:57:57 -04:00
Jade (Rose) Rowland 57ad278137 fixed osc server 2024-04-21 17:52:22 -04:00
Jade (Rose) Rowland 5d8eea7299 fixed 2024-04-21 17:29:29 -04:00
Alex McLean 0a3694fb82 Stepwise functions from Tidal (#1060)
* rename new stepwise functions to match tidal, adding s_expand and s_contract
* created a `stepJoin` for stepwise patternification
2024-04-21 21:17:07 +01:00
Alexandre G.-Raymond a194180130 Fix wchooseCycles not picking the whole pattern (#1061) 2024-04-21 10:03:55 +01:00
Felix Roos 64cc8caeec Merge pull request #1038 from tidalcycles/swing
add swing + swingBy
2024-04-19 00:05:52 +02:00
Felix Roos f427b56062 Merge pull request #1059 from tidalcycles/dollar-colon
anonymous patterns + muting
2024-04-19 00:05:08 +02:00
Felix Roos 93d151b943 anonymous patterns + muting with _ 2024-04-17 12:37:19 +02:00
Felix Roos e0c4997f1e Merge pull request #1053 from tidalcycles/workshop-edits
improve tutorial + custom samples doc
2024-04-12 12:34:27 +02:00
Felix Roos 07791001dd Merge branch 'main' into workshop-edits 2024-04-12 12:32:14 +02:00
Felix Roos 936d6fdafb Merge pull request #1054 from tidalcycles/midin-fix
fix: do not reset cc input values on each eval
2024-04-12 12:31:49 +02:00
Felix Roos 81b33afcdb fix: do not reset cc input values on each eval 2024-04-12 12:30:13 +02:00
Felix Roos 0190604a7f fix: don't reset on each eval 2024-04-12 12:28:03 +02:00
Felix Roos 8494353ace bump @strudel/sampler 2024-04-12 10:57:59 +02:00
Felix Roos 971d497e81 docs: custom samples
- strudel.json
- github shortcut
- "import sounds"
- @strudel/sampler
- improve structure
- add drumset
2024-04-12 10:56:31 +02:00
Felix Roos ca36a31f79 slightly reword 2024-04-11 12:55:02 +02:00
Felix Roos 559f31792f update german tutorial 2024-04-11 11:58:38 +02:00
Felix Roos 83eedcb2f5 bring back division 2024-04-11 11:40:18 +02:00
Felix Roos a3bd04732c tutorial tweaks:
- add drumset image
- introduce angle brackets earlier
2024-04-11 11:26:46 +02:00
Felix Roos 97d292adbb add swing / swingBy to time modifiers page 2024-04-03 21:32:39 +02:00
Felix Roos 16851cf8f8 snap 2024-04-01 03:01:04 +02:00
Felix Roos 8f06bc4b57 add swing + swingBy 2024-04-01 02:59:17 +02:00
40 changed files with 1554 additions and 574 deletions
+2 -1
View File
@@ -21,4 +21,5 @@ vite.config.js
/src-tauri/target/**/*
reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
jsdoc-synonyms.js
packages/node/pattern.mjs
+1
View File
@@ -11,3 +11,4 @@ pnpm-lock.yaml
pnpm-workspace.yaml
**/dev-dist
website/.astro
packages/node/pattern.mjs
+35 -1
View File
@@ -6,6 +6,7 @@ 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 () {
@@ -47,6 +48,10 @@ 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;
};
@@ -60,6 +65,22 @@ 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;
@@ -85,11 +106,24 @@ 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) => {
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce(
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
fraction(1),
);
};
fraction._original = Fraction;
+267 -88
View File
@@ -10,12 +10,29 @@ import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
import { compose, removeUndefineds, flatten, id, listRange, curry, _mod, numeralArgs, parseNumeral } from './util.mjs';
import {
uniqsortr,
removeUndefineds,
flatten,
id,
listRange,
curry,
_mod,
numeralArgs,
parseNumeral,
pairs,
} 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
@@ -31,16 +48,16 @@ export class Pattern {
*/
constructor(query, tactus = undefined) {
this.query = query;
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
this._Pattern = true; // this property is used to detectinstance of another Pattern
this.tactus = tactus; // in terms of number of steps per cycle
}
get tactus() {
return this.__tactus ?? Fraction(1);
return this.__tactus;
}
set tactus(tactus) {
this.__tactus = Fraction(tactus);
this.__tactus = tactus === undefined ? undefined : Fraction(tactus);
}
setTactus(tactus) {
@@ -48,6 +65,17 @@ 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
@@ -130,7 +158,9 @@ export class Pattern {
return span_a.intersection_e(span_b);
};
const result = pat_func.appWhole(whole_func, pat_val);
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
if (__tactus) {
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
}
return result;
}
@@ -242,12 +272,12 @@ export class Pattern {
}
outerBind(func) {
return this.bindWhole((a) => a, func);
return this.bindWhole((a) => a, func).setTactus(this.tactus);
}
outerJoin() {
// Flattens a pattern of patterns into a pattern, where wholes are
// taken from inner haps.
// taken from outer haps.
return this.outerBind(id);
}
@@ -1137,24 +1167,24 @@ function _composeOp(a, b, func) {
export const polyrhythm = stack;
export const pr = stack;
export const pm = polymeter;
export const pm = s_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
@@ -1166,7 +1196,7 @@ Pattern.prototype.factories = {
* @example
* gap(3) // "~@3"
*/
export const gap = (tactus) => new Pattern(() => [], Fraction(tactus));
export const gap = (tactus) => new Pattern(() => [], tactus);
/**
* Does absolutely nothing..
@@ -1235,7 +1265,9 @@ 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);
result.tactus = lcm(...pats.map((pat) => pat.tactus));
if (__tactus) {
result.tactus = lcm(...pats.map((pat) => pat.tactus));
}
return result;
}
@@ -1248,20 +1280,20 @@ function _stackWith(func, pats) {
return pats[0];
}
const [left, ...right] = pats.map((pat) => pat.tactus);
const tactus = left.maximum(...right);
const tactus = __tactus ? left.maximum(...right) : undefined;
return stack(...func(tactus, pats));
}
export function stackLeft(...pats) {
return _stackWith(
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(pat, gap(tactus.sub(pat.tactus))))),
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(pat, gap(tactus.sub(pat.tactus))))),
pats,
);
}
export function stackRight(...pats) {
return _stackWith(
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(gap(tactus.sub(pat.tactus)), pat))),
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(gap(tactus.sub(pat.tactus)), pat))),
pats,
);
}
@@ -1274,7 +1306,7 @@ export function stackCentre(...pats) {
return pat;
}
const g = gap(tactus.sub(pat.tactus).div(2));
return timecat(g, pat, g);
return s_cat(g, pat, g);
}),
pats,
);
@@ -1288,7 +1320,7 @@ export function stackBy(by, ...pats) {
left: stackLeft,
right: stackRight,
expand: stack,
repeat: (...args) => polymeterSteps(tactus, ...args),
repeat: (...args) => s_polymeterSteps(tactus, ...args),
};
return by
.inhabit(lookup)
@@ -1328,7 +1360,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 = lcm(...pats.map((x) => x.tactus));
const tactus = __tactus ? lcm(...pats.map((x) => x.tactus)) : undefined;
return new Pattern(query).splitQueries().setTactus(tactus);
}
@@ -1374,7 +1406,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 timecat(...sections).slow(total);
return s_cat(...sections).slow(total);
}
export function fastcat(...pats) {
@@ -1454,7 +1486,7 @@ export const func = curry((a, b) => reify(b).func(a));
* @noAutocomplete
*
*/
export function register(name, func, patternify = true, preserveTactus = false) {
export function register(name, func, patternify = true, preserveTactus = false, join = (x) => x.innerJoin()) {
if (Array.isArray(name)) {
const result = {};
for (const name_item of name) {
@@ -1491,7 +1523,7 @@ export function register(name, func, patternify = true, preserveTactus = false)
return func(...args, pat);
};
mapFn = curry(mapFn, null, arity - 1);
result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
result = join(right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)));
}
}
if (preserveTactus) {
@@ -1535,6 +1567,11 @@ 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
@@ -1742,7 +1779,9 @@ 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();
result.tactus = pat.tactus.mul(factor);
if (__tactus) {
result.tactus = Fraction(factor).mulmaybe(pat.tactus);
}
return result;
});
@@ -1757,16 +1796,19 @@ 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));
const result = fastQuery.withHapTime((t) => t.div(factor));
result.tactus = factor.mul(pat.tactus);
return result;
});
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,
);
/**
* Both speeds up the pattern (like 'fast') and the sample playback (like 'speed').
@@ -1934,7 +1976,7 @@ export const zoom = register('zoom', function (s, e, pat) {
return nothing;
}
const d = e.sub(s);
const tactus = pat.tactus.mul(d);
const tactus = __tactus ? pat.tactus.mulmaybe(d) : undefined;
return pat
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
@@ -1976,6 +2018,24 @@ 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
@@ -2131,7 +2191,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(lcm(left.tactus, right.tactus));
return stack(left, right).setTactus(__tactus ? lcm(left.tactus, right.tactus) : undefined);
});
/**
@@ -2250,7 +2310,13 @@ export const { iterBack, iterback } = register(
export const { repeatCycles } = register(
'repeatCycles',
function (n, pat) {
return slowcat(...Array(n).fill(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();
},
true,
true,
@@ -2357,14 +2423,69 @@ 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 metrical 'tactus'.
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given number of steps per cycle (aka tactus).
* @example
* s("bd sd cp").toTactus(4)
* s("bd sd cp").steps(4)
* // The same as s("{bd sd cp}%4")
*/
export const toTactus = register('toTactus', function (targetTactus, pat) {
if (pat.tactus.eq(0)) {
export const steps = register('steps', function (targetTactus, pat) {
if (pat.tactus === undefined) {
return pat;
}
if (pat.tactus.eq(Fraction(0))) {
// avoid divide by zero..
return nothing;
}
@@ -2397,14 +2518,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 polymeterSteps
* @name s_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"
* polymeterSteps(4, "c d", "e f g")
* s_polymeterSteps(4, "c d", "e f g")
*/
export function polymeterSteps(steps, ...args) {
export function s_polymeterSteps(steps, ...args) {
if (args.length == 0) {
return silence;
}
@@ -2413,7 +2534,7 @@ export function polymeterSteps(steps, ...args) {
return _polymeterListSteps(steps, ...args);
}
return polymeter(...args).toTactus(steps);
return s_polymeter(...args).steps(steps);
}
/**
@@ -2421,19 +2542,25 @@ export function polymeterSteps(steps, ...args) {
* @synonyms pm
* @example
* // The same as "{c eb g, c2 g2}"
* polymeter("c eb g", "c2 g2")
* s_polymeter("c eb g", "c2 g2")
*
*/
export function polymeter(...args) {
export function s_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))));
@@ -2443,18 +2570,36 @@ export function 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.
* the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`.
* @return {Pattern}
* @example
* timecat([3,"e3"],[1, "g3"]).note()
* s_cat([3,"e3"],[1, "g3"]).note()
* // the same as "e3@3 g3".note()
* @example
* timecat("bd sd cp","hh hh").sound()
* s_cat("bd sd cp","hh hh").sound()
* // the same as "bd sd cp hh hh".sound()
*/
export function timecat(...timepats) {
export function s_cat(...timepats) {
if (timepats.length === 0) {
return nothing;
}
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];
@@ -2477,18 +2622,19 @@ export function timecat(...timepats) {
return result;
}
/** Deprecated alias for `timecat` */
export const timeCat = timecat;
/** Aliases for `s_cat` */
export const timecat = s_cat;
export const timeCat = s_cat;
/**
* *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'.
* Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
* Similar to `s_cat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
*
* @return {Pattern}
* @example
* stepcat(["bd cp", "mt"], "bd").sound()
* s_alt(["bd cp", "mt"], "bd").sound()
*/
export function stepcat(...groups) {
export function s_alt(...groups) {
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
@@ -2497,9 +2643,9 @@ export function stepcat(...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.tactus > 0);
result = result.filter((x) => x.hasTactus && x.tactus > 0);
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
result = timecat(...result);
result = s_cat(...result);
result.tactus = tactus;
return result;
}
@@ -2507,7 +2653,10 @@ export function stepcat(...groups) {
/**
* *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'.
*/
export const stepwax = register('stepwax', function (i, pat) {
export const s_add = stepRegister('s_add', function (i, pat) {
if (!pat.hasTactus) {
return nothing;
}
if (pat.tactus.lte(0)) {
return nothing;
}
@@ -2535,23 +2684,40 @@ export const stepwax = register('stepwax', function (i, pat) {
/**
* *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'.
*/
export const stepwane = register('stepwane', function (i, pat) {
export const s_sub = stepRegister('s_sub', function (i, pat) {
if (!pat.hasTactus) {
return nothing;
}
i = Fraction(i);
if (i.lt(0)) {
return pat.stepwax(Fraction(0).sub(pat.tactus.add(i)));
return pat.s_add(Fraction(0).sub(pat.tactus.add(i)));
}
return pat.stepwax(pat.tactus.sub(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)));
});
/**
* *EXPERIMENTAL*
*/
Pattern.prototype.taperlist = function (amount, times) {
Pattern.prototype.s_taperlist = function (amount, times) {
const pat = this;
if (!pat.hasTactus) {
return [pat];
}
times = times - 1;
if (times === 0) {
return pat;
return [pat];
}
const list = [];
@@ -2568,23 +2734,33 @@ Pattern.prototype.taperlist = function (amount, times) {
}
return list;
};
export const taperlist = (amount, times, pat) => pat.taperlist(amount, times);
export const s_taperlist = (amount, times, pat) => pat.s_taperlist(amount, times);
/**
* *EXPERIMENTAL*
*/
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;
});
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(),
);
/**
* *EXPERIMENTAL*
*/
Pattern.prototype.steptour = function (...many) {
return stepcat(
Pattern.prototype.s_tour = function (...many) {
return s_cat(
...[].concat(
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
this,
@@ -2593,8 +2769,8 @@ Pattern.prototype.steptour = function (...many) {
);
};
export const steptour = function (pat, ...many) {
return pat.steptour(...many);
export const s_tour = function (pat, ...many) {
return pat.s_tour(...many);
};
//////////////////////////////////////////////////////////////////////
@@ -2621,7 +2797,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(pat.tactus.mul(n));
return pat.squeezeBind(func).setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
});
/**
@@ -2636,7 +2812,10 @@ 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);
return pat
.set(slicePat)
._fast(n)
.setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
});
/**
+11
View File
@@ -54,10 +54,12 @@ 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;
};
@@ -82,6 +84,15 @@ 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;
};
+3 -1
View File
@@ -486,8 +486,10 @@ 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, ...pairs).innerJoin();
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
export const wrandcat = wchooseCycles;
+69 -22
View File
@@ -21,8 +21,8 @@ import {
cat,
sequence,
palindrome,
polymeter,
polymeterSteps,
s_polymeter,
s_polymeterSteps,
polyrhythm,
silence,
fast,
@@ -47,6 +47,11 @@ import {
time,
run,
pick,
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -603,18 +608,18 @@ describe('Pattern', () => {
);
});
});
describe('polymeter()', () => {
describe('s_polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
expect(s_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(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
});
@@ -1123,8 +1128,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(16));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16));
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).rev().tactus).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
@@ -1136,30 +1141,72 @@ 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('steptaper', () => {
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', () => {
it('can taper', () => {
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)));
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)));
});
it('can taper backwards', () => {
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)),
);
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)));
});
});
describe('wax and wane, left', () => {
it('can wax from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
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)));
});
it('can wane to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
it('can sub to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
});
it('can wax from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
it('can add from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
});
it('can wane to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 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'))));
}
});
});
});
+37
View File
@@ -61,6 +61,11 @@ 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
@@ -231,6 +236,14 @@ 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 */
@@ -289,6 +302,30 @@ 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) {
+4 -2
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, hap) => {
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@@ -38,9 +38,11 @@ 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
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
timeOffset, // 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, noteToMidi } from '@strudel/core';
import { Pattern, getEventOffsetMs, 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 latency = 0.034;
const offset = (targetTime - currentTime + latency) * 1000;
const latencyMs = 34;
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs;
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 } from '@strudel/core';
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
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() + (time - currentTime) * 1000);
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime));
Object.keys(controls).forEach((key) => {
const val = controls[key];
+4 -5
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, isPattern, logger, ref } from '@strudel/core';
import { Pattern, getEventOffsetMs, 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,10 +120,9 @@ 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 latency = 0.034;
const latencyMs = 34;
// 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 = `+${(targetTime - currentTime + latency) * 1000}`;
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// destructure value
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
@@ -177,7 +176,7 @@ export async function midin(input) {
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial || WebMidi.enabled) {
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
+9 -1
View File
@@ -295,7 +295,15 @@ 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 => x.options_['reps'] = (x.options_['reps'] ?? 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$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' }}) };
+8 -1
View File
@@ -135,7 +135,14 @@ op_weight = ws ("@" / "_") a:number?
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
op_replicate = ws "!" a:number?
{ return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }
{ 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;
}
}
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 }}) }
+7 -14
View File
@@ -27,6 +27,12 @@ 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));
@@ -67,26 +73,13 @@ 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
@@ -0,0 +1,19 @@
# @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
@@ -0,0 +1,64 @@
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
@@ -0,0 +1,76 @@
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
@@ -0,0 +1,37 @@
{
"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
@@ -0,0 +1,2 @@
freq("110 220 [330 440,550,660]")
.attack(.1).gain(.4)
+4 -3
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 } from '@strudel/core';
import { logger, parseNumeral, Pattern, getEventOffsetMs } 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) => {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
hap.ensureObjectValue();
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
@@ -56,7 +56,8 @@ 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 = (time - currentTime) * 1000;
const offset = getEventOffsetMs(targetTime, currentTime);
// 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/>.
*/
const OSC = require('osc-js');
import OSC from '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.8",
"version": "0.0.9",
"description": "",
"keywords": [
"tidalcycles",
+2 -2
View File
@@ -74,6 +74,6 @@ export const dough = async (code) => {
worklet.node.connect(ac.destination);
};
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
}
+60 -55
View File
@@ -188,61 +188,66 @@ 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
}
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;
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
}
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;
if (isNote(step)) {
// legacy..
return pure(step);
}
}
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
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;
}
}
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 }))
);
});
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
);
+62
View File
@@ -302,6 +302,33 @@ 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':
@@ -3467,6 +3494,22 @@ 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'}
@@ -10717,6 +10760,20 @@ 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}
@@ -14193,6 +14250,11 @@ 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'}
+122 -114
View File
@@ -1,22 +1,22 @@
use rosc::{ encoder, OscTime };
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
use rosc::{encoder, OscTime};
use rosc::{OscBundle, OscMessage, OscPacket, OscType};
use std::net::UdpSocket;
use std::time::Duration;
use std::sync::Arc;
use tokio::sync::{ mpsc, Mutex };
use serde::Deserialize;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
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,133 +25,141 @@ 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()));
/* ...........................................................
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 {
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()));
/* ...........................................................
Open OSC Ports
Listen For incoming messages and add to queue
............................................................*/
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()
);
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);
}
}
}
return false;
});
});
sleep(Duration::from_millis(1));
}
});
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));
}
});
}
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 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 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())
async_proc_input_tx
.send(messages_to_process)
.await
.map_err(|e| e.to_string())
}
+312 -3
View File
@@ -5972,6 +5972,135 @@ 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 ]",
@@ -7112,6 +7241,27 @@ 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 ]",
@@ -7298,6 +7448,112 @@ 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 ]",
@@ -7879,12 +8135,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:bd ]",
"[ 3/4 → 7/8 | s:sd ]",
"[ 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:bd ]",
"[ 11/8 → 3/2 | s:sd ]",
"[ 3/2 → 13/8 | s:bd ]",
"[ 13/8 → 7/4 | s:bd ]",
"[ 7/4 → 15/8 | s:bd ]",
@@ -7895,7 +8151,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:bd ]",
"[ 11/4 → 23/8 | s:hh ]",
"[ 23/8 → 3/1 | s:bd ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 25/8 → 13/4 | s:bd ]",
@@ -7908,6 +8164,59 @@ 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.

After

Width:  |  Height:  |  Size: 338 KiB

@@ -146,15 +146,13 @@ 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 = 4s.
Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 8s.
Jede Note ist nun also 1s lang.
Jede Note ist nun also 2s lang.
Schreib noch mehr Töne in die Klammern und achte darauf dass es schneller wird.
@@ -164,6 +162,9 @@ 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>
+64 -29
View File
@@ -78,13 +78,24 @@ Strudel kommt von Haus aus mit einer breiten Auswahl an Drum Sounds:
<Box>
Diese 2-Buchstaben Kombinationen stehen für verschiedene Teile eines Schlagzeugs:
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>
- `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!
@@ -131,34 +142,58 @@ 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.
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>
**Tempo ändern mit `cpm`**
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
<Box>
cpm = **c**ycles per **m**inute = Cycles pro Minute
Das Tempo ist standardmäßig auf 60cpm eingestellt, also 1 Cycle pro Sekunde.
Das Tempo is standardmäßig is 30 Cycles pro Minute = 120/4 = 1 Cycle alle 2 Sekunden
`cpm` ist angelehnt an `bpm` (=beats per minute).
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.
</Box>
Wir werden später noch mehr Möglichkeiten kennen lernen das Tempo zu verändern.
**Pausen mit '~'**
**Pausen mit '-' oder '~'**
<MiniRepl client:visible tune={`sound("bd hh ~ rim")`} punchcard />
<MiniRepl client:visible tune={`sound("bd hh - rim")`} punchcard />
<Box>
Tilde tippen:
- Windows / Linux: `Alt Gr` + `~`
- Mac: `option` + `N`
Du siehst wahrscheinlich auch Patterns von anderen Leuten mit '~' als Pausensymbol.
Besonders für deutsche Tastaturen ist `-` eine Alternative zum schwer tippbaren `~`.
</Box>
@@ -219,7 +254,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:
@@ -237,9 +272,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
/>
@@ -269,7 +304,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")`} /> |
@@ -293,7 +328,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>
@@ -310,7 +345,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
/>
@@ -320,10 +355,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
/>
@@ -333,10 +368,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
/>
@@ -346,9 +381,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
/>
+112 -132
View File
@@ -24,20 +24,31 @@ 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 |
| 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 |
| ----------------------------------- | ------------ |
| 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 |
@@ -63,11 +74,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 bd sd,hh*16").bank("RolandTR808")`} />
<MiniRepl client:idle tune={`s("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 bd sd,hh*16").bank("RolandTR808 RolandTR909")`} />
<MiniRepl client:idle tune={`s("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.
@@ -97,159 +108,128 @@ Selecting sounds also works inside the mini notation, using "`:`" like this:
# Loading Custom Samples
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:
You can load a non-standard sample map using the `samples` function.
<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")`}
/>
## Loading samples from file URLs
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.
In this example we assign names `bassdrum`, `hihat` and `snaredrum` to specific audio files on a server:
<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 bassdrum snaredrum, hihat*16")`}
s("bassdrum snaredrum:0 bassdrum snaredrum:1, hihat*16")`}
/>
Here we have changed the "map" to include longer sample names.
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!
## The `samples` function
In the above example, `bassdrum` will load:
The `samples` function has two arguments:
```
https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/bd/BT0AADA.wav
|----------------------base path --------------------------------|--sample path-|
```
- 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!
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`.
To see how this looks in practice, compare the [DirtSamples GitHub repo](https://github.com/tidalcycles/Dirt-Samples) with the previous sample map example.
## Loading Samples from a strudel.json file
Because GitHub is a popular place for uploading open source samples, it has its own shortcut:
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:
<MiniRepl
client:idle
tune={`samples({
bd: 'bd/BT0AADA.wav',
sd: 'sd/rytm-01-classic.wav',
hh: 'hh27/000_hh27closedhh.wav',
}, 'github:tidalcycles/dirt-samples');
tune={`samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json')
s("bd sd bd sd,hh*16")`}
/>
The format is `github:user/repo/branch/`.
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:
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:
```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:
<MiniRepl
client:idle
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")`}
tune={`samples('github:tidalcycles/dirt-samples')
s("bd sd bd sd,hh*16")`}
/>
## Multiple Samples per Sound
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:
It is also possible, to declare multiple files for one sound, using the array notation:
```
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:
<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:0 bd:1,~ <sd:0 sd:1> ~ sd:0,[hh:0 hh:1]*4")`}
tune={`samples('http://localhost:5432/');
n("<0 1 2>").s("swoop smash")`}
/>
The `:0` `:1` etc. are the indices of the array.
The sample number can also be set using `n`:
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.
<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>")`}
/>
Note: You need [NodeJS](https://nodejs.org/) installed on your system for this to work.
In that case, we might load our guitar sample map a different way:
## Specifying Pitch
<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:
To make sure your samples are in tune when playing them with `note`, you can specify a base pitch like this:
<MiniRepl
client:idle
@@ -261,8 +241,6 @@ 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
@@ -280,6 +258,8 @@ 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,4 +106,12 @@ 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] 20');
('10 [11 21] 22');
```
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.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.
- `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.
- `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.
+42 -41
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="Lösung anzeigen" client:visible>
<QA q="Click to see solution" 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,15 +307,16 @@ 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>")`} /> |
| 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>")`} /> |
| 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))`} /> |
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
+16 -8
View File
@@ -147,35 +147,43 @@ 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 (=4s).
The `/4` plays the sequence in brackets over 4 cycles (=8s).
So each of the 4 notes is 1s long.
So each of the 4 notes is 2s long.
Try adding more notes inside the brackets and notice how it gets faster.
</Box>
Because it is so common to just play one thing per cycle, you can..
**Play one per cycle with `< ... >`**
**Play one per cycle with \< \>**
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:
<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 it does **not** get faster.
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`
...
</Box>
**Play one sequence per cycle**
{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4> */}
We can combine the 2 types of brackets in all sorts of different ways.
Here is an example of a repetitive bassline:
<MiniRepl
client:visible
+69 -32
View File
@@ -78,11 +78,22 @@ 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!
@@ -129,29 +140,54 @@ 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.
The content of a sequence will be squished into what's called a cycle. A cycle is 2s long by default.
**One way to change the tempo is using `cpm`**
**One per cycle with `< .. >`**
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
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 />
<Box>
cpm = cycles per minute
By default, the tempo is 30 cycles per minute = 1 half cycle per second.
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.
</Box>
We will look at other ways to change the tempo later!
**Add a rests in a sequence with '-' or '~'**
**Add a rests in a sequence with '~'**
<MiniRepl client:visible tune={`sound("bd hh ~ rim ~ bd hh rim")`} punchcard />
<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>
@@ -163,7 +199,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**
@@ -195,7 +231,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:
@@ -213,9 +249,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
/>
@@ -232,13 +268,14 @@ 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 leared so far:
This is what we've learned 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 | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
| Rests | - or ~ | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
| Alternate | \<\> | <MiniRepl client:visible tune={`sound("<bd hh rim oh bd rim>")`} /> |
| 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")`} /> |
@@ -248,9 +285,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
@@ -266,7 +303,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>
@@ -283,7 +320,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
/>
@@ -293,10 +330,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
/>
@@ -306,10 +343,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
/>
@@ -319,9 +356,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,12 +155,13 @@ In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying.
</Box>
off is also useful for sounds:
off is also useful for modifying other sounds, and can even be nested:
<MiniRepl
client:visible
tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1")
.off(1/16, x=>x.speed(1.5).gain(.25))`}
.off(2/16, x=>x.speed(1.5).gain(.25)
.off(3/16, y=>y.vowel("<a e i o>*8")))`}
/>
| name | description | example |
+6 -1
View File
@@ -36,7 +36,12 @@ 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. You can find the source code at{' '}
, 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{' '}
<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];
const hash = initialUrl.split('?')[1]?.split('#')?.[0]?.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')