mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 21:23:21 -04:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35b9c82bdd | |||
| 0fcfc308fd | |||
| 931016143c | |||
| 8e2d883e86 | |||
| 8db443ba26 | |||
| 34e9d73908 | |||
| deed379dba | |||
| 5b4322c964 | |||
| 91bb0a1d36 | |||
| 47b65846e9 | |||
| 55eafaf4d8 | |||
| 2e3869c0e0 | |||
| 4015a11bd2 | |||
| 15f16006d0 | |||
| ce79a44848 | |||
| d253e35b6c | |||
| 19ef941c9d | |||
| ac7658d0a0 | |||
| 635a6c62b0 | |||
| acc100da5c | |||
| 69f8498d4b | |||
| 975eb684b9 | |||
| 5c4ad57309 | |||
| 0be5f77247 | |||
| bbf8577f85 | |||
| d8eb186ee4 | |||
| 54829bd28c | |||
| 5c01760b28 | |||
| a189626e8b | |||
| 6a1fa7e67f | |||
| 267f58b7be | |||
| 9348a8015a | |||
| 57ad278137 | |||
| 5d8eea7299 | |||
| 0a3694fb82 | |||
| a194180130 | |||
| 64cc8caeec | |||
| f427b56062 | |||
| 93d151b943 | |||
| e0c4997f1e | |||
| 07791001dd | |||
| 936d6fdafb | |||
| 81b33afcdb | |||
| 0190604a7f | |||
| 8494353ace | |||
| 971d497e81 | |||
| ca36a31f79 | |||
| 559f31792f | |||
| 83eedcb2f5 | |||
| a3bd04732c | |||
| 97d292adbb | |||
| 16851cf8f8 | |||
| 8f06bc4b57 |
@@ -22,3 +22,4 @@ vite.config.js
|
|||||||
reverbGen.mjs
|
reverbGen.mjs
|
||||||
hydra.mjs
|
hydra.mjs
|
||||||
jsdoc-synonyms.js
|
jsdoc-synonyms.js
|
||||||
|
packages/node/pattern.mjs
|
||||||
@@ -11,3 +11,4 @@ pnpm-lock.yaml
|
|||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
website/.astro
|
website/.astro
|
||||||
|
packages/node/pattern.mjs
|
||||||
@@ -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 Fraction from 'fraction.js';
|
||||||
import { TimeSpan } from './timespan.mjs';
|
import { TimeSpan } from './timespan.mjs';
|
||||||
|
import { removeUndefineds } from './util.mjs';
|
||||||
|
|
||||||
// Returns the start of the cycle.
|
// Returns the start of the cycle.
|
||||||
Fraction.prototype.sam = function () {
|
Fraction.prototype.sam = function () {
|
||||||
@@ -47,6 +48,10 @@ Fraction.prototype.eq = function (other) {
|
|||||||
return this.compare(other) == 0;
|
return this.compare(other) == 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.ne = function (other) {
|
||||||
|
return this.compare(other) != 0;
|
||||||
|
};
|
||||||
|
|
||||||
Fraction.prototype.max = function (other) {
|
Fraction.prototype.max = function (other) {
|
||||||
return this.gt(other) ? this : other;
|
return this.gt(other) ? this : other;
|
||||||
};
|
};
|
||||||
@@ -60,6 +65,22 @@ Fraction.prototype.min = function (other) {
|
|||||||
return this.lt(other) ? this : 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 */) {
|
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||||
// return this.toFraction(excludeWhole);
|
// return this.toFraction(excludeWhole);
|
||||||
return this.s * this.n + '/' + this.d;
|
return this.s * this.n + '/' + this.d;
|
||||||
@@ -85,11 +106,24 @@ const fraction = (n) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const gcd = (...fractions) => {
|
export const gcd = (...fractions) => {
|
||||||
|
fractions = removeUndefineds(fractions);
|
||||||
|
if (fractions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lcm = (...fractions) => {
|
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;
|
fraction._original = Fraction;
|
||||||
|
|||||||
+258
-79
@@ -10,12 +10,29 @@ import Hap from './hap.mjs';
|
|||||||
import State from './state.mjs';
|
import State from './state.mjs';
|
||||||
import { unionWithObj } from './value.mjs';
|
import { unionWithObj } from './value.mjs';
|
||||||
|
|
||||||
import { 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 drawLine from './drawLine.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
|
let __tactus = true;
|
||||||
|
|
||||||
|
export const calculateTactus = function (x) {
|
||||||
|
__tactus = x ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
// parser is expected to turn a string into a pattern
|
// parser is expected to turn a string into a pattern
|
||||||
// if set, the reify function will parse all strings with it
|
// if set, the reify function will parse all strings with it
|
||||||
// intended to use with mini to automatically interpret all strings as mini notation
|
// intended to use with mini to automatically interpret all strings as mini notation
|
||||||
@@ -31,16 +48,16 @@ export class Pattern {
|
|||||||
*/
|
*/
|
||||||
constructor(query, tactus = undefined) {
|
constructor(query, tactus = undefined) {
|
||||||
this.query = query;
|
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._Pattern = true; // this property is used to detectinstance of another Pattern
|
||||||
this.__tactus = tactus; // in terms of number of beats per cycle
|
this.tactus = tactus; // in terms of number of steps per cycle
|
||||||
}
|
}
|
||||||
|
|
||||||
get tactus() {
|
get tactus() {
|
||||||
return this.__tactus ?? Fraction(1);
|
return this.__tactus;
|
||||||
}
|
}
|
||||||
|
|
||||||
set tactus(tactus) {
|
set tactus(tactus) {
|
||||||
this.__tactus = Fraction(tactus);
|
this.__tactus = tactus === undefined ? undefined : Fraction(tactus);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTactus(tactus) {
|
setTactus(tactus) {
|
||||||
@@ -48,6 +65,17 @@ export class Pattern {
|
|||||||
return this;
|
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
|
// Haskell-style functor, applicative and monadic operations
|
||||||
|
|
||||||
@@ -130,7 +158,9 @@ export class Pattern {
|
|||||||
return span_a.intersection_e(span_b);
|
return span_a.intersection_e(span_b);
|
||||||
};
|
};
|
||||||
const result = pat_func.appWhole(whole_func, pat_val);
|
const result = pat_func.appWhole(whole_func, pat_val);
|
||||||
|
if (__tactus) {
|
||||||
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
|
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,12 +272,12 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
outerBind(func) {
|
outerBind(func) {
|
||||||
return this.bindWhole((a) => a, func);
|
return this.bindWhole((a) => a, func).setTactus(this.tactus);
|
||||||
}
|
}
|
||||||
|
|
||||||
outerJoin() {
|
outerJoin() {
|
||||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||||
// taken from inner haps.
|
// taken from outer haps.
|
||||||
return this.outerBind(id);
|
return this.outerBind(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,24 +1167,24 @@ function _composeOp(a, b, func) {
|
|||||||
export const polyrhythm = stack;
|
export const polyrhythm = stack;
|
||||||
export const pr = 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
|
// methods that create patterns, which are added to patternified Pattern methods
|
||||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||||
Pattern.prototype.factories = {
|
// Pattern.prototype.factories = {
|
||||||
pure,
|
// pure,
|
||||||
stack,
|
// stack,
|
||||||
slowcat,
|
// slowcat,
|
||||||
fastcat,
|
// fastcat,
|
||||||
cat,
|
// cat,
|
||||||
timecat,
|
// timecat,
|
||||||
sequence,
|
// sequence,
|
||||||
seq,
|
// seq,
|
||||||
polymeter,
|
// polymeter,
|
||||||
pm,
|
// pm,
|
||||||
polyrhythm,
|
// polyrhythm,
|
||||||
pr,
|
// pr,
|
||||||
};
|
// };
|
||||||
// the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)
|
// the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)
|
||||||
|
|
||||||
// Elemental patterns
|
// Elemental patterns
|
||||||
@@ -1166,7 +1196,7 @@ Pattern.prototype.factories = {
|
|||||||
* @example
|
* @example
|
||||||
* gap(3) // "~@3"
|
* gap(3) // "~@3"
|
||||||
*/
|
*/
|
||||||
export const gap = (tactus) => new Pattern(() => [], Fraction(tactus));
|
export const gap = (tactus) => new Pattern(() => [], tactus);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does absolutely nothing..
|
* Does absolutely nothing..
|
||||||
@@ -1235,7 +1265,9 @@ export function stack(...pats) {
|
|||||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||||
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
|
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
|
||||||
const result = new Pattern(query);
|
const result = new Pattern(query);
|
||||||
|
if (__tactus) {
|
||||||
result.tactus = lcm(...pats.map((pat) => pat.tactus));
|
result.tactus = lcm(...pats.map((pat) => pat.tactus));
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,20 +1280,20 @@ function _stackWith(func, pats) {
|
|||||||
return pats[0];
|
return pats[0];
|
||||||
}
|
}
|
||||||
const [left, ...right] = pats.map((pat) => pat.tactus);
|
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));
|
return stack(...func(tactus, pats));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stackLeft(...pats) {
|
export function stackLeft(...pats) {
|
||||||
return _stackWith(
|
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,
|
pats,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stackRight(...pats) {
|
export function stackRight(...pats) {
|
||||||
return _stackWith(
|
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,
|
pats,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1274,7 +1306,7 @@ export function stackCentre(...pats) {
|
|||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
const g = gap(tactus.sub(pat.tactus).div(2));
|
const g = gap(tactus.sub(pat.tactus).div(2));
|
||||||
return timecat(g, pat, g);
|
return s_cat(g, pat, g);
|
||||||
}),
|
}),
|
||||||
pats,
|
pats,
|
||||||
);
|
);
|
||||||
@@ -1288,7 +1320,7 @@ export function stackBy(by, ...pats) {
|
|||||||
left: stackLeft,
|
left: stackLeft,
|
||||||
right: stackRight,
|
right: stackRight,
|
||||||
expand: stack,
|
expand: stack,
|
||||||
repeat: (...args) => polymeterSteps(tactus, ...args),
|
repeat: (...args) => s_polymeterSteps(tactus, ...args),
|
||||||
};
|
};
|
||||||
return by
|
return by
|
||||||
.inhabit(lookup)
|
.inhabit(lookup)
|
||||||
@@ -1328,7 +1360,7 @@ export function slowcat(...pats) {
|
|||||||
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
|
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))));
|
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);
|
return new Pattern(query).splitQueries().setTactus(tactus);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1374,7 +1406,7 @@ export function cat(...pats) {
|
|||||||
export function arrange(...sections) {
|
export function arrange(...sections) {
|
||||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||||
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||||
return timecat(...sections).slow(total);
|
return s_cat(...sections).slow(total);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fastcat(...pats) {
|
export function fastcat(...pats) {
|
||||||
@@ -1454,7 +1486,7 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||||||
* @noAutocomplete
|
* @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)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
for (const name_item of name) {
|
||||||
@@ -1491,7 +1523,7 @@ export function register(name, func, patternify = true, preserveTactus = false)
|
|||||||
return func(...args, pat);
|
return func(...args, pat);
|
||||||
};
|
};
|
||||||
mapFn = curry(mapFn, null, arity - 1);
|
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) {
|
if (preserveTactus) {
|
||||||
@@ -1535,6 +1567,11 @@ export function register(name, func, patternify = true, preserveTactus = false)
|
|||||||
return curry(pfunc, null, arity);
|
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
|
// Numerical transformations
|
||||||
|
|
||||||
@@ -1742,7 +1779,9 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
|
|||||||
*/
|
*/
|
||||||
export const ply = register('ply', function (factor, pat) {
|
export const ply = register('ply', function (factor, pat) {
|
||||||
const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
|
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;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1757,16 +1796,19 @@ export const ply = register('ply', function (factor, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
|
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
|
||||||
*/
|
*/
|
||||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
export const { fast, density } = register(
|
||||||
|
['fast', 'density'],
|
||||||
|
function (factor, pat) {
|
||||||
if (factor === 0) {
|
if (factor === 0) {
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
factor = Fraction(factor);
|
factor = Fraction(factor);
|
||||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||||
const result = fastQuery.withHapTime((t) => t.div(factor));
|
return fastQuery.withHapTime((t) => t.div(factor)).setTactus(pat.tactus);
|
||||||
result.tactus = factor.mul(pat.tactus);
|
},
|
||||||
return result;
|
true,
|
||||||
});
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Both speeds up the pattern (like 'fast') and the sample playback (like 'speed').
|
* 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;
|
return nothing;
|
||||||
}
|
}
|
||||||
const d = e.sub(s);
|
const d = e.sub(s);
|
||||||
const tactus = pat.tactus.mul(d);
|
const tactus = __tactus ? pat.tactus.mulmaybe(d) : undefined;
|
||||||
return pat
|
return pat
|
||||||
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||||
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
.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);
|
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.
|
* Swaps 1s and 0s in a binary pattern.
|
||||||
* @name invert
|
* @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 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 })));
|
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(
|
export const { repeatCycles } = register(
|
||||||
'repeatCycles',
|
'repeatCycles',
|
||||||
function (n, pat) {
|
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,
|
||||||
true,
|
true,
|
||||||
@@ -2357,14 +2423,69 @@ Pattern.prototype.tag = function (tag) {
|
|||||||
// Tactus-related functions, i.e. ones that do stepwise
|
// Tactus-related functions, i.e. ones that do stepwise
|
||||||
// transformations
|
// 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
|
* @example
|
||||||
* s("bd sd cp").toTactus(4)
|
* s("bd sd cp").steps(4)
|
||||||
* // The same as s("{bd sd cp}%4")
|
* // The same as s("{bd sd cp}%4")
|
||||||
*/
|
*/
|
||||||
export const toTactus = register('toTactus', function (targetTactus, pat) {
|
export const steps = register('steps', function (targetTactus, pat) {
|
||||||
if (pat.tactus.eq(0)) {
|
if (pat.tactus === undefined) {
|
||||||
|
return pat;
|
||||||
|
}
|
||||||
|
if (pat.tactus.eq(Fraction(0))) {
|
||||||
// avoid divide by zero..
|
// avoid divide by zero..
|
||||||
return nothing;
|
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.
|
* 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,
|
* 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 {number} steps how many items are placed in one cycle
|
||||||
* @param {any[]} patterns one or more patterns
|
* @param {any[]} patterns one or more patterns
|
||||||
* @example
|
* @example
|
||||||
* // the same as "{c d, e f g}%4"
|
* // 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) {
|
if (args.length == 0) {
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
@@ -2413,7 +2534,7 @@ export function polymeterSteps(steps, ...args) {
|
|||||||
return _polymeterListSteps(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
|
* @synonyms pm
|
||||||
* @example
|
* @example
|
||||||
* // The same as "{c eb g, c2 g2}"
|
* // 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])) {
|
if (Array.isArray(args[0])) {
|
||||||
// Support old behaviour
|
// Support old behaviour
|
||||||
return _polymeterListSteps(0, ...args);
|
return _polymeterListSteps(0, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO currently ignoring arguments without tactus...
|
||||||
|
args = args.filter((arg) => arg.hasTactus);
|
||||||
|
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
const tactus = args[0].tactus;
|
const tactus = args[0].tactus;
|
||||||
|
if (tactus.eq(Fraction(0))) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
const [head, ...tail] = args;
|
const [head, ...tail] = args;
|
||||||
|
|
||||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
|
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.
|
/** 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
|
* 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}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* timecat([3,"e3"],[1, "g3"]).note()
|
* s_cat([3,"e3"],[1, "g3"]).note()
|
||||||
* // the same as "e3@3 g3".note()
|
* // the same as "e3@3 g3".note()
|
||||||
* @example
|
* @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()
|
* // 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]);
|
const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]);
|
||||||
timepats = timepats.map(findtactus);
|
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) {
|
if (timepats.length == 1) {
|
||||||
const result = reify(timepats[0][1]);
|
const result = reify(timepats[0][1]);
|
||||||
result.tactus = timepats[0][0];
|
result.tactus = timepats[0][0];
|
||||||
@@ -2477,18 +2622,19 @@ export function timecat(...timepats) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deprecated alias for `timecat` */
|
/** Aliases for `s_cat` */
|
||||||
export const timeCat = timecat;
|
export const timecat = s_cat;
|
||||||
|
export const timeCat = s_cat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'.
|
* *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}
|
* @return {Pattern}
|
||||||
* @example
|
* @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)]));
|
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
|
||||||
|
|
||||||
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
|
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
|
||||||
@@ -2497,9 +2643,9 @@ export function stepcat(...groups) {
|
|||||||
for (let cycle = 0; cycle < cycles; ++cycle) {
|
for (let cycle = 0; cycle < cycles; ++cycle) {
|
||||||
result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length])));
|
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));
|
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||||
result = timecat(...result);
|
result = s_cat(...result);
|
||||||
result.tactus = tactus;
|
result.tactus = tactus;
|
||||||
return result;
|
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'.
|
* *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)) {
|
if (pat.tactus.lte(0)) {
|
||||||
return nothing;
|
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'.
|
* *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);
|
i = Fraction(i);
|
||||||
if (i.lt(0)) {
|
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*
|
* *EXPERIMENTAL*
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.taperlist = function (amount, times) {
|
Pattern.prototype.s_taperlist = function (amount, times) {
|
||||||
const pat = this;
|
const pat = this;
|
||||||
|
|
||||||
|
if (!pat.hasTactus) {
|
||||||
|
return [pat];
|
||||||
|
}
|
||||||
|
|
||||||
times = times - 1;
|
times = times - 1;
|
||||||
|
|
||||||
if (times === 0) {
|
if (times === 0) {
|
||||||
return pat;
|
return [pat];
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = [];
|
const list = [];
|
||||||
@@ -2568,23 +2734,33 @@ Pattern.prototype.taperlist = function (amount, times) {
|
|||||||
}
|
}
|
||||||
return list;
|
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*
|
* *EXPERIMENTAL*
|
||||||
*/
|
*/
|
||||||
export const steptaper = register('steptaper', function (amount, times, pat) {
|
export const s_taper = register(
|
||||||
const list = pat.taperlist(amount, times);
|
's_taper',
|
||||||
const result = timecat(...list);
|
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));
|
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||||
return result;
|
return result;
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
(x) => x.stepJoin(),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* *EXPERIMENTAL*
|
* *EXPERIMENTAL*
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.steptour = function (...many) {
|
Pattern.prototype.s_tour = function (...many) {
|
||||||
return stepcat(
|
return s_cat(
|
||||||
...[].concat(
|
...[].concat(
|
||||||
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
|
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
|
||||||
this,
|
this,
|
||||||
@@ -2593,8 +2769,8 @@ Pattern.prototype.steptour = function (...many) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const steptour = function (pat, ...many) {
|
export const s_tour = function (pat, ...many) {
|
||||||
return pat.steptour(...many);
|
return pat.s_tour(...many);
|
||||||
};
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -2621,7 +2797,7 @@ export const chop = register('chop', function (n, pat) {
|
|||||||
const func = function (o) {
|
const func = function (o) {
|
||||||
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_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 slices = Array.from({ length: n }, (x, i) => i);
|
||||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||||
const slicePat = slowcat(...slice_objects);
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ export function repl({
|
|||||||
const scheduler =
|
const scheduler =
|
||||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
|
let anonymousIndex = 0;
|
||||||
let allTransform;
|
let allTransform;
|
||||||
|
|
||||||
const hush = function () {
|
const hush = function () {
|
||||||
pPatterns = {};
|
pPatterns = {};
|
||||||
|
anonymousIndex = 0;
|
||||||
allTransform = undefined;
|
allTransform = undefined;
|
||||||
return silence;
|
return silence;
|
||||||
};
|
};
|
||||||
@@ -82,6 +84,15 @@ export function repl({
|
|||||||
// set pattern methods that use this repl via closure
|
// set pattern methods that use this repl via closure
|
||||||
const injectPatternMethods = () => {
|
const injectPatternMethods = () => {
|
||||||
Pattern.prototype.p = function (id) {
|
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;
|
pPatterns[id] = this;
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -486,8 +486,10 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
* 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;
|
export const wrandcat = wchooseCycles;
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
palindrome,
|
palindrome,
|
||||||
polymeter,
|
s_polymeter,
|
||||||
polymeterSteps,
|
s_polymeterSteps,
|
||||||
polyrhythm,
|
polyrhythm,
|
||||||
silence,
|
silence,
|
||||||
fast,
|
fast,
|
||||||
@@ -47,6 +47,11 @@ import {
|
|||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
pick,
|
pick,
|
||||||
|
stackLeft,
|
||||||
|
stackRight,
|
||||||
|
stackCentre,
|
||||||
|
s_cat,
|
||||||
|
calculateTactus,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { steady } from '../signal.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', () => {
|
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(),
|
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(),
|
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', () => {
|
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', () => {
|
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).linger(4).tactus).toStrictEqual(Fraction(4));
|
||||||
expect(sequence(0, 1, 2, 3).iter(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).fast(4).tactus).toStrictEqual(Fraction(4));
|
||||||
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16));
|
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(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
|
||||||
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
|
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
|
||||||
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
|
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(
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
|
||||||
Fraction(4),
|
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', () => {
|
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', () => {
|
it('can taper backwards', () => {
|
||||||
expect(
|
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)));
|
||||||
sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('wax and wane, left', () => {
|
describe('s_add and s_sub', () => {
|
||||||
it('can wax from the left', () => {
|
it('can add from the left', () => {
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
|
||||||
});
|
});
|
||||||
it('can wane to the left', () => {
|
it('can sub to the left', () => {
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
|
||||||
});
|
});
|
||||||
it('can wax from the right', () => {
|
it('can add from the right', () => {
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
|
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
|
||||||
});
|
});
|
||||||
it('can wane to the right', () => {
|
it('can sub to the right', () => {
|
||||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4)));
|
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'))));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
return 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
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
* @noAutocomplete
|
* @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 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);
|
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
/* solmization, not used yet */
|
/* solmization, not used yet */
|
||||||
@@ -289,6 +302,30 @@ export const sol2note = (n, notation = 'letters') => {
|
|||||||
return note + oct;
|
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
|
// code hashing helpers
|
||||||
|
|
||||||
export function unicodeToBase64(text) {
|
export function unicodeToBase64(text) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
instrument = instrument || 'triangle';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
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)
|
// 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) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -38,9 +38,11 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
.join('/');
|
.join('/');
|
||||||
// TODO: find out how to send a precise ctx based time
|
// TODO: find out how to send a precise ctx based time
|
||||||
// http://www.csounds.com/manual/html/i.html
|
// http://www.csounds.com/manual/html/i.html
|
||||||
|
const timeOffset = targetTime - currentTime; // latency ?
|
||||||
|
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
||||||
const params = [
|
const params = [
|
||||||
`"${instrument}"`, // p1: instrument name
|
`"${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
|
hap.duration + 0, // p3: duration in beats
|
||||||
// instrument specific params:
|
// instrument specific params:
|
||||||
freq, //.toFixed(precision), // p4: frequency
|
freq, //.toFixed(precision), // p4: frequency
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { Pattern, noteToMidi } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
||||||
|
|
||||||
const ON_MESSAGE = 0x90;
|
const ON_MESSAGE = 0x90;
|
||||||
const OFF_MESSAGE = 0x80;
|
const OFF_MESSAGE = 0x80;
|
||||||
@@ -9,8 +9,8 @@ Pattern.prototype.midi = function (output) {
|
|||||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
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
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latency = 0.034;
|
const latencyMs = 34;
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs;
|
||||||
velocity = Math.floor(gain * velocity * 100);
|
velocity = Math.floor(gain * velocity * 100);
|
||||||
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
|
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
|
||||||
const roundedOffset = Math.round(offset);
|
const roundedOffset = Math.round(offset);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { parseNumeral, Pattern } from '@strudel/core';
|
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
|
|
||||||
Pattern.prototype.osc = function () {
|
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();
|
hap.ensureObjectValue();
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.valueOf();
|
const delta = hap.duration.valueOf();
|
||||||
@@ -13,7 +13,7 @@ Pattern.prototype.osc = function () {
|
|||||||
|
|
||||||
const params = [];
|
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) => {
|
Object.keys(controls).forEach((key) => {
|
||||||
const val = controls[key];
|
const val = controls[key];
|
||||||
|
|||||||
@@ -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 * 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 { noteToMidi } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
@@ -120,10 +120,9 @@ Pattern.prototype.midi = function (output) {
|
|||||||
const device = getDevice(output, WebMidi.outputs);
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//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
|
// 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
|
// destructure value
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.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 initial = await enableWebMidi(); // only returns on first init
|
||||||
const device = getDevice(input, WebMidi.inputs);
|
const device = getDevice(input, WebMidi.inputs);
|
||||||
if (initial || WebMidi.enabled) {
|
if (initial) {
|
||||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||||
logger(
|
logger(
|
||||||
`Midi enabled! Using "${device.name}". ${
|
`Midi enabled! Using "${device.name}". ${
|
||||||
|
|||||||
@@ -295,7 +295,15 @@ function peg$parse(input, options) {
|
|||||||
var peg$f6 = function(a) { return a };
|
var peg$f6 = function(a) { return a };
|
||||||
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
|
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$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$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$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' }}) };
|
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||||
|
|||||||
@@ -135,7 +135,14 @@ op_weight = ws ("@" / "_") a:number?
|
|||||||
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
|
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
|
||||||
|
|
||||||
op_replicate = ws "!" a:number?
|
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 ")"
|
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 }}) }
|
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
||||||
|
|||||||
+7
-14
@@ -27,6 +27,12 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
|||||||
pat = strudel.reify(pat)[type](enter(amount));
|
pat = strudel.reify(pat)[type](enter(amount));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'replicate': {
|
||||||
|
const { amount } = op.arguments_;
|
||||||
|
pat = strudel.reify(pat);
|
||||||
|
pat = pat._repeatCycles(amount)._fast(amount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'bjorklund': {
|
case 'bjorklund': {
|
||||||
if (op.arguments_.rotation) {
|
if (op.arguments_.rotation) {
|
||||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(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;
|
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
|
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
||||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||||
onEnter?.(ast);
|
onEnter?.(ast);
|
||||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
||||||
switch (ast.type_) {
|
switch (ast.type_) {
|
||||||
case 'pattern': {
|
case 'pattern': {
|
||||||
resolveReplications(ast);
|
// resolveReplications(ast);
|
||||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
||||||
const alignment = ast.arguments_.alignment;
|
const alignment = ast.arguments_.alignment;
|
||||||
const with_tactus = children.filter((child) => child.__tactus_source);
|
const with_tactus = children.filter((child) => child.__tactus_source);
|
||||||
|
|||||||
@@ -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!
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
freq("110 220 [330 440,550,660]")
|
||||||
|
.attack(.1).gain(.4)
|
||||||
@@ -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 OSC from 'osc-js';
|
||||||
|
|
||||||
import { logger, parseNumeral, Pattern } from '@strudel/core';
|
import { logger, parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -44,7 +44,7 @@ function connect() {
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
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();
|
hap.ensureObjectValue();
|
||||||
const osc = await connect();
|
const osc = await connect();
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
@@ -56,7 +56,8 @@ Pattern.prototype.osc = function () {
|
|||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
// time should be audio time of onset
|
// time should be audio time of onset
|
||||||
// currentTime should be current time of audio context (slightly before time)
|
// 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
|
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
||||||
const ts = Math.floor(Date.now() + offset);
|
const ts = Math.floor(Date.now() + offset);
|
||||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const OSC = require('osc-js');
|
import OSC from 'osc-js';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/sampler",
|
"name": "@strudel/sampler",
|
||||||
"version": "0.0.8",
|
"version": "0.0.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tidalcycles",
|
"tidalcycles",
|
||||||
|
|||||||
@@ -74,6 +74,6 @@ export const dough = async (code) => {
|
|||||||
worklet.node.connect(ac.destination);
|
worklet.node.connect(ac.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doughTrigger(t, hap, currentTime, duration, cps) {
|
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
|
||||||
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
|
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
|
|||||||
* .s("piano")
|
* .s("piano")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scale = register('scale', function (scale, pat) {
|
export const scale = register(
|
||||||
|
'scale',
|
||||||
|
function (scale, pat) {
|
||||||
// Supports ':' list syntax in mininotation
|
// Supports ':' list syntax in mininotation
|
||||||
if (Array.isArray(scale)) {
|
if (Array.isArray(scale)) {
|
||||||
scale = scale.flat().join(' ');
|
scale = scale.flat().join(' ');
|
||||||
@@ -245,4 +247,7 @@ export const scale = register('scale', function (scale, pat) {
|
|||||||
// legacy:
|
// legacy:
|
||||||
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
|
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
true,
|
||||||
|
true, // preserve tactus
|
||||||
|
);
|
||||||
|
|||||||
Generated
+62
@@ -302,6 +302,33 @@ importers:
|
|||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(@vitest/ui@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:
|
packages/osc:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
@@ -3467,6 +3494,22 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
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:
|
/@nodelib/fs.scandir@2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -10717,6 +10760,20 @@ packages:
|
|||||||
'@babel/parser': 7.23.6
|
'@babel/parser': 7.23.6
|
||||||
dev: true
|
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:
|
/nopt@7.1.0:
|
||||||
resolution: {integrity: sha512-ZFPLe9Iu0tnx7oWhFxAo4s7QTn8+NNDDxYNaKLjE7Dp0tbakQ3M1QhQzsnzXHQBTUO3K9BmwaxnyO8Ayn2I95Q==}
|
resolution: {integrity: sha512-ZFPLe9Iu0tnx7oWhFxAo4s7QTn8+NNDDxYNaKLjE7Dp0tbakQ3M1QhQzsnzXHQBTUO3K9BmwaxnyO8Ayn2I95Q==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
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==}
|
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/webidl-conversions@7.0.0:
|
||||||
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/webmidi@3.1.8:
|
/webmidi@3.1.8:
|
||||||
resolution: {integrity: sha512-PCRic1iTpKxeheb888G0mDe6MBdz/u+s/xfdV6+1ZhZnS6dKtV9gMBth5cpmMip2Livv5lL+ep4/S9DCzHfumg==}
|
resolution: {integrity: sha512-PCRic1iTpKxeheb888G0mDe6MBdz/u+s/xfdV6+1ZhZnS6dKtV9gMBth5cpmMip2Livv5lL+ep4/S9DCzHfumg==}
|
||||||
engines: {node: '>=8.5'}
|
engines: {node: '>=8.5'}
|
||||||
|
|||||||
+23
-15
@@ -1,13 +1,13 @@
|
|||||||
use rosc::{encoder, OscTime};
|
use rosc::{encoder, OscTime};
|
||||||
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
|
use rosc::{OscBundle, OscMessage, OscPacket, OscType};
|
||||||
|
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::{ mpsc, Mutex };
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
use crate::loggerbridge::Logger;
|
use crate::loggerbridge::Logger;
|
||||||
pub struct OscMsg {
|
pub struct OscMsg {
|
||||||
@@ -28,9 +28,11 @@ pub fn init(
|
|||||||
logger: Logger,
|
logger: Logger,
|
||||||
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
||||||
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
||||||
async_output_transmitter: mpsc::Sender<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 });
|
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()));
|
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
/* ...........................................................
|
/* ...........................................................
|
||||||
Listen For incoming messages and add to queue
|
Listen For incoming messages and add to queue
|
||||||
@@ -56,7 +58,8 @@ pub fn init(
|
|||||||
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
|
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
|
||||||
let to_addr = String::from("127.0.0.1:57120");
|
let to_addr = String::from("127.0.0.1:57120");
|
||||||
sock.set_nonblocking(true).unwrap();
|
sock.set_nonblocking(true).unwrap();
|
||||||
sock.connect(to_addr).expect("could not connect to OSC address");
|
sock.connect(to_addr)
|
||||||
|
.expect("could not connect to OSC address");
|
||||||
|
|
||||||
/* ...........................................................
|
/* ...........................................................
|
||||||
Process queued messages
|
Process queued messages
|
||||||
@@ -69,8 +72,10 @@ pub fn init(
|
|||||||
let result = sock.send(&message.msg_buf);
|
let result = sock.send(&message.msg_buf);
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
logger.log(
|
logger.log(
|
||||||
format!("OSC Message failed to send, the server might no longer be available"),
|
format!(
|
||||||
"error".to_string()
|
"OSC Message failed to send, the server might no longer be available"
|
||||||
|
),
|
||||||
|
"error".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -83,7 +88,7 @@ pub fn init(
|
|||||||
|
|
||||||
pub async fn async_process_model(
|
pub async fn async_process_model(
|
||||||
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
|
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
|
||||||
output_transmitter: mpsc::Sender<Vec<OscMsg>>
|
output_transmitter: mpsc::Sender<Vec<OscMsg>>,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
while let Some(input) = input_reciever.recv().await {
|
while let Some(input) = input_reciever.recv().await {
|
||||||
let output = input;
|
let output = input;
|
||||||
@@ -108,7 +113,7 @@ pub struct MessageFromJS {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn sendosc(
|
pub async fn sendosc(
|
||||||
messagesfromjs: Vec<MessageFromJS>,
|
messagesfromjs: Vec<MessageFromJS>,
|
||||||
state: tauri::State<'_, AsyncInputTransmit>
|
state: tauri::State<'_, AsyncInputTransmit>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let async_proc_input_tx = state.inner.lock().await;
|
let async_proc_input_tx = state.inner.lock().await;
|
||||||
let mut messages_to_process: Vec<OscMsg> = Vec::new();
|
let mut messages_to_process: Vec<OscMsg> = Vec::new();
|
||||||
@@ -123,10 +128,10 @@ pub async fn sendosc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
|
let duration_since_epoch =
|
||||||
|
Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
|
||||||
|
|
||||||
let seconds = u32
|
let seconds = u32::try_from(duration_since_epoch.as_secs())
|
||||||
::try_from(duration_since_epoch.as_secs())
|
|
||||||
.map_err(|_| "bit conversion failed for osc message timetag")?;
|
.map_err(|_| "bit conversion failed for osc message timetag")?;
|
||||||
|
|
||||||
let nanos = duration_since_epoch.subsec_nanos() as f64;
|
let nanos = duration_since_epoch.subsec_nanos() as f64;
|
||||||
@@ -153,5 +158,8 @@ pub async fn sendosc(
|
|||||||
messages_to_process.push(message_to_process);
|
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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`runs examples > example "samples" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:bd ]",
|
"[ 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`] = `
|
exports[`runs examples > example "striate" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]",
|
"[ 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`] = `
|
exports[`runs examples > example "timecat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 3/4 | note:e3 ]",
|
"[ 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 ]",
|
"[ 3/8 → 1/2 | s:bd ]",
|
||||||
"[ 1/2 → 5/8 | s:bd ]",
|
"[ 1/2 → 5/8 | s:bd ]",
|
||||||
"[ 5/8 → 3/4 | 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 ]",
|
"[ 7/8 → 1/1 | s:bd ]",
|
||||||
"[ 1/1 → 9/8 | s:bd ]",
|
"[ 1/1 → 9/8 | s:bd ]",
|
||||||
"[ 9/8 → 5/4 | s:bd ]",
|
"[ 9/8 → 5/4 | s:bd ]",
|
||||||
"[ 5/4 → 11/8 | 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 ]",
|
"[ 3/2 → 13/8 | s:bd ]",
|
||||||
"[ 13/8 → 7/4 | s:bd ]",
|
"[ 13/8 → 7/4 | s:bd ]",
|
||||||
"[ 7/4 → 15/8 | 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 ]",
|
"[ 19/8 → 5/2 | s:bd ]",
|
||||||
"[ 5/2 → 21/8 | s:bd ]",
|
"[ 5/2 → 21/8 | s:bd ]",
|
||||||
"[ 21/8 → 11/4 | 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 ]",
|
"[ 23/8 → 3/1 | s:bd ]",
|
||||||
"[ 3/1 → 25/8 | s:bd ]",
|
"[ 3/1 → 25/8 | s:bd ]",
|
||||||
"[ 25/8 → 13/4 | 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`] = `
|
exports[`runs examples > example "when" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/3 | note:c3 ]",
|
"[ 0/1 → 1/3 | note:c3 ]",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 338 KiB |
@@ -1,49 +0,0 @@
|
|||||||
export const laserclock = ({ onTick, audioContext }) => {
|
|
||||||
const ready = new Promise((resolve) => {
|
|
||||||
const workletCode = `class LaserClock extends AudioWorkletProcessor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.block = 0;
|
|
||||||
this.tick = 0;
|
|
||||||
this.started = false;
|
|
||||||
this.blocksPerCallback = 20;
|
|
||||||
this.port.onmessage = (e) => {
|
|
||||||
switch (e.data) {
|
|
||||||
case "start":
|
|
||||||
console.log('start!')
|
|
||||||
this.started = true;
|
|
||||||
break;
|
|
||||||
case "stop":
|
|
||||||
console.log('stop!')
|
|
||||||
this.started = false;
|
|
||||||
this.block = 0;
|
|
||||||
this.tick = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
process(inputs, outputs, parameters) {
|
|
||||||
if(!this.started) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if(this.block % this.blocksPerCallback === 0) {
|
|
||||||
this.port.postMessage({tick: this.tick, currentTime});
|
|
||||||
this.tick++;
|
|
||||||
}
|
|
||||||
this.block++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registerProcessor("laserclock-processor", LaserClock);`;
|
|
||||||
const dataURL = `data:text/javascript;base64,${btoa(workletCode)}`;
|
|
||||||
audioContext.audioWorklet.addModule(dataURL).then(() => {
|
|
||||||
const clock = new AudioWorkletNode(audioContext, 'laserclock-processor');
|
|
||||||
clock.port.onmessage = (e) => onTick(e.data);
|
|
||||||
clock.connect(audioContext.destination);
|
|
||||||
resolve(clock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const start = () => ready.then((clock) => clock.port.postMessage('start'));
|
|
||||||
const stop = () => ready.then((clock) => clock.port.postMessage('stop'));
|
|
||||||
return { ready, start, stop };
|
|
||||||
};
|
|
||||||
@@ -146,15 +146,13 @@ Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert.
|
|||||||
|
|
||||||
**Sequenzen verlangsamen mit `/`**
|
**Sequenzen verlangsamen mit `/`**
|
||||||
|
|
||||||
{/* [c2 bb1 f2 eb2] */}
|
|
||||||
|
|
||||||
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
|
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
|
||||||
|
|
||||||
<Box>
|
<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.
|
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 \< \>**
|
**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 />
|
<MiniRepl client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -78,13 +78,24 @@ Strudel kommt von Haus aus mit einer breiten Auswahl an Drum Sounds:
|
|||||||
|
|
||||||
<Box>
|
<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
|
- `bd` = **b**ass **d**rum - Basstrommel
|
||||||
- `sd` = **s**nare **d**rum - Schnarrtrommel
|
- `sd` = **s**nare **d**rum - Schnarrtrommel
|
||||||
- `rim` = **rim**shot - Rahmenschlag
|
- `rim` = **rim**shot - Rahmenschlag
|
||||||
- `hh` = **h**i**h**at - Hallo Hut
|
- `hh` = **h**i**h**at - Hallo Hut
|
||||||
- `oh` = **o**pen **h**ihat - Offener 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!
|
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 />
|
<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`**
|
**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>
|
<Box>
|
||||||
|
|
||||||
cpm = **c**ycles per **m**inute = Cycles pro Minute
|
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>
|
</Box>
|
||||||
|
|
||||||
Wir werden später noch mehr Möglichkeiten kennen lernen das Tempo zu verändern.
|
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>
|
<Box>
|
||||||
|
|
||||||
Tilde tippen:
|
Du siehst wahrscheinlich auch Patterns von anderen Leuten mit '~' als Pausensymbol.
|
||||||
|
Besonders für deutsche Tastaturen ist `-` eine Alternative zum schwer tippbaren `~`.
|
||||||
- Windows / Linux: `Alt Gr` + `~`
|
|
||||||
- Mac: `option` + `N`
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -219,7 +254,7 @@ Du kannst so tief verschachteln wie du willst!
|
|||||||
|
|
||||||
Du kannst so viele Kommas benutzen wie du möchtest:
|
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:
|
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
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound(\`bd*2, ~ cp,
|
tune={`sound(\`bd*2, - cp,
|
||||||
~ ~ ~ oh, hh*4,
|
- - - oh, hh*4,
|
||||||
[~ casio]*2\`)`}
|
[- casio]*2\`)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -269,7 +304,7 @@ Das haben wir bisher gelernt:
|
|||||||
| --------------------- | ----------- | --------------------------------------------------------------------- |
|
| --------------------- | ----------- | --------------------------------------------------------------------- |
|
||||||
| Sequenz | Leerzeichen | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
|
| 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")`} /> |
|
| 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-Sequenzen | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
|
||||||
| Unter-Unter-Sequenzen | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
|
| Unter-Unter-Sequenzen | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
|
||||||
| Schneller | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
|
| Schneller | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
|
||||||
@@ -293,7 +328,7 @@ Die folgenden Funktionen haben wir bereits gesehen:
|
|||||||
|
|
||||||
**Klassischer House**
|
**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>
|
<Box>
|
||||||
|
|
||||||
@@ -310,7 +345,7 @@ We Will Rock you
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
|
tune={`sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
|
||||||
.bank("RolandCompurhythm1000")`}
|
.bank("RolandCompurhythm1000")`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
@@ -320,10 +355,10 @@ We Will Rock you
|
|||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound(\`
|
tune={`sound(\`
|
||||||
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
|
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
|
||||||
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
|
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
|
||||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
|
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
|
||||||
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
|
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
|
||||||
\`).cpm(90/4)`}
|
\`).cpm(90/4)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
@@ -333,10 +368,10 @@ We Will Rock you
|
|||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound(\`
|
tune={`sound(\`
|
||||||
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
|
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
|
||||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
|
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
|
||||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
|
[- - - - ] [cp - - - ] [- - - - ] [- cp - - ],
|
||||||
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
|
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
|
||||||
\`).bank("RolandTR808").cpm(88/4)`}
|
\`).bank("RolandTR808").cpm(88/4)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
@@ -346,9 +381,9 @@ We Will Rock you
|
|||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`s(\`jazz*2,
|
tune={`s(\`jazz*2,
|
||||||
insect [crow metal] ~ ~,
|
insect [crow metal] - -,
|
||||||
~ space:4 ~ space:1,
|
- space:4 - space:1,
|
||||||
~ wind\`)
|
- wind\`)
|
||||||
.cpm(100/2)`}
|
.cpm(100/2)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<body>
|
|
||||||
<button id="playButton">play</button>
|
|
||||||
<button id="stopButton">stop</button>
|
|
||||||
<script>
|
|
||||||
import { laserclock } from '../laserclock.mjs';
|
|
||||||
import { evaluate } from '@strudel/transpiler';
|
|
||||||
import { evalScope } from '@strudel/core';
|
|
||||||
import { superdough, getAudioContext, registerSynthSounds, initAudioOnFirstClick } from '@strudel/webaudio';
|
|
||||||
const init = async () => {
|
|
||||||
initAudioOnFirstClick();
|
|
||||||
await evalScope(
|
|
||||||
import('@strudel/core'),
|
|
||||||
import('@strudel/mini'),
|
|
||||||
import('@strudel/tonal'),
|
|
||||||
import('@strudel/webaudio'),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
init().then(async () => {
|
|
||||||
const ctx = getAudioContext();
|
|
||||||
const code = `
|
|
||||||
// "tupper class" @by eddyflux
|
|
||||||
|
|
||||||
samples('github:yaxu/clean-breaks')
|
|
||||||
samples('github:eddyflux/crate')
|
|
||||||
samples('github:eddyflux/wax')
|
|
||||||
|
|
||||||
let cps = 68/60/4;
|
|
||||||
// setcps(cps) // currently not supported by lasterclock.. still sounds funky :D
|
|
||||||
|
|
||||||
stack(
|
|
||||||
s("loop4/4").fit()
|
|
||||||
.layer(
|
|
||||||
x=>x
|
|
||||||
.chop("64 [32 64]")
|
|
||||||
.juxBy(.6, rev)
|
|
||||||
.hpf(500)
|
|
||||||
.dec(sine.range(.05,.18).slow(8))
|
|
||||||
.room(.5).sometimes(mul(speed("2 | -2")))
|
|
||||||
.shape(.6).postgain(sine.range(0,.8).slow(16))
|
|
||||||
//.hush()
|
|
||||||
,
|
|
||||||
x=>x.chop(64).lpf(400)
|
|
||||||
.shape(.6).cut(4)
|
|
||||||
.dec(.2).postgain(.9)
|
|
||||||
.mask("<0!4 1!16>")
|
|
||||||
)
|
|
||||||
//.hush()
|
|
||||||
//.hpf(500)
|
|
||||||
,
|
|
||||||
s("riffin").fit().shape(.2).chop(32).dec(.1).postgain(.6)
|
|
||||||
.sometimesBy("0 [0 .5] 0 0", x=>x.set.squeeze(gain(".9 .6 .2 .1")).room(.3))
|
|
||||||
.sometimesBy("0 [0 0] 0 .5", x=>x.mul(speed("-2")))
|
|
||||||
.hurry("<.5!4 1!16>")
|
|
||||||
//.hush()
|
|
||||||
,
|
|
||||||
s("useme/2").fit()
|
|
||||||
.chop("<16 32>/32")//.rarely(ply("2"))
|
|
||||||
.sometimesBy("0 [.2 0]",ply("3"))
|
|
||||||
.dec(.19).room(.4).cut(9)
|
|
||||||
.hpf(800)
|
|
||||||
.mask("[1!4 0 1 1 0]*2")
|
|
||||||
//.hush()
|
|
||||||
,
|
|
||||||
s("[bd rim:1 [~ bd] rim:2]*2").bank('crate')
|
|
||||||
)
|
|
||||||
.reset("<x@3 x*[4 [8 [16 32]]] x@16>")
|
|
||||||
.late("[0 .002]*16").fast(cps)
|
|
||||||
`;
|
|
||||||
let { pattern } = await evaluate(code);
|
|
||||||
let last;
|
|
||||||
let minLatency = 0.1;
|
|
||||||
const { start, stop } = laserclock({
|
|
||||||
audioContext: ctx,
|
|
||||||
onTick: (data) => {
|
|
||||||
const { currentTime } = data;
|
|
||||||
if (last) {
|
|
||||||
const haps = pattern.queryArc(last, currentTime).filter((h) => h.hasOnset());
|
|
||||||
haps.forEach((hap) => {
|
|
||||||
const start = hap.whole.begin + minLatency;
|
|
||||||
superdough(hap.value, `=${start}`, hap.duration);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
last = currentTime;
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
registerSynthSounds();
|
|
||||||
|
|
||||||
document.getElementById('playButton')?.addEventListener('click', () => start());
|
|
||||||
document.getElementById('stopButton')?.addEventListener('click', () => stop());
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
+102
-122
@@ -25,7 +25,7 @@ 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:
|
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 |
|
| Bass drum, Kick drum | bd |
|
||||||
| Snare drum | sd |
|
| Snare drum | sd |
|
||||||
| Rimshot | rim |
|
| Rimshot | rim |
|
||||||
@@ -34,10 +34,21 @@ For drum sounds, strudel uses the comprehensive [tidal-drum-machines](https://gi
|
|||||||
| Open hi-hat | oh |
|
| Open hi-hat | oh |
|
||||||
| Crash | cr |
|
| Crash | cr |
|
||||||
| Ride | rd |
|
| Ride | rd |
|
||||||
| Shakers (and maracas, cabasas, etc) | sh |
|
|
||||||
| High tom | ht |
|
| High tom | ht |
|
||||||
| Medium tom | mt |
|
| Medium tom | mt |
|
||||||
| Low tom | lt |
|
| 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 |
|
||||||
|
| ----------------------------------- | ------------ |
|
||||||
|
| Shakers (and maracas, cabasas, etc) | sh |
|
||||||
| Cowbell | cb |
|
| Cowbell | cb |
|
||||||
| Tambourine | tb |
|
| Tambourine | tb |
|
||||||
| Other percussions | perc |
|
| 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:
|
... 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:
|
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.
|
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.
|
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
|
# Loading Custom Samples
|
||||||
|
|
||||||
You can load your own sample map using the `samples` function.
|
You can load a non-standard sample map using the `samples` function.
|
||||||
In this example we create a map using sounds from the default sample map:
|
|
||||||
|
|
||||||
<MiniRepl
|
## Loading samples from file URLs
|
||||||
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")`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
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.
|
In this example we assign names `bassdrum`, `hihat` and `snaredrum` to specific audio files on a server:
|
||||||
Compare with this example which uses the same samples, but with different names.
|
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`samples({
|
tune={`samples({
|
||||||
bassdrum: 'bd/BT0AADA.wav',
|
bassdrum: 'bd/BT0AADA.wav',
|
||||||
snaredrum: 'sd/rytm-01-classic.wav',
|
|
||||||
hihat: 'hh27/000_hh27closedhh.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/');
|
}, '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.
|
Note that we can either load a single file, like for `bassdrum` and `hihat`, or a list of files like for `snaredrum`!
|
||||||
- A base URL that comes before each path describing where the sample folder can be found online.
|
As soon as you run the code, your chosen sample names will be listed in `sounds` -> `user`.
|
||||||
- Make sure your base URL ends with a slash, while your sample paths do **not** begin with one!
|
|
||||||
|
|
||||||
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
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`samples({
|
tune={`samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json')
|
||||||
bd: 'bd/BT0AADA.wav',
|
|
||||||
sd: 'sd/rytm-01-classic.wav',
|
|
||||||
hh: 'hh27/000_hh27closedhh.wav',
|
|
||||||
}, 'github:tidalcycles/dirt-samples');
|
|
||||||
s("bd sd bd sd,hh*16")`}
|
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.
|
```json
|
||||||
We can see there are some guitar samples inside the `/samples` folder, so let's try to load them:
|
{
|
||||||
|
"_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
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`samples({
|
tune={`samples('github:tidalcycles/dirt-samples')
|
||||||
g0: 'samples/guitar/guitar_0.wav',
|
s("bd sd bd sd,hh*16")`}
|
||||||
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")`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
## 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
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`samples({
|
tune={`samples('http://localhost:5432/');
|
||||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
|
||||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
n("<0 1 2>").s("swoop smash")`}
|
||||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
|
||||||
}, 'github:tidalcycles/dirt-samples');
|
|
||||||
s("bd:0 bd:1,~ <sd:0 sd:1> ~ sd:0,[hh:0 hh:1]*4")`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
The `:0` `:1` etc. are the indices of the array.
|
The handy thing about `@strudel/sampler` is that it auto-generates the `strudel.json` file based on your folder structure.
|
||||||
The sample number can also be set using `n`:
|
You can see what it generated by going to `http://localhost:5432` with your browser.
|
||||||
|
|
||||||
<MiniRepl
|
Note: You need [NodeJS](https://nodejs.org/) installed on your system for this to work.
|
||||||
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>")`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
In that case, we might load our guitar sample map a different way:
|
## Specifying Pitch
|
||||||
|
|
||||||
<MiniRepl
|
To make sure your samples are in tune when playing them with `note`, you can specify a base pitch like this:
|
||||||
client:idle
|
|
||||||
tune={`samples({
|
|
||||||
guitar: [
|
|
||||||
'samples/guitar/guitar_0.wav',
|
|
||||||
'samples/guitar/guitar_1.wav',
|
|
||||||
'samples/guitar/guitar_2.wav',
|
|
||||||
'samples/guitar/guitar_3.wav',
|
|
||||||
'samples/guitar/guitar_4.wav'
|
|
||||||
]
|
|
||||||
}, 'github:jarmitage/jarmitage.github.io/master/');
|
|
||||||
s("<guitar:0 guitar:1 guitar:2 guitar:3 guitar:4>*2")`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
And as above, we can choose the sample number using `n` for even more flexibility:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`samples({
|
|
||||||
guitar: [
|
|
||||||
'samples/guitar/guitar_0.wav',
|
|
||||||
'samples/guitar/guitar_1.wav',
|
|
||||||
'samples/guitar/guitar_2.wav',
|
|
||||||
'samples/guitar/guitar_3.wav',
|
|
||||||
'samples/guitar/guitar_4.wav'
|
|
||||||
]
|
|
||||||
}, 'github:jarmitage/jarmitage.github.io/master/');
|
|
||||||
n("<0 1 2 3 4>*2").s("guitar")`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
## Pitched Sounds
|
|
||||||
|
|
||||||
For pitched sounds, you can use `note`, just like with synths:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`samples({
|
|
||||||
'gtr': 'gtr/0001_cleanC.wav',
|
|
||||||
}, 'github:tidalcycles/dirt-samples');
|
|
||||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').gain(.5)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Here, the guitar samples will overlap, because they always play till the end.
|
|
||||||
If we want them to behave more like a synth, we can add `clip(1)`:
|
|
||||||
|
|
||||||
<MiniRepl
|
|
||||||
client:idle
|
|
||||||
tune={`samples({
|
|
||||||
'gtr': 'gtr/0001_cleanC.wav',
|
|
||||||
}, 'github:tidalcycles/dirt-samples');
|
|
||||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').clip(1)
|
|
||||||
.gain(.5)`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
## Base Pitch
|
|
||||||
|
|
||||||
If we have 2 samples with different base pitches, we can make them in tune by specifying the pitch like this:
|
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
@@ -261,8 +241,6 @@ note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s("gtr,moog").clip(1)
|
|||||||
.gain(.5)`}
|
.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:
|
We can also declare different samples for different regions of the keyboard:
|
||||||
|
|
||||||
<MiniRepl
|
<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!
|
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
|
## Shabda
|
||||||
|
|
||||||
If you don't want to select samples by hand, there is also the wonderful tool called [shabda](https://shabda.ndre.gr/).
|
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} />
|
<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/).
|
Apart from modifying time, there are ways to [Control Parameters](/functions/value-modifiers/).
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ following example:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
'0 1 2'.add('10 20');
|
'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).
|
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.
|
- `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]"`.
|
- `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]`.
|
- `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.
|
- `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.
|
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.
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third num
|
|||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<QA q="Lösung anzeigen" client:visible>
|
<QA q="Click to see solution" client:visible>
|
||||||
|
|
||||||
`delay("a:b:c")`:
|
`delay("a:b:c")`:
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ Add a delay too!
|
|||||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||||
.scale("D4:minor").sound("gm_accordion:2")
|
.scale("D4:minor").sound("gm_accordion:2")
|
||||||
.room(2).gain(.5)
|
room(2).gain(.5)
|
||||||
)`}
|
)`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ The whole automation will now take 8 cycles to repeat.
|
|||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
| name | example |
|
| name | example |
|
||||||
| ----- | ---------------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||||
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
|
| 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>")`} /> |
|
| 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")`} /> |
|
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
|
||||||
@@ -316,6 +316,7 @@ The whole automation will now take 8 cycles to repeat.
|
|||||||
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.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")`} /> |
|
| 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>")`} /> |
|
| 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))`} /> |
|
| 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).
|
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
|
||||||
|
|||||||
@@ -147,35 +147,43 @@ We will see more ways to combine patterns later..
|
|||||||
|
|
||||||
**Divide sequences with `/` to slow them down**
|
**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 />
|
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
|
||||||
|
|
||||||
<Box>
|
<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.
|
Try adding more notes inside the brackets and notice how it gets faster.
|
||||||
|
|
||||||
</Box>
|
</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 />
|
<MiniRepl client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
|
||||||
|
|
||||||
<Box>
|
<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>
|
</Box>
|
||||||
|
|
||||||
**Play one sequence per cycle**
|
**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
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
|
|||||||
@@ -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:
|
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
|
- `bd` = **b**ass **d**rum
|
||||||
- `sd` = **s**nare **d**rum
|
- `sd` = **s**nare **d**rum
|
||||||
- `rim` = **rim**shot
|
- `rim` = **rim**shot
|
||||||
- `hh` = **h**i**h**at
|
- `hh` = **h**i**h**at
|
||||||
- `oh` = **o**pen **h**ihat
|
- `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!
|
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 />
|
<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>
|
<Box>
|
||||||
|
|
||||||
cpm = cycles per minute
|
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>
|
</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]**
|
**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>
|
<Box>
|
||||||
|
|
||||||
@@ -163,7 +199,7 @@ Similar to the whole sequence, the content of a sub-sequence will be squished to
|
|||||||
|
|
||||||
**Multiplication: Speed things up**
|
**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**
|
**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:
|
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:
|
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
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound(\`bd*2, ~ cp,
|
tune={`sound(\`bd*2, - cp,
|
||||||
~ ~ ~ oh, hh*4,
|
- - - oh, hh*4,
|
||||||
[~ casio]*2\`)`}
|
[- casio]*2\`)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -232,13 +268,14 @@ This is shorter and more readable than:
|
|||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
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 |
|
| Concept | Syntax | Example |
|
||||||
| ----------------- | -------- | ----------------------------------------------------------------------- |
|
| ----------------- | -------- | ----------------------------------------------------------------------- |
|
||||||
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
|
| 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")`} /> |
|
| 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-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
|
||||||
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> |
|
| 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")`} /> |
|
| 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 |
|
| Name | Description | Example |
|
||||||
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
|
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
|
||||||
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd")`} /> |
|
| 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")`} /> |
|
| 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)`} /> |
|
| 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")`} /> |
|
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2 0 6 3 2").sound("jazz")`} /> |
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -266,7 +303,7 @@ The Mini-Notation is usually used inside some function. These are the functions
|
|||||||
|
|
||||||
**Classic house**
|
**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>
|
<Box>
|
||||||
|
|
||||||
@@ -283,7 +320,7 @@ We Will Rock you
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
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)`}
|
.bank("RolandCompurhythm1000").cpm(120/2)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
@@ -293,10 +330,10 @@ We Will Rock you
|
|||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound(\`
|
tune={`sound(\`
|
||||||
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
|
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
|
||||||
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
|
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
|
||||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
|
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
|
||||||
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
|
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
|
||||||
\`).cpm(90/4)`}
|
\`).cpm(90/4)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
@@ -306,10 +343,10 @@ We Will Rock you
|
|||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`sound(\`
|
tune={`sound(\`
|
||||||
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
|
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
|
||||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
|
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
|
||||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
|
[- - - - ] [cp - - - ] [- - - - ] [~ cp - - ],
|
||||||
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
|
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
|
||||||
\`).bank("RolandTR808").cpm(88/4)`}
|
\`).bank("RolandTR808").cpm(88/4)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
@@ -319,9 +356,9 @@ We Will Rock you
|
|||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`s(\`jazz*2,
|
tune={`s(\`jazz*2,
|
||||||
insect [crow metal] ~ ~,
|
insect [crow metal] - -,
|
||||||
~ space:4 ~ space:1,
|
- space:4 - space:1,
|
||||||
~ wind\`)
|
- wind\`)
|
||||||
.cpm(100/2)`}
|
.cpm(100/2)`}
|
||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -155,12 +155,13 @@ In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying.
|
|||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
off is also useful for sounds:
|
off is also useful for modifying other sounds, and can even be nested:
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:visible
|
client:visible
|
||||||
tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1")
|
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 |
|
| name | description | example |
|
||||||
|
|||||||
@@ -36,7 +36,12 @@ export function WelcomeTab({ context }) {
|
|||||||
<a href="https://tidalcycles.org/" target="_blank">
|
<a href="https://tidalcycles.org/" target="_blank">
|
||||||
tidalcycles
|
tidalcycles
|
||||||
</a>
|
</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">
|
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||||
github
|
github
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ export async function initCode() {
|
|||||||
// load code from url hash (either short hash from database or decode long hash)
|
// load code from url hash (either short hash from database or decode long hash)
|
||||||
try {
|
try {
|
||||||
const initialUrl = window.location.href;
|
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] || '';
|
const codeParam = window.location.href.split('#')[1] || '';
|
||||||
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
|
|
||||||
if (codeParam) {
|
if (codeParam) {
|
||||||
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
|
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
|
||||||
return hash2code(codeParam);
|
return hash2code(codeParam);
|
||||||
} else if (hash) {
|
} else if (hash) {
|
||||||
|
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
|
||||||
return supabase
|
return supabase
|
||||||
.from('code_v1')
|
.from('code_v1')
|
||||||
.select('code')
|
.select('code')
|
||||||
|
|||||||
Reference in New Issue
Block a user