Compare commits

..

48 Commits

Author SHA1 Message Date
Mehmet Ali Akmanalp bb347badbd Format fix 2025-03-06 13:07:53 +03:00
Mehmet Ali "Mali" Akmanalp b135d3dc63 Add elongation _ 2025-03-06 03:02:43 +03:00
Mehmet Ali "Mali" Akmanalp 297712e7ef Add degradation to mini notation page 2025-03-06 02:51:08 +03:00
Alex McLean b15843f3a7 change behaviour of polymeter, and remove polymeterSteps (#1302)
* change behaviour of polymeter, and remove polymeterSteps

* format
2025-03-04 18:17:37 +00:00
Jade (Rose) Rowland 86333ab6f6 Merge pull request #1304 from daslyfe/jade/pwm
feat: Create Pulse Oscillator  with variable PWM
2025-03-03 21:36:57 -05:00
Jade (Rose) Rowland 003e3b790d bounds 2025-03-03 21:29:30 -05:00
Jade (Rose) Rowland 4048ced7e6 format 2025-03-03 21:27:05 -05:00
Jade (Rose) Rowland 64b99f5a8c gain adj 2025-03-03 21:26:42 -05:00
Jade (Rose) Rowland b4af840240 docs 2025-03-03 21:05:23 -05:00
Jade (Rose) Rowland 8a502e886e vibrato working 2025-03-03 20:36:31 -05:00
Jade (Rose) Rowland c7ee90922f pwm 2025-03-03 01:32:04 -05:00
Jade (Rose) Rowland 530339ed8f Merge pull request #1295 from daslyfe/jade/uwu2000
feat: Theme improvements
2025-03-02 11:08:31 -05:00
Alex McLean fece4e8b57 bugfix zoom stepcount (#1301)
* bugfix zoom stepcount
2025-03-02 10:58:46 +00:00
Jade (Rose) Rowland afdcd42039 atomify log 2025-02-28 19:48:58 -05:00
Jade (Rose) Rowland 690b8648a8 Merge pull request #1298 from nkymut/fixtesterror
Fix test error #1297
2025-02-28 10:01:20 -05:00
Yuta Nakayama 7c1e968d41 Fix test error #1297 2025-02-28 19:13:46 +08:00
Jade (Rose) Rowland c0b3187029 Merge pull request #1297 from daslyfe/jade/fix/importerror
Hotfix: prevent undefined pattern code from crashing strudel on load
2025-02-27 20:53:41 -05:00
Jade (Rose) Rowland af1f7658c7 Merge branch 'main' into jade/fix/importerror 2025-02-27 20:47:24 -05:00
Jade (Rose) Rowland b531962b1f fixed 2025-02-27 20:42:40 -05:00
Jade (Rose) Rowland 8867a843ac fallback 2025-02-27 20:17:06 -05:00
Mehmet Ali "Mali" Akmanalp 6e972f4380 Fix misplaced ending sentence (#1296) 2025-02-27 07:24:58 +00:00
Jade (Rose) Rowland 65282bf5ea lint 2025-02-24 22:26:22 -05:00
Jade (Rose) Rowland 3df9744d79 theme attr 2025-02-24 22:24:29 -05:00
Jade (Rose) Rowland 5a5bbfc8da lint 2025-02-24 22:22:03 -05:00
Jade (Rose) Rowland f66478b9da button hover color 2025-02-24 22:15:38 -05:00
Jade (Rose) Rowland 904c213d03 foreground text color 2025-02-24 22:03:50 -05:00
Jade (Rose) Rowland d9c3f051b9 Merge branch 'main' into jade/uwu2000 2025-02-24 20:12:12 -05:00
Alex McLean 470a345296 Signpost licenses for source code and samples a bit more, ref #1277 (#1289)
* Signpost licenses for source code and samples a bit more, ref #1277

* format
2025-02-23 09:54:13 +00:00
Alex McLean 0925ea56dd Allow wchooseCycles probabilities to be patterned (#1292)
* allow wchooseCycles probabilities to be patterned
2025-02-23 09:52:27 +00:00
Jade (Rose) Rowland 1f233b9e7d Merge pull request #1287 from daslyfe/jade/supabasequery
Create Pattern Page Pagination
2025-02-21 20:18:57 -05:00
Jade (Rose) Rowland 5c5d411588 lint 2025-02-21 20:11:39 -05:00
Jade (Rose) Rowland bbb88a2302 accessibility 2025-02-21 20:03:17 -05:00
Alex McLean 64c94b3623 showcase tweaks (#1291) 2025-02-21 21:38:10 +00:00
Jade (Rose) Rowland 42c2262576 working 2025-02-21 14:41:01 -05:00
Jade (Rose) Rowland 4959bff060 font settings 2025-02-21 11:06:21 -05:00
Jade (Rose) Rowland 1ded398468 fixed style 2025-02-21 01:31:06 -05:00
Jade (Rose) Rowland b664bd3d1f girl 2025-02-20 00:54:33 -05:00
Jade (Rose) Rowland dfb7d7bb7f prettier 2025-02-18 11:13:33 -05:00
Jade (Rose) Rowland 3e6a45cf8b fix offset 2025-02-18 11:10:15 -05:00
Jade (Rose) Rowland f47f5c1344 prettier 2025-02-18 10:52:48 -05:00
Jade (Rose) Rowland 47f65da621 remove unesseccary code 2025-02-18 10:47:48 -05:00
Jade (Rose) Rowland c210ea3f89 fix paginator 2025-02-18 10:41:16 -05:00
Jade (Rose) Rowland 50f076084f working 2025-02-18 01:48:17 -05:00
Jade (Rose) Rowland 5cd98880ca reorganize panels 2025-02-17 12:22:45 -05:00
Alex McLean aabc82bedd Fixes inverted triangle wave by renaming it to itri, making non-inverted tri (#1283)
* add itri / itri2 .. but should tri and itri be swapped??

* swap tri/tri2 with itri/itri2. Add some basic docs for bipolar signals
2025-02-11 15:34:23 +00:00
Alex McLean 84581b22aa Fix for squeezejoin and functions using it, including bite (#1286) 2025-02-11 09:02:34 +00:00
Alex McLean 3575fc3cc8 rename repeat back to extend (#1285) 2025-02-09 15:18:28 +00:00
Alex McLean cf8203a4db mqtt bugfix - connection check (#1282) 2025-02-07 17:11:52 +00:00
48 changed files with 1233 additions and 446 deletions
+4
View File
@@ -31,6 +31,10 @@ This project is organized into many [packages](./packages), which are also avail
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
## Contributing
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
+1
View File
@@ -59,6 +59,7 @@
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@tauri-apps/cli": "^2.2.7",
"@vitest/coverage-v8": "3.0.4",
"@vitest/ui": "^3.0.4",
"acorn": "^8.14.0",
"dependency-tree": "^11.0.1",
+1
View File
@@ -67,6 +67,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
initTheme(settings.theme);
let state = EditorState.create({
doc: initialCode,
+3
View File
@@ -4,6 +4,7 @@ import blackscreen, { settings as blackscreenSettings } from './themes/blackscre
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
@@ -55,6 +56,7 @@ export const themes = {
androidstudio,
duotoneDark,
githubDark,
CutiePi,
gruvboxDark,
materialDark,
nord,
@@ -98,6 +100,7 @@ export const settings = {
duotoneLight: duotoneLightSettings,
duotoneDark: duotoneDarkSettings,
eclipse: eclipseSettings,
CutiePi: CutiePiSettings,
githubLight: githubLightSettings,
githubDark: githubDarkSettings,
gruvboxDark: gruvboxDarkSettings,
+45
View File
@@ -0,0 +1,45 @@
/**
* @name Cutie Pi
* by Switch Angel
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const deepPurple = '#5c019a';
const yellowPink = '#fbeffc';
const grey = '#272C35';
const pinkAccent = '#fee1ff';
const lightGrey = '#465063';
const bratGreen = '#9acd3f';
const lighterGrey = '#97a1b7';
const pink = '#f6a6fd';
export const settings = {
background: 'white',
lineBackground: 'transparent',
foreground: deepPurple,
caret: '#797977',
selection: yellowPink,
selectionMatch: '#2B323D',
gutterBackground: grey,
gutterForeground: lightGrey,
gutterBorder: 'transparent',
lineHighlight: pinkAccent,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: deepPurple,
},
{ tag: [t.tagName, t.heading], color: settings.foreground },
{ tag: t.comment, color: lighterGrey },
{ tag: [t.variableName, t.propertyName, t.labelName], color: pink },
{ tag: [t.attributeName, t.number], color: '#d19a66' },
{ tag: t.className, color: grey },
{ tag: t.keyword, color: deepPurple },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: bratGreen },
],
});
-5
View File
@@ -140,8 +140,3 @@ registerWidget('_spectrum', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spectrum({ ...options, ctx, id });
});
registerWidget('_cyclecounter', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.cyclecounter({ ...options, ctx, id });
});
+1 -20
View File
@@ -23,8 +23,6 @@ export class Cyclist {
this.beforeStart = beforeStart;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
this.loopStart = 0;
this.loopLength = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
@@ -50,10 +48,6 @@ export class Cyclist {
this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
if (this.loopLength > 0 && this.lastEnd >= this.loopStart + this.loopLength) {
this.lastEnd = this.loopStart + (this.lastEnd % 1);
this.num_ticks_since_cps_change = 0;
}
this.lastTick = phase;
if (phase < t) {
@@ -122,7 +116,7 @@ export class Cyclist {
stop() {
logger('[cyclist] stop');
this.clock.stop();
this.lastEnd = this.loopStart;
this.lastEnd = 0;
this.setStarted(false);
}
async setPattern(pat, autostart = false) {
@@ -138,19 +132,6 @@ export class Cyclist {
this.cps = cps;
this.num_ticks_since_cps_change = 0;
}
setLoop(start = 0, length = 0) {
if (start >= 0 && length >= 0) {
start = Math.floor(start);
length = Math.floor(length);
if (start != this.loopStart || length != this.loopLength) {
this.loopStart = Math.floor(start);
this.loopLength = Math.floor(length);
this.lastEnd = this.loopStart + (this.lastEnd % 1);
this.num_ticks_since_cps_change = 0;
}
}
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
+27 -41
View File
@@ -388,7 +388,7 @@ export class Pattern {
polyJoin = function () {
const pp = this;
return pp.fmap((p) => p.repeat(pp._steps.div(p._steps))).outerJoin();
return pp.fmap((p) => p.extend(pp._steps.div(p._steps))).outerJoin();
};
polyBind(func) {
@@ -1244,6 +1244,16 @@ export function reify(thing) {
return pure(thing);
}
/** Takes a list of patterns, and returns a pattern of lists.
*/
export function sequenceP(pats) {
let result = pure([]);
for (const pat of pats) {
result = result.bind((list) => pat.fmap((v) => list.concat([v])));
}
return result;
}
/** The given items are played at the same time at the same length.
*
* @return {Pattern}
@@ -1318,7 +1328,7 @@ export function stackBy(by, ...pats) {
left: stackLeft,
right: stackRight,
expand: stack,
repeat: (...args) => polymeterSteps(steps, ...args),
repeat: (...args) => polymeter(...args).steps(steps),
};
return by
.inhabit(lookup)
@@ -1820,7 +1830,10 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
export const focus = register('focus', function (b, e, pat) {
b = Fraction(b);
e = Fraction(e);
return pat._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
return pat
._early(b.sam())
._fast(Fraction(1).div(e.sub(b)))
._late(b);
});
export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], function (span, pat) {
@@ -2030,7 +2043,7 @@ export const zoom = register('zoom', function (s, e, pat) {
return nothing;
}
const d = e.sub(s);
const steps = __steps ? pat._steps.mulmaybe(d) : undefined;
const steps = __steps ? pat._steps?.mulmaybe(d) : undefined;
return pat
.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
.withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
@@ -2635,34 +2648,10 @@ export function _polymeterListSteps(steps, ...args) {
/**
* *Experimental*
*
* Aligns the steps of the patterns, to match the given number of steps per cycle, creating polymeters.
*
* @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"
* polymeterSteps(4, "c d", "e f g").note()
*/
export function polymeterSteps(steps, ...args) {
if (args.length == 0) {
return silence;
}
if (Array.isArray(args[0])) {
// Support old behaviour
return _polymeterListSteps(steps, ...args);
}
return polymeter(...args).pace(steps);
}
/**
* *Experimental*
*
* Aligns the steps of the patterns, to match the steps per cycle of the first pattern, creating polymeters. See `polymeterSteps` to set the target steps explicitly.
* Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps.
* @synonyms pm
* @example
* // The same as note("{c eb g, c2 g2}")
* // The same as note("{c eb g, c2 g2}%6")
* polymeter("c eb g", "c2 g2").note()
*
*/
@@ -2678,13 +2667,12 @@ export function polymeter(...args) {
if (args.length == 0) {
return silence;
}
const steps = args[0]._steps;
const steps = lcm(...args.map((x) => x._steps));
if (steps.eq(Fraction(0))) {
return nothing;
}
const [head, ...tail] = args;
const result = stack(head, ...tail.map((pat) => pat._slow(pat._steps.div(steps))));
const result = stack(...args.map((x) => x.pace(steps)));
result._steps = steps;
return result;
}
@@ -2845,16 +2833,16 @@ export const drop = stepRegister('drop', function (i, pat) {
/**
* *Experimental*
*
* `repeat` is similar to `fast` in that it 'speeds up' the pattern, but it also increases the step count
* accordingly. So `stepcat("a b".repeat(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `extend` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".extend(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
* @example
* stepcat(
* sound("bd bd - cp").repeat(2),
* sound("bd bd - cp").extend(2),
* sound("bd - sd -")
* ).pace(8)
*/
export const repeat = stepRegister('repeat', function (factor, pat) {
export const extend = stepRegister('extend', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
@@ -3052,8 +3040,6 @@ export const timeCat = stepcat;
// Deprecated stepwise aliases
export const s_cat = stepcat;
export const s_alt = stepalt;
export const s_polymeterSteps = polymeterSteps;
Pattern.prototype.s_polymeterSteps = Pattern.prototype.polymeterSteps;
export const s_polymeter = polymeter;
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
export const s_taper = shrink;
@@ -3066,8 +3052,8 @@ export const s_sub = drop;
Pattern.prototype.s_sub = Pattern.prototype.drop;
export const s_expand = expand;
Pattern.prototype.s_expand = Pattern.prototype.expand;
export const s_extend = repeat;
Pattern.prototype.s_extend = Pattern.prototype.repeat;
export const s_extend = extend;
Pattern.prototype.s_extend = Pattern.prototype.extend;
export const s_contract = contract;
Pattern.prototype.s_contract = Pattern.prototype.contract;
export const s_tour = tour;
-3
View File
@@ -86,7 +86,6 @@ export function repl({
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const setLoop = (start, length) => scheduler.setLoop(start, length);
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@@ -168,8 +167,6 @@ export function repl({
setcps: setCps,
setCpm,
setcpm: setCpm,
setLoop,
setloop: setLoop,
});
};
+96 -13
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
@@ -20,9 +20,6 @@ export const signal = (func) => {
return new Pattern(query);
};
export const isaw = signal((t) => 1 - (t % 1));
export const isaw2 = isaw.toBipolar();
/**
* A sawtooth signal between 0 and 1.
*
@@ -36,8 +33,40 @@ export const isaw2 = isaw.toBipolar();
*
*/
export const saw = signal((t) => t % 1);
/**
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
*
* @return {Pattern}
*/
export const saw2 = saw.toBipolar();
/**
* A sawtooth signal between 1 and 0 (like `saw`, but flipped).
*
* @return {Pattern}
* @example
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(isaw.slow(2))
* @example
* n(isaw.range(0,8).segment(8))
* .scale('C major')
*
*/
export const isaw = signal((t) => 1 - (t % 1));
/**
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
*
* @return {Pattern}
*/
export const isaw2 = isaw.toBipolar();
/**
* A sine signal between -1 and 1 (like `sine`, but bipolar).
*
* @return {Pattern}
*/
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
/**
@@ -61,6 +90,12 @@ export const sine = sine2.fromBipolar();
*
*/
export const cosine = sine._early(Fraction(1).div(4));
/**
* A cosine signal between -1 and 1 (like `cosine`, but bipolar).
*
* @return {Pattern}
*/
export const cosine2 = sine2._early(Fraction(1).div(4));
/**
@@ -72,6 +107,12 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
/**
* A square signal between -1 and 1 (like `square`, but bipolar).
*
* @return {Pattern}
*/
export const square2 = square.toBipolar();
/**
@@ -82,9 +123,37 @@ export const square2 = square.toBipolar();
* n(tri.segment(8).range(0,7)).scale("C:minor")
*
*/
export const tri = fastcat(isaw, saw);
export const tri2 = fastcat(isaw2, saw2);
export const tri = fastcat(saw, isaw);
/**
* A triangle signal between -1 and 1 (like `tri`, but bipolar).
*
* @return {Pattern}
*/
export const tri2 = fastcat(saw2, isaw2);
/**
* An inverted triangle signal between 1 and 0 (like `tri`, but flipped).
*
* @return {Pattern}
* @example
* n(itri.segment(8).range(0,7)).scale("C:minor")
*
*/
export const itri = fastcat(isaw, saw);
/**
* An inverted triangle signal between -1 and 1 (like `itri`, but bipolar).
*
* @return {Pattern}
*/
export const itri2 = fastcat(isaw2, saw2);
/**
* A signal representing the cycle time.
*
* @return {Pattern}
*/
export const time = signal(id);
/**
@@ -364,19 +433,30 @@ export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
export const randcat = chooseCycles;
const _wchooseWith = function (pat, ...pairs) {
// A list of patterns of values
const values = pairs.map((pair) => reify(pair[0]));
// A list of weight patterns
const weights = [];
let accum = 0;
let total = pure(0);
for (const pair of pairs) {
accum += pair[1];
weights.push(accum);
// 'add' accepts either values or patterns of values here, so no need
// to explicitly reify
total = total.add(pair[1]);
// accumulate our list of weight patterns
weights.push(total);
}
const total = accum;
// a pattern of lists of weights
const weightspat = sequenceP(weights);
// Takes a number from 0-1, returns a pattern of patterns of values
const match = function (r) {
const find = r * total;
return values[weights.findIndex((x) => x > find, weights)];
const findpat = total.mul(r);
return weightspat.fmap((weights) => (find) => values[weights.findIndex((x) => x > find, weights)]).appLeft(findpat);
};
return pat.fmap(match);
// This returns a pattern of patterns.. The innerJoin is in wchooseCycles
return pat.bind(match);
};
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
@@ -398,6 +478,9 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
* 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()
* @example
* // The probability can itself be a pattern
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
*/
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
+10 -8
View File
@@ -22,7 +22,6 @@ import {
sequence,
palindrome,
polymeter,
polymeterSteps,
polyrhythm,
silence,
fast,
@@ -611,17 +610,10 @@ describe('Pattern', () => {
});
describe('polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
);
});
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
});
describe('firstOf()', () => {
@@ -1265,4 +1257,14 @@ describe('Pattern', () => {
expect(s('bev').chop(8).loopAt(2)._steps).toStrictEqual(Fraction(4));
});
});
describe('bite', () => {
it('works with uneven patterns', () => {
sameFirst(
fastcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5))
.bite(2, stepcat(pure(0), pure(1).expand(2)))
.fast(5),
stepcat(slowcat('a', 'b', 'c', 'd', 'e'), slowcat(1, 2, 3, 4, 5).expand(2)).fast(5),
);
});
});
});
-16
View File
@@ -1,16 +0,0 @@
import { Pattern } from '@strudel/core';
Pattern.prototype.cyclecounter = function (options = {}) {
return this.onPaint((ctx, time, haps, drawTime) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.font = options.font || '2em monospace';
ctx.fillStyle = options.color || 'red';
ctx.textBaseline = 'bottom';
ctx.textAlign = 'right';
const div = options.div || 1;
ctx.fillText('Cycle' + (div > 1 ? '/' + div : '') + ' ' + ((time / div) >> 0), ctx.canvas.width, ctx.canvas.height);
});
};
-1
View File
@@ -4,4 +4,3 @@ export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
export * from './cyclecounter.mjs';
+4 -6
View File
@@ -38,15 +38,10 @@ Pattern.prototype.mqtt = function (
add_meta = true,
) {
const key = host + '-' + client;
let connected = false;
let password_entered = false;
if (!client) {
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
}
function onConnect() {
console.log('Connected to mqtt broker');
connected = true;
if (password_entered) {
document.cookie = 'mqtt_pass=' + password;
}
@@ -56,6 +51,9 @@ Pattern.prototype.mqtt = function (
if (connections[key]) {
cx = connections[key];
} else {
if (!client) {
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
}
cx = new Paho.Client(host, client);
connections[key] = cx;
cx.onConnectionLost = onConnectionLost;
@@ -86,7 +84,7 @@ Pattern.prototype.mqtt = function (
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
let msg_topic = topic;
if (!connected) {
if (!cx || !cx.isConnected()) {
return;
}
let message = '';
+59
View File
@@ -133,6 +133,65 @@ export function registerSynthSounds() {
{ prebake: true, type: 'synth' },
);
registerSound(
'pulse',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n: pulsewidth = 0.5 } = value;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
ac,
'pulse-oscillator',
{
frequency,
begin,
end,
pulsewidth,
},
{
outputChannelCount: [2],
},
);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
ac,
() => {
o.disconnect();
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
};
},
{ prebake: true, type: 'synth' },
);
[...noises].forEach((s) => {
registerSound(
s,
+112 -2
View File
@@ -75,7 +75,12 @@ const waveshapes = {
return v - polyBlep(phase, dt);
},
};
function getParamValue(block, param) {
if (param.length > 1) {
return param[block];
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
@@ -362,6 +367,11 @@ function getUnisonDetune(unison, detune, voiceIndex) {
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
function applySemitoneDetuneToFrequency(frequency, detune) {
return frequency * Math.pow(2, detune / 12);
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
@@ -438,7 +448,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
@@ -648,3 +658,103 @@ class PhaseVocoderProcessor extends OLAProcessor {
}
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
class PulseOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.pi = _PI;
this.phi = -this.pi; // phase
this.Y0 = 0; // feedback memories
this.Y1 = 0;
this.PW = this.pi; // pulse width
this.B = 2.3; // feedback coefficient
this.dphif = 0; // filtered phase increment
this.envf = 0; // filtered envelope
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'detune',
defaultValue: 0,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
},
{
name: 'pulsewidth',
defaultValue: 1,
min: 0,
max: Number.POSITIVE_INFINITY,
},
];
}
process(inputs, outputs, params) {
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) {
return false;
}
const output = outputs[0];
let env = 1,
dphi;
for (let i = 0; i < (output[0].length ?? 0); i++) {
const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi;
const detune = getParamValue(i, params.detune);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment
this.dphif += 0.1 * (dphi - this.dphif);
env *= 0.9998; // exponential decay envelope
this.envf += 0.1 * (env - this.envf);
// Feedback coefficient control
this.B = 2.3 * (1 - 0.0001 * freq); // feedback limitation
if (this.B < 0) this.B = 0;
// Waveform generation (half-Tomisawa oscillators)
this.phi += this.dphif; // phase increment
if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping
// First half-Tomisawa generator
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
this.Y0 = 0.5 * (out0 + this.Y0); // anti-hunting filter
// Second half-Tomisawa generator (with phase offset for pulse width)
let out1 = Math.cos(this.phi + this.B * this.Y1 + pw);
this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter
for (let o = 0; o < output.length; o++) {
// Combination of both oscillators with envelope applied
output[o][i] = 0.15 * (out0 - out1) * this.envf;
}
}
return true; // keep the audio processing going
}
}
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
+1 -1
View File
@@ -5,7 +5,7 @@
"module": "tidal.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/felixroos/hs2js.git"
"url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal"
},
"keywords": [
"haskell",
+95
View File
@@ -39,6 +39,9 @@ importers:
'@tauri-apps/cli':
specifier: ^2.2.7
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -1412,6 +1415,10 @@ packages:
resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@codemirror/autocomplete@6.18.4':
resolution: {integrity: sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==}
@@ -1834,6 +1841,10 @@ packages:
'@isaacs/string-locale-compare@1.1.0':
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
'@istanbuljs/schema@0.1.3':
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
'@jest/schemas@29.6.3':
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -2818,6 +2829,15 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
'@vitest/coverage-v8@3.0.4':
resolution: {integrity: sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==}
peerDependencies:
'@vitest/browser': 3.0.4
vitest: 3.0.4
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@3.0.4':
resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==}
@@ -4487,6 +4507,9 @@ packages:
hs2js@0.1.0:
resolution: {integrity: sha512-THlUIMX8tZf6gtbz5RUZ8xQUyKJEItsx7bxEBcouFIEWjeo90376WMocj3JEz6qTv5nM+tjo3vNvLf89XruMvg==}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
@@ -4837,6 +4860,22 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-lib-source-maps@5.0.6:
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
engines: {node: '>=10'}
istanbul-reports@3.1.7:
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
engines: {node: '>=8'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
@@ -6887,6 +6926,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
text-encoding-shim@1.0.5:
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
@@ -7505,6 +7548,7 @@ packages:
workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
@@ -8568,6 +8612,8 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@bcoe/v8-coverage@1.0.2': {}
'@codemirror/autocomplete@6.18.4':
dependencies:
'@codemirror/language': 6.10.8
@@ -8934,6 +8980,8 @@ snapshots:
'@isaacs/string-locale-compare@1.1.0': {}
'@istanbuljs/schema@0.1.3': {}
'@jest/schemas@29.6.3':
dependencies:
'@sinclair/typebox': 0.27.8
@@ -10187,6 +10235,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
debug: 4.4.0
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
istanbul-reports: 3.1.7
magic-string: 0.30.17
magicast: 0.3.5
std-env: 3.8.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
vitest: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
transitivePeerDependencies:
- supports-color
'@vitest/expect@3.0.4':
dependencies:
'@vitest/spy': 3.0.4
@@ -12202,6 +12268,8 @@ snapshots:
dependencies:
web-tree-sitter: 0.20.8
html-escaper@2.0.2: {}
html-escaper@3.0.3: {}
html-void-elements@3.0.0: {}
@@ -12540,6 +12608,27 @@ snapshots:
isobject@3.0.1: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.0
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
istanbul-reports@3.1.7:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -15262,6 +15351,12 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
test-exclude@7.0.1:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 10.4.5
minimatch: 9.0.5
text-encoding-shim@1.0.5: {}
text-extensions@1.9.0: {}
+279 -112
View File
@@ -3019,6 +3019,33 @@ exports[`runs examples > example "expand" example index 0 1`] = `
]
`;
exports[`runs examples > example "extend" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd ]",
"[ 1/8 → 1/4 | s:bd ]",
"[ 3/8 → 1/2 | s:cp ]",
"[ 1/2 → 5/8 | s:bd ]",
"[ 5/8 → 3/4 | s:bd ]",
"[ 7/8 → 1/1 | s:cp ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 5/4 → 11/8 | s:sd ]",
"[ 3/2 → 13/8 | s:bd ]",
"[ 13/8 → 7/4 | s:bd ]",
"[ 15/8 → 2/1 | s:cp ]",
"[ 2/1 → 17/8 | s:bd ]",
"[ 17/8 → 9/4 | s:bd ]",
"[ 19/8 → 5/2 | s:cp ]",
"[ 5/2 → 21/8 | s:bd ]",
"[ 11/4 → 23/8 | s:sd ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 25/8 → 13/4 | s:bd ]",
"[ 27/8 → 7/2 | s:cp ]",
"[ 7/2 → 29/8 | s:bd ]",
"[ 29/8 → 15/4 | s:bd ]",
"[ 31/8 → 4/1 | s:cp ]",
]
`;
exports[`runs examples > example "fanchor" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]",
@@ -4234,6 +4261,96 @@ exports[`runs examples > example "iresponse" example index 0 1`] = `
]
`;
exports[`runs examples > example "isaw" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
"[ 1/8 → 1/4 | note:eb3 clip:0.9375 ]",
"[ 1/8 → 1/4 | note:g3 clip:0.9375 ]",
"[ 1/4 → 3/8 | note:g2 clip:0.875 ]",
"[ 3/8 → 1/2 | note:g3 clip:0.8125 ]",
"[ 3/8 → 1/2 | note:bb3 clip:0.8125 ]",
"[ 1/2 → 5/8 | note:c3 clip:0.75 ]",
"[ 5/8 → 3/4 | note:eb3 clip:0.6875 ]",
"[ 5/8 → 3/4 | note:g3 clip:0.6875 ]",
"[ 3/4 → 7/8 | note:g2 clip:0.625 ]",
"[ 7/8 → 1/1 | note:g3 clip:0.5625 ]",
"[ 7/8 → 1/1 | note:bb3 clip:0.5625 ]",
"[ 1/1 → 9/8 | note:c3 clip:0.5 ]",
"[ 9/8 → 5/4 | note:eb3 clip:0.4375 ]",
"[ 9/8 → 5/4 | note:g3 clip:0.4375 ]",
"[ 5/4 → 11/8 | note:g2 clip:0.375 ]",
"[ 11/8 → 3/2 | note:g3 clip:0.3125 ]",
"[ 11/8 → 3/2 | note:bb3 clip:0.3125 ]",
"[ 3/2 → 13/8 | note:c3 clip:0.25 ]",
"[ 13/8 → 7/4 | note:eb3 clip:0.1875 ]",
"[ 13/8 → 7/4 | note:g3 clip:0.1875 ]",
"[ 7/4 → 15/8 | note:g2 clip:0.125 ]",
"[ 15/8 → 2/1 | note:g3 clip:0.0625 ]",
"[ 15/8 → 2/1 | note:bb3 clip:0.0625 ]",
"[ 2/1 → 17/8 | note:c3 clip:1 ]",
"[ 17/8 → 9/4 | note:eb3 clip:0.9375 ]",
"[ 17/8 → 9/4 | note:g3 clip:0.9375 ]",
"[ 9/4 → 19/8 | note:g2 clip:0.875 ]",
"[ 19/8 → 5/2 | note:g3 clip:0.8125 ]",
"[ 19/8 → 5/2 | note:bb3 clip:0.8125 ]",
"[ 5/2 → 21/8 | note:c3 clip:0.75 ]",
"[ 21/8 → 11/4 | note:eb3 clip:0.6875 ]",
"[ 21/8 → 11/4 | note:g3 clip:0.6875 ]",
"[ 11/4 → 23/8 | note:g2 clip:0.625 ]",
"[ 23/8 → 3/1 | note:g3 clip:0.5625 ]",
"[ 23/8 → 3/1 | note:bb3 clip:0.5625 ]",
"[ 3/1 → 25/8 | note:c3 clip:0.5 ]",
"[ 25/8 → 13/4 | note:eb3 clip:0.4375 ]",
"[ 25/8 → 13/4 | note:g3 clip:0.4375 ]",
"[ 13/4 → 27/8 | note:g2 clip:0.375 ]",
"[ 27/8 → 7/2 | note:g3 clip:0.3125 ]",
"[ 27/8 → 7/2 | note:bb3 clip:0.3125 ]",
"[ 7/2 → 29/8 | note:c3 clip:0.25 ]",
"[ 29/8 → 15/4 | note:eb3 clip:0.1875 ]",
"[ 29/8 → 15/4 | note:g3 clip:0.1875 ]",
"[ 15/4 → 31/8 | note:g2 clip:0.125 ]",
"[ 31/8 → 4/1 | note:g3 clip:0.0625 ]",
"[ 31/8 → 4/1 | note:bb3 clip:0.0625 ]",
]
`;
exports[`runs examples > example "isaw" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:D4 ]",
"[ 1/8 → 1/4 | note:C4 ]",
"[ 1/4 → 3/8 | note:B3 ]",
"[ 3/8 → 1/2 | note:A3 ]",
"[ 1/2 → 5/8 | note:G3 ]",
"[ 5/8 → 3/4 | note:F3 ]",
"[ 3/4 → 7/8 | note:E3 ]",
"[ 7/8 → 1/1 | note:D3 ]",
"[ 1/1 → 9/8 | note:D4 ]",
"[ 9/8 → 5/4 | note:C4 ]",
"[ 5/4 → 11/8 | note:B3 ]",
"[ 11/8 → 3/2 | note:A3 ]",
"[ 3/2 → 13/8 | note:G3 ]",
"[ 13/8 → 7/4 | note:F3 ]",
"[ 7/4 → 15/8 | note:E3 ]",
"[ 15/8 → 2/1 | note:D3 ]",
"[ 2/1 → 17/8 | note:D4 ]",
"[ 17/8 → 9/4 | note:C4 ]",
"[ 9/4 → 19/8 | note:B3 ]",
"[ 19/8 → 5/2 | note:A3 ]",
"[ 5/2 → 21/8 | note:G3 ]",
"[ 21/8 → 11/4 | note:F3 ]",
"[ 11/4 → 23/8 | note:E3 ]",
"[ 23/8 → 3/1 | note:D3 ]",
"[ 3/1 → 25/8 | note:D4 ]",
"[ 25/8 → 13/4 | note:C4 ]",
"[ 13/4 → 27/8 | note:B3 ]",
"[ 27/8 → 7/2 | note:A3 ]",
"[ 7/2 → 29/8 | note:G3 ]",
"[ 29/8 → 15/4 | note:F3 ]",
"[ 15/4 → 31/8 | note:E3 ]",
"[ 31/8 → 4/1 | note:D3 ]",
]
`;
exports[`runs examples > example "iter" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A3 ]",
@@ -4276,6 +4393,43 @@ exports[`runs examples > example "iterBack" example index 0 1`] = `
]
`;
exports[`runs examples > example "itri" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:C4 ]",
"[ 1/8 → 1/4 | note:Bb3 ]",
"[ 1/4 → 3/8 | note:G3 ]",
"[ 3/8 → 1/2 | note:Eb3 ]",
"[ 1/2 → 5/8 | note:C3 ]",
"[ 5/8 → 3/4 | note:Eb3 ]",
"[ 3/4 → 7/8 | note:G3 ]",
"[ 7/8 → 1/1 | note:Bb3 ]",
"[ 1/1 → 9/8 | note:C4 ]",
"[ 9/8 → 5/4 | note:Bb3 ]",
"[ 5/4 → 11/8 | note:G3 ]",
"[ 11/8 → 3/2 | note:Eb3 ]",
"[ 3/2 → 13/8 | note:C3 ]",
"[ 13/8 → 7/4 | note:Eb3 ]",
"[ 7/4 → 15/8 | note:G3 ]",
"[ 15/8 → 2/1 | note:Bb3 ]",
"[ 2/1 → 17/8 | note:C4 ]",
"[ 17/8 → 9/4 | note:Bb3 ]",
"[ 9/4 → 19/8 | note:G3 ]",
"[ 19/8 → 5/2 | note:Eb3 ]",
"[ 5/2 → 21/8 | note:C3 ]",
"[ 21/8 → 11/4 | note:Eb3 ]",
"[ 11/4 → 23/8 | note:G3 ]",
"[ 23/8 → 3/1 | note:Bb3 ]",
"[ 3/1 → 25/8 | note:C4 ]",
"[ 25/8 → 13/4 | note:Bb3 ]",
"[ 13/4 → 27/8 | note:G3 ]",
"[ 27/8 → 7/2 | note:Eb3 ]",
"[ 7/2 → 29/8 | note:C3 ]",
"[ 29/8 → 15/4 | note:Eb3 ]",
"[ 15/4 → 31/8 | note:G3 ]",
"[ 31/8 → 4/1 | note:Bb3 ]",
]
`;
exports[`runs examples > example "jux" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd pan:0 ]",
@@ -6024,67 +6178,54 @@ exports[`runs examples > example "ply" example index 0 1`] = `
exports[`runs examples > example "polymeter" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c ]",
"[ 0/1 → 1/3 | note:c2 ]",
"[ 1/32/3 | note:eb ]",
"[ 1/32/3 | note:g2 ]",
"[ 2/3 → 1/1 | note:g ]",
"[ 2/3 → 1/1 | note:c2 ]",
"[ 1/14/3 | note:c ]",
"[ 1/14/3 | note:g2 ]",
"[ 4/3 → 5/3 | note:eb ]",
"[ 4/3 → 5/3 | note:c2 ]",
"[ 5/32/1 | note:g ]",
"[ 5/32/1 | note:g2 ]",
"[ 2/1 → 7/3 | note:c ]",
"[ 2/1 → 7/3 | note:c2 ]",
"[ 7/38/3 | note:eb ]",
"[ 7/38/3 | note:g2 ]",
"[ 8/3 → 3/1 | note:g ]",
"[ 8/3 → 3/1 | note:c2 ]",
"[ 3/110/3 | note:c ]",
"[ 3/110/3 | note:g2 ]",
"[ 10/3 → 11/3 | note:eb ]",
"[ 10/3 → 11/3 | note:c2 ]",
"[ 11/34/1 | note:g ]",
"[ 11/34/1 | note:g2 ]",
]
`;
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
"[ 0/1 → 1/4 | note:e ]",
"[ 1/4 → 1/2 | note:d ]",
"[ 1/4 → 1/2 | note:f ]",
"[ 1/2 → 3/4 | note:c ]",
"[ 1/2 → 3/4 | note:g ]",
"[ 3/41/1 | note:d ]",
"[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | note:c ]",
"[ 1/1 → 5/4 | note:f ]",
"[ 5/4 → 3/2 | note:d ]",
"[ 5/43/2 | note:g ]",
"[ 3/2 → 7/4 | note:c ]",
"[ 3/2 → 7/4 | note:e ]",
"[ 7/42/1 | note:d ]",
"[ 7/4 → 2/1 | note:f ]",
"[ 2/1 → 9/4 | note:c ]",
"[ 2/1 → 9/4 | note:g ]",
"[ 9/4 → 5/2 | note:d ]",
"[ 9/4 → 5/2 | note:e ]",
"[ 5/2 → 11/4 | note:c ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:d ]",
"[ 11/4 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | note:c ]",
"[ 3/1 → 13/4 | note:e ]",
"[ 13/4 → 7/2 | note:d ]",
"[ 13/4 → 7/2 | note:f ]",
"[ 7/2 → 15/4 | note:c ]",
"[ 7/2 → 15/4 | note:g ]",
"[ 15/4 → 4/1 | note:d ]",
"[ 15/4 → 4/1 | note:e ]",
"[ 0/1 → 1/6 | note:c ]",
"[ 0/1 → 1/6 | note:c2 ]",
"[ 1/61/3 | note:eb ]",
"[ 1/61/3 | note:g2 ]",
"[ 1/3 → 1/2 | note:g ]",
"[ 1/3 → 1/2 | note:c2 ]",
"[ 1/22/3 | note:c ]",
"[ 1/22/3 | note:g2 ]",
"[ 2/3 → 5/6 | note:eb ]",
"[ 2/3 → 5/6 | note:c2 ]",
"[ 5/61/1 | note:g ]",
"[ 5/61/1 | note:g2 ]",
"[ 1/1 → 7/6 | note:c ]",
"[ 1/1 → 7/6 | note:c2 ]",
"[ 7/64/3 | note:eb ]",
"[ 7/64/3 | note:g2 ]",
"[ 4/3 → 3/2 | note:g ]",
"[ 4/3 → 3/2 | note:c2 ]",
"[ 3/25/3 | note:c ]",
"[ 3/25/3 | note:g2 ]",
"[ 5/3 → 11/6 | note:eb ]",
"[ 5/3 → 11/6 | note:c2 ]",
"[ 11/62/1 | note:g ]",
"[ 11/62/1 | note:g2 ]",
"[ 2/1 → 13/6 | note:c ]",
"[ 2/1 → 13/6 | note:c2 ]",
"[ 13/6 → 7/3 | note:eb ]",
"[ 13/6 → 7/3 | note:g2 ]",
"[ 7/3 → 5/2 | note:g ]",
"[ 7/3 → 5/2 | note:c2 ]",
"[ 5/2 → 8/3 | note:c ]",
"[ 5/2 → 8/3 | note:g2 ]",
"[ 8/3 → 17/6 | note:eb ]",
"[ 8/3 → 17/6 | note:c2 ]",
"[ 17/6 → 3/1 | note:g ]",
"[ 17/63/1 | note:g2 ]",
"[ 3/1 → 19/6 | note:c ]",
"[ 3/1 → 19/6 | note:c2 ]",
"[ 19/6 → 10/3 | note:eb ]",
"[ 19/6 → 10/3 | note:g2 ]",
"[ 10/37/2 | note:g ]",
"[ 10/3 → 7/2 | note:c2 ]",
"[ 7/2 → 11/3 | note:c ]",
"[ 7/211/3 | note:g2 ]",
"[ 11/3 → 23/6 | note:eb ]",
"[ 11/3 → 23/6 | note:c2 ]",
"[ 23/6 → 4/1 | note:g ]",
"[ 23/6 → 4/1 | note:g2 ]",
]
`;
@@ -6490,33 +6631,6 @@ exports[`runs examples > example "release" example index 0 1`] = `
]
`;
exports[`runs examples > example "repeat" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd ]",
"[ 1/8 → 1/4 | s:bd ]",
"[ 3/8 → 1/2 | s:cp ]",
"[ 1/2 → 5/8 | s:bd ]",
"[ 5/8 → 3/4 | s:bd ]",
"[ 7/8 → 1/1 | s:cp ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 5/4 → 11/8 | s:sd ]",
"[ 3/2 → 13/8 | s:bd ]",
"[ 13/8 → 7/4 | s:bd ]",
"[ 15/8 → 2/1 | s:cp ]",
"[ 2/1 → 17/8 | s:bd ]",
"[ 17/8 → 9/4 | s:bd ]",
"[ 19/8 → 5/2 | s:cp ]",
"[ 5/2 → 21/8 | s:bd ]",
"[ 11/4 → 23/8 | s:sd ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 25/8 → 13/4 | s:bd ]",
"[ 27/8 → 7/2 | s:cp ]",
"[ 7/2 → 29/8 | s:bd ]",
"[ 29/8 → 15/4 | s:bd ]",
"[ 31/8 → 4/1 | s:cp ]",
]
`;
exports[`runs examples > example "repeatCycles" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:34 s:gm_acoustic_guitar_nylon ]",
@@ -8999,38 +9113,38 @@ exports[`runs examples > example "transpose" example index 1 1`] = `
exports[`runs examples > example "tri" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:C4 ]",
"[ 1/8 → 1/4 | note:Bb3 ]",
"[ 0/1 → 1/8 | note:C3 ]",
"[ 1/8 → 1/4 | note:Eb3 ]",
"[ 1/4 → 3/8 | note:G3 ]",
"[ 3/8 → 1/2 | note:Eb3 ]",
"[ 1/2 → 5/8 | note:C3 ]",
"[ 5/8 → 3/4 | note:Eb3 ]",
"[ 3/8 → 1/2 | note:Bb3 ]",
"[ 1/2 → 5/8 | note:C4 ]",
"[ 5/8 → 3/4 | note:Bb3 ]",
"[ 3/4 → 7/8 | note:G3 ]",
"[ 7/8 → 1/1 | note:Bb3 ]",
"[ 1/1 → 9/8 | note:C4 ]",
"[ 9/8 → 5/4 | note:Bb3 ]",
"[ 7/8 → 1/1 | note:Eb3 ]",
"[ 1/1 → 9/8 | note:C3 ]",
"[ 9/8 → 5/4 | note:Eb3 ]",
"[ 5/4 → 11/8 | note:G3 ]",
"[ 11/8 → 3/2 | note:Eb3 ]",
"[ 3/2 → 13/8 | note:C3 ]",
"[ 13/8 → 7/4 | note:Eb3 ]",
"[ 11/8 → 3/2 | note:Bb3 ]",
"[ 3/2 → 13/8 | note:C4 ]",
"[ 13/8 → 7/4 | note:Bb3 ]",
"[ 7/4 → 15/8 | note:G3 ]",
"[ 15/8 → 2/1 | note:Bb3 ]",
"[ 2/1 → 17/8 | note:C4 ]",
"[ 17/8 → 9/4 | note:Bb3 ]",
"[ 15/8 → 2/1 | note:Eb3 ]",
"[ 2/1 → 17/8 | note:C3 ]",
"[ 17/8 → 9/4 | note:Eb3 ]",
"[ 9/4 → 19/8 | note:G3 ]",
"[ 19/8 → 5/2 | note:Eb3 ]",
"[ 5/2 → 21/8 | note:C3 ]",
"[ 21/8 → 11/4 | note:Eb3 ]",
"[ 19/8 → 5/2 | note:Bb3 ]",
"[ 5/2 → 21/8 | note:C4 ]",
"[ 21/8 → 11/4 | note:Bb3 ]",
"[ 11/4 → 23/8 | note:G3 ]",
"[ 23/8 → 3/1 | note:Bb3 ]",
"[ 3/1 → 25/8 | note:C4 ]",
"[ 25/8 → 13/4 | note:Bb3 ]",
"[ 23/8 → 3/1 | note:Eb3 ]",
"[ 3/1 → 25/8 | note:C3 ]",
"[ 25/8 → 13/4 | note:Eb3 ]",
"[ 13/4 → 27/8 | note:G3 ]",
"[ 27/8 → 7/2 | note:Eb3 ]",
"[ 7/2 → 29/8 | note:C3 ]",
"[ 29/8 → 15/4 | note:Eb3 ]",
"[ 27/8 → 7/2 | note:Bb3 ]",
"[ 7/2 → 29/8 | note:C4 ]",
"[ 29/8 → 15/4 | note:Bb3 ]",
"[ 15/4 → 31/8 | note:G3 ]",
"[ 31/8 → 4/1 | note:Bb3 ]",
"[ 31/8 → 4/1 | note:Eb3 ]",
]
`;
@@ -9560,6 +9674,59 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = `
]
`;
exports[`runs examples > example "wchooseCycles" example index 2 1`] = `
[
"[ 0/1 → 1/32 | s:bd ]",
"[ 3/32 → 1/8 | s:bd ]",
"[ 3/16 → 7/32 | 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:hh ]",
"[ 7/12 → 2/3 | s:hh ]",
"[ 2/3 → 3/4 | s:hh ]",
"[ 3/4 → 5/6 | s:hh ]",
"[ 5/6 → 11/12 | s:hh ]",
"[ 11/12 → 1/1 | s:hh ]",
"[ 1/1 → 33/32 | s:bd ]",
"[ 35/32 → 9/8 | s:bd ]",
"[ 19/16 → 39/32 | s:bd ]",
"[ 5/4 → 4/3 | s:hh ]",
"[ 4/3 → 17/12 | s:hh ]",
"[ 17/12 → 3/2 | s:hh ]",
"[ 3/2 → 49/32 | s:bd ]",
"[ 51/32 → 13/8 | s:bd ]",
"[ 27/16 → 55/32 | s:bd ]",
"[ 7/4 → 11/6 | s:hh ]",
"[ 11/6 → 23/12 | s:hh ]",
"[ 23/12 → 2/1 | s:hh ]",
"[ 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 → 81/32 | s:bd ]",
"[ 83/32 → 21/8 | s:bd ]",
"[ 43/16 → 87/32 | s:bd ]",
"[ 11/4 → 17/6 | s:hh ]",
"[ 17/6 → 35/12 | s:hh ]",
"[ 35/12 → 3/1 | s:hh ]",
"[ 3/1 → 97/32 | s:bd ]",
"[ 99/32 → 25/8 | s:bd ]",
"[ 51/16 → 103/32 | s:bd ]",
"[ 13/4 → 10/3 | s:hh ]",
"[ 10/3 → 41/12 | s:hh ]",
"[ 41/12 → 7/2 | s:hh ]",
"[ 7/2 → 43/12 | s:hh ]",
"[ 43/12 → 11/3 | s:hh ]",
"[ 11/3 → 15/4 | s:hh ]",
"[ 15/4 → 23/6 | s:hh ]",
"[ 23/6 → 47/12 | s:hh ]",
"[ 47/12 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "when" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c3 ]",
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
100% free for personal and commercial use.
However it's limited on basic latin only,
contact riedjal@gmail.com for full glyph (based on ANSI encoding)
and OTF features (alternates).
src: https://www.dafont.com/cute-aurora.font?text=%24%3A+s%28%22bd%285%2C8%29%22%29.superimpose%28x+%3D%3E+x.note%28%22c2%22%29.midi%28device%29%29
+5 -3
View File
@@ -36,8 +36,6 @@ export function Showcase() {
}
let _videos = [
{ title: 'Coding Music With Strudel Workhop by Dan Gorelick and Viola He', id: 'oqyAJ4WeKoU' },
{ title: 'Hexe - playing w strudel live coding music', id: '03m3F5xVOMg' },
{ title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' },
{ title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
@@ -58,7 +56,6 @@ let _videos = [
},
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
{ title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' },
//
/* { // not sure if this is copyrighted ...
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
@@ -126,6 +123,11 @@ let _videos = [
'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.',
},
{ title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' },
{ title: 'todepasta 1.5', id: 'gCwaVu1Mijg' },
{ title: 'Djenerative Music by Bogdan Vera @ TOPLAP solstice Dec 2024', id: 'LtMX4Lr1nzY' },
{ title: 'La musique by BuboBubo @ TOPLAP solstice Dec 2024', id: 'Oz00Y_f80wU' },
{ title: 'Livecode and vocal breaks by Switch Angel @ TOPLAP solstice Dec 2024', id: '2kzjOIsL6CM' },
{ title: 'Eddyflux algorave set @ rudolf5', id: 'MXz8131Ut0A' },
];
_shuffled = shuffleArray(_videos);
+2 -2
View File
@@ -9,11 +9,11 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'
// }
export default function UdelsEditor(Props) {
const { context } = Props;
const { context, ...editorProps } = Props;
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
return (
<div className={'h-full flex w-full flex-col relative'}>
<div className={'h-full flex w-full flex-col relative'} {...editorProps}>
<Loader active={pending} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden">
+1 -1
View File
@@ -4,7 +4,7 @@ export default function UdelsHeader(Props) {
const { numWindows, setNumWindows } = Props;
return (
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-800">
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
+4
View File
@@ -1,6 +1,10 @@
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
export function getMetadata(raw_code) {
if (raw_code == null) {
console.error('could not extract metadata from missing pattern code');
raw_code = '';
}
const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm;
const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
const tags = {};
+2 -2
View File
@@ -293,8 +293,6 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="iresponse" h={0} />
Next, we'll look at strudel's support for [Csound](/learn/csound).
## Phaser
### phaser
@@ -312,3 +310,5 @@ Next, we'll look at strudel's support for [Csound](/learn/csound).
### phasersweep
<JsDoc client:idle name="phasersweep" h={0} />
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
+17 -2
View File
@@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc';
# Mini-notation
Just like [Tidal Cycles](https://tidalcycles.org/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
Just like [Tidal Cycles](https://tidalcycles.org/docs/reference/mini_notation/), Strudel uses a so called "Mini-Notation", which is a custom language that is designed for writing rhythmic patterns using little amounts of text.
## Note
@@ -156,7 +156,11 @@ But to play multiple chords in a sequence, we have to wrap them in brackets:
## Elongation
With the "@" symbol, we can specify temporal "weight" of a sequence child:
With the "\_" symbol, we can manually extend the duration of an event:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ _ [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
With the "@" symbol, we can do the same thing more conveniently by specifying the temporal "weight" of a sequence child:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
@@ -168,6 +172,16 @@ Using "!" we can repeat without speeding up:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
## Degradation
Using "?" we can randomly drop events from a pattern while preserving their time slot:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4]? [b3,d3,f#4]>*2")`} punchcard />
We can include a number after the "?" to represent the probability of being dropped, otherwise defaulting to 0.5:
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4]?0.9 [b3,d3,f#4]>*2")`} punchcard />
## Mini-notation review
To recap what we've learned so far, compare the following patterns:
@@ -179,6 +193,7 @@ To recap what we've learned so far, compare the following patterns:
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
<MiniRepl client:idle tune={`note("<[g3,b3,e4]?0.9 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
## Euclidian rhythms
+2 -6
View File
@@ -100,9 +100,9 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
<JsDoc client:idle name="contract" h={0} />
### repeat
### extend
<JsDoc client:idle name="repeat" h={0} />
<JsDoc client:idle name="extend" h={0} />
### take
@@ -116,10 +116,6 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
<JsDoc client:idle name="polymeter" h={0} />
### polymeterSteps
<JsDoc client:idle name="polymeterSteps" h={0} />
### shrink
<JsDoc client:idle name="shrink" h={0} />
+3 -1
View File
@@ -9,10 +9,12 @@ import UdelsEditor from '@components/Udels/UdelsEditor';
import ReplEditor from './components/ReplEditor';
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
import { useReplContext } from './useReplContext';
import { useSettings } from '@src/settings.mjs';
export function Repl({ embedded = false }) {
const isEmbedded = embedded || isIframe();
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
const context = useReplContext();
return <Editor context={context} />;
const { fontFamily } = useSettings();
return <Editor context={context} style={{ fontFamily }} />;
}
@@ -9,10 +9,10 @@ import { Header } from './Header';
// }
export default function EmbeddedReplEditor(Props) {
const { context } = Props;
const { context, ...editorProps } = Props;
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
return (
<div className="h-full flex flex-col relative">
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} embedded={true} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
+3 -2
View File
@@ -11,7 +11,7 @@ export function Header({ context, embedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
context;
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<header
@@ -22,6 +22,7 @@ export function Header({ context, embedded = false }) {
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
isEmbedded ? 'flex' : 'md:flex',
)}
style={{ fontFamily }}
>
<div className="px-4 flex space-x-2 md:pt-0 select-none">
<h1
@@ -46,7 +47,7 @@ export function Header({ context, embedded = false }) {
}
}}
>
<span className="block rotate-90"></span>
<span className="block text-foreground rotate-90"></span>
</div>
{!isZen && (
<div className="space-x-2">
+2 -2
View File
@@ -10,13 +10,13 @@ import { useSettings } from '@src/settings.mjs';
// }
export default function ReplEditor(Props) {
const { context } = Props;
const { context, ...editorProps } = Props;
const { containerRef, editorRef, error, init, pending } = context;
const settings = useSettings();
const { panelPosition, isZen } = settings;
return (
<div className="h-full flex flex-col relative">
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} />
<div className="grow flex relative overflow-hidden">
@@ -0,0 +1,67 @@
import { Textbox } from '../textbox/Textbox';
import cx from '@src/cx.mjs';
function IncButton({ children, className, ...buttonProps }) {
return (
<button
tabIndex={-1}
className={cx(
'border border-transparent p-1 text-center hover:text-background text-sm transition-all hover:bg-foreground active:bg-lineBackground disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none',
className,
)}
type="button"
{...buttonProps}
>
{children}
</button>
);
}
export function Incrementor({
onChange,
value,
min = -Infinity,
max = Infinity,
className,
incrementLabel = 'next page',
decrementLabel = 'prev page',
...incrementorProps
}) {
value = parseInt(value);
value = isNaN(value) ? '' : value;
return (
<div className={cx('w-fit bg-background relative flex items-center"> rounded-md', className)}>
<Textbox
min={min}
max={max}
onChange={(v) => {
if (v.length && v < min) {
return;
}
onChange(v);
}}
type="number"
placeholder=""
value={value}
className="w-32 mb-0 mt-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
{...incrementorProps}
/>
<div className="flex gap-1 ">
<IncButton disabled={value <= min} onClick={() => onChange(value - 1)} aria-label={decrementLabel}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</IncButton>
<IncButton
className="rounded-r-md"
disabled={value >= max}
onClick={() => onChange(value + 1)}
aria-label={incrementLabel}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
</svg>
</IncButton>
</div>
</div>
);
}
@@ -0,0 +1,5 @@
import { Incrementor } from '../incrementor/Incrementor';
export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) {
return <Incrementor min={1} value={currPage} onChange={onPageChange} className={className} {...incrementorProps} />;
}
@@ -1,53 +1,32 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useSettings } from '../../../settings.mjs';
import { useStore } from '@nanostores/react';
import { $strudel_log_history } from '../useLogger';
export function ConsoleTab() {
const [log, setLog] = useState([]);
const { fontFamily, fontSize } = useSettings();
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
const log = useStore($strudel_log_history);
const { fontFamily } = useSettings();
return (
<div
id="console-tab"
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
style={{ fontFamily, fontSize }}
>
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return (
<div
key={l.id}
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
);
})}
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return (
<div
key={l.id}
className={cx(
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
l.type === 'highlight' && 'underline',
)}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
);
})}
</div>
</div>
);
}
@@ -72,7 +51,3 @@ function linkify(inputText) {
return replacedText;
}
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
@@ -5,6 +5,7 @@ import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { useLogger } from '../useLogger';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
@@ -115,6 +116,7 @@ function PanelNav({ children, className, settings, ...props }) {
}
function PanelContent({ context, tab }) {
useLogger();
switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;
+173 -96
View File
@@ -1,6 +1,8 @@
import {
exportPatterns,
importPatterns,
loadAndSetFeaturedPatterns,
loadAndSetPublicPatterns,
patternFilterName,
useActivePattern,
useViewingPatternData,
@@ -12,10 +14,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { settingsMap, useSettings } from '../../../settings.mjs';
function classNames(...classes) {
return classes.filter(Boolean).join(' ');
}
import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
@@ -25,21 +27,19 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const date = new Date(pattern.created_at);
if (!isNaN(date)) {
title = date.toLocaleDateString();
} else {
title = 'unnamed';
}
}
if (title == null) {
title = pattern.hash;
}
if (title == null) {
title = 'unnamed';
}
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
}
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
return (
<a
className={classNames(
className={cx(
'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1',
showHiglight && 'bg-selection',
@@ -56,7 +56,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternID = viewingPatternData.id;
return (
<div className="font-mono text-sm">
<div className="">
{Object.values(patterns)
.reverse()
.map((pattern) => {
@@ -84,82 +84,72 @@ function ActionButton({ children, onClick, label, labelIsHidden }) {
);
}
export function PatternsTab({ context }) {
const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const autoResetPatternOnChange = !isUdels();
function UserPatterns({ context }) {
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter } = useSettings();
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const updateCodeWindow = (patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const viewingPatternID = viewingPatternData?.id;
const autoResetPatternOnChange = !isUdels();
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user && (
<div>
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow({ ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(data);
}}
/>
</div>
</div>
)}
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div>
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
<div className="overflow-auto h-full bg-background p-2 rounded-md">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
updateCodeWindow(
context,
{ ...userPatterns[id], collection: userPattern.collection },
autoResetPatternOnChange,
)
}
patterns={userPatterns}
started={context.started}
@@ -167,24 +157,111 @@ export function PatternsTab({ context }) {
viewingPatternID={viewingPatternID}
/>
)}
{patternFilter !== patternFilterName.user &&
Array.from(collections.keys()).map((collection) => {
const patterns = collections.get(collection);
return (
<section key={collection} className="py-2">
<h2 className="text-xl mb-2">{collection}</h2>
<div className="font-mono text-sm">
<PatternButtons
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
started={context.started}
patterns={patterns}
activePattern={activePattern}
/>
</div>
</section>
);
})}
</section>
</div>
</div>
);
}
function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) {
const [page, setPage] = useState(initialPage);
const debouncedPageChange = useDebounce(() => {
paginationOnChange(page);
});
const onPageChange = (pageNum) => {
setPage(pageNum);
debouncedPageChange();
};
const activePattern = useActivePattern();
return (
<div className="flex flex-grow flex-col h-full overflow-hidden justify-between">
<div className="overflow-auto flex flex-col flex-grow bg-background p-2 rounded-md ">
<PatternButtons
onClick={(id) => patternOnClick(id)}
started={context.started}
patterns={patterns}
activePattern={activePattern}
/>
</div>
<div className="flex items-center gap-2 py-2">
<label htmlFor="pattern pagination">Page</label>
<Pagination id="pattern pagination" currPage={page} onPageChange={onPageChange} />
</div>
</div>
);
}
let featuredPageNum = 1;
function FeaturedPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.featured);
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={featuredPageNum}
patternOnClick={(id) => {
updateCodeWindow(
context,
{ ...patterns[id], collection: patternFilterName.featured },
autoResetPatternOnChange,
);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetFeaturedPatterns(pageNum - 1);
featuredPageNum = pageNum;
}}
/>
);
}
let latestPageNum = 1;
function LatestPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.public);
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={latestPageNum}
patternOnClick={(id) => {
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetPublicPatterns(pageNum - 1);
latestPageNum = pageNum;
}}
/>
);
}
function PublicPatterns({ context }) {
const { patternFilter } = useSettings();
if (patternFilter === patternFilterName.featured) {
return <FeaturedPatterns context={context} />;
}
return <LatestPatterns context={context} />;
}
export function PatternsTab({ context }) {
const { patternFilter } = useSettings();
return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user ? (
<UserPatterns context={context} />
) : (
<PublicPatterns context={context} />
)}
</div>
);
}
@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
@@ -25,21 +26,16 @@ export function Reference() {
}, [search]);
return (
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
<div className="flex h-full w-full p-2 overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full flex">
<input
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
</div>
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
@@ -79,7 +75,9 @@ export function Reference() {
))}
</ul>
{entry.examples?.map((example, j) => (
<pre key={j}>{example}</pre>
<pre className="bg-background" key={j}>
{example}
</pre>
))}
</section>
))}
@@ -66,6 +66,7 @@ const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
const fontFamilyOptions = {
monospace: 'monospace',
Courier: 'Courier',
CutiePi: 'CutiePi',
JetBrains: 'JetBrains',
Hack: 'Hack',
FiraCode: 'FiraCode',
@@ -108,7 +109,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4 w-full">
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@@ -141,7 +142,7 @@ export function SettingsTab({ started }) {
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
<FormItem label="Font Family">
<SelectInput
options={fontFamilyOptions}
@@ -5,6 +5,7 @@ import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx';
const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
@@ -52,13 +53,8 @@ export function SoundsTab() {
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<input
className="w-full p-1 bg-background rounded-md my-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
<div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
@@ -74,7 +70,7 @@ export function SoundsTab() {
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -1,11 +1,12 @@
import cx from '@src/cx.mjs';
import { useSettings } from '@src/settings.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function WelcomeTab({ context }) {
const { fontFamily } = useSettings();
return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}>
<h3> welcome</h3>
<p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
@@ -43,7 +44,8 @@ export function WelcomeTab({ context }) {
<a href="https://github.com/tidalcycles/strudel" target="_blank">
github
</a>
. Please consider to{' '}
. You can also find <a href="https://github.com/felixroos/dough-samples/blob/main/README.md">licensing info</a>{' '}
for the default sound banks there. Please consider to{' '}
<a href="https://opencollective.com/tidalcycles" target="_blank">
support this project
</a>{' '}
@@ -0,0 +1,11 @@
import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input
className={cx('p-1 bg-background rounded-md my-2 border-foreground', className)}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
/>
);
}
+33
View File
@@ -0,0 +1,33 @@
import useEvent from '@src/useEvent.mjs';
import { logger } from '@strudel/core';
import { nanoid } from 'nanoid';
import { atom } from 'nanostores';
export const $strudel_log_history = atom([]);
function useLoggerEvent(onTrigger) {
useEvent(logger.key, onTrigger);
}
function getUpdatedLog(log, event) {
const { message, type, data } = event.detail;
const lastLog = log.length ? log[log.length - 1] : undefined;
const id = nanoid(12);
if (type === 'loaded-sample') {
const loadIndex = log.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
log[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
log = log.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
log = log.concat([{ message, type, id, data }]);
}
return log.slice(-20);
}
export function useLogger() {
useLoggerEvent((event) => {
const log = $strudel_log_history.get();
const newLog = getUpdatedLog(log, event);
$strudel_log_history.set(newLog);
});
}
@@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useRef } from 'react';
function debounce(fn, wait) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => fn(...args), wait);
};
}
export function useDebounce(callback) {
const ref = useRef;
useEffect(() => {
ref.current = callback;
}, [callback]);
const debouncedCallback = useMemo(() => {
const func = () => {
ref.current?.();
};
return debounce(func, 1000);
}, []);
return debouncedCallback;
}
+4 -4
View File
@@ -1,4 +1,4 @@
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs';
import { $featuredPatterns, $publicPatterns, patternFilterName } from '../user_pattern_utils.mjs';
import { useStore } from '@nanostores/react';
import { useMemo } from 'react';
import * as tunes from '../repl/tunes.mjs';
@@ -12,9 +12,9 @@ export const useExamplePatterns = () => {
const publicPatterns = useStore($publicPatterns);
const collections = useMemo(() => {
const pats = new Map();
pats.set(collectionName.featured, featuredPatterns);
pats.set(collectionName.public, publicPatterns);
// pats.set(collectionName.stock, stockPatterns);
pats.set(patternFilterName.featured, featuredPatterns);
pats.set(patternFilterName.public, publicPatterns);
// pats.set(patternFilterName.stock, stockPatterns);
return pats;
}, [featuredPatterns, publicPatterns]);
+9
View File
@@ -1,6 +1,7 @@
@font-face {
font-family: 'PressStart';
src: url('/fonts/PressStart2P/PressStart2P-Regular.ttf');
size-adjust: 65%;
}
@font-face {
font-family: 'BigBlueTerminal';
@@ -14,6 +15,11 @@
font-family: 'galactico';
src: url('/fonts/galactico/Galactico-Basic.otf');
}
@font-face {
font-family: 'CutiePi';
src: url('/fonts/CutiePi/Cute_Aurora_demo.ttf');
size-adjust: 120%;
}
@font-face {
font-family: 'JetBrains';
src: url('/fonts/JetBrains/JetBrainsMono.woff2');
@@ -21,6 +27,7 @@
@font-face {
font-family: 'Monocraft';
src: url('/fonts/Monocraft/Monocraft.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'Hack';
@@ -41,10 +48,12 @@
@font-face {
font-family: 'teletext';
src: url('/fonts/teletext/EuropeanTeletext.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'mode7';
src: url('/fonts/mode7/MODE7GX3.TTF');
size-adjust: 82%;
}
.prose > h1:not(:first-child) {
+45 -24
View File
@@ -8,16 +8,12 @@ import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
export let $publicPatterns = atom([]);
export let $featuredPatterns = atom([]);
export const collectionName = {
user: 'user',
public: 'Last Creations',
stock: 'Stock Examples',
featured: 'Featured',
};
const patternQueryLimit = 20;
export const patternFilterName = {
community: 'community',
public: 'latest',
featured: 'featured',
user: 'user',
// stock: 'stock examples',
};
const sessionAtom = (name, initial = undefined) => {
@@ -36,7 +32,7 @@ const sessionAtom = (name, initial = undefined) => {
export let $viewingPatternData = sessionAtom('viewingPatternData', {
id: '',
code: '',
collection: collectionName.user,
collection: patternFilterName.user,
created_at: Date.now(),
});
@@ -51,25 +47,50 @@ export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data));
};
export function loadPublicPatterns() {
return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false });
function parsePageNum(page) {
return isNaN(page) ? 0 : page;
}
export function loadPublicPatterns(page) {
page = parsePageNum(page);
const offset = page * patternQueryLimit;
return supabase
.from('code_v1')
.select()
.eq('public', true)
.range(offset, offset + patternQueryLimit)
.order('id', { ascending: false });
}
export function loadFeaturedPatterns() {
return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false });
export function loadFeaturedPatterns(page = 0) {
page = parsePageNum(page);
const offset = page * patternQueryLimit;
return supabase
.from('code_v1')
.select()
.eq('featured', true)
.range(offset, offset + patternQueryLimit)
.order('id', { ascending: false });
}
export async function loadAndSetPublicPatterns(page) {
const p = await loadPublicPatterns(page);
const data = p?.data;
const pats = {};
data?.forEach((data, key) => (pats[data.id ?? key] = data));
$publicPatterns.set(pats);
}
export async function loadAndSetFeaturedPatterns(page) {
const p = await loadFeaturedPatterns(page);
const data = p?.data;
const pats = {};
data?.forEach((data, key) => (pats[data.id ?? key] = data));
$featuredPatterns.set(pats);
}
export async function loadDBPatterns() {
try {
const { data: publicPatterns } = await loadPublicPatterns();
const { data: featuredPatterns } = await loadFeaturedPatterns();
const featured = {};
const pub = {};
publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data));
featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data));
$publicPatterns.set(pub);
$featuredPatterns.set(featured);
await loadAndSetPublicPatterns();
await loadAndSetFeaturedPatterns();
} catch (err) {
console.error('error loading patterns', err);
}
@@ -90,9 +111,9 @@ export function useActivePattern() {
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
const defaultCode = '';
export const defaultCode = '';
export const userPattern = {
collection: collectionName.user,
collection: patternFilterName.user,
getAll() {
const patterns = parseJSON(settingsMap.get().userPatterns);
return patterns ?? {};
+23
View File
@@ -46,6 +46,29 @@ module.exports = {
'code::after': {
content: 'none',
},
color: 'var(--foreground)',
a: {
color: 'var(--foreground)',
},
h1: {
color: 'var(--foreground)',
},
h2: {
color: 'var(--foreground)',
},
h3: {
color: 'var(--foreground)',
},
h4: {
color: 'var(--foreground)',
},
pre: {
color: 'var(--foreground)',
background: 'var(--background)',
},
code: {
color: 'var(--foreground)',
},
},
},
};