mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b8b0154da | |||
| 2b5c653c56 | |||
| 244d91cc8d | |||
| 3f368d6438 | |||
| 36ee4cfc98 |
+1
-2
@@ -21,5 +21,4 @@ vite.config.js
|
||||
/src-tauri/target/**/*
|
||||
reverbGen.mjs
|
||||
hydra.mjs
|
||||
jsdoc-synonyms.js
|
||||
packages/node/pattern.mjs
|
||||
jsdoc-synonyms.js
|
||||
@@ -11,4 +11,3 @@ pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
**/dev-dist
|
||||
website/.astro
|
||||
packages/node/pattern.mjs
|
||||
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
import { TimeSpan } from './timespan.mjs';
|
||||
import { removeUndefineds } from './util.mjs';
|
||||
|
||||
// Returns the start of the cycle.
|
||||
Fraction.prototype.sam = function () {
|
||||
@@ -48,10 +47,6 @@ Fraction.prototype.eq = function (other) {
|
||||
return this.compare(other) == 0;
|
||||
};
|
||||
|
||||
Fraction.prototype.ne = function (other) {
|
||||
return this.compare(other) != 0;
|
||||
};
|
||||
|
||||
Fraction.prototype.max = function (other) {
|
||||
return this.gt(other) ? this : other;
|
||||
};
|
||||
@@ -65,22 +60,6 @@ Fraction.prototype.min = function (other) {
|
||||
return this.lt(other) ? this : other;
|
||||
};
|
||||
|
||||
Fraction.prototype.mulmaybe = function (other) {
|
||||
return other !== undefined ? this.mul(other) : undefined;
|
||||
};
|
||||
|
||||
Fraction.prototype.divmaybe = function (other) {
|
||||
return other !== undefined ? this.div(other) : undefined;
|
||||
};
|
||||
|
||||
Fraction.prototype.addmaybe = function (other) {
|
||||
return other !== undefined ? this.add(other) : undefined;
|
||||
};
|
||||
|
||||
Fraction.prototype.submaybe = function (other) {
|
||||
return other !== undefined ? this.sub(other) : undefined;
|
||||
};
|
||||
|
||||
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||
// return this.toFraction(excludeWhole);
|
||||
return this.s * this.n + '/' + this.d;
|
||||
@@ -106,24 +85,11 @@ const fraction = (n) => {
|
||||
};
|
||||
|
||||
export const gcd = (...fractions) => {
|
||||
fractions = removeUndefineds(fractions);
|
||||
if (fractions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||
};
|
||||
|
||||
export const lcm = (...fractions) => {
|
||||
fractions = removeUndefineds(fractions);
|
||||
if (fractions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fractions.reduce(
|
||||
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
|
||||
fraction(1),
|
||||
);
|
||||
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
|
||||
};
|
||||
|
||||
fraction._original = Fraction;
|
||||
|
||||
+88
-267
@@ -10,29 +10,12 @@ import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
import { unionWithObj } from './value.mjs';
|
||||
|
||||
import {
|
||||
uniqsortr,
|
||||
removeUndefineds,
|
||||
flatten,
|
||||
id,
|
||||
listRange,
|
||||
curry,
|
||||
_mod,
|
||||
numeralArgs,
|
||||
parseNumeral,
|
||||
pairs,
|
||||
} from './util.mjs';
|
||||
import { compose, removeUndefineds, flatten, id, listRange, curry, _mod, numeralArgs, parseNumeral } from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
let stringParser;
|
||||
|
||||
let __tactus = true;
|
||||
|
||||
export const calculateTactus = function (x) {
|
||||
__tactus = x ? true : false;
|
||||
};
|
||||
|
||||
// parser is expected to turn a string into a pattern
|
||||
// if set, the reify function will parse all strings with it
|
||||
// intended to use with mini to automatically interpret all strings as mini notation
|
||||
@@ -48,16 +31,16 @@ export class Pattern {
|
||||
*/
|
||||
constructor(query, tactus = undefined) {
|
||||
this.query = query;
|
||||
this._Pattern = true; // this property is used to detectinstance of another Pattern
|
||||
this.tactus = tactus; // in terms of number of steps per cycle
|
||||
this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
|
||||
this.__tactus = tactus; // in terms of number of beats per cycle
|
||||
}
|
||||
|
||||
get tactus() {
|
||||
return this.__tactus;
|
||||
return this.__tactus ?? Fraction(1);
|
||||
}
|
||||
|
||||
set tactus(tactus) {
|
||||
this.__tactus = tactus === undefined ? undefined : Fraction(tactus);
|
||||
this.__tactus = Fraction(tactus);
|
||||
}
|
||||
|
||||
setTactus(tactus) {
|
||||
@@ -65,17 +48,6 @@ export class Pattern {
|
||||
return this;
|
||||
}
|
||||
|
||||
withTactus(f) {
|
||||
if (!__tactus) {
|
||||
return this;
|
||||
}
|
||||
return new Pattern(this.query, this.tactus === undefined ? undefined : f(this.tactus));
|
||||
}
|
||||
|
||||
get hasTactus() {
|
||||
return this.tactus !== undefined;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Haskell-style functor, applicative and monadic operations
|
||||
|
||||
@@ -158,9 +130,7 @@ export class Pattern {
|
||||
return span_a.intersection_e(span_b);
|
||||
};
|
||||
const result = pat_func.appWhole(whole_func, pat_val);
|
||||
if (__tactus) {
|
||||
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
|
||||
}
|
||||
result.tactus = lcm(pat_val.tactus, pat_func.tactus);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -272,12 +242,12 @@ export class Pattern {
|
||||
}
|
||||
|
||||
outerBind(func) {
|
||||
return this.bindWhole((a) => a, func).setTactus(this.tactus);
|
||||
return this.bindWhole((a) => a, func);
|
||||
}
|
||||
|
||||
outerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from outer haps.
|
||||
// taken from inner haps.
|
||||
return this.outerBind(id);
|
||||
}
|
||||
|
||||
@@ -1167,24 +1137,24 @@ function _composeOp(a, b, func) {
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
export const pm = s_polymeter;
|
||||
export const pm = polymeter;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||
// Pattern.prototype.factories = {
|
||||
// pure,
|
||||
// stack,
|
||||
// slowcat,
|
||||
// fastcat,
|
||||
// cat,
|
||||
// timecat,
|
||||
// sequence,
|
||||
// seq,
|
||||
// polymeter,
|
||||
// pm,
|
||||
// polyrhythm,
|
||||
// pr,
|
||||
// };
|
||||
Pattern.prototype.factories = {
|
||||
pure,
|
||||
stack,
|
||||
slowcat,
|
||||
fastcat,
|
||||
cat,
|
||||
timecat,
|
||||
sequence,
|
||||
seq,
|
||||
polymeter,
|
||||
pm,
|
||||
polyrhythm,
|
||||
pr,
|
||||
};
|
||||
// the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)
|
||||
|
||||
// Elemental patterns
|
||||
@@ -1196,7 +1166,7 @@ export const pm = s_polymeter;
|
||||
* @example
|
||||
* gap(3) // "~@3"
|
||||
*/
|
||||
export const gap = (tactus) => new Pattern(() => [], tactus);
|
||||
export const gap = (tactus) => new Pattern(() => [], Fraction(tactus));
|
||||
|
||||
/**
|
||||
* Does absolutely nothing..
|
||||
@@ -1265,9 +1235,7 @@ export function stack(...pats) {
|
||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
|
||||
const result = new Pattern(query);
|
||||
if (__tactus) {
|
||||
result.tactus = lcm(...pats.map((pat) => pat.tactus));
|
||||
}
|
||||
result.tactus = lcm(...pats.map((pat) => pat.tactus));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1280,20 +1248,20 @@ function _stackWith(func, pats) {
|
||||
return pats[0];
|
||||
}
|
||||
const [left, ...right] = pats.map((pat) => pat.tactus);
|
||||
const tactus = __tactus ? left.maximum(...right) : undefined;
|
||||
const tactus = left.maximum(...right);
|
||||
return stack(...func(tactus, pats));
|
||||
}
|
||||
|
||||
export function stackLeft(...pats) {
|
||||
return _stackWith(
|
||||
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(pat, gap(tactus.sub(pat.tactus))))),
|
||||
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(pat, gap(tactus.sub(pat.tactus))))),
|
||||
pats,
|
||||
);
|
||||
}
|
||||
|
||||
export function stackRight(...pats) {
|
||||
return _stackWith(
|
||||
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : s_cat(gap(tactus.sub(pat.tactus)), pat))),
|
||||
(tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(gap(tactus.sub(pat.tactus)), pat))),
|
||||
pats,
|
||||
);
|
||||
}
|
||||
@@ -1306,7 +1274,7 @@ export function stackCentre(...pats) {
|
||||
return pat;
|
||||
}
|
||||
const g = gap(tactus.sub(pat.tactus).div(2));
|
||||
return s_cat(g, pat, g);
|
||||
return timecat(g, pat, g);
|
||||
}),
|
||||
pats,
|
||||
);
|
||||
@@ -1320,7 +1288,7 @@ export function stackBy(by, ...pats) {
|
||||
left: stackLeft,
|
||||
right: stackRight,
|
||||
expand: stack,
|
||||
repeat: (...args) => s_polymeterSteps(tactus, ...args),
|
||||
repeat: (...args) => polymeterSteps(tactus, ...args),
|
||||
};
|
||||
return by
|
||||
.inhabit(lookup)
|
||||
@@ -1360,7 +1328,7 @@ export function slowcat(...pats) {
|
||||
const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
|
||||
return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
|
||||
};
|
||||
const tactus = __tactus ? lcm(...pats.map((x) => x.tactus)) : undefined;
|
||||
const tactus = lcm(...pats.map((x) => x.tactus));
|
||||
return new Pattern(query).splitQueries().setTactus(tactus);
|
||||
}
|
||||
|
||||
@@ -1406,7 +1374,7 @@ export function cat(...pats) {
|
||||
export function arrange(...sections) {
|
||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||
return s_cat(...sections).slow(total);
|
||||
return timecat(...sections).slow(total);
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
@@ -1486,7 +1454,7 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
* @noAutocomplete
|
||||
*
|
||||
*/
|
||||
export function register(name, func, patternify = true, preserveTactus = false, join = (x) => x.innerJoin()) {
|
||||
export function register(name, func, patternify = true, preserveTactus = false) {
|
||||
if (Array.isArray(name)) {
|
||||
const result = {};
|
||||
for (const name_item of name) {
|
||||
@@ -1523,7 +1491,7 @@ export function register(name, func, patternify = true, preserveTactus = false,
|
||||
return func(...args, pat);
|
||||
};
|
||||
mapFn = curry(mapFn, null, arity - 1);
|
||||
result = join(right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)));
|
||||
result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||
}
|
||||
}
|
||||
if (preserveTactus) {
|
||||
@@ -1567,11 +1535,6 @@ export function register(name, func, patternify = true, preserveTactus = false,
|
||||
return curry(pfunc, null, arity);
|
||||
}
|
||||
|
||||
// Like register, but defaults to stepJoin
|
||||
function stepRegister(name, func, patternify = true, preserveTactus = false, join = (x) => x.stepJoin()) {
|
||||
return register(name, func, patternify, preserveTactus, join);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Numerical transformations
|
||||
|
||||
@@ -1779,9 +1742,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
|
||||
*/
|
||||
export const ply = register('ply', function (factor, pat) {
|
||||
const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
|
||||
if (__tactus) {
|
||||
result.tactus = Fraction(factor).mulmaybe(pat.tactus);
|
||||
}
|
||||
result.tactus = pat.tactus.mul(factor);
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -1796,19 +1757,16 @@ export const ply = register('ply', function (factor, pat) {
|
||||
* @example
|
||||
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
|
||||
*/
|
||||
export const { fast, density } = register(
|
||||
['fast', 'density'],
|
||||
function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
factor = Fraction(factor);
|
||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||
return fastQuery.withHapTime((t) => t.div(factor)).setTactus(pat.tactus);
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
factor = Fraction(factor);
|
||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||
const result = fastQuery.withHapTime((t) => t.div(factor));
|
||||
result.tactus = factor.mul(pat.tactus);
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Both speeds up the pattern (like 'fast') and the sample playback (like 'speed').
|
||||
@@ -1976,7 +1934,7 @@ export const zoom = register('zoom', function (s, e, pat) {
|
||||
return nothing;
|
||||
}
|
||||
const d = e.sub(s);
|
||||
const tactus = __tactus ? pat.tactus.mulmaybe(d) : undefined;
|
||||
const tactus = pat.tactus.mul(d);
|
||||
return pat
|
||||
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||
@@ -2018,24 +1976,6 @@ export const segment = register('segment', function (rate, pat) {
|
||||
return pat.struct(pure(true)._fast(rate)).setTactus(rate);
|
||||
});
|
||||
|
||||
/**
|
||||
* The function `swingBy x n` breaks each cycle into `n` slices, and then delays events in the second half of each slice by the amount `x`, which is relative to the size of the (half) slice. So if `x` is 0 it does nothing, `0.5` delays for half the note duration, and 1 will wrap around to doing nothing again. The end result is a shuffle or swing-like rhythm
|
||||
* @param {number} subdivision
|
||||
* @param {number} offset
|
||||
* @example
|
||||
* s("hh*8").swingBy(1/3, 4)
|
||||
*/
|
||||
export const swingBy = register('swingBy', (swing, n, pat) => pat.inside(n, late(seq(0, swing / 2))));
|
||||
|
||||
/**
|
||||
* Shorthand for swingBy with 1/3:
|
||||
* @param {number} subdivision
|
||||
* @example
|
||||
* s("hh*8").swing(4)
|
||||
* // s("hh*8").swingBy(1/3, 4)
|
||||
*/
|
||||
export const swing = register('swing', (n, pat) => pat.swingBy(1 / 3, n));
|
||||
|
||||
/**
|
||||
* Swaps 1s and 0s in a binary pattern.
|
||||
* @name invert
|
||||
@@ -2191,7 +2131,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
|
||||
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
|
||||
const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by })));
|
||||
|
||||
return stack(left, right).setTactus(__tactus ? lcm(left.tactus, right.tactus) : undefined);
|
||||
return stack(left, right).setTactus(lcm(left.tactus, right.tactus));
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -2310,13 +2250,7 @@ export const { iterBack, iterback } = register(
|
||||
export const { repeatCycles } = register(
|
||||
'repeatCycles',
|
||||
function (n, pat) {
|
||||
return new Pattern(function (state) {
|
||||
const cycle = state.span.begin.sam();
|
||||
const source_cycle = cycle.div(n).sam();
|
||||
const delta = cycle.sub(source_cycle);
|
||||
state = state.withSpan((span) => span.withTime((spant) => spant.sub(delta)));
|
||||
return pat.query(state).map((hap) => hap.withSpan((span) => span.withTime((spant) => spant.add(delta))));
|
||||
}).splitQueries();
|
||||
return slowcat(...Array(n).fill(pat));
|
||||
},
|
||||
true,
|
||||
true,
|
||||
@@ -2423,69 +2357,14 @@ Pattern.prototype.tag = function (tag) {
|
||||
// Tactus-related functions, i.e. ones that do stepwise
|
||||
// transformations
|
||||
|
||||
Pattern.prototype.stepJoin = function () {
|
||||
const pp = this;
|
||||
const first_t = s_cat(..._retime(_slices(pp.queryArc(0, 1)))).tactus;
|
||||
const q = function (state) {
|
||||
const shifted = pp.early(state.span.begin.sam());
|
||||
const haps = shifted.query(state.setSpan(new TimeSpan(Fraction(0), Fraction(1))));
|
||||
const pat = s_cat(..._retime(_slices(haps)));
|
||||
return pat.query(state);
|
||||
};
|
||||
return new Pattern(q, first_t);
|
||||
};
|
||||
|
||||
export function _retime(timedHaps) {
|
||||
const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0));
|
||||
const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce(
|
||||
(a, b) => a.add(b),
|
||||
Fraction(0),
|
||||
);
|
||||
const total_tactus = occupied_perc.eq(0) ? undefined : occupied_tactus.div(occupied_perc);
|
||||
function adjust(dur, pat) {
|
||||
if (pat.tactus === undefined) {
|
||||
return [dur.mulmaybe(total_tactus), pat];
|
||||
}
|
||||
return [pat.tactus, pat];
|
||||
}
|
||||
return timedHaps.map((x) => adjust(...x));
|
||||
}
|
||||
|
||||
export function _slices(haps) {
|
||||
// slices evs = map (\s -> ((snd s - fst s), stack $ map value $ fit s evs))
|
||||
// $ pairs $ sort $ nubOrd $ 0:1:concatMap (\ev -> start (part ev):stop (part ev):[]) evs
|
||||
const breakpoints = flatten(haps.map((hap) => [hap.part.begin, hap.part.end]));
|
||||
const unique = uniqsortr([Fraction(0), Fraction(1), ...breakpoints]);
|
||||
const slicespans = pairs(unique);
|
||||
return slicespans.map((s) => [
|
||||
s[1].sub(s[0]),
|
||||
stack(..._fitslice(new TimeSpan(...s), haps).map((x) => x.value.withHap((h) => h.setContext(h.combineContext(x))))),
|
||||
]);
|
||||
}
|
||||
|
||||
export function _fitslice(span, haps) {
|
||||
return removeUndefineds(haps.map((hap) => _match(span, hap)));
|
||||
}
|
||||
|
||||
export function _match(span, hap_p) {
|
||||
const subspan = span.intersection(hap_p.part);
|
||||
if (subspan == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new Hap(hap_p.whole, subspan, hap_p.value, hap_p.context);
|
||||
}
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given number of steps per cycle (aka tactus).
|
||||
* *EXPERIMENTAL* - Speeds a pattern up or down, to fit to the given metrical 'tactus'.
|
||||
* @example
|
||||
* s("bd sd cp").steps(4)
|
||||
* s("bd sd cp").toTactus(4)
|
||||
* // The same as s("{bd sd cp}%4")
|
||||
*/
|
||||
export const steps = register('steps', function (targetTactus, pat) {
|
||||
if (pat.tactus === undefined) {
|
||||
return pat;
|
||||
}
|
||||
if (pat.tactus.eq(Fraction(0))) {
|
||||
export const toTactus = register('toTactus', function (targetTactus, pat) {
|
||||
if (pat.tactus.eq(0)) {
|
||||
// avoid divide by zero..
|
||||
return nothing;
|
||||
}
|
||||
@@ -2518,14 +2397,14 @@ export function _polymeterListSteps(steps, ...args) {
|
||||
* Aligns one or more given patterns to the given number of steps per cycle.
|
||||
* This relies on patterns having coherent number of steps per cycle,
|
||||
*
|
||||
* @name s_polymeterSteps
|
||||
* @name polymeterSteps
|
||||
* @param {number} steps how many items are placed in one cycle
|
||||
* @param {any[]} patterns one or more patterns
|
||||
* @example
|
||||
* // the same as "{c d, e f g}%4"
|
||||
* s_polymeterSteps(4, "c d", "e f g")
|
||||
* polymeterSteps(4, "c d", "e f g")
|
||||
*/
|
||||
export function s_polymeterSteps(steps, ...args) {
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
@@ -2534,7 +2413,7 @@ export function s_polymeterSteps(steps, ...args) {
|
||||
return _polymeterListSteps(steps, ...args);
|
||||
}
|
||||
|
||||
return s_polymeter(...args).steps(steps);
|
||||
return polymeter(...args).toTactus(steps);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2542,25 +2421,19 @@ export function s_polymeterSteps(steps, ...args) {
|
||||
* @synonyms pm
|
||||
* @example
|
||||
* // The same as "{c eb g, c2 g2}"
|
||||
* s_polymeter("c eb g", "c2 g2")
|
||||
* polymeter("c eb g", "c2 g2")
|
||||
*
|
||||
*/
|
||||
export function s_polymeter(...args) {
|
||||
export function polymeter(...args) {
|
||||
if (Array.isArray(args[0])) {
|
||||
// Support old behaviour
|
||||
return _polymeterListSteps(0, ...args);
|
||||
}
|
||||
|
||||
// TODO currently ignoring arguments without tactus...
|
||||
args = args.filter((arg) => arg.hasTactus);
|
||||
|
||||
if (args.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
const tactus = args[0].tactus;
|
||||
if (tactus.eq(Fraction(0))) {
|
||||
return nothing;
|
||||
}
|
||||
const [head, ...tail] = args;
|
||||
|
||||
const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus))));
|
||||
@@ -2570,36 +2443,18 @@ export function s_polymeter(...args) {
|
||||
|
||||
/** Sequences patterns like `seq`, but each pattern has a length, relative to the whole.
|
||||
* This length can either be provided as a [length, pattern] pair, or inferred from
|
||||
* the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`.
|
||||
* the pattern's 'tactus', generally inferred by the mininotation.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* s_cat([3,"e3"],[1, "g3"]).note()
|
||||
* timecat([3,"e3"],[1, "g3"]).note()
|
||||
* // the same as "e3@3 g3".note()
|
||||
* @example
|
||||
* s_cat("bd sd cp","hh hh").sound()
|
||||
* timecat("bd sd cp","hh hh").sound()
|
||||
* // the same as "bd sd cp hh hh".sound()
|
||||
*/
|
||||
export function s_cat(...timepats) {
|
||||
if (timepats.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
export function timecat(...timepats) {
|
||||
const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]);
|
||||
timepats = timepats.map(findtactus);
|
||||
if (timepats.find((x) => x[0] === undefined)) {
|
||||
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
|
||||
if (times.length === 0) {
|
||||
return fastcat(...timepats.map((x) => x[1]));
|
||||
}
|
||||
if (times.length === timepats.length) {
|
||||
return nothing;
|
||||
}
|
||||
const avg = times.reduce((a, b) => a.add(b), Fraction(0)).div(times.length);
|
||||
for (let timepat of timepats) {
|
||||
if (timepat[0] === undefined) {
|
||||
timepat[0] = avg;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (timepats.length == 1) {
|
||||
const result = reify(timepats[0][1]);
|
||||
result.tactus = timepats[0][0];
|
||||
@@ -2622,19 +2477,18 @@ export function s_cat(...timepats) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Aliases for `s_cat` */
|
||||
export const timecat = s_cat;
|
||||
export const timeCat = s_cat;
|
||||
/** Deprecated alias for `timecat` */
|
||||
export const timeCat = timecat;
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL* - Concatenates patterns stepwise, according to their 'tactus'.
|
||||
* Similar to `s_cat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
|
||||
* Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* s_alt(["bd cp", "mt"], "bd").sound()
|
||||
* stepcat(["bd cp", "mt"], "bd").sound()
|
||||
*/
|
||||
export function s_alt(...groups) {
|
||||
export function stepcat(...groups) {
|
||||
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
|
||||
|
||||
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
|
||||
@@ -2643,9 +2497,9 @@ export function s_alt(...groups) {
|
||||
for (let cycle = 0; cycle < cycles; ++cycle) {
|
||||
result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length])));
|
||||
}
|
||||
result = result.filter((x) => x.hasTactus && x.tactus > 0);
|
||||
result = result.filter((x) => x.tactus > 0);
|
||||
const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||
result = s_cat(...result);
|
||||
result = timecat(...result);
|
||||
result.tactus = tactus;
|
||||
return result;
|
||||
}
|
||||
@@ -2653,10 +2507,7 @@ export function s_alt(...groups) {
|
||||
/**
|
||||
* *EXPERIMENTAL* - Retains the given number of steps in a pattern (and dropping the rest), according to its 'tactus'.
|
||||
*/
|
||||
export const s_add = stepRegister('s_add', function (i, pat) {
|
||||
if (!pat.hasTactus) {
|
||||
return nothing;
|
||||
}
|
||||
export const stepwax = register('stepwax', function (i, pat) {
|
||||
if (pat.tactus.lte(0)) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -2684,40 +2535,23 @@ export const s_add = stepRegister('s_add', function (i, pat) {
|
||||
/**
|
||||
* *EXPERIMENTAL* - Removes the given number of steps from a pattern, according to its 'tactus'.
|
||||
*/
|
||||
export const s_sub = stepRegister('s_sub', function (i, pat) {
|
||||
if (!pat.hasTactus) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
export const stepwane = register('stepwane', function (i, pat) {
|
||||
i = Fraction(i);
|
||||
if (i.lt(0)) {
|
||||
return pat.s_add(Fraction(0).sub(pat.tactus.add(i)));
|
||||
return pat.stepwax(Fraction(0).sub(pat.tactus.add(i)));
|
||||
}
|
||||
return pat.s_add(pat.tactus.sub(i));
|
||||
});
|
||||
|
||||
export const s_expand = stepRegister('s_expand', function (factor, pat) {
|
||||
return pat.withTactus((t) => t.mul(Fraction(factor)));
|
||||
});
|
||||
|
||||
export const s_contract = stepRegister('s_contract', function (factor, pat) {
|
||||
return pat.withTactus((t) => t.div(Fraction(factor)));
|
||||
return pat.stepwax(pat.tactus.sub(i));
|
||||
});
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL*
|
||||
*/
|
||||
Pattern.prototype.s_taperlist = function (amount, times) {
|
||||
Pattern.prototype.taperlist = function (amount, times) {
|
||||
const pat = this;
|
||||
|
||||
if (!pat.hasTactus) {
|
||||
return [pat];
|
||||
}
|
||||
|
||||
times = times - 1;
|
||||
|
||||
if (times === 0) {
|
||||
return [pat];
|
||||
return pat;
|
||||
}
|
||||
|
||||
const list = [];
|
||||
@@ -2734,33 +2568,23 @@ Pattern.prototype.s_taperlist = function (amount, times) {
|
||||
}
|
||||
return list;
|
||||
};
|
||||
export const s_taperlist = (amount, times, pat) => pat.s_taperlist(amount, times);
|
||||
export const taperlist = (amount, times, pat) => pat.taperlist(amount, times);
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL*
|
||||
*/
|
||||
export const s_taper = register(
|
||||
's_taper',
|
||||
function (amount, times, pat) {
|
||||
if (!pat.hasTactus) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const list = pat.s_taperlist(amount, times);
|
||||
const result = s_cat(...list);
|
||||
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||
return result;
|
||||
},
|
||||
true,
|
||||
false,
|
||||
(x) => x.stepJoin(),
|
||||
);
|
||||
export const steptaper = register('steptaper', function (amount, times, pat) {
|
||||
const list = pat.taperlist(amount, times);
|
||||
const result = timecat(...list);
|
||||
result.tactus = list.reduce((a, b) => a.add(b.tactus), Fraction(0));
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* *EXPERIMENTAL*
|
||||
*/
|
||||
Pattern.prototype.s_tour = function (...many) {
|
||||
return s_cat(
|
||||
Pattern.prototype.steptour = function (...many) {
|
||||
return stepcat(
|
||||
...[].concat(
|
||||
...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
|
||||
this,
|
||||
@@ -2769,8 +2593,8 @@ Pattern.prototype.s_tour = function (...many) {
|
||||
);
|
||||
};
|
||||
|
||||
export const s_tour = function (pat, ...many) {
|
||||
return pat.s_tour(...many);
|
||||
export const steptour = function (pat, ...many) {
|
||||
return pat.steptour(...many);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -2797,7 +2621,7 @@ export const chop = register('chop', function (n, pat) {
|
||||
const func = function (o) {
|
||||
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
|
||||
};
|
||||
return pat.squeezeBind(func).setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
|
||||
return pat.squeezeBind(func).setTactus(pat.tactus.mul(n));
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -2812,10 +2636,7 @@ export const striate = register('striate', function (n, pat) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
const slicePat = slowcat(...slice_objects);
|
||||
return pat
|
||||
.set(slicePat)
|
||||
._fast(n)
|
||||
.setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
|
||||
return pat.set(slicePat)._fast(n);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,12 +54,10 @@ export function repl({
|
||||
const scheduler =
|
||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||
let pPatterns = {};
|
||||
let anonymousIndex = 0;
|
||||
let allTransform;
|
||||
|
||||
const hush = function () {
|
||||
pPatterns = {};
|
||||
anonymousIndex = 0;
|
||||
allTransform = undefined;
|
||||
return silence;
|
||||
};
|
||||
@@ -84,15 +82,6 @@ export function repl({
|
||||
// set pattern methods that use this repl via closure
|
||||
const injectPatternMethods = () => {
|
||||
Pattern.prototype.p = function (id) {
|
||||
if (id.startsWith('_') || id.endsWith('_')) {
|
||||
// allows muting a pattern x with x_ or _x
|
||||
return silence;
|
||||
}
|
||||
if (id === '$') {
|
||||
// allows adding anonymous patterns with $:
|
||||
id = `$${anonymousIndex}`;
|
||||
anonymousIndex++;
|
||||
}
|
||||
pPatterns[id] = this;
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -486,10 +486,8 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||
* @example
|
||||
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||
*/
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||
|
||||
export const wrandcat = wchooseCycles;
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
cat,
|
||||
sequence,
|
||||
palindrome,
|
||||
s_polymeter,
|
||||
s_polymeterSteps,
|
||||
polymeter,
|
||||
polymeterSteps,
|
||||
polyrhythm,
|
||||
silence,
|
||||
fast,
|
||||
@@ -47,11 +47,6 @@ import {
|
||||
time,
|
||||
run,
|
||||
pick,
|
||||
stackLeft,
|
||||
stackRight,
|
||||
stackCentre,
|
||||
s_cat,
|
||||
calculateTactus,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
@@ -608,18 +603,18 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('s_polymeter()', () => {
|
||||
describe('polymeter()', () => {
|
||||
it('Can layer up cycles, stepwise, with lists', () => {
|
||||
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||
);
|
||||
|
||||
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can layer up cycles, stepwise, with weighted patterns', () => {
|
||||
sameFirst(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
||||
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1128,8 +1123,8 @@ describe('Pattern', () => {
|
||||
it('Is correctly preserved/calculated through transformations', () => {
|
||||
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
|
||||
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
|
||||
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4));
|
||||
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4));
|
||||
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(16));
|
||||
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16));
|
||||
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
|
||||
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
|
||||
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
|
||||
@@ -1141,72 +1136,30 @@ describe('Pattern', () => {
|
||||
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
|
||||
Fraction(4),
|
||||
);
|
||||
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
|
||||
expect(
|
||||
pure((x) => x + 1)
|
||||
.setTactus(3)
|
||||
.appBoth(pure(1).setTactus(2)).tactus,
|
||||
).toStrictEqual(Fraction(6));
|
||||
expect(
|
||||
pure((x) => x + 1)
|
||||
.setTactus(undefined)
|
||||
.appBoth(pure(1).setTactus(2)).tactus,
|
||||
).toStrictEqual(Fraction(2));
|
||||
expect(
|
||||
pure((x) => x + 1)
|
||||
.setTactus(3)
|
||||
.appBoth(pure(1).setTactus(undefined)).tactus,
|
||||
).toStrictEqual(Fraction(3));
|
||||
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
|
||||
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
|
||||
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
|
||||
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
|
||||
// maybe this should double when they are either all even or all odd
|
||||
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
|
||||
expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6));
|
||||
expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined);
|
||||
expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2));
|
||||
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe('s_cat', () => {
|
||||
it('can cat', () => {
|
||||
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
|
||||
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
|
||||
});
|
||||
it('calculates undefined tactuses as the average', () => {
|
||||
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
|
||||
});
|
||||
});
|
||||
describe('s_taper', () => {
|
||||
describe('steptaper', () => {
|
||||
it('can taper', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).steptaper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
|
||||
});
|
||||
it('can taper backwards', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
|
||||
expect(
|
||||
sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('s_add and s_sub', () => {
|
||||
it('can add from the left', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
|
||||
describe('wax and wane, left', () => {
|
||||
it('can wax from the left', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
|
||||
});
|
||||
it('can sub to the left', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
|
||||
it('can wane to the left', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
|
||||
});
|
||||
it('can add from the right', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
|
||||
it('can wax from the right', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
|
||||
});
|
||||
it('can sub to the right', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
|
||||
});
|
||||
it('can subtract nothing', () => {
|
||||
expect(sameFirst(pure('a').s_sub(0), pure('a')));
|
||||
});
|
||||
it('can subtract nothing, repeatedly', () => {
|
||||
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
|
||||
for (var i = 0; i < 100; ++i) {
|
||||
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
|
||||
}
|
||||
it('can wane to the right', () => {
|
||||
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4)));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,11 +61,6 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
return fallbackValue;
|
||||
};
|
||||
|
||||
// used to schedule external event like midi and osc out
|
||||
export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
|
||||
return (targetTimeSeconds - currentTimeSeconds) * 1000;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
* @noAutocomplete
|
||||
@@ -236,14 +231,6 @@ export const splitAt = function (index, value) {
|
||||
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
export const pairs = function (xs) {
|
||||
const result = [];
|
||||
for (let i = 0; i < xs.length - 1; ++i) {
|
||||
result.push([xs[i], xs[i + 1]]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
/* solmization, not used yet */
|
||||
@@ -302,30 +289,6 @@ export const sol2note = (n, notation = 'letters') => {
|
||||
return note + oct;
|
||||
};
|
||||
|
||||
// Remove duplicates from list
|
||||
export function uniq(a) {
|
||||
var seen = {};
|
||||
return a.filter(function (item) {
|
||||
return seen.hasOwn(item) ? false : (seen[item] = true);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove duplicates from list, sorting in the process. Mutates argument!
|
||||
export function uniqsort(a) {
|
||||
return a.sort().filter(function (item, pos, ary) {
|
||||
return !pos || item != ary[pos - 1];
|
||||
});
|
||||
}
|
||||
|
||||
// rational version
|
||||
export function uniqsortr(a) {
|
||||
return a
|
||||
.sort((x, y) => x.compare(y))
|
||||
.filter(function (item, pos, ary) {
|
||||
return !pos || item.ne(ary[pos - 1]);
|
||||
});
|
||||
}
|
||||
|
||||
// code hashing helpers
|
||||
|
||||
export function unicodeToBase64(text) {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
instrument = instrument || 'triangle';
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
|
||||
return pat.onTrigger((time, hap) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -38,11 +38,9 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
.join('/');
|
||||
// TODO: find out how to send a precise ctx based time
|
||||
// http://www.csounds.com/manual/html/i.html
|
||||
const timeOffset = targetTime - currentTime; // latency ?
|
||||
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
||||
const params = [
|
||||
`"${instrument}"`, // p1: instrument name
|
||||
timeOffset, // p2: starting time in arbitrary unit called beats
|
||||
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
|
||||
hap.duration + 0, // p3: duration in beats
|
||||
// instrument specific params:
|
||||
freq, //.toFixed(precision), // p4: frequency
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Invoke } from './utils.mjs';
|
||||
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
||||
import { Pattern, noteToMidi } from '@strudel/core';
|
||||
|
||||
const ON_MESSAGE = 0x90;
|
||||
const OFF_MESSAGE = 0x80;
|
||||
@@ -9,8 +9,8 @@ Pattern.prototype.midi = function (output) {
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||
const latencyMs = 34;
|
||||
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs;
|
||||
const latency = 0.034;
|
||||
const offset = (targetTime - currentTime + latency) * 1000;
|
||||
velocity = Math.floor(gain * velocity * 100);
|
||||
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
|
||||
const roundedOffset = Math.round(offset);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
||||
import { parseNumeral, Pattern } from '@strudel/core';
|
||||
import { Invoke } from './utils.mjs';
|
||||
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
@@ -13,7 +13,7 @@ Pattern.prototype.osc = function () {
|
||||
|
||||
const params = [];
|
||||
|
||||
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime));
|
||||
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
|
||||
|
||||
Object.keys(controls).forEach((key) => {
|
||||
const val = controls[key];
|
||||
|
||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
||||
import { Pattern, isPattern, logger, ref } from '@strudel/core';
|
||||
import { noteToMidi } from '@strudel/core';
|
||||
import { Note } from 'webmidi';
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
@@ -120,9 +120,10 @@ Pattern.prototype.midi = function (output) {
|
||||
const device = getDevice(output, WebMidi.outputs);
|
||||
hap.ensureObjectValue();
|
||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||
const latencyMs = 34;
|
||||
const latency = 0.034;
|
||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
||||
const timeOffsetString = `+${(targetTime - currentTime + latency) * 1000}`;
|
||||
|
||||
// destructure value
|
||||
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
|
||||
|
||||
@@ -176,7 +177,7 @@ export async function midin(input) {
|
||||
}
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
const device = getDevice(input, WebMidi.inputs);
|
||||
if (initial) {
|
||||
if (initial || WebMidi.enabled) {
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
`Midi enabled! Using "${device.name}". ${
|
||||
|
||||
@@ -295,15 +295,7 @@ function peg$parse(input, options) {
|
||||
var peg$f6 = function(a) { return a };
|
||||
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
|
||||
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 };
|
||||
var peg$f9 = function(a) { return x => {const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
|
||||
x.options_['reps'] = reps;
|
||||
console.log("reps: ", reps)
|
||||
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
|
||||
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
|
||||
x.options_['weight'] = reps;
|
||||
console.log("options: ", x.options_);
|
||||
}
|
||||
};
|
||||
var peg$f9 = function(a) { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 };
|
||||
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||
|
||||
@@ -135,14 +135,7 @@ op_weight = ws ("@" / "_") a:number?
|
||||
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
|
||||
|
||||
op_replicate = ws "!" a:number?
|
||||
{ return x => {// A bit fiddly, to support both x!4 and x!!! as equivalent..
|
||||
const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
|
||||
x.options_['reps'] = reps;
|
||||
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
|
||||
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
|
||||
x.options_['weight'] = reps;
|
||||
}
|
||||
}
|
||||
{ return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }
|
||||
|
||||
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
|
||||
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
||||
|
||||
+14
-7
@@ -27,12 +27,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
||||
pat = strudel.reify(pat)[type](enter(amount));
|
||||
break;
|
||||
}
|
||||
case 'replicate': {
|
||||
const { amount } = op.arguments_;
|
||||
pat = strudel.reify(pat);
|
||||
pat = pat._repeatCycles(amount)._fast(amount);
|
||||
break;
|
||||
}
|
||||
case 'bjorklund': {
|
||||
if (op.arguments_.rotation) {
|
||||
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
|
||||
@@ -73,13 +67,26 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
||||
return pat;
|
||||
};
|
||||
|
||||
function resolveReplications(ast) {
|
||||
ast.source_ = strudel.flatten(
|
||||
ast.source_.map((child) => {
|
||||
const { reps } = child.options_ || {};
|
||||
if (!reps) {
|
||||
return [child];
|
||||
}
|
||||
delete child.options_.reps;
|
||||
return Array(reps).fill(child);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
|
||||
export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||
onEnter?.(ast);
|
||||
const enter = (node) => patternifyAST(node, code, onEnter, offset);
|
||||
switch (ast.type_) {
|
||||
case 'pattern': {
|
||||
// resolveReplications(ast);
|
||||
resolveReplications(ast);
|
||||
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
|
||||
const alignment = ast.arguments_.alignment;
|
||||
const with_tactus = children.filter((child) => child.__tactus_source);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# @strudel/node
|
||||
|
||||
This is an experiment to run strudel in node.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
# Setup
|
||||
# - make sure node >= 20 is installed
|
||||
# - make sure pnpm is installed
|
||||
cd packages/node
|
||||
pnpm i
|
||||
# Usage
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Then run `sclang` with superdirt in another terminal.
|
||||
|
||||
You can now edit and save the file `pattern.mjs` to update your pattern!
|
||||
@@ -1,64 +0,0 @@
|
||||
import { createClock, evalScope } from '@strudel/core';
|
||||
import { evaluate } from '@strudel/transpiler';
|
||||
import watch from 'node-watch';
|
||||
import fs from 'node:fs/promises';
|
||||
import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api';
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
let file = 'pattern.mjs';
|
||||
let pattern;
|
||||
async function evaluateFile() {
|
||||
try {
|
||||
console.log('// file evaluated:');
|
||||
const code = await fs.readFile(file, { encoding: 'utf8' });
|
||||
console.log(code);
|
||||
const res = await evaluate(code);
|
||||
pattern = res.pattern;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// const getTime = () => performance.now() / 1000;
|
||||
const getTime = () => audioContext.currentTime;
|
||||
let minLatency = 50;
|
||||
async function main() {
|
||||
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
|
||||
await evaluateFile();
|
||||
watch(file, { recursive: true }, () => evaluateFile());
|
||||
let lastEnd;
|
||||
const clock = createClock(getTime, (phase) => {
|
||||
if (!lastEnd) {
|
||||
lastEnd = phase;
|
||||
return;
|
||||
}
|
||||
const haps = pattern.queryArc(lastEnd, phase);
|
||||
lastEnd = phase;
|
||||
const cps = 1;
|
||||
const cycle = Math.floor(phase);
|
||||
haps
|
||||
.filter((h) => h.hasOnset())
|
||||
.forEach((hap) => {
|
||||
const env = new GainNode(audioContext, { gain: 0 });
|
||||
const { attack = 0.01, gain = 1 } = hap.value;
|
||||
env.connect(audioContext.destination);
|
||||
const now = hap.whole.begin;
|
||||
const duration = hap.duration;
|
||||
env.gain
|
||||
.setValueAtTime(0, now)
|
||||
.linearRampToValueAtTime(gain * 0.2, now + attack)
|
||||
.exponentialRampToValueAtTime(0.0001, now + duration);
|
||||
const frequency = hap.value.freq;
|
||||
|
||||
const osc = new OscillatorNode(audioContext, { frequency });
|
||||
osc.connect(env);
|
||||
osc.start(now);
|
||||
osc.stop(now + duration);
|
||||
});
|
||||
});
|
||||
|
||||
clock.start();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,76 +0,0 @@
|
||||
import { createClock, evalScope } from '@strudel/core';
|
||||
import { evaluate } from '@strudel/transpiler';
|
||||
import OSC from 'osc-js';
|
||||
import watch from 'node-watch';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const config = {
|
||||
receiver: 'udp', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||
open: {
|
||||
host: 'localhost', // @param {string} Hostname of udp server to bind to
|
||||
port: 57121, // @param {number} Port of udp client for messaging
|
||||
// enabling the following line will receive tidal messages:
|
||||
// port: 57120, // @param {number} Port of udp client for messaging
|
||||
exclusive: false, // @param {boolean} Exclusive flag
|
||||
},
|
||||
send: {
|
||||
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
||||
port: 57120, // @param {number} Port of udp client for messaging
|
||||
},
|
||||
};
|
||||
|
||||
const osc = new OSC({ plugin: new OSC.DatagramPlugin(config) });
|
||||
|
||||
osc.open(); // start a WebSocket server on port 8080
|
||||
|
||||
console.log('osc client running on port', config.open.port);
|
||||
console.log('osc server running on port', config.send.port);
|
||||
|
||||
let file = 'pattern.mjs';
|
||||
let pattern;
|
||||
async function evaluateFile() {
|
||||
try {
|
||||
console.log('// file evaluated:');
|
||||
const code = await fs.readFile(file, { encoding: 'utf8' });
|
||||
console.log(code);
|
||||
const res = await evaluate(code);
|
||||
pattern = res.pattern;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const getTime = () => performance.now() / 1000;
|
||||
async function main() {
|
||||
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
|
||||
await evaluateFile();
|
||||
watch(file, { recursive: true }, () => evaluateFile());
|
||||
let lastEnd;
|
||||
let minLatency = 50;
|
||||
const clock = createClock(getTime, (phase) => {
|
||||
if (!lastEnd) {
|
||||
lastEnd = phase;
|
||||
return;
|
||||
}
|
||||
const haps = pattern.queryArc(lastEnd, phase);
|
||||
lastEnd = phase;
|
||||
const cps = 1;
|
||||
const cycle = Math.floor(phase);
|
||||
haps
|
||||
.filter((h) => h.hasOnset())
|
||||
.forEach((hap) => {
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(performance.timeOrigin + hap.whole.begin * 1000 + minLatency);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
});
|
||||
});
|
||||
|
||||
clock.start();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "@strudel/node",
|
||||
"version": "1.1.0",
|
||||
"description": "Strudel running in node",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"osc": "node osc-superdirt.mjs",
|
||||
"waa": "node node-web-audio-api.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"node-watch": "^0.7.4",
|
||||
"node-web-audio-api": "^0.20.0",
|
||||
"osc-js": "^2.4.0"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
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 { logger, parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
||||
import { logger, parseNumeral, Pattern } from '@strudel/core';
|
||||
|
||||
let connection; // Promise<OSC>
|
||||
function connect() {
|
||||
@@ -44,7 +44,7 @@ function connect() {
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||
hap.ensureObjectValue();
|
||||
const osc = await connect();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
@@ -56,8 +56,7 @@ Pattern.prototype.osc = function () {
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
const offset = getEventOffsetMs(targetTime, currentTime);
|
||||
|
||||
const offset = (time - currentTime) * 1000;
|
||||
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
||||
const ts = Math.floor(Date.now() + offset);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
|
||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import OSC from 'osc-js';
|
||||
const OSC = require('osc-js');
|
||||
|
||||
const config = {
|
||||
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
|
||||
@@ -74,6 +74,6 @@ export const dough = async (code) => {
|
||||
worklet.node.connect(ac.destination);
|
||||
};
|
||||
|
||||
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
|
||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||
export function doughTrigger(t, hap, currentTime, duration, cps) {
|
||||
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
|
||||
}
|
||||
|
||||
+55
-60
@@ -188,66 +188,61 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
|
||||
* .s("piano")
|
||||
*/
|
||||
|
||||
export const scale = register(
|
||||
'scale',
|
||||
function (scale, pat) {
|
||||
// Supports ':' list syntax in mininotation
|
||||
if (Array.isArray(scale)) {
|
||||
scale = scale.flat().join(' ');
|
||||
}
|
||||
return (
|
||||
pat
|
||||
.fmap((value) => {
|
||||
const isObject = typeof value === 'object';
|
||||
let step = isObject ? value.n : value;
|
||||
if (isObject) {
|
||||
delete value.n; // remove n so it won't cause trouble
|
||||
export const scale = register('scale', function (scale, pat) {
|
||||
// Supports ':' list syntax in mininotation
|
||||
if (Array.isArray(scale)) {
|
||||
scale = scale.flat().join(' ');
|
||||
}
|
||||
return (
|
||||
pat
|
||||
.fmap((value) => {
|
||||
const isObject = typeof value === 'object';
|
||||
let step = isObject ? value.n : value;
|
||||
if (isObject) {
|
||||
delete value.n; // remove n so it won't cause trouble
|
||||
}
|
||||
if (isNote(step)) {
|
||||
// legacy..
|
||||
return pure(step);
|
||||
}
|
||||
let asNumber = Number(step);
|
||||
let semitones = 0;
|
||||
if (isNaN(asNumber)) {
|
||||
step = String(step);
|
||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
||||
logger(
|
||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
||||
'error',
|
||||
);
|
||||
return silence;
|
||||
}
|
||||
if (isNote(step)) {
|
||||
// legacy..
|
||||
return pure(step);
|
||||
const isharp = step.indexOf('#');
|
||||
if (isharp >= 0) {
|
||||
asNumber = Number(step.substring(0, isharp));
|
||||
semitones = step.length - isharp;
|
||||
} else {
|
||||
const iflat = step.indexOf('b');
|
||||
asNumber = Number(step.substring(0, iflat));
|
||||
semitones = iflat - step.length;
|
||||
}
|
||||
let asNumber = Number(step);
|
||||
let semitones = 0;
|
||||
if (isNaN(asNumber)) {
|
||||
step = String(step);
|
||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
||||
logger(
|
||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
||||
'error',
|
||||
);
|
||||
return silence;
|
||||
}
|
||||
const isharp = step.indexOf('#');
|
||||
if (isharp >= 0) {
|
||||
asNumber = Number(step.substring(0, isharp));
|
||||
semitones = step.length - isharp;
|
||||
} else {
|
||||
const iflat = step.indexOf('b');
|
||||
asNumber = Number(step.substring(0, iflat));
|
||||
semitones = iflat - step.length;
|
||||
}
|
||||
}
|
||||
try {
|
||||
let note;
|
||||
if (isObject && value.anchor) {
|
||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(asNumber, scale);
|
||||
}
|
||||
try {
|
||||
let note;
|
||||
if (isObject && value.anchor) {
|
||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(asNumber, scale);
|
||||
}
|
||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
value = silence;
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.outerJoin()
|
||||
// legacy:
|
||||
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
|
||||
);
|
||||
},
|
||||
true,
|
||||
true, // preserve tactus
|
||||
);
|
||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
value = silence;
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.outerJoin()
|
||||
// legacy:
|
||||
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
|
||||
);
|
||||
});
|
||||
|
||||
Generated
-62
@@ -302,33 +302,6 @@ importers:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@vitest/ui@1.1.0)
|
||||
|
||||
packages/node:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../mini
|
||||
'@strudel/osc':
|
||||
specifier: workspace:*
|
||||
version: link:../osc
|
||||
'@strudel/tonal':
|
||||
specifier: workspace:*
|
||||
version: link:../tonal
|
||||
'@strudel/transpiler':
|
||||
specifier: workspace:*
|
||||
version: link:../transpiler
|
||||
node-watch:
|
||||
specifier: ^0.7.4
|
||||
version: 0.7.4
|
||||
node-web-audio-api:
|
||||
specifier: ^0.20.0
|
||||
version: 0.20.0
|
||||
osc-js:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
|
||||
packages/osc:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -3494,22 +3467,6 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@napi-rs/cli@2.18.2:
|
||||
resolution: {integrity: sha512-IXQji3IF5eStxTHe/PxDSRjPrFymYAQ5FbIvqurxzxyWR8nJql9mtvmCP8y2g8tSoW5xhaToLQW0+mO3lUZq4w==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/@napi-rs/triples@1.2.0:
|
||||
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
|
||||
dev: false
|
||||
|
||||
/@node-rs/helper@1.6.0:
|
||||
resolution: {integrity: sha512-2OTh/tokcLA1qom1zuCJm2gQzaZljCCbtX1YCrwRVd/toz7KxaDRFeLTAPwhs8m9hWgzrBn5rShRm6IaZofCPw==}
|
||||
dependencies:
|
||||
'@napi-rs/triples': 1.2.0
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir@2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -10760,20 +10717,6 @@ packages:
|
||||
'@babel/parser': 7.23.6
|
||||
dev: true
|
||||
|
||||
/node-watch@0.7.4:
|
||||
resolution: {integrity: sha512-RinNxoz4W1cep1b928fuFhvAQ5ag/+1UlMDV7rbyGthBIgsiEouS4kvRayvvboxii4m8eolKOIBo3OjDqbc+uQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/node-web-audio-api@0.20.0:
|
||||
resolution: {integrity: sha512-DPsRSG3IsI8cLCdejpruir+XgSwUFFJBSfilrtoT+BU/uFcXv/eFyKkulnKIE2642j5b00z9aOHxTHbzdUvTtw==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
'@napi-rs/cli': 2.18.2
|
||||
'@node-rs/helper': 1.6.0
|
||||
webidl-conversions: 7.0.0
|
||||
dev: false
|
||||
|
||||
/nopt@7.1.0:
|
||||
resolution: {integrity: sha512-ZFPLe9Iu0tnx7oWhFxAo4s7QTn8+NNDDxYNaKLjE7Dp0tbakQ3M1QhQzsnzXHQBTUO3K9BmwaxnyO8Ayn2I95Q==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
@@ -14250,11 +14193,6 @@ packages:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/webmidi@3.1.8:
|
||||
resolution: {integrity: sha512-PCRic1iTpKxeheb888G0mDe6MBdz/u+s/xfdV6+1ZhZnS6dKtV9gMBth5cpmMip2Livv5lL+ep4/S9DCzHfumg==}
|
||||
engines: {node: '>=8.5'}
|
||||
|
||||
+115
-123
@@ -1,22 +1,22 @@
|
||||
use rosc::{encoder, OscTime};
|
||||
use rosc::{OscBundle, OscMessage, OscPacket, OscType};
|
||||
use rosc::{ encoder, OscTime };
|
||||
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
|
||||
|
||||
use std::net::UdpSocket;
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{ mpsc, Mutex };
|
||||
use serde::Deserialize;
|
||||
use std::thread::sleep;
|
||||
|
||||
use crate::loggerbridge::Logger;
|
||||
pub struct OscMsg {
|
||||
pub msg_buf: Vec<u8>,
|
||||
pub timestamp: u64,
|
||||
pub msg_buf: Vec<u8>,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
pub struct AsyncInputTransmit {
|
||||
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
|
||||
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
|
||||
}
|
||||
|
||||
const UNIX_OFFSET: u64 = 2_208_988_800; // 70 years in seconds
|
||||
@@ -25,141 +25,133 @@ const NANOS_PER_SECOND: f64 = 1.0e9;
|
||||
const SECONDS_PER_NANO: f64 = 1.0 / NANOS_PER_SECOND;
|
||||
|
||||
pub fn init(
|
||||
logger: Logger,
|
||||
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
||||
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
||||
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>,
|
||||
logger: Logger,
|
||||
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
||||
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
|
||||
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>
|
||||
) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
async_process_model(async_input_receiver, async_output_transmitter).await
|
||||
});
|
||||
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
|
||||
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
/* ...........................................................
|
||||
Listen For incoming messages and add to queue
|
||||
............................................................*/
|
||||
let message_queue_clone = Arc::clone(&message_queue);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if let Some(package) = async_output_receiver.recv().await {
|
||||
let mut message_queue = message_queue_clone.lock().await;
|
||||
let messages = package;
|
||||
for message in messages {
|
||||
(*message_queue).push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let message_queue_clone = Arc::clone(&message_queue);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
/* ...........................................................
|
||||
Listen For incoming messages and add to queue
|
||||
Open OSC Ports
|
||||
............................................................*/
|
||||
let message_queue_clone = Arc::clone(&message_queue);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if let Some(package) = async_output_receiver.recv().await {
|
||||
let mut message_queue = message_queue_clone.lock().await;
|
||||
let messages = package;
|
||||
for message in messages {
|
||||
(*message_queue).push(message);
|
||||
}
|
||||
}
|
||||
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
|
||||
let to_addr = String::from("127.0.0.1:57120");
|
||||
sock.set_nonblocking(true).unwrap();
|
||||
sock.connect(to_addr).expect("could not connect to OSC address");
|
||||
|
||||
/* ...........................................................
|
||||
Process queued messages
|
||||
............................................................*/
|
||||
|
||||
loop {
|
||||
let mut message_queue = message_queue_clone.lock().await;
|
||||
|
||||
message_queue.retain(|message| {
|
||||
let result = sock.send(&message.msg_buf);
|
||||
if result.is_err() {
|
||||
logger.log(
|
||||
format!("OSC Message failed to send, the server might no longer be available"),
|
||||
"error".to_string()
|
||||
);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
let message_queue_clone = Arc::clone(&message_queue);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
/* ...........................................................
|
||||
Open OSC Ports
|
||||
............................................................*/
|
||||
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
|
||||
let to_addr = String::from("127.0.0.1:57120");
|
||||
sock.set_nonblocking(true).unwrap();
|
||||
sock.connect(to_addr)
|
||||
.expect("could not connect to OSC address");
|
||||
|
||||
/* ...........................................................
|
||||
Process queued messages
|
||||
............................................................*/
|
||||
|
||||
loop {
|
||||
let mut message_queue = message_queue_clone.lock().await;
|
||||
|
||||
message_queue.retain(|message| {
|
||||
let result = sock.send(&message.msg_buf);
|
||||
if result.is_err() {
|
||||
logger.log(
|
||||
format!(
|
||||
"OSC Message failed to send, the server might no longer be available"
|
||||
),
|
||||
"error".to_string(),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
});
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn async_process_model(
|
||||
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
|
||||
output_transmitter: mpsc::Sender<Vec<OscMsg>>,
|
||||
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
|
||||
output_transmitter: mpsc::Sender<Vec<OscMsg>>
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
while let Some(input) = input_reciever.recv().await {
|
||||
let output = input;
|
||||
output_transmitter.send(output).await?;
|
||||
}
|
||||
Ok(())
|
||||
while let Some(input) = input_reciever.recv().await {
|
||||
let output = input;
|
||||
output_transmitter.send(output).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Param {
|
||||
name: String,
|
||||
value: String,
|
||||
valueisnumber: bool,
|
||||
name: String,
|
||||
value: String,
|
||||
valueisnumber: bool,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
pub struct MessageFromJS {
|
||||
params: Vec<Param>,
|
||||
timestamp: u64,
|
||||
target: String,
|
||||
params: Vec<Param>,
|
||||
timestamp: u64,
|
||||
target: String,
|
||||
}
|
||||
// Called from JS
|
||||
#[tauri::command]
|
||||
pub async fn sendosc(
|
||||
messagesfromjs: Vec<MessageFromJS>,
|
||||
state: tauri::State<'_, AsyncInputTransmit>,
|
||||
messagesfromjs: Vec<MessageFromJS>,
|
||||
state: tauri::State<'_, AsyncInputTransmit>
|
||||
) -> Result<(), String> {
|
||||
let async_proc_input_tx = state.inner.lock().await;
|
||||
let mut messages_to_process: Vec<OscMsg> = Vec::new();
|
||||
for m in messagesfromjs {
|
||||
let mut args = Vec::new();
|
||||
for p in m.params {
|
||||
args.push(OscType::String(p.name));
|
||||
if p.valueisnumber {
|
||||
args.push(OscType::Float(p.value.parse().unwrap()));
|
||||
} else {
|
||||
args.push(OscType::String(p.value));
|
||||
}
|
||||
}
|
||||
|
||||
let duration_since_epoch =
|
||||
Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
|
||||
|
||||
let seconds = u32::try_from(duration_since_epoch.as_secs())
|
||||
.map_err(|_| "bit conversion failed for osc message timetag")?;
|
||||
|
||||
let nanos = duration_since_epoch.subsec_nanos() as f64;
|
||||
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
|
||||
|
||||
let timetag = OscTime::from((seconds, fractional));
|
||||
|
||||
let packet = OscPacket::Message(OscMessage {
|
||||
addr: m.target,
|
||||
args,
|
||||
});
|
||||
|
||||
let bundle = OscBundle {
|
||||
content: vec![packet],
|
||||
timetag,
|
||||
};
|
||||
|
||||
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
|
||||
|
||||
let message_to_process = OscMsg {
|
||||
msg_buf,
|
||||
timestamp: m.timestamp,
|
||||
};
|
||||
messages_to_process.push(message_to_process);
|
||||
let async_proc_input_tx = state.inner.lock().await;
|
||||
let mut messages_to_process: Vec<OscMsg> = Vec::new();
|
||||
for m in messagesfromjs {
|
||||
let mut args = Vec::new();
|
||||
for p in m.params {
|
||||
args.push(OscType::String(p.name));
|
||||
if p.valueisnumber {
|
||||
args.push(OscType::Float(p.value.parse().unwrap()));
|
||||
} else {
|
||||
args.push(OscType::String(p.value));
|
||||
}
|
||||
}
|
||||
|
||||
async_proc_input_tx
|
||||
.send(messages_to_process)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
|
||||
|
||||
let seconds = u32
|
||||
::try_from(duration_since_epoch.as_secs())
|
||||
.map_err(|_| "bit conversion failed for osc message timetag")?;
|
||||
|
||||
let nanos = duration_since_epoch.subsec_nanos() as f64;
|
||||
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
|
||||
|
||||
let timetag = OscTime::from((seconds, fractional));
|
||||
|
||||
let packet = OscPacket::Message(OscMessage {
|
||||
addr: m.target,
|
||||
args,
|
||||
});
|
||||
|
||||
let bundle = OscBundle {
|
||||
content: vec![packet],
|
||||
timetag,
|
||||
};
|
||||
|
||||
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
|
||||
|
||||
let message_to_process = OscMsg {
|
||||
msg_buf,
|
||||
timestamp: m.timestamp,
|
||||
};
|
||||
messages_to_process.push(message_to_process);
|
||||
}
|
||||
|
||||
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -5972,135 +5972,6 @@ exports[`runs examples > example "s" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s_alt" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/5 | s:bd ]",
|
||||
"[ 1/5 → 2/5 | s:cp ]",
|
||||
"[ 2/5 → 3/5 | s:bd ]",
|
||||
"[ 3/5 → 4/5 | s:mt ]",
|
||||
"[ 4/5 → 1/1 | s:bd ]",
|
||||
"[ 1/1 → 6/5 | s:bd ]",
|
||||
"[ 6/5 → 7/5 | s:cp ]",
|
||||
"[ 7/5 → 8/5 | s:bd ]",
|
||||
"[ 8/5 → 9/5 | s:mt ]",
|
||||
"[ 9/5 → 2/1 | s:bd ]",
|
||||
"[ 2/1 → 11/5 | s:bd ]",
|
||||
"[ 11/5 → 12/5 | s:cp ]",
|
||||
"[ 12/5 → 13/5 | s:bd ]",
|
||||
"[ 13/5 → 14/5 | s:mt ]",
|
||||
"[ 14/5 → 3/1 | s:bd ]",
|
||||
"[ 3/1 → 16/5 | s:bd ]",
|
||||
"[ 16/5 → 17/5 | s:cp ]",
|
||||
"[ 17/5 → 18/5 | s:bd ]",
|
||||
"[ 18/5 → 19/5 | s:mt ]",
|
||||
"[ 19/5 → 4/1 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s_cat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 3/4 | note:e3 ]",
|
||||
"[ 3/4 → 1/1 | note:g3 ]",
|
||||
"[ 1/1 → 7/4 | note:e3 ]",
|
||||
"[ 7/4 → 2/1 | note:g3 ]",
|
||||
"[ 2/1 → 11/4 | note:e3 ]",
|
||||
"[ 11/4 → 3/1 | note:g3 ]",
|
||||
"[ 3/1 → 15/4 | note:e3 ]",
|
||||
"[ 15/4 → 4/1 | note:g3 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s_cat" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/5 | s:bd ]",
|
||||
"[ 1/5 → 2/5 | s:sd ]",
|
||||
"[ 2/5 → 3/5 | s:cp ]",
|
||||
"[ 3/5 → 4/5 | s:hh ]",
|
||||
"[ 4/5 → 1/1 | s:hh ]",
|
||||
"[ 1/1 → 6/5 | s:bd ]",
|
||||
"[ 6/5 → 7/5 | s:sd ]",
|
||||
"[ 7/5 → 8/5 | s:cp ]",
|
||||
"[ 8/5 → 9/5 | s:hh ]",
|
||||
"[ 9/5 → 2/1 | s:hh ]",
|
||||
"[ 2/1 → 11/5 | s:bd ]",
|
||||
"[ 11/5 → 12/5 | s:sd ]",
|
||||
"[ 12/5 → 13/5 | s:cp ]",
|
||||
"[ 13/5 → 14/5 | s:hh ]",
|
||||
"[ 14/5 → 3/1 | s:hh ]",
|
||||
"[ 3/1 → 16/5 | s:bd ]",
|
||||
"[ 16/5 → 17/5 | s:sd ]",
|
||||
"[ 17/5 → 18/5 | s:cp ]",
|
||||
"[ 18/5 → 19/5 | s:hh ]",
|
||||
"[ 19/5 → 4/1 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s_polymeter" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | c ]",
|
||||
"[ 0/1 → 1/3 | c2 ]",
|
||||
"[ 1/3 → 2/3 | eb ]",
|
||||
"[ 1/3 → 2/3 | g2 ]",
|
||||
"[ 2/3 → 1/1 | g ]",
|
||||
"[ 2/3 → 1/1 | c2 ]",
|
||||
"[ 1/1 → 4/3 | c ]",
|
||||
"[ 1/1 → 4/3 | g2 ]",
|
||||
"[ 4/3 → 5/3 | eb ]",
|
||||
"[ 4/3 → 5/3 | c2 ]",
|
||||
"[ 5/3 → 2/1 | g ]",
|
||||
"[ 5/3 → 2/1 | g2 ]",
|
||||
"[ 2/1 → 7/3 | c ]",
|
||||
"[ 2/1 → 7/3 | c2 ]",
|
||||
"[ 7/3 → 8/3 | eb ]",
|
||||
"[ 7/3 → 8/3 | g2 ]",
|
||||
"[ 8/3 → 3/1 | g ]",
|
||||
"[ 8/3 → 3/1 | c2 ]",
|
||||
"[ 3/1 → 10/3 | c ]",
|
||||
"[ 3/1 → 10/3 | g2 ]",
|
||||
"[ 10/3 → 11/3 | eb ]",
|
||||
"[ 10/3 → 11/3 | c2 ]",
|
||||
"[ 11/3 → 4/1 | g ]",
|
||||
"[ 11/3 → 4/1 | g2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | c ]",
|
||||
"[ 0/1 → 1/4 | e ]",
|
||||
"[ 1/4 → 1/2 | d ]",
|
||||
"[ 1/4 → 1/2 | f ]",
|
||||
"[ 1/2 → 3/4 | c ]",
|
||||
"[ 1/2 → 3/4 | g ]",
|
||||
"[ 3/4 → 1/1 | d ]",
|
||||
"[ 3/4 → 1/1 | e ]",
|
||||
"[ 1/1 → 5/4 | c ]",
|
||||
"[ 1/1 → 5/4 | f ]",
|
||||
"[ 5/4 → 3/2 | d ]",
|
||||
"[ 5/4 → 3/2 | g ]",
|
||||
"[ 3/2 → 7/4 | c ]",
|
||||
"[ 3/2 → 7/4 | e ]",
|
||||
"[ 7/4 → 2/1 | d ]",
|
||||
"[ 7/4 → 2/1 | f ]",
|
||||
"[ 2/1 → 9/4 | c ]",
|
||||
"[ 2/1 → 9/4 | g ]",
|
||||
"[ 9/4 → 5/2 | d ]",
|
||||
"[ 9/4 → 5/2 | e ]",
|
||||
"[ 5/2 → 11/4 | c ]",
|
||||
"[ 5/2 → 11/4 | f ]",
|
||||
"[ 11/4 → 3/1 | d ]",
|
||||
"[ 11/4 → 3/1 | g ]",
|
||||
"[ 3/1 → 13/4 | c ]",
|
||||
"[ 3/1 → 13/4 | e ]",
|
||||
"[ 13/4 → 7/2 | d ]",
|
||||
"[ 13/4 → 7/2 | f ]",
|
||||
"[ 7/2 → 15/4 | c ]",
|
||||
"[ 7/2 → 15/4 | g ]",
|
||||
"[ 15/4 → 4/1 | d ]",
|
||||
"[ 15/4 → 4/1 | e ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "samples" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd ]",
|
||||
@@ -7241,27 +7112,6 @@ exports[`runs examples > example "stepcat" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "steps" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd ]",
|
||||
"[ 1/4 → 1/2 | s:sd ]",
|
||||
"[ 1/2 → 3/4 | s:cp ]",
|
||||
"[ 3/4 → 1/1 | s:bd ]",
|
||||
"[ 1/1 → 5/4 | s:sd ]",
|
||||
"[ 5/4 → 3/2 | s:cp ]",
|
||||
"[ 3/2 → 7/4 | s:bd ]",
|
||||
"[ 7/4 → 2/1 | s:sd ]",
|
||||
"[ 2/1 → 9/4 | s:cp ]",
|
||||
"[ 9/4 → 5/2 | s:bd ]",
|
||||
"[ 5/2 → 11/4 | s:sd ]",
|
||||
"[ 11/4 → 3/1 | s:cp ]",
|
||||
"[ 3/1 → 13/4 | s:bd ]",
|
||||
"[ 13/4 → 7/2 | s:sd ]",
|
||||
"[ 7/2 → 15/4 | s:cp ]",
|
||||
"[ 15/4 → 4/1 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "striate" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]",
|
||||
@@ -7448,112 +7298,6 @@ exports[`runs examples > example "sustain" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "swing" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh ]",
|
||||
"[ 1/24 ⇜ (1/8 → 1/6) | s:hh ]",
|
||||
"[ (1/6 → 1/4) ⇝ 7/24 | s:hh ]",
|
||||
"[ 1/4 → 3/8 | s:hh ]",
|
||||
"[ 7/24 ⇜ (3/8 → 5/12) | s:hh ]",
|
||||
"[ (5/12 → 1/2) ⇝ 13/24 | s:hh ]",
|
||||
"[ 1/2 → 5/8 | s:hh ]",
|
||||
"[ 13/24 ⇜ (5/8 → 2/3) | s:hh ]",
|
||||
"[ (2/3 → 3/4) ⇝ 19/24 | s:hh ]",
|
||||
"[ 3/4 → 7/8 | s:hh ]",
|
||||
"[ 19/24 ⇜ (7/8 → 11/12) | s:hh ]",
|
||||
"[ (11/12 → 1/1) ⇝ 25/24 | s:hh ]",
|
||||
"[ 1/1 → 9/8 | s:hh ]",
|
||||
"[ 25/24 ⇜ (9/8 → 7/6) | s:hh ]",
|
||||
"[ (7/6 → 5/4) ⇝ 31/24 | s:hh ]",
|
||||
"[ 5/4 → 11/8 | s:hh ]",
|
||||
"[ 31/24 ⇜ (11/8 → 17/12) | s:hh ]",
|
||||
"[ (17/12 → 3/2) ⇝ 37/24 | s:hh ]",
|
||||
"[ 3/2 → 13/8 | s:hh ]",
|
||||
"[ 37/24 ⇜ (13/8 → 5/3) | s:hh ]",
|
||||
"[ (5/3 → 7/4) ⇝ 43/24 | s:hh ]",
|
||||
"[ 7/4 → 15/8 | s:hh ]",
|
||||
"[ 43/24 ⇜ (15/8 → 23/12) | s:hh ]",
|
||||
"[ (23/12 → 2/1) ⇝ 49/24 | s:hh ]",
|
||||
"[ 2/1 → 17/8 | s:hh ]",
|
||||
"[ 49/24 ⇜ (17/8 → 13/6) | s:hh ]",
|
||||
"[ (13/6 → 9/4) ⇝ 55/24 | s:hh ]",
|
||||
"[ 9/4 → 19/8 | s:hh ]",
|
||||
"[ 55/24 ⇜ (19/8 → 29/12) | s:hh ]",
|
||||
"[ (29/12 → 5/2) ⇝ 61/24 | s:hh ]",
|
||||
"[ 5/2 → 21/8 | s:hh ]",
|
||||
"[ 61/24 ⇜ (21/8 → 8/3) | s:hh ]",
|
||||
"[ (8/3 → 11/4) ⇝ 67/24 | s:hh ]",
|
||||
"[ 11/4 → 23/8 | s:hh ]",
|
||||
"[ 67/24 ⇜ (23/8 → 35/12) | s:hh ]",
|
||||
"[ (35/12 → 3/1) ⇝ 73/24 | s:hh ]",
|
||||
"[ 3/1 → 25/8 | s:hh ]",
|
||||
"[ 73/24 ⇜ (25/8 → 19/6) | s:hh ]",
|
||||
"[ (19/6 → 13/4) ⇝ 79/24 | s:hh ]",
|
||||
"[ 13/4 → 27/8 | s:hh ]",
|
||||
"[ 79/24 ⇜ (27/8 → 41/12) | s:hh ]",
|
||||
"[ (41/12 → 7/2) ⇝ 85/24 | s:hh ]",
|
||||
"[ 7/2 → 29/8 | s:hh ]",
|
||||
"[ 85/24 ⇜ (29/8 → 11/3) | s:hh ]",
|
||||
"[ (11/3 → 15/4) ⇝ 91/24 | s:hh ]",
|
||||
"[ 15/4 → 31/8 | s:hh ]",
|
||||
"[ 91/24 ⇜ (31/8 → 47/12) | s:hh ]",
|
||||
"[ (47/12 → 4/1) ⇝ 97/24 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "swingBy" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh ]",
|
||||
"[ 1/24 ⇜ (1/8 → 1/6) | s:hh ]",
|
||||
"[ (1/6 → 1/4) ⇝ 7/24 | s:hh ]",
|
||||
"[ 1/4 → 3/8 | s:hh ]",
|
||||
"[ 7/24 ⇜ (3/8 → 5/12) | s:hh ]",
|
||||
"[ (5/12 → 1/2) ⇝ 13/24 | s:hh ]",
|
||||
"[ 1/2 → 5/8 | s:hh ]",
|
||||
"[ 13/24 ⇜ (5/8 → 2/3) | s:hh ]",
|
||||
"[ (2/3 → 3/4) ⇝ 19/24 | s:hh ]",
|
||||
"[ 3/4 → 7/8 | s:hh ]",
|
||||
"[ 19/24 ⇜ (7/8 → 11/12) | s:hh ]",
|
||||
"[ (11/12 → 1/1) ⇝ 25/24 | s:hh ]",
|
||||
"[ 1/1 → 9/8 | s:hh ]",
|
||||
"[ 25/24 ⇜ (9/8 → 7/6) | s:hh ]",
|
||||
"[ (7/6 → 5/4) ⇝ 31/24 | s:hh ]",
|
||||
"[ 5/4 → 11/8 | s:hh ]",
|
||||
"[ 31/24 ⇜ (11/8 → 17/12) | s:hh ]",
|
||||
"[ (17/12 → 3/2) ⇝ 37/24 | s:hh ]",
|
||||
"[ 3/2 → 13/8 | s:hh ]",
|
||||
"[ 37/24 ⇜ (13/8 → 5/3) | s:hh ]",
|
||||
"[ (5/3 → 7/4) ⇝ 43/24 | s:hh ]",
|
||||
"[ 7/4 → 15/8 | s:hh ]",
|
||||
"[ 43/24 ⇜ (15/8 → 23/12) | s:hh ]",
|
||||
"[ (23/12 → 2/1) ⇝ 49/24 | s:hh ]",
|
||||
"[ 2/1 → 17/8 | s:hh ]",
|
||||
"[ 49/24 ⇜ (17/8 → 13/6) | s:hh ]",
|
||||
"[ (13/6 → 9/4) ⇝ 55/24 | s:hh ]",
|
||||
"[ 9/4 → 19/8 | s:hh ]",
|
||||
"[ 55/24 ⇜ (19/8 → 29/12) | s:hh ]",
|
||||
"[ (29/12 → 5/2) ⇝ 61/24 | s:hh ]",
|
||||
"[ 5/2 → 21/8 | s:hh ]",
|
||||
"[ 61/24 ⇜ (21/8 → 8/3) | s:hh ]",
|
||||
"[ (8/3 → 11/4) ⇝ 67/24 | s:hh ]",
|
||||
"[ 11/4 → 23/8 | s:hh ]",
|
||||
"[ 67/24 ⇜ (23/8 → 35/12) | s:hh ]",
|
||||
"[ (35/12 → 3/1) ⇝ 73/24 | s:hh ]",
|
||||
"[ 3/1 → 25/8 | s:hh ]",
|
||||
"[ 73/24 ⇜ (25/8 → 19/6) | s:hh ]",
|
||||
"[ (19/6 → 13/4) ⇝ 79/24 | s:hh ]",
|
||||
"[ 13/4 → 27/8 | s:hh ]",
|
||||
"[ 79/24 ⇜ (27/8 → 41/12) | s:hh ]",
|
||||
"[ (41/12 → 7/2) ⇝ 85/24 | s:hh ]",
|
||||
"[ 7/2 → 29/8 | s:hh ]",
|
||||
"[ 85/24 ⇜ (29/8 → 11/3) | s:hh ]",
|
||||
"[ (11/3 → 15/4) ⇝ 91/24 | s:hh ]",
|
||||
"[ 15/4 → 31/8 | s:hh ]",
|
||||
"[ 91/24 ⇜ (31/8 → 47/12) | s:hh ]",
|
||||
"[ (47/12 → 4/1) ⇝ 97/24 | s:hh ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "timecat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 3/4 | note:e3 ]",
|
||||
@@ -8135,12 +7879,12 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
|
||||
"[ 3/8 → 1/2 | s:bd ]",
|
||||
"[ 1/2 → 5/8 | s:bd ]",
|
||||
"[ 5/8 → 3/4 | s:bd ]",
|
||||
"[ 3/4 → 7/8 | s:sd ]",
|
||||
"[ 3/4 → 7/8 | s:bd ]",
|
||||
"[ 7/8 → 1/1 | s:bd ]",
|
||||
"[ 1/1 → 9/8 | s:bd ]",
|
||||
"[ 9/8 → 5/4 | s:bd ]",
|
||||
"[ 5/4 → 11/8 | s:bd ]",
|
||||
"[ 11/8 → 3/2 | s:sd ]",
|
||||
"[ 11/8 → 3/2 | s:bd ]",
|
||||
"[ 3/2 → 13/8 | s:bd ]",
|
||||
"[ 13/8 → 7/4 | s:bd ]",
|
||||
"[ 7/4 → 15/8 | s:bd ]",
|
||||
@@ -8151,7 +7895,7 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
|
||||
"[ 19/8 → 5/2 | s:bd ]",
|
||||
"[ 5/2 → 21/8 | s:bd ]",
|
||||
"[ 21/8 → 11/4 | s:bd ]",
|
||||
"[ 11/4 → 23/8 | s:hh ]",
|
||||
"[ 11/4 → 23/8 | s:bd ]",
|
||||
"[ 23/8 → 3/1 | s:bd ]",
|
||||
"[ 3/1 → 25/8 | s:bd ]",
|
||||
"[ 25/8 → 13/4 | s:bd ]",
|
||||
@@ -8164,59 +7908,6 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/12 | s:bd ]",
|
||||
"[ 1/12 → 1/6 | s:bd ]",
|
||||
"[ 1/6 → 1/4 | s:bd ]",
|
||||
"[ 1/4 → 1/3 | s:hh ]",
|
||||
"[ 1/3 → 5/12 | s:hh ]",
|
||||
"[ 5/12 → 1/2 | s:hh ]",
|
||||
"[ 1/2 → 7/12 | s:bd ]",
|
||||
"[ 7/12 → 2/3 | s:bd ]",
|
||||
"[ 2/3 → 3/4 | s:bd ]",
|
||||
"[ 3/4 → 5/6 | s:bd ]",
|
||||
"[ 5/6 → 11/12 | s:bd ]",
|
||||
"[ 11/12 → 1/1 | s:bd ]",
|
||||
"[ 1/1 → 13/12 | s:bd ]",
|
||||
"[ 13/12 → 7/6 | s:bd ]",
|
||||
"[ 7/6 → 5/4 | s:bd ]",
|
||||
"[ 5/4 → 4/3 | s:bd ]",
|
||||
"[ 4/3 → 17/12 | s:bd ]",
|
||||
"[ 17/12 → 3/2 | s:bd ]",
|
||||
"[ 3/2 → 19/12 | s:sd ]",
|
||||
"[ 19/12 → 5/3 | s:sd ]",
|
||||
"[ 5/3 → 7/4 | s:sd ]",
|
||||
"[ 7/4 → 11/6 | s:bd ]",
|
||||
"[ 11/6 → 23/12 | s:bd ]",
|
||||
"[ 23/12 → 2/1 | s:bd ]",
|
||||
"[ 2/1 → 25/12 | s:hh ]",
|
||||
"[ 25/12 → 13/6 | s:hh ]",
|
||||
"[ 13/6 → 9/4 | s:hh ]",
|
||||
"[ 9/4 → 7/3 | s:hh ]",
|
||||
"[ 7/3 → 29/12 | s:hh ]",
|
||||
"[ 29/12 → 5/2 | s:hh ]",
|
||||
"[ 5/2 → 31/12 | s:hh ]",
|
||||
"[ 31/12 → 8/3 | s:hh ]",
|
||||
"[ 8/3 → 11/4 | s:hh ]",
|
||||
"[ 11/4 → 17/6 | s:sd ]",
|
||||
"[ 17/6 → 35/12 | s:sd ]",
|
||||
"[ 35/12 → 3/1 | s:sd ]",
|
||||
"[ 3/1 → 37/12 | s:bd ]",
|
||||
"[ 37/12 → 19/6 | s:bd ]",
|
||||
"[ 19/6 → 13/4 | s:bd ]",
|
||||
"[ 13/4 → 10/3 | s:bd ]",
|
||||
"[ 10/3 → 41/12 | s:bd ]",
|
||||
"[ 41/12 → 7/2 | s:bd ]",
|
||||
"[ 7/2 → 43/12 | s:hh ]",
|
||||
"[ 43/12 → 11/3 | s:hh ]",
|
||||
"[ 11/3 → 15/4 | s:hh ]",
|
||||
"[ 15/4 → 23/6 | s:bd ]",
|
||||
"[ 23/6 → 47/12 | s:bd ]",
|
||||
"[ 47/12 → 4/1 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "when" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | note:c3 ]",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 338 KiB |
@@ -0,0 +1,49 @@
|
||||
export const laserclock = ({ onTick, audioContext }) => {
|
||||
const ready = new Promise((resolve) => {
|
||||
const workletCode = `class LaserClock extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.block = 0;
|
||||
this.tick = 0;
|
||||
this.started = false;
|
||||
this.blocksPerCallback = 20;
|
||||
this.port.onmessage = (e) => {
|
||||
switch (e.data) {
|
||||
case "start":
|
||||
console.log('start!')
|
||||
this.started = true;
|
||||
break;
|
||||
case "stop":
|
||||
console.log('stop!')
|
||||
this.started = false;
|
||||
this.block = 0;
|
||||
this.tick = 0;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
process(inputs, outputs, parameters) {
|
||||
if(!this.started) {
|
||||
return true;
|
||||
}
|
||||
if(this.block % this.blocksPerCallback === 0) {
|
||||
this.port.postMessage({tick: this.tick, currentTime});
|
||||
this.tick++;
|
||||
}
|
||||
this.block++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor("laserclock-processor", LaserClock);`;
|
||||
const dataURL = `data:text/javascript;base64,${btoa(workletCode)}`;
|
||||
audioContext.audioWorklet.addModule(dataURL).then(() => {
|
||||
const clock = new AudioWorkletNode(audioContext, 'laserclock-processor');
|
||||
clock.port.onmessage = (e) => onTick(e.data);
|
||||
clock.connect(audioContext.destination);
|
||||
resolve(clock);
|
||||
});
|
||||
});
|
||||
const start = () => ready.then((clock) => clock.port.postMessage('start'));
|
||||
const stop = () => ready.then((clock) => clock.port.postMessage('stop'));
|
||||
return { ready, start, stop };
|
||||
};
|
||||
@@ -146,13 +146,15 @@ Wir schauen uns später noch mehr Möglichkeiten an wie man patterns kombiniert.
|
||||
|
||||
**Sequenzen verlangsamen mit `/`**
|
||||
|
||||
{/* [c2 bb1 f2 eb2] */}
|
||||
|
||||
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 8s.
|
||||
Das `/4` spielt die Sequenz 4 mal so langsam, also insgesamt 4 cycles = 4s.
|
||||
|
||||
Jede Note ist nun also 2s lang.
|
||||
Jede Note ist nun also 1s lang.
|
||||
|
||||
Schreib noch mehr Töne in die Klammern und achte darauf dass es schneller wird.
|
||||
|
||||
@@ -162,9 +164,6 @@ Wenn eine Sequenz unabhängig von ihrem Inhalt immer gleich schnell bleiben soll
|
||||
|
||||
**Eins pro Cycle per \< \>**
|
||||
|
||||
Im letzten Kapitel haben wir schon gelernt dass `< ... >` (angle brackets) nur ein Element pro Cycle spielt.
|
||||
Das ist für Melodien auch sehr nützlich:
|
||||
|
||||
<MiniRepl client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -78,24 +78,13 @@ Strudel kommt von Haus aus mit einer breiten Auswahl an Drum Sounds:
|
||||
|
||||
<Box>
|
||||
|
||||
Diese Kombinationen von Buchstaben stehen für verschiedene Teile eines Schlagzeugs:
|
||||
|
||||
<img src="/img/drumset.png" />
|
||||
|
||||
<a class="text-right text-xs" href="https://de.wikipedia.org/wiki/Schlagzeug#/media/Datei:Drum_set.svg" target="_blank">
|
||||
original von Pbroks13
|
||||
</a>
|
||||
Diese 2-Buchstaben Kombinationen stehen für verschiedene Teile eines Schlagzeugs:
|
||||
|
||||
- `bd` = **b**ass **d**rum - Basstrommel
|
||||
- `sd` = **s**nare **d**rum - Schnarrtrommel
|
||||
- `rim` = **rim**shot - Rahmenschlag
|
||||
- `hh` = **h**i**h**at - Hallo Hut
|
||||
- `oh` = **o**pen **h**ihat - Offener Hallo Hut
|
||||
- `lt` = **l**ow tom
|
||||
- `mt` = **m**iddle tom
|
||||
- `ht` = **h**igh tom
|
||||
- `rd` = **r**i**d**e cymbal
|
||||
- `rd` = **cr**ash cymbal
|
||||
|
||||
Probier verschiedene Sounds aus!
|
||||
|
||||
@@ -142,58 +131,34 @@ Versuch noch mehr Sounds hinzuzfügen!
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd")`} punchcard />
|
||||
|
||||
Der Inhalt einer Sequence wird in einen sogenannten Cycle (=Zyklus) zusammengequetscht. Ein Cycle ist standardmäßig 2 Sekunden lang.
|
||||
|
||||
**Eins pro Cycle mit `< .. >`**
|
||||
|
||||
Hier ist die gleiche Sequence, aber dieses mal umgeben von `< .. >` (angle brackets):
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>")`} punchcard />
|
||||
|
||||
Jetzt spielt nur ein Sound pro Cycle. Mit diesen Klammern bleibt das Tempo immer gleich, ganz egal wieviele Elemente enhalten sind!
|
||||
|
||||
Das ist jetzt aber etwas langsam, machen wir es schneller mit `*`:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>*8")`} punchcard />
|
||||
|
||||
Die `*8` macht die ganze Sequenz 8 mal so schnell.
|
||||
|
||||
<Box>
|
||||
|
||||
Warte mal, ist das jetzt nicht das gleiche Ergebnis wie ohne `< ... >*8`? Wofür ist das dann gut?
|
||||
|
||||
Korrekt, der echte Vorteil dieser Schreibweise zeigt sich wenn du Elemente entfernst oder hinzufügst. Versuch es mal!
|
||||
|
||||
Ändere auch mal die Zahl am Ende um das Tempo zu verändern.
|
||||
|
||||
</Box>
|
||||
Der Inhalt einer Sequence wird in einen sogenannten Cycle (=Zyklus) zusammengequetscht.
|
||||
|
||||
**Tempo ändern mit `cpm`**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
cpm = **c**ycles per **m**inute = Cycles pro Minute
|
||||
|
||||
Das Tempo is standardmäßig is 30 Cycles pro Minute = 120/4 = 1 Cycle alle 2 Sekunden
|
||||
Das Tempo ist standardmäßig auf 60cpm eingestellt, also 1 Cycle pro Sekunde.
|
||||
|
||||
In taditioneller Musik-Terminologie könnte man sagen, das Pattern oben besteht aus 8tel Noten auf 90bpm im 4/4 Takt.
|
||||
|
||||
Kein Sorge wenn dir diese Begriffe nichts sagen, das ist nicht notwendig um mit Strudel Musik zu machen.
|
||||
`cpm` ist angelehnt an `bpm` (=beats per minute).
|
||||
|
||||
</Box>
|
||||
|
||||
Wir werden später noch mehr Möglichkeiten kennen lernen das Tempo zu verändern.
|
||||
|
||||
**Pausen mit '-' oder '~'**
|
||||
**Pausen mit '~'**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd hh - rim")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd hh ~ rim")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
Du siehst wahrscheinlich auch Patterns von anderen Leuten mit '~' als Pausensymbol.
|
||||
Besonders für deutsche Tastaturen ist `-` eine Alternative zum schwer tippbaren `~`.
|
||||
Tilde tippen:
|
||||
|
||||
- Windows / Linux: `Alt Gr` + `~`
|
||||
- Mac: `option` + `N`
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -254,7 +219,7 @@ Du kannst so tief verschachteln wie du willst!
|
||||
|
||||
Du kannst so viele Kommas benutzen wie du möchtest:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, - casio")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, ~ casio")`} punchcard />
|
||||
|
||||
Kommas können auch in Unter-Sequenzen verwendet werden:
|
||||
|
||||
@@ -272,9 +237,9 @@ Es kommt öfter vor, dass man die gleiche Idee auf verschiedene Arten ausdrücke
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`bd*2, - cp,
|
||||
- - - oh, hh*4,
|
||||
[- casio]*2\`)`}
|
||||
tune={`sound(\`bd*2, ~ cp,
|
||||
~ ~ ~ oh, hh*4,
|
||||
[~ casio]*2\`)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -304,7 +269,7 @@ Das haben wir bisher gelernt:
|
||||
| --------------------- | ----------- | --------------------------------------------------------------------- |
|
||||
| Sequenz | Leerzeichen | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
|
||||
| Sound Nummer | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
|
||||
| Pausen | - | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
|
||||
| Pausen | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
|
||||
| Unter-Sequenzen | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
|
||||
| Unter-Unter-Sequenzen | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz sd]]")`} /> |
|
||||
| Schneller | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
|
||||
@@ -328,7 +293,7 @@ Die folgenden Funktionen haben wir bereits gesehen:
|
||||
|
||||
**Klassischer House**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd*2, - cp, [- hh]*2").bank("RolandTR909")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd*2, ~ cp, [~ hh]*2").bank("RolandTR909")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -345,7 +310,7 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
|
||||
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
|
||||
.bank("RolandCompurhythm1000")`}
|
||||
punchcard
|
||||
/>
|
||||
@@ -355,10 +320,10 @@ We Will Rock you
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
|
||||
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
|
||||
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
|
||||
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
|
||||
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
|
||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
|
||||
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
|
||||
\`).cpm(90/4)`}
|
||||
punchcard
|
||||
/>
|
||||
@@ -368,10 +333,10 @@ We Will Rock you
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
|
||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [- cp - - ],
|
||||
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
|
||||
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
|
||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
|
||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
|
||||
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
|
||||
\`).bank("RolandTR808").cpm(88/4)`}
|
||||
punchcard
|
||||
/>
|
||||
@@ -381,9 +346,9 @@ We Will Rock you
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`jazz*2,
|
||||
insect [crow metal] - -,
|
||||
- space:4 - space:1,
|
||||
- wind\`)
|
||||
insect [crow metal] ~ ~,
|
||||
~ space:4 ~ space:1,
|
||||
~ wind\`)
|
||||
.cpm(100/2)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<body>
|
||||
<button id="playButton">play</button>
|
||||
<button id="stopButton">stop</button>
|
||||
<script>
|
||||
import { laserclock } from '../laserclock.mjs';
|
||||
import { evaluate } from '@strudel/transpiler';
|
||||
import { evalScope } from '@strudel/core';
|
||||
import { superdough, getAudioContext, registerSynthSounds, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
const init = async () => {
|
||||
initAudioOnFirstClick();
|
||||
await evalScope(
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/webaudio'),
|
||||
);
|
||||
};
|
||||
init().then(async () => {
|
||||
const ctx = getAudioContext();
|
||||
const code = `
|
||||
// "tupper class" @by eddyflux
|
||||
|
||||
samples('github:yaxu/clean-breaks')
|
||||
samples('github:eddyflux/crate')
|
||||
samples('github:eddyflux/wax')
|
||||
|
||||
let cps = 68/60/4;
|
||||
// setcps(cps) // currently not supported by lasterclock.. still sounds funky :D
|
||||
|
||||
stack(
|
||||
s("loop4/4").fit()
|
||||
.layer(
|
||||
x=>x
|
||||
.chop("64 [32 64]")
|
||||
.juxBy(.6, rev)
|
||||
.hpf(500)
|
||||
.dec(sine.range(.05,.18).slow(8))
|
||||
.room(.5).sometimes(mul(speed("2 | -2")))
|
||||
.shape(.6).postgain(sine.range(0,.8).slow(16))
|
||||
//.hush()
|
||||
,
|
||||
x=>x.chop(64).lpf(400)
|
||||
.shape(.6).cut(4)
|
||||
.dec(.2).postgain(.9)
|
||||
.mask("<0!4 1!16>")
|
||||
)
|
||||
//.hush()
|
||||
//.hpf(500)
|
||||
,
|
||||
s("riffin").fit().shape(.2).chop(32).dec(.1).postgain(.6)
|
||||
.sometimesBy("0 [0 .5] 0 0", x=>x.set.squeeze(gain(".9 .6 .2 .1")).room(.3))
|
||||
.sometimesBy("0 [0 0] 0 .5", x=>x.mul(speed("-2")))
|
||||
.hurry("<.5!4 1!16>")
|
||||
//.hush()
|
||||
,
|
||||
s("useme/2").fit()
|
||||
.chop("<16 32>/32")//.rarely(ply("2"))
|
||||
.sometimesBy("0 [.2 0]",ply("3"))
|
||||
.dec(.19).room(.4).cut(9)
|
||||
.hpf(800)
|
||||
.mask("[1!4 0 1 1 0]*2")
|
||||
//.hush()
|
||||
,
|
||||
s("[bd rim:1 [~ bd] rim:2]*2").bank('crate')
|
||||
)
|
||||
.reset("<x@3 x*[4 [8 [16 32]]] x@16>")
|
||||
.late("[0 .002]*16").fast(cps)
|
||||
`;
|
||||
let { pattern } = await evaluate(code);
|
||||
let last;
|
||||
let minLatency = 0.1;
|
||||
const { start, stop } = laserclock({
|
||||
audioContext: ctx,
|
||||
onTick: (data) => {
|
||||
const { currentTime } = data;
|
||||
if (last) {
|
||||
const haps = pattern.queryArc(last, currentTime).filter((h) => h.hasOnset());
|
||||
haps.forEach((hap) => {
|
||||
const start = hap.whole.begin + minLatency;
|
||||
superdough(hap.value, `=${start}`, hap.duration);
|
||||
});
|
||||
}
|
||||
last = currentTime;
|
||||
},
|
||||
} as any);
|
||||
registerSynthSounds();
|
||||
|
||||
document.getElementById('playButton')?.addEventListener('click', () => start());
|
||||
document.getElementById('stopButton')?.addEventListener('click', () => stop());
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
+132
-112
@@ -24,31 +24,20 @@ Here, we are using the `s` function to play back different default samples (`bd`
|
||||
|
||||
For drum sounds, strudel uses the comprehensive [tidal-drum-machines](https://github.com/ritchse/tidal-drum-machines) library, with the following naming convention:
|
||||
|
||||
| Drum | Abbreviation |
|
||||
| -------------------- | ------------ |
|
||||
| Bass drum, Kick drum | bd |
|
||||
| Snare drum | sd |
|
||||
| Rimshot | rim |
|
||||
| Clap | cp |
|
||||
| Closed hi-hat | hh |
|
||||
| Open hi-hat | oh |
|
||||
| Crash | cr |
|
||||
| Ride | rd |
|
||||
| High tom | ht |
|
||||
| Medium tom | mt |
|
||||
| Low tom | lt |
|
||||
|
||||
<img src="/img/drumset.png" />
|
||||
|
||||
<a class="text-right text-xs" href="https://de.wikipedia.org/wiki/Schlagzeug#/media/Datei:Drum_set.svg" target="_blank">
|
||||
original von Pbroks13
|
||||
</a>
|
||||
|
||||
More percussive sounds:
|
||||
|
||||
| Source | Abbreviation |
|
||||
| Drum | Abbreviation |
|
||||
| ----------------------------------- | ------------ |
|
||||
| Bass drum, Kick drum | bd |
|
||||
| Snare drum | sd |
|
||||
| Rimshot | rim |
|
||||
| Clap | cp |
|
||||
| Closed hi-hat | hh |
|
||||
| Open hi-hat | oh |
|
||||
| Crash | cr |
|
||||
| Ride | rd |
|
||||
| Shakers (and maracas, cabasas, etc) | sh |
|
||||
| High tom | ht |
|
||||
| Medium tom | mt |
|
||||
| Low tom | lt |
|
||||
| Cowbell | cb |
|
||||
| Tambourine | tb |
|
||||
| Other percussions | perc |
|
||||
@@ -74,11 +63,11 @@ We _could_ use them like this:
|
||||
|
||||
... but thats obviously a bit much to write. Using the `bank` function, we can shorten this to:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd sd,hh*16").bank("RolandTR808")`} />
|
||||
<MiniRepl client:idle tune={`s("bd sd bd sd,hh*16").bank("RolandTR808")`} />
|
||||
|
||||
You could even pattern the bank to switch between different drum machines:
|
||||
|
||||
<MiniRepl client:idle tune={`s("bd sd,hh*16").bank("<RolandTR808 RolandTR909>")`} />
|
||||
<MiniRepl client:idle tune={`s("bd sd bd sd,hh*16").bank("RolandTR808 RolandTR909")`} />
|
||||
|
||||
Behind the scenes, `bank` will just prepend the drum machine name to the sample name with `_` to get the full name.
|
||||
This of course only works because the name after `_` (`bd`, `sd` etc..) is standardized.
|
||||
@@ -108,128 +97,159 @@ Selecting sounds also works inside the mini notation, using "`:`" like this:
|
||||
|
||||
# Loading Custom Samples
|
||||
|
||||
You can load a non-standard sample map using the `samples` function.
|
||||
You can load your own sample map using the `samples` function.
|
||||
In this example we create a map using sounds from the default sample map:
|
||||
|
||||
## Loading samples from file URLs
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
bd: 'bd/BT0AADA.wav',
|
||||
sd: 'sd/rytm-01-classic.wav',
|
||||
hh: 'hh27/000_hh27closedhh.wav',
|
||||
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
||||
s("bd sd bd sd,hh*16")`}
|
||||
/>
|
||||
|
||||
In this example we assign names `bassdrum`, `hihat` and `snaredrum` to specific audio files on a server:
|
||||
When you load your own samples, you can choose the names that you will then refer to in your pattern string inside the `s` function.
|
||||
Compare with this example which uses the same samples, but with different names.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
bassdrum: 'bd/BT0AADA.wav',
|
||||
snaredrum: 'sd/rytm-01-classic.wav',
|
||||
hihat: 'hh27/000_hh27closedhh.wav',
|
||||
snaredrum: ['sd/rytm-01-classic.wav', 'sd/rytm-00-hard.wav'],
|
||||
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
||||
|
||||
s("bassdrum snaredrum:0 bassdrum snaredrum:1, hihat*16")`}
|
||||
s("bassdrum snaredrum bassdrum snaredrum, hihat*16")`}
|
||||
/>
|
||||
|
||||
You can freely choose any combination of letters for each sample name. It is even possible to override the default sounds.
|
||||
The names you pick will be made available in the `s` function.
|
||||
Make sure that the URL and each sample path form a correct URL!
|
||||
Here we have changed the "map" to include longer sample names.
|
||||
|
||||
In the above example, `bassdrum` will load:
|
||||
## The `samples` function
|
||||
|
||||
```
|
||||
https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/bd/BT0AADA.wav
|
||||
|----------------------base path --------------------------------|--sample path-|
|
||||
```
|
||||
The `samples` function has two arguments:
|
||||
|
||||
Note that we can either load a single file, like for `bassdrum` and `hihat`, or a list of files like for `snaredrum`!
|
||||
As soon as you run the code, your chosen sample names will be listed in `sounds` -> `user`.
|
||||
- A [JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) that maps sound names to audio file paths.
|
||||
- A base URL that comes before each path describing where the sample folder can be found online.
|
||||
- Make sure your base URL ends with a slash, while your sample paths do **not** begin with one!
|
||||
|
||||
## Loading Samples from a strudel.json file
|
||||
To see how this looks in practice, compare the [DirtSamples GitHub repo](https://github.com/tidalcycles/Dirt-Samples) with the previous sample map example.
|
||||
|
||||
The above way to load samples might be tedious to write out / copy paste each time you write a new pattern.
|
||||
To avoid that, you can simply pass a URL to a `strudel.json` file somewhere on the internet:
|
||||
Because GitHub is a popular place for uploading open source samples, it has its own shortcut:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json')
|
||||
tune={`samples({
|
||||
bd: 'bd/BT0AADA.wav',
|
||||
sd: 'sd/rytm-01-classic.wav',
|
||||
hh: 'hh27/000_hh27closedhh.wav',
|
||||
}, 'github:tidalcycles/dirt-samples');
|
||||
s("bd sd bd sd,hh*16")`}
|
||||
/>
|
||||
|
||||
The file is expected to define a sample map using JSON, in the same format as described above.
|
||||
Additionally, the base path can be defined with the `_base` key.
|
||||
The last section could be written as:
|
||||
The format is `github:user/repo/branch/`.
|
||||
|
||||
```json
|
||||
{
|
||||
"_base": "https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/",
|
||||
"bassdrum": "bd/BT0AADA.wav",
|
||||
"snaredrum": "sd/rytm-01-classic.wav",
|
||||
"hihat": "hh27/000_hh27closedhh.wav"
|
||||
}
|
||||
```
|
||||
|
||||
## Github Shortcut
|
||||
|
||||
Because loading samples from github is common, there is a shortcut:
|
||||
Let's see another example, this time based on the following GitHub repo: https://github.com/jarmitage/jarmitage.github.io.
|
||||
We can see there are some guitar samples inside the `/samples` folder, so let's try to load them:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples('github:tidalcycles/dirt-samples')
|
||||
s("bd sd bd sd,hh*16")`}
|
||||
tune={`samples({
|
||||
g0: 'samples/guitar/guitar_0.wav',
|
||||
g1: 'samples/guitar/guitar_1.wav',
|
||||
g2: 'samples/guitar/guitar_2.wav',
|
||||
g3: 'samples/guitar/guitar_3.wav',
|
||||
g4: 'samples/guitar/guitar_4.wav'
|
||||
}, 'github:jarmitage/jarmitage.github.io/master/');
|
||||
s("<g0 g1 g2 g3 g4>/2")`}
|
||||
/>
|
||||
|
||||
The format is `samples('github:<user>/<repo>/<branch>')`. If you omit `branch` (like above), the `main` branch will be used.
|
||||
It assumes a `strudel.json` file to be present at the root of the repository:
|
||||
## Multiple Samples per Sound
|
||||
|
||||
```
|
||||
https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json
|
||||
```
|
||||
|
||||
## From Disk via "Import Sounds"
|
||||
|
||||
If you don't want to upload your samples to the internet, you can also load them from your local disk.
|
||||
Go to the `sounds` tab in the REPL and press "import sounds".
|
||||
Then you can select a folder that contains audio files. The folder you select can also contain subfolders with audio files.
|
||||
Example:
|
||||
|
||||
```
|
||||
└─ samples
|
||||
├─ swoop
|
||||
│ ├─ swoopshort.wav
|
||||
│ ├─ swooplong.wav
|
||||
│ └─ swooptight.wav
|
||||
└─ smash
|
||||
├─ smashhigh.wav
|
||||
├─ smashlow.wav
|
||||
└─ smashmiddle.wav
|
||||
```
|
||||
|
||||
In the above example the folder `samples` contains 2 subfolders `swoop` and `smash`, which contain audio files.
|
||||
If you select that `samples` folder, the `user` tab (next to the import sounds button) will then contain 2 new sounds: `swoop(3) smash(3)`
|
||||
The individual samples can the be played normally like `s("swoop:0 swoop:1 smash:2")`.
|
||||
|
||||
## From Disk via @strudel/sampler
|
||||
|
||||
Instead of loading your samples into your browser with the "import sounds" button, you can also serve the samples from a local file server.
|
||||
The easiest way to do this is using [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler):
|
||||
|
||||
```sh
|
||||
cd samples
|
||||
npx @strudel/sampler
|
||||
```
|
||||
|
||||
Then you can load it via:
|
||||
It is also possible, to declare multiple files for one sound, using the array notation:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples('http://localhost:5432/');
|
||||
|
||||
n("<0 1 2>").s("swoop smash")`}
|
||||
tune={`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/dirt-samples');
|
||||
s("bd:0 bd:1,~ <sd:0 sd:1> ~ sd:0,[hh:0 hh:1]*4")`}
|
||||
/>
|
||||
|
||||
The handy thing about `@strudel/sampler` is that it auto-generates the `strudel.json` file based on your folder structure.
|
||||
You can see what it generated by going to `http://localhost:5432` with your browser.
|
||||
The `:0` `:1` etc. are the indices of the array.
|
||||
The sample number can also be set using `n`:
|
||||
|
||||
Note: You need [NodeJS](https://nodejs.org/) installed on your system for this to work.
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/dirt-samples');
|
||||
s("bd bd,~ sd ~ sd,hh*8").n("<0 1>")`}
|
||||
/>
|
||||
|
||||
## Specifying Pitch
|
||||
In that case, we might load our guitar sample map a different way:
|
||||
|
||||
To make sure your samples are in tune when playing them with `note`, you can specify a base pitch like this:
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
guitar: [
|
||||
'samples/guitar/guitar_0.wav',
|
||||
'samples/guitar/guitar_1.wav',
|
||||
'samples/guitar/guitar_2.wav',
|
||||
'samples/guitar/guitar_3.wav',
|
||||
'samples/guitar/guitar_4.wav'
|
||||
]
|
||||
}, 'github:jarmitage/jarmitage.github.io/master/');
|
||||
s("<guitar:0 guitar:1 guitar:2 guitar:3 guitar:4>*2")`}
|
||||
/>
|
||||
|
||||
And as above, we can choose the sample number using `n` for even more flexibility:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
guitar: [
|
||||
'samples/guitar/guitar_0.wav',
|
||||
'samples/guitar/guitar_1.wav',
|
||||
'samples/guitar/guitar_2.wav',
|
||||
'samples/guitar/guitar_3.wav',
|
||||
'samples/guitar/guitar_4.wav'
|
||||
]
|
||||
}, 'github:jarmitage/jarmitage.github.io/master/');
|
||||
n("<0 1 2 3 4>*2").s("guitar")`}
|
||||
/>
|
||||
|
||||
## Pitched Sounds
|
||||
|
||||
For pitched sounds, you can use `note`, just like with synths:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
'gtr': 'gtr/0001_cleanC.wav',
|
||||
}, 'github:tidalcycles/dirt-samples');
|
||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').gain(.5)`}
|
||||
/>
|
||||
|
||||
Here, the guitar samples will overlap, because they always play till the end.
|
||||
If we want them to behave more like a synth, we can add `clip(1)`:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples({
|
||||
'gtr': 'gtr/0001_cleanC.wav',
|
||||
}, 'github:tidalcycles/dirt-samples');
|
||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').clip(1)
|
||||
.gain(.5)`}
|
||||
/>
|
||||
|
||||
## Base Pitch
|
||||
|
||||
If we have 2 samples with different base pitches, we can make them in tune by specifying the pitch like this:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
@@ -241,6 +261,8 @@ note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s("gtr,moog").clip(1)
|
||||
.gain(.5)`}
|
||||
/>
|
||||
|
||||
If a sample has no pitch set, `c3` is the default.
|
||||
|
||||
We can also declare different samples for different regions of the keyboard:
|
||||
|
||||
<MiniRepl
|
||||
@@ -258,8 +280,6 @@ note("g2!2 <bb2 c3>!2, <c4@3 [<eb4 bb3> g4 f4]>")
|
||||
|
||||
The sampler will always pick the closest matching sample for the current note!
|
||||
|
||||
Note that this notation for pitched sounds also works inside a `strudel.json` file.
|
||||
|
||||
## Shabda
|
||||
|
||||
If you don't want to select samples by hand, there is also the wonderful tool called [shabda](https://shabda.ndre.gr/).
|
||||
|
||||
@@ -106,12 +106,4 @@ Some of these have equivalent operators in the Mini Notation:
|
||||
|
||||
<JsDoc client:idle name="ribbon" h={0} />
|
||||
|
||||
## swingBy
|
||||
|
||||
<JsDoc client:idle name="swingBy" h={0} />
|
||||
|
||||
## swing
|
||||
|
||||
<JsDoc client:idle name="swing" h={0} />
|
||||
|
||||
Apart from modifying time, there are ways to [Control Parameters](/functions/value-modifiers/).
|
||||
|
||||
@@ -23,7 +23,7 @@ following example:
|
||||
|
||||
```js
|
||||
'0 1 2'.add('10 20');
|
||||
('10 [11 21] 22');
|
||||
('10 [11 21] 20');
|
||||
```
|
||||
|
||||
They are similar to the previous example in that the number `1` is split in two, with its two halves added to `10` and `20` respectively. However, the `11` 'remembers' that it is a fragment of that original `1` event, and so is treated as having a duration of a third of a cycle, despite only being active for a sixth of a cycle. Likewise, the `21` is also a fragment of that original `1` event, but a fragment of its second half. Because the start of its event is missing, it wouldn't actually trigger a sound (unless it underwent further pattern transformations/combinations).
|
||||
@@ -41,7 +41,7 @@ This makes way for other ways to align the pattern, and several are already defi
|
||||
- `mix` - structures from both patterns are combined, so that the new events are not fragments but are created at intersections of events from both sides.
|
||||
- `squeeze` - cycles from the pattern on the right are squeezed into events on the left. So that e.g. `"0 1 2".add.squeeze("10 20")` is equivalent to `"[10 20] [11 21] [12 22]"`.
|
||||
- `squeezeout` - as with `squeeze`, but cycles from the left are squeezed into events on the right. So, `"0 1 2".add.squeezeout("10 20")` is equivalent to `[10 11 12] [20 21 22]`.
|
||||
- `reset` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.reset("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'reset' cycles on the left.
|
||||
- `reset` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'trigger' cycles on the left.
|
||||
- `restart` is similar to `reset`, but the pattern is 'restarted' from its very first cycle, rather than from the current cycle. `reset` and `restart` therefore only give different results where the leftmost pattern differs from one cycle to the next.
|
||||
|
||||
We will save going deeper into the background, design and practicalities of these alignment functions for future publications. However in the next section, we take them as a case study for looking at the different design affordances offered by Haskell to Tidal, and JavaScript to Strudel.
|
||||
|
||||
@@ -83,15 +83,15 @@ Let's combine all of the above into a little tune:
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
stack(
|
||||
sound("hh*8").gain("[.25 1]*4"),
|
||||
sound("bd*4,[~ sd:1]*2")
|
||||
),
|
||||
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("sawtooth").lpf("200 1000 200 1000"),
|
||||
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
|
||||
.sound("sawtooth").vowel("<a e i o>")
|
||||
)`}
|
||||
stack(
|
||||
sound("hh*8").gain("[.25 1]*4"),
|
||||
sound("bd*4,[~ sd:1]*2")
|
||||
),
|
||||
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("sawtooth").lpf("200 1000 200 1000"),
|
||||
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
|
||||
.sound("sawtooth").vowel("<a e i o>")
|
||||
) `}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
@@ -152,9 +152,9 @@ Can you guess what they do?
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted"),
|
||||
sound("<bd rim>").bank("RolandTR707")
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted"),
|
||||
sound("<bd rim>").bank("RolandTR707")
|
||||
).delay(".5")`}
|
||||
/>
|
||||
|
||||
@@ -168,7 +168,7 @@ What happens if you use `.delay(".8:.06:.8")` ? Can you guess what the third num
|
||||
|
||||
</Box>
|
||||
|
||||
<QA q="Click to see solution" client:visible>
|
||||
<QA q="Lösung anzeigen" client:visible>
|
||||
|
||||
`delay("a:b:c")`:
|
||||
|
||||
@@ -200,12 +200,12 @@ Add a delay too!
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5),
|
||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
.scale("D4:minor").sound("gm_accordion:2")
|
||||
room(2).gain(.5)
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5),
|
||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
.scale("D4:minor").sound("gm_accordion:2")
|
||||
.room(2).gain(.5)
|
||||
)`}
|
||||
/>
|
||||
|
||||
@@ -214,15 +214,15 @@ Let's add a bass to make this complete:
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5),
|
||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
.scale("D4:minor").sound("gm_accordion:2")
|
||||
.room(2).gain(.4),
|
||||
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
|
||||
.scale("D2:minor")
|
||||
.sound("sawtooth,triangle").lpf(800)
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5),
|
||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
.scale("D4:minor").sound("gm_accordion:2")
|
||||
.room(2).gain(.4),
|
||||
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
|
||||
.scale("D2:minor")
|
||||
.sound("sawtooth,triangle").lpf(800)
|
||||
)`}
|
||||
/>
|
||||
|
||||
@@ -237,7 +237,7 @@ Try adding `.hush()` at the end of one of the patterns in the stack...
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound("numbers:1 numbers:2 numbers:3 numbers:4")
|
||||
.pan("0 0.3 .6 1")`}
|
||||
.pan("0 0.3 .6 1")`}
|
||||
/>
|
||||
|
||||
**speed**
|
||||
@@ -295,8 +295,8 @@ We can change the automation speed with slow / fast:
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("sawtooth")
|
||||
.lpf(sine.range(100, 2000).slow(4))`}
|
||||
.sound("sawtooth")
|
||||
.lpf(sine.range(100, 2000).slow(4))`}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
@@ -307,16 +307,15 @@ The whole automation will now take 8 cycles to repeat.
|
||||
|
||||
## Recap
|
||||
|
||||
| name | example |
|
||||
| ------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
|
||||
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
|
||||
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
|
||||
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
|
||||
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
|
||||
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
|
||||
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
|
||||
| signals | `sine`, `saw`, `square`, `tri`, `rand`, `perlin`<br/><MiniRepl client:visible tune={`s("hh*16").gain (saw)`} /> |
|
||||
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
|
||||
| name | example |
|
||||
| ----- | ---------------------------------------------------------------------------------------- |
|
||||
| lpf | <MiniRepl client:visible tune={`note("c2 c3 c2 c3").s("sawtooth").lpf("<400 2000>")`} /> |
|
||||
| vowel | <MiniRepl client:visible tune={`note("c3 eb3 g3").s("sawtooth").vowel("<a e i o>")`} /> |
|
||||
| gain | <MiniRepl client:visible tune={`s("hh*16").gain("[.25 1]*2")`} /> |
|
||||
| delay | <MiniRepl client:visible tune={`s("bd rim bd cp").delay(.5)`} /> |
|
||||
| room | <MiniRepl client:visible tune={`s("bd rim bd cp").room(.5)`} /> |
|
||||
| pan | <MiniRepl client:visible tune={`s("bd rim bd cp").pan("0 1")`} /> |
|
||||
| speed | <MiniRepl client:visible tune={`s("bd rim bd cp").speed("<1 2 -1 -2>")`} /> |
|
||||
| range | <MiniRepl client:visible tune={`s("hh*16").lpf(saw.range(200,4000))`} /> |
|
||||
|
||||
Let us now take a look at some of Tidal's typical [pattern effects](/workshop/pattern-effects).
|
||||
|
||||
@@ -147,43 +147,35 @@ We will see more ways to combine patterns later..
|
||||
|
||||
**Divide sequences with `/` to slow them down**
|
||||
|
||||
{/* [c2 bb1 f2 eb2] */}
|
||||
|
||||
<MiniRepl client:visible tune={`note("[36 34 41 39]/4").sound("gm_acoustic_bass")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
The `/4` plays the sequence in brackets over 4 cycles (=8s).
|
||||
The `/4` plays the sequence in brackets over 4 cycles (=4s).
|
||||
|
||||
So each of the 4 notes is 2s long.
|
||||
So each of the 4 notes is 1s long.
|
||||
|
||||
Try adding more notes inside the brackets and notice how it gets faster.
|
||||
|
||||
</Box>
|
||||
|
||||
**Play one per cycle with `< ... >`**
|
||||
Because it is so common to just play one thing per cycle, you can..
|
||||
|
||||
In the last section, we learned that `< ... >` (angle brackets) can be used to play only one thing per cycle,
|
||||
which is useful for longer melodies too:
|
||||
**Play one per cycle with \< \>**
|
||||
|
||||
<MiniRepl client:visible tune={`note("<36 34 41 39>").sound("gm_acoustic_bass")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
Try adding more notes inside the brackets and notice how the tempo stays the same.
|
||||
|
||||
The angle brackets are actually just a shortcut:
|
||||
|
||||
`<a b c>` = `[a b c]/3`
|
||||
|
||||
`<a b c d>` = `[a b c d]/4`
|
||||
|
||||
...
|
||||
Try adding more notes inside the brackets and notice how it does **not** get faster.
|
||||
|
||||
</Box>
|
||||
|
||||
**Play one sequence per cycle**
|
||||
|
||||
We can combine the 2 types of brackets in all sorts of different ways.
|
||||
Here is an example of a repetitive bassline:
|
||||
{/* <[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4> */}
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
|
||||
@@ -78,22 +78,11 @@ By default, Strudel comes with a wide selection of drum sounds:
|
||||
|
||||
These letter combinations stand for different parts of a drum set:
|
||||
|
||||
<img src="/img/drumset.png" />
|
||||
|
||||
<a class="text-right text-xs" href="https://de.wikipedia.org/wiki/Schlagzeug#/media/Datei:Drum_set.svg" target="_blank">
|
||||
original image by Pbroks13
|
||||
</a>
|
||||
|
||||
- `bd` = **b**ass **d**rum
|
||||
- `sd` = **s**nare **d**rum
|
||||
- `rim` = **rim**shot
|
||||
- `hh` = **h**i**h**at
|
||||
- `oh` = **o**pen **h**ihat
|
||||
- `lt` = **l**ow tom
|
||||
- `mt` = **m**iddle tom
|
||||
- `ht` = **h**igh tom
|
||||
- `rd` = **r**i**d**e cymbal
|
||||
- `cr` = **cr**ash cymbal
|
||||
|
||||
Try out different drum sounds!
|
||||
|
||||
@@ -140,54 +129,29 @@ Try adding more sounds to the sequence!
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd")`} punchcard />
|
||||
|
||||
The content of a sequence will be squished into what's called a cycle. A cycle is 2s long by default.
|
||||
The content of a sequence will be squished into what's called a cycle.
|
||||
|
||||
**One per cycle with `< .. >`**
|
||||
**One way to change the tempo is using `cpm`**
|
||||
|
||||
Here is the same sequence, but this time sourrounded with `< .. >` (angle brackets):
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>")`} punchcard />
|
||||
|
||||
This will play only one sound per cycle. With these brackets, the tempo doesn't change when we add or remove elements!
|
||||
|
||||
Because this is now very slow, we can speed it up again like this:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd bd hh bd rim bd hh bd>*8")`} punchcard />
|
||||
|
||||
Here, the `*8` means we make the whole thing 8 times faster.
|
||||
|
||||
<Box>
|
||||
|
||||
Wait a minute, isn't this the same as without `< ... >*8`? Why do we need it then?
|
||||
|
||||
That's true, the special thing about this notation is that the tempo won't change when you add or remove elements, try it!
|
||||
|
||||
Try also changing the number at the end to change the tempo!
|
||||
|
||||
</Box>
|
||||
|
||||
**changing the tempo with cpm**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("<bd hh rim hh>*8").cpm(90/4)`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd bd hh bd rim bd hh bd").cpm(40)`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
cpm = cycles per minute
|
||||
|
||||
By default, the tempo is 30 cycles per minute = 120/4 = 1 cycle every 2 seconds
|
||||
|
||||
In western music terms, you could say the above are 8ths notes at 90bpm in 4/4 time.
|
||||
But don't worry if you don't know these terms, as they are not required to make music with Strudel.
|
||||
By default, the tempo is 30 cycles per minute = 1 half cycle per second.
|
||||
|
||||
</Box>
|
||||
|
||||
**Add a rests in a sequence with '-' or '~'**
|
||||
We will look at other ways to change the tempo later!
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd hh - rim - bd hh rim")`} punchcard />
|
||||
**Add a rests in a sequence with '~'**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd hh ~ rim ~ bd hh rim")`} punchcard />
|
||||
|
||||
**Sub-Sequences with [brackets]**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd [hh hh] sd [hh bd] bd - [hh sd] cp")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd [hh hh] sd [hh bd] bd ~ [hh sd] cp")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -199,7 +163,7 @@ Similar to the whole sequence, the content of a sub-sequence will be squished to
|
||||
|
||||
**Multiplication: Speed things up**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd hh*2 rim hh*3 bd [- hh*2] rim hh*2")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd hh*2 rim hh*3 bd [~ hh*2] rim hh*2")`} punchcard />
|
||||
|
||||
**Multiplication: Speed up subsequences**
|
||||
|
||||
@@ -231,7 +195,7 @@ You can go as deep as you want!
|
||||
|
||||
You can use as many commas as you want:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, - casio")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("hh hh hh, bd bd, ~ casio")`} punchcard />
|
||||
|
||||
Commas can also be used inside sub-sequences:
|
||||
|
||||
@@ -249,9 +213,9 @@ It is quite common that there are many ways to express the same idea.
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`bd*2, - cp,
|
||||
- - - oh, hh*4,
|
||||
[- casio]*2\`)`}
|
||||
tune={`sound(\`bd*2, ~ cp,
|
||||
~ ~ ~ oh, hh*4,
|
||||
[~ casio]*2\`)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -268,14 +232,13 @@ This is shorter and more readable than:
|
||||
## Recap
|
||||
|
||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
||||
This is what we've learned so far:
|
||||
This is what we've leared so far:
|
||||
|
||||
| Concept | Syntax | Example |
|
||||
| ----------------- | -------- | ----------------------------------------------------------------------- |
|
||||
| Sequence | space | <MiniRepl client:visible tune={`sound("bd bd sd hh")`} /> |
|
||||
| Sample Number | :x | <MiniRepl client:visible tune={`sound("hh:0 hh:1 hh:2 hh:3")`} /> |
|
||||
| Rests | - or ~ | <MiniRepl client:visible tune={`sound("metal - jazz jazz:1")`} /> |
|
||||
| Alternate | \<\> | <MiniRepl client:visible tune={`sound("<bd hh rim oh bd rim>")`} /> |
|
||||
| Rests | ~ | <MiniRepl client:visible tune={`sound("metal ~ jazz jazz:1")`} /> |
|
||||
| Sub-Sequences | \[\] | <MiniRepl client:visible tune={`sound("bd wind [metal jazz] hh")`} /> |
|
||||
| Sub-Sub-Sequences | \[\[\]\] | <MiniRepl client:visible tune={`sound("bd [metal [jazz [sd cp]]]")`} /> |
|
||||
| Speed up | \* | <MiniRepl client:visible tune={`sound("bd sd*2 cp*3")`} /> |
|
||||
@@ -285,9 +248,9 @@ The Mini-Notation is usually used inside some function. These are the functions
|
||||
|
||||
| Name | Description | Example |
|
||||
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd")`} /> |
|
||||
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").bank("RolandTR909")`} /> |
|
||||
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [- bd] sd").cpm(45)`} /> |
|
||||
| sound | plays the sound of the given name | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd")`} /> |
|
||||
| bank | selects the sound bank | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").bank("RolandTR909")`} /> |
|
||||
| cpm | sets the tempo in cycles per minute | <MiniRepl client:visible tune={`sound("bd sd [~ bd] sd").cpm(45)`} /> |
|
||||
| n | select sample number | <MiniRepl client:visible tune={`n("0 1 4 2 0 6 3 2").sound("jazz")`} /> |
|
||||
|
||||
## Examples
|
||||
@@ -303,7 +266,7 @@ The Mini-Notation is usually used inside some function. These are the functions
|
||||
|
||||
**Classic house**
|
||||
|
||||
<MiniRepl client:visible tune={`sound("bd*4, [- cp]*2, [- hh]*4").bank("RolandTR909")`} punchcard />
|
||||
<MiniRepl client:visible tune={`sound("bd*4, [~ cp]*2, [~ hh]*4").bank("RolandTR909")`} punchcard />
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -320,7 +283,7 @@ We Will Rock you
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound("bd sd, - - - hh - hh - -, - perc - perc:1*2")
|
||||
tune={`sound("bd sd, ~ ~ ~ hh ~ hh ~ ~, ~ perc ~ perc:1*2")
|
||||
.bank("RolandCompurhythm1000").cpm(120/2)`}
|
||||
punchcard
|
||||
/>
|
||||
@@ -330,10 +293,10 @@ We Will Rock you
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
[- - oh - ] [- - - - ] [- - - - ] [- - - - ],
|
||||
[hh hh - - ] [hh - hh - ] [hh - hh - ] [hh - hh - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [cp - - - ],
|
||||
[bd - - - ] [- - - bd] [- - bd - ] [- - - bd]
|
||||
[~ ~ oh ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ],
|
||||
[hh hh ~ ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ] [hh ~ hh ~ ],
|
||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [cp ~ ~ ~ ],
|
||||
[bd ~ ~ ~ ] [~ ~ ~ bd] [~ ~ bd ~ ] [~ ~ ~ bd]
|
||||
\`).cpm(90/4)`}
|
||||
punchcard
|
||||
/>
|
||||
@@ -343,10 +306,10 @@ We Will Rock you
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`sound(\`
|
||||
[- - - - ] [- - - - ] [- - - - ] [- - oh:1 - ],
|
||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh - - ],
|
||||
[- - - - ] [cp - - - ] [- - - - ] [~ cp - - ],
|
||||
[bd bd - - ] [- - bd - ] [bd bd - bd ] [- - - - ]
|
||||
[~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ ~ ~ ] [~ ~ oh:1 ~ ],
|
||||
[hh hh hh hh] [hh hh hh hh] [hh hh hh hh] [hh hh ~ ~ ],
|
||||
[~ ~ ~ ~ ] [cp ~ ~ ~ ] [~ ~ ~ ~ ] [~ cp ~ ~ ],
|
||||
[bd bd ~ ~ ] [~ ~ bd ~ ] [bd bd ~ bd ] [~ ~ ~ ~ ]
|
||||
\`).bank("RolandTR808").cpm(88/4)`}
|
||||
punchcard
|
||||
/>
|
||||
@@ -356,9 +319,9 @@ We Will Rock you
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s(\`jazz*2,
|
||||
insect [crow metal] - -,
|
||||
- space:4 - space:1,
|
||||
- wind\`)
|
||||
insect [crow metal] ~ ~,
|
||||
~ space:4 ~ space:1,
|
||||
~ wind\`)
|
||||
.cpm(100/2)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -155,13 +155,12 @@ In the notation `x=>x.`, the `x` is the shifted pattern, which where modifying.
|
||||
|
||||
</Box>
|
||||
|
||||
off is also useful for modifying other sounds, and can even be nested:
|
||||
off is also useful for sounds:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1")
|
||||
.off(2/16, x=>x.speed(1.5).gain(.25)
|
||||
.off(3/16, y=>y.vowel("<a e i o>*8")))`}
|
||||
.off(1/16, x=>x.speed(1.5).gain(.25))`}
|
||||
/>
|
||||
|
||||
| name | description | example |
|
||||
|
||||
@@ -36,12 +36,7 @@ export function WelcomeTab({ context }) {
|
||||
<a href="https://tidalcycles.org/" target="_blank">
|
||||
tidalcycles
|
||||
</a>
|
||||
, which is a popular live coding language for music, written in Haskell. Strudel is free/open source software:
|
||||
you can redistribute and/or modify it under the terms of the{' '}
|
||||
<a href="https://github.com/tidalcycles/strudel/blob/main/LICENSE" target="_blank">
|
||||
GNU Affero General Public License
|
||||
</a>
|
||||
. You can find the source code at{' '}
|
||||
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
|
||||
@@ -26,13 +26,13 @@ export async function initCode() {
|
||||
// load code from url hash (either short hash from database or decode long hash)
|
||||
try {
|
||||
const initialUrl = window.location.href;
|
||||
const hash = initialUrl.split('?')[1]?.split('#')?.[0]?.split('&')[0];
|
||||
const hash = initialUrl.split('?')[1]?.split('#')?.[0];
|
||||
const codeParam = window.location.href.split('#')[1] || '';
|
||||
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
|
||||
if (codeParam) {
|
||||
// looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
|
||||
return hash2code(codeParam);
|
||||
} else if (hash) {
|
||||
// looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
|
||||
return supabase
|
||||
.from('code_v1')
|
||||
.select('code')
|
||||
|
||||
Reference in New Issue
Block a user