mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd12b49f4d | |||
| 18d4fe0785 | |||
| 4e38988777 | |||
| 308eda677c | |||
| fdd167d62d | |||
| 85bccdcc19 | |||
| f514cd85b1 | |||
| d53929cf68 | |||
| a4bd0ae100 | |||
| 283a071c86 | |||
| 6350cfdb9c | |||
| b72e71b0bd | |||
| 758b94776d | |||
| e62d5759e7 |
@@ -1,4 +1,5 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
|
||||
@@ -127,12 +127,6 @@ registerWidget('_scope', (id, options = {}, pat) => {
|
||||
return pat.tag(id).scope({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
registerWidget('_claviature', (id, options = {}, pat) => {
|
||||
options = { height: 75, width: 640, ...options };
|
||||
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||
return pat.tag(id).claviature({ ...options, ctx, id });
|
||||
});
|
||||
|
||||
registerWidget('_pitchwheel', (id, options = {}, pat) => {
|
||||
let _size = options.size || 200;
|
||||
options = { width: _size, height: _size, ...options, size: _size / 5 };
|
||||
|
||||
@@ -1525,7 +1525,7 @@ export const { cps } = registerControl('cps');
|
||||
* note("c a f e").s("piano").clip("<.5 1 2>")
|
||||
*
|
||||
*/
|
||||
export const { clip, legato } = registerControl('clip', 'legato');
|
||||
export const { clip } = registerControl('clip');
|
||||
|
||||
/**
|
||||
* Sets the duration of the event in cycles. Similar to clip / legato, it also cuts samples off at the end if they exceed the duration.
|
||||
|
||||
@@ -119,10 +119,10 @@ export const lcm = (...fractions) => {
|
||||
if (fractions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const x = fractions.pop();
|
||||
return fractions.reduce(
|
||||
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
|
||||
fraction(1),
|
||||
x,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1729,6 +1729,75 @@ export const { compressSpan, compressspan } = register(['compressSpan', 'compres
|
||||
return pat._compress(span.begin, span.end);
|
||||
});
|
||||
|
||||
/**
|
||||
* fills in the gap between consecutive notes
|
||||
*
|
||||
* @name fill
|
||||
* @memberof Pattern
|
||||
* @param {Pattern | number | array} fill relative note length
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("sawtooth").euclid(11,16).fill("<1 .5>")._pianoroll()
|
||||
* @example
|
||||
* // second array parameter is "lookahead" which is the number of cycles in the future to query for the next event
|
||||
* s("supersaw").euclid(7,16).fill("1:<1 .15>")._pianoroll()
|
||||
*/
|
||||
export const fill = register('fill', function (legato, pat) {
|
||||
let multiplier = legato;
|
||||
let lookahead = 1;
|
||||
if (Array.isArray(legato)) {
|
||||
multiplier = legato[0] ?? 1;
|
||||
lookahead = legato[1] ?? lookahead;
|
||||
}
|
||||
|
||||
let spanEnd;
|
||||
let spanBegin;
|
||||
return pat
|
||||
.withQuerySpan((span) => {
|
||||
spanEnd = span.end;
|
||||
spanBegin = span.begin;
|
||||
|
||||
return span.withEnd((e) => {
|
||||
return e.add(lookahead);
|
||||
})
|
||||
// .withBegin(e => e.sub(lookahead));
|
||||
})
|
||||
.withHaps((haps) => {
|
||||
const newHaps = [];
|
||||
haps
|
||||
.sort((a, b) => {
|
||||
return a.part.begin.sub(b.part.begin).valueOf();
|
||||
})
|
||||
.forEach((hap, i) => {
|
||||
const nextHap = haps[i + 1];
|
||||
const partbegin = hap.part.begin;
|
||||
let partend = hap.part.end;
|
||||
const wholebegin = hap.whole.begin;
|
||||
// filter out haps that were not part of the original span
|
||||
if (wholebegin.gte(spanEnd) || wholebegin.lt(spanBegin)) {
|
||||
return;
|
||||
}
|
||||
let wholeend = hap.whole.end;
|
||||
if (nextHap != null) {
|
||||
partend = nextHap.part.begin;
|
||||
wholeend = nextHap.whole.begin;
|
||||
}
|
||||
|
||||
let wholeduration = wholeend.sub(wholebegin).mul(multiplier);
|
||||
let partduration = partend.sub(partbegin).mul(multiplier);
|
||||
|
||||
partend = partbegin.add(partduration);
|
||||
wholeend = wholebegin.add(wholeduration);
|
||||
|
||||
const newPart = new TimeSpan(partbegin, partend);
|
||||
const newWhole = new TimeSpan(wholebegin, wholeend);
|
||||
|
||||
newHaps.push(new Hap(newWhole, newPart, hap.value, hap.context));
|
||||
});
|
||||
return newHaps;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* speeds up a pattern like fast, but rather than it playing multiple times as fast would it instead leaves a gap in the remaining space of the cycle. For example, the following will play the sound pattern "bd sn" only once but compressed into the first half of the cycle, i.e. twice as fast.
|
||||
* @name fastGap
|
||||
@@ -2804,8 +2873,16 @@ export const s_tour = function (pat, ...many) {
|
||||
export const chop = register('chop', 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 merge = function (a, b) {
|
||||
if ('begin' in a && 'end' in a && a.begin !== undefined && a.end !== undefined) {
|
||||
const d = a.end - a.begin;
|
||||
b = { begin: a.begin + b.begin * d, end: a.begin + b.end * d };
|
||||
}
|
||||
// return a;
|
||||
return Object.assign({}, a, b);
|
||||
};
|
||||
const func = function (o) {
|
||||
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
|
||||
return sequence(slice_objects.map((slice_o) => merge(o, slice_o)));
|
||||
};
|
||||
return pat.squeezeBind(func).setTactus(__tactus ? Fraction(n).mulmaybe(pat.tactus) : undefined);
|
||||
});
|
||||
|
||||
@@ -937,6 +937,9 @@ describe('Pattern', () => {
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can chop chops', () => {
|
||||
expect(pure({ s: 'bev' }).chop(2).chop(2).firstCycle()).toStrictEqual(pure({ s: 'bev' }).chop(4).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('range', () => {
|
||||
it('Can be patterned', () => {
|
||||
@@ -1209,4 +1212,40 @@ describe('Pattern', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('s_expand', () => {
|
||||
it('can expand four things in half', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence(0, 1, 2, 3).s_expand(1, 0.5),
|
||||
s_cat(sequence(0, 1, 2, 3), sequence(0, 1, 2, 3).s_expand(0.5)),
|
||||
),
|
||||
);
|
||||
});
|
||||
it('can expand five things in half', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence(0, 1, 2, 3, 4).s_expand(1, 0.5),
|
||||
s_cat(sequence(0, 1, 2, 3, 4), sequence(0, 1, 2, 3, 4).s_expand(0.5)),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('stepJoin', () => {
|
||||
it('can join a pattern with a tactus of 2', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence(pure(pure('a')), pure(pure('b').setTactus(2))).stepJoin(),
|
||||
s_cat(pure('a'), pure('b').setTactus(2)),
|
||||
),
|
||||
);
|
||||
});
|
||||
it('can join a pattern with a tactus of 0.5', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence(pure(pure('a')), pure(pure('b').setTactus(0.5))).stepJoin(),
|
||||
s_cat(pure('a'), pure('b').setTactus(0.5)),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +61,14 @@ export class TimeSpan {
|
||||
// Applies given function to the end time of the timespan"""
|
||||
return new TimeSpan(this.begin, func_time(this.end));
|
||||
}
|
||||
withBegin(func_time) {
|
||||
// Applies given function to the end time of the timespan"""
|
||||
return new TimeSpan(func_time(this.begin), this.end);
|
||||
}
|
||||
withDuration(func_time) {
|
||||
const duration = this.end.sub(this.begin)
|
||||
return new TimeSpan(this.begin, this.begin.add(func_time(duration)))
|
||||
}
|
||||
|
||||
withCycle(func_time) {
|
||||
// Like withTime, but time is relative to relative to the cycle (i.e. the
|
||||
|
||||
@@ -4,6 +4,7 @@ import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
|
||||
@@ -53,7 +53,7 @@ Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } =
|
||||
return silence;
|
||||
};
|
||||
|
||||
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'fill', 'smear');
|
||||
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'smear');
|
||||
|
||||
export const rescale = register('rescale', function (f, pat) {
|
||||
return pat.mul(x(f).w(f).y(f).h(f));
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Pattern, noteToMidi } from '@strudel/core';
|
||||
|
||||
const blackPattern = [0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0];
|
||||
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
return [pc, acc, oct ? Number(oct) : undefined];
|
||||
};
|
||||
const accs = { '#': 1, b: -1, s: 1 };
|
||||
const toMidi = (note) => {
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
}
|
||||
const [pc, acc, oct] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
|
||||
const getMidiKeys = (range, offset) => {
|
||||
const white /* : number[] */ = [];
|
||||
const black /* : number[] */ = [];
|
||||
const to = noteToMidi(range[1]);
|
||||
for (let i = offset; i <= to; i++) {
|
||||
//
|
||||
(blackPattern[i % 12] ? black : white).push(i);
|
||||
}
|
||||
return [white, black];
|
||||
};
|
||||
|
||||
const whiteWidth = (midi, topWidth) => (midi % 12 > 4 ? 7 / 4 : 5 / 3) * topWidth;
|
||||
|
||||
const whiteX = (midi, offset, topWidth) =>
|
||||
Array.from({ length: midi - offset }, (_, i) => i + offset).reduce(
|
||||
(sum, m) => (!blackPattern[m % 12] ? sum + whiteWidth(m, topWidth) : sum),
|
||||
0,
|
||||
); // TODO: calculate mathematically
|
||||
|
||||
/* const blackX = (index, offset, topWidth) => {
|
||||
const cDiff = 12 - (offset % 12);
|
||||
console.log('cDiff', cDiff);
|
||||
const cOffset = whiteX(cDiff + offset);
|
||||
const blackOffset = cOffset + cDiff * topWidth;
|
||||
return (index - offset) * topWidth + blackOffset;
|
||||
}; */
|
||||
|
||||
const parseNote = (note) => (typeof note === 'number' ? note : toMidi(note));
|
||||
|
||||
export function claviature(haps, options) {
|
||||
const {
|
||||
ctx = getDrawContext(),
|
||||
range = ['C1', 'D3'],
|
||||
scaleX = 1,
|
||||
scaleY = 1,
|
||||
palette = [getTheme().foreground, getTheme().background],
|
||||
strokeWidth = 0,
|
||||
stroke = getTheme().foreground,
|
||||
upperWidth = 14,
|
||||
upperHeight = 100,
|
||||
lowerHeight = 45,
|
||||
} = options || {};
|
||||
const offset = parseNote(range[0]);
|
||||
const colorizedMidi = haps.map((hap) => ({
|
||||
keys: [parseNote(hap.value.note)],
|
||||
color: hap.value.color || getTheme().selection,
|
||||
}));
|
||||
/* const to = parseNote(range[1]);
|
||||
const totalKeys = to - offset + 1; */
|
||||
/* const width = totalKeys * topWidth + topWidth + strokeWidth * 2;
|
||||
const height = whiteHeight; */
|
||||
|
||||
const topWidth = upperWidth * scaleX;
|
||||
const [white, black] = getMidiKeys(range, offset);
|
||||
|
||||
const whiteHeight = (upperHeight + lowerHeight) * scaleY;
|
||||
const blackHeight = upperHeight * scaleY;
|
||||
|
||||
const cDiff = 12 - (offset % 12);
|
||||
const cOffset = whiteX(cDiff + offset);
|
||||
const blackOffset = cOffset;
|
||||
|
||||
const blackX = (midi) => (midi - offset) * topWidth + blackOffset;
|
||||
|
||||
const getColor = (midi) => colorizedMidi.find(({ keys }) => keys.includes(midi))?.color;
|
||||
ctx.clearRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height * 2);
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.strokeWidth = strokeWidth;
|
||||
white.forEach((midi) => {
|
||||
ctx.fillStyle = getColor(midi) ?? palette[1];
|
||||
const x = whiteX(midi, offset, topWidth);
|
||||
const width = whiteWidth(midi, topWidth);
|
||||
ctx.fillRect(x, 0, width, whiteHeight);
|
||||
ctx.strokeRect(x, 0, width, whiteHeight);
|
||||
});
|
||||
black.forEach((midi) => {
|
||||
ctx.fillStyle = getColor(midi) ?? palette[0];
|
||||
const x = blackX(midi, offset, topWidth);
|
||||
//ctx.strokeRect(x, 0, topWidth, blackHeight);
|
||||
ctx.fillRect(x, 0, topWidth, blackHeight);
|
||||
});
|
||||
}
|
||||
|
||||
Pattern.prototype.claviature = function (options) {
|
||||
return this.draw((haps) => claviature(haps, options), { id: options.id });
|
||||
};
|
||||
@@ -3,5 +3,4 @@ export * from './color.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './claviature.mjs';
|
||||
export * from './pitchwheel.mjs';
|
||||
|
||||
@@ -6,6 +6,7 @@ import replace from '@rollup/plugin-replace';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import replace from '@rollup/plugin-replace';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
|
||||
@@ -3953,6 +3953,88 @@ exports[`runs examples > example "layer" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "legato" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:sawtooth ]",
|
||||
"[ 1/8 → 3/16 | s:sawtooth ]",
|
||||
"[ 3/16 → 5/16 | s:sawtooth ]",
|
||||
"[ 5/16 → 3/8 | s:sawtooth ]",
|
||||
"[ 3/8 → 1/2 | s:sawtooth ]",
|
||||
"[ 1/2 → 9/16 | s:sawtooth ]",
|
||||
"[ 9/16 → 11/16 | s:sawtooth ]",
|
||||
"[ 11/16 → 3/4 | s:sawtooth ]",
|
||||
"[ 3/4 → 7/8 | s:sawtooth ]",
|
||||
"[ 7/8 → 15/16 | s:sawtooth ]",
|
||||
"[ 15/16 → 1/1 | s:sawtooth ]",
|
||||
"[ 1/1 → 17/16 | s:sawtooth ]",
|
||||
"[ 9/8 → 37/32 | s:sawtooth ]",
|
||||
"[ 19/16 → 5/4 | s:sawtooth ]",
|
||||
"[ 21/16 → 43/32 | s:sawtooth ]",
|
||||
"[ 11/8 → 23/16 | s:sawtooth ]",
|
||||
"[ 3/2 → 49/32 | s:sawtooth ]",
|
||||
"[ 25/16 → 13/8 | s:sawtooth ]",
|
||||
"[ 27/16 → 55/32 | s:sawtooth ]",
|
||||
"[ 7/4 → 29/16 | s:sawtooth ]",
|
||||
"[ 15/8 → 61/32 | s:sawtooth ]",
|
||||
"[ 31/16 → 63/32 | s:sawtooth ]",
|
||||
"[ 2/1 → 17/8 | s:sawtooth ]",
|
||||
"[ 17/8 → 35/16 | s:sawtooth ]",
|
||||
"[ 35/16 → 37/16 | s:sawtooth ]",
|
||||
"[ 37/16 → 19/8 | s:sawtooth ]",
|
||||
"[ 19/8 → 5/2 | s:sawtooth ]",
|
||||
"[ 5/2 → 41/16 | s:sawtooth ]",
|
||||
"[ 41/16 → 43/16 | s:sawtooth ]",
|
||||
"[ 43/16 → 11/4 | s:sawtooth ]",
|
||||
"[ 11/4 → 23/8 | s:sawtooth ]",
|
||||
"[ 23/8 → 47/16 | s:sawtooth ]",
|
||||
"[ 47/16 → 3/1 | s:sawtooth ]",
|
||||
"[ 3/1 → 49/16 | s:sawtooth ]",
|
||||
"[ 25/8 → 101/32 | s:sawtooth ]",
|
||||
"[ 51/16 → 13/4 | s:sawtooth ]",
|
||||
"[ 53/16 → 107/32 | s:sawtooth ]",
|
||||
"[ 27/8 → 55/16 | s:sawtooth ]",
|
||||
"[ 7/2 → 113/32 | s:sawtooth ]",
|
||||
"[ 57/16 → 29/8 | s:sawtooth ]",
|
||||
"[ 59/16 → 119/32 | s:sawtooth ]",
|
||||
"[ 15/4 → 61/16 | s:sawtooth ]",
|
||||
"[ 31/8 → 125/32 | s:sawtooth ]",
|
||||
"[ 63/16 → 127/32 | s:sawtooth ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "legato" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 3/16 | s:supersaw ]",
|
||||
"[ 3/16 → 5/16 | s:supersaw ]",
|
||||
"[ 5/16 → 7/16 | s:supersaw ]",
|
||||
"[ 7/16 → 5/8 | s:supersaw ]",
|
||||
"[ 5/8 → 3/4 | s:supersaw ]",
|
||||
"[ 3/4 → 7/8 | s:supersaw ]",
|
||||
"[ 7/8 → 1/1 | s:supersaw ]",
|
||||
"[ 1/1 → 19/16 | s:supersaw ]",
|
||||
"[ 19/16 → 21/16 | s:supersaw ]",
|
||||
"[ 21/16 → 23/16 | s:supersaw ]",
|
||||
"[ 23/16 → 13/8 | s:supersaw ]",
|
||||
"[ 13/8 → 7/4 | s:supersaw ]",
|
||||
"[ 7/4 → 15/8 | s:supersaw ]",
|
||||
"[ 15/8 → 2/1 | s:supersaw ]",
|
||||
"[ 2/1 → 35/16 | s:supersaw ]",
|
||||
"[ 35/16 → 37/16 | s:supersaw ]",
|
||||
"[ 37/16 → 39/16 | s:supersaw ]",
|
||||
"[ 39/16 → 21/8 | s:supersaw ]",
|
||||
"[ 21/8 → 11/4 | s:supersaw ]",
|
||||
"[ 11/4 → 23/8 | s:supersaw ]",
|
||||
"[ 23/8 → 3/1 | s:supersaw ]",
|
||||
"[ 3/1 → 51/16 | s:supersaw ]",
|
||||
"[ 51/16 → 53/16 | s:supersaw ]",
|
||||
"[ 53/16 → 55/16 | s:supersaw ]",
|
||||
"[ 55/16 → 29/8 | s:supersaw ]",
|
||||
"[ 29/8 → 15/4 | s:supersaw ]",
|
||||
"[ 15/4 → 31/8 | s:supersaw ]",
|
||||
"[ 31/8 → 4/1 | s:supersaw ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "leslie" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | n:0 s:supersquare leslie:0 ]",
|
||||
|
||||
Reference in New Issue
Block a user