diff --git a/README.md b/README.md index 200faaae..12ee8503 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/package.json b/package.json index 9068b2ca..df50f1f4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 5886c476..e96b533e 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -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, diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 47d6c3c1..c2f49065 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -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, diff --git a/packages/codemirror/themes/CutiePi.mjs b/packages/codemirror/themes/CutiePi.mjs new file mode 100644 index 00000000..f649a117 --- /dev/null +++ b/packages/codemirror/themes/CutiePi.mjs @@ -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 }, + ], +}); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6bfe39ce..c1e95211 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -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; diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index ade43e32..5348173d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -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("*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(); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 0696f0e5..08611032 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -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), + ); + }); + }); }); diff --git a/packages/gamepad/README.md b/packages/gamepad/README.md new file mode 100644 index 00000000..239353fb --- /dev/null +++ b/packages/gamepad/README.md @@ -0,0 +1,93 @@ +# @strudel/gamepad + +This package adds gamepad input functionality to strudel Patterns. + +## Install + +```sh +npm i @strudel/gamepad --save +``` + +## Usage + +```javascript +import { gamepad } from '@strudel/gamepad'; + +// Initialize gamepad (optional index parameter, defaults to 0) +const pad = gamepad(0); + +// Use gamepad inputs in patterns +const pattern = sequence([ + // Button inputs + pad.a, // A button value (0-1) + pad.tglA, // A button toggle (0 or 1) + + // Analog stick inputs + pad.x1, // Left stick X (0-1) + pad.x1_2, // Left stick X (-1 to 1) +]); +``` + +## Available Controls + +### Buttons +- Face Buttons + - `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) + - Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` +- Shoulder Buttons + - `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) + - Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` +- D-Pad + - `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) + - Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`) + +### Analog Sticks +- Left Stick + - `x1`, `y1` (0 to 1 range) + - `x1_2`, `y1_2` (-1 to 1 range) +- Right Stick + - `x2`, `y2` (0 to 1 range) + - `x2_2`, `y2_2` (-1 to 1 range) + +## Examples + +```javascript +// Use button values to control amplitude +$: sequence([ + s("bd").gain(pad.X), // X button controls gain + s("[hh oh]").gain(pad.tglY), // Y button toggles gain +]); + +// Use analog stick for continuous control +$: note("c4*4".add(pad.y1_2.range(-24,24))) // Left stick Y controls pitch shift + .pan(pad.x1_2); // Left stick X controls panning + +// Use toggle buttons to switch patterns on/off + +// Define button sequences +const HADOKEN = [ + 'd', // Down + 'r', // Right + 'a', // A +]; + +const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA + +// Add these lines to enable buttons(but why?) +$:pad.D.segment(16).gain(0) +$:pad.R.segment(16).gain(0) +$:pad.A.segment(16).gain(0) + +// Check button sequence (returns 1 when detected, 0 when not within last 1 second) +$: sound("hadoken").gain(pad.checkSequence(HADOKEN)) + +``` + +## Multiple Gamepads + +You can connect multiple gamepads by specifying the gamepad index: + +```javascript +const pad1 = gamepad(0); // First gamepad +const pad2 = gamepad(1); // Second gamepad +``` diff --git a/packages/gamepad/docs/gamepad.mdx b/packages/gamepad/docs/gamepad.mdx new file mode 100644 index 00000000..219d8ae0 --- /dev/null +++ b/packages/gamepad/docs/gamepad.mdx @@ -0,0 +1,117 @@ +import { MiniRepl } from '../../../website/src/docs/MiniRepl'; + +# Gamepad + +The Gamepad module allows you to integrate gamepad input functionality into your musical patterns. This can be particularly useful for live performances or interactive installations where you want to manipulate sounds using a game controller. + +## Getting Started + +Initialize a gamepad by calling the gamepad() function with an optional index parameter. + + + +## Available Controls + +The gamepad module provides access to buttons and analog sticks as normalized signals (0-1) that can modulate your patterns. + +### Buttons + +| Type | Controls | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| Face Buttons | `a`, `b`, `x`, `y` (or uppercase `A`, `B`, `X`, `Y`) | +| | Toggle versions: `tglA`, `tglB`, `tglX`, `tglY` | +| Shoulder Buttons | `lb`, `rb`, `lt`, `rt` (or uppercase `LB`, `RB`, `LT`, `RT`) | +| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` | +| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) | +| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) | + +### Analog Sticks + +| Stick | Controls | +| ----------- | ------------------------------ | +| Left Stick | `x1`, `y1` (0 to 1 range) | +| | `x1_2`, `y1_2` (-1 to 1 range) | +| Right Stick | `x2`, `y2` (0 to 1 range) | +| | `x2_2`, `y2_2` (-1 to 1 range) | + +### Button Sequence + +| Stick | Controls | +| --------------- | --------------------------------------- | +| Button Sequence | `btnSequence()`, `btnSeq()`, `btnseq()` | + +## Using Gamepad Inputs + +Once initialized, you can use various gamepad inputs in your patterns. Here are some examples: + +### Button Inputs + +You can use button inputs to control different aspects of your music, such as gain or triggering events. + + + +### Analog Stick Inputs + +Analog sticks can be used for continuous control, such as pitch shifting or panning. + + + +### Button Sequences + +You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected. + + + +## Multiple Gamepads + +Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices. + + diff --git a/packages/gamepad/gamepad.mjs b/packages/gamepad/gamepad.mjs new file mode 100644 index 00000000..7667a362 --- /dev/null +++ b/packages/gamepad/gamepad.mjs @@ -0,0 +1,246 @@ +// @strudel/gamepad/index.mjs + +import { signal } from '@strudel/core'; + +// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216) +export const buttonMap = { + a: 0, + b: 1, + x: 2, + y: 3, + lb: 4, + rb: 5, + lt: 6, + rt: 7, + back: 8, + start: 9, + u: 12, + up: 12, + d: 13, + down: 13, + l: 14, + left: 14, + r: 15, + right: 15, +}; + +class ButtonSequenceDetector { + constructor(timeWindow = 1000) { + this.sequence = []; + this.timeWindow = timeWindow; + this.lastInputTime = 0; + this.buttonStates = Array(16).fill(0); // Track previous state of each button + // Button mapping for character inputs + } + + addInput(buttonIndex, buttonValue) { + const currentTime = Date.now(); + + // Only add input on button press (rising edge) + if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) { + // Clear sequence if too much time has passed + if (currentTime - this.lastInputTime > this.timeWindow) { + this.sequence = []; + } + + // Store the button name instead of index + const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString(); + + this.sequence.push({ + input: buttonName, + timestamp: currentTime, + }); + + this.lastInputTime = currentTime; + + //console.log(this.sequence); + // Keep only inputs within the time window + this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow); + } + + // Update button state + this.buttonStates[buttonIndex] = buttonValue; + } + + checkSequence(targetSequence) { + if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') { + console.error('ButtonSequenceDetector: targetSequence must be an array or string'); + return 0; + } + + if (this.sequence.length < targetSequence.length) return 0; + + // Convert string input to array if needed + const sequence = + typeof targetSequence === 'string' + ? targetSequence.toLowerCase().split('') + : targetSequence.map((s) => s.toString().toLowerCase()); + + //console.log(this.sequence); + + // Get the last n inputs where n is the target sequence length + const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input); + + // Compare sequences + return lastInputs.every((input, index) => { + const target = sequence[index]; + // Check if either the input matches directly or they refer to the same button in the map + return ( + input === target || + buttonMap[input] === buttonMap[target] || + // Also check if the numerical index matches + buttonMap[input] === parseInt(target) + ); + }) + ? 1 + : 0; + } +} + +class GamepadHandler { + constructor(index = 0) { + // Add index parameter + this._gamepads = {}; + this._activeGamepad = index; // Use provided index + this._axes = [0, 0, 0, 0]; + this._buttons = Array(16).fill(0); + this.setupEventListeners(); + } + + setupEventListeners() { + window.addEventListener('gamepadconnected', (e) => { + this._gamepads[e.gamepad.index] = e.gamepad; + if (!this._activeGamepad) { + this._activeGamepad = e.gamepad.index; + } + }); + + window.addEventListener('gamepaddisconnected', (e) => { + delete this._gamepads[e.gamepad.index]; + if (this._activeGamepad === e.gamepad.index) { + this._activeGamepad = Object.keys(this._gamepads)[0] || null; + } + }); + } + + poll() { + if (this._activeGamepad !== null) { + const gamepad = navigator.getGamepads()[this._activeGamepad]; + if (gamepad) { + // Update axes (normalized to 0-1 range) + this._axes = gamepad.axes.map((axis) => (axis + 1) / 2); + // Update buttons + this._buttons = gamepad.buttons.map((button) => button.value); + } + } + } + + getAxes() { + return this._axes; + } + getButtons() { + return this._buttons; + } +} + +// Module-level state store for toggle states +const gamepadStates = new Map(); + +export const gamepad = (index = 0) => { + const handler = new GamepadHandler(index); + const sequenceDetector = new ButtonSequenceDetector(2000); + + // Base signal that polls gamepad state and handles sequence detection + const baseSignal = signal((t) => { + handler.poll(); + const axes = handler.getAxes(); + const buttons = handler.getButtons(); + + // Add all button inputs to sequence detector + buttons.forEach((value, i) => { + sequenceDetector.addInput(i, value); + }); + + return { axes, buttons, t }; + }); + + // Create axes patterns + const axes = { + x1: baseSignal.fmap((state) => state.axes[0]), + y1: baseSignal.fmap((state) => state.axes[1]), + x2: baseSignal.fmap((state) => state.axes[2]), + y2: baseSignal.fmap((state) => state.axes[3]), + }; + + // Add bipolar versions + axes.x1_2 = axes.x1.toBipolar(); + axes.y1_2 = axes.y1.toBipolar(); + axes.x2_2 = axes.x2.toBipolar(); + axes.y2_2 = axes.y2.toBipolar(); + + // Create button patterns + const buttons = Array(16) + .fill(null) + .map((_, i) => { + // Create unique key for this gamepad+button combination + const stateKey = `gamepad${index}_btn${i}`; + + // Initialize toggle state if it doesn't exist + if (!gamepadStates.has(stateKey)) { + gamepadStates.set(stateKey, { + lastButtonState: 0, + toggleState: 0, + }); + } + + // Direct button value pattern (no longer needs to call addInput) + const btn = baseSignal.fmap((state) => state.buttons[i]); + + // Button toggle pattern with persistent state + const toggle = baseSignal.fmap((state) => { + const currentState = state.buttons[i]; + const buttonState = gamepadStates.get(stateKey); + + if (currentState === 1 && buttonState.lastButtonState === 0) { + // Toggle the state on rising edge + buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0; + } + + buttonState.lastButtonState = currentState; + return buttonState.toggleState; + }); + + return { value: btn, toggle }; + }); + + // Create sequence checker pattern + const btnSequence = (sequence) => { + return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence)); + }; + const checkSequence = btnSequence; + const btnSeq = btnSequence; + const btnseq = btnSeq; + + // Return an object with all controls + return { + ...axes, + buttons, + ...Object.fromEntries( + Object.entries(buttonMap).flatMap(([key, index]) => [ + [key.toLowerCase(), buttons[index].value], + [key.toUpperCase(), buttons[index].value], + [`tgl${key.toLowerCase()}`, buttons[index].toggle], + [`tgl${key.toUpperCase()}`, buttons[index].toggle], + ]), + ), + checkSequence, + btnSequence, + btnSeq, + btnseq, + raw: baseSignal, + }; +}; + +// Optional: Export for debugging or state management +export const getGamepadStates = () => Object.fromEntries(gamepadStates); +export const clearGamepadStates = () => gamepadStates.clear(); diff --git a/packages/gamepad/index.mjs b/packages/gamepad/index.mjs new file mode 100644 index 00000000..c5558fb9 --- /dev/null +++ b/packages/gamepad/index.mjs @@ -0,0 +1,3 @@ +import './gamepad.mjs'; + +export * from './gamepad.mjs'; diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json new file mode 100644 index 00000000..199c5d03 --- /dev/null +++ b/packages/gamepad/package.json @@ -0,0 +1,38 @@ +{ + "name": "@strudel/gamepad", + "version": "1.1.0", + "description": "Gamepad Inputs for strudel", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "vite": "^6.0.11" + } +} + \ No newline at end of file diff --git a/packages/gamepad/vite.config.js b/packages/gamepad/vite.config.js new file mode 100644 index 00000000..5df3edc1 --- /dev/null +++ b/packages/gamepad/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'index.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'index.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 12520992..d1e56108 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import cowsay from 'cowsay'; -import { createReadStream } from 'fs'; +import { createReadStream, existsSync } from 'fs'; import { readdir } from 'fs/promises'; import http from 'http'; import { join, sep } from 'path'; @@ -70,12 +70,15 @@ const server = http.createServer(async (req, res) => { return res.end(JSON.stringify(banks)); } let subpath = decodeURIComponent(req.url); - if (!files.includes(subpath)) { + const filePath = join(directory, subpath.split('/').join(sep)); + + //console.log('GET:', filePath); + const isFound = existsSync(filePath); + if (!isFound) { res.statusCode = 404; res.end('File not found'); return; } - const filePath = join(directory, subpath.split('/').join(sep)); const readStream = createReadStream(filePath); readStream.on('error', (err) => { res.statusCode = 500; @@ -99,12 +102,6 @@ Object.keys(networkInterfaces).forEach((key) => { }); }); -if (!IP) { - console.error("Unable to determine server's IP address."); - // eslint-disable-next-line - process.exit(1); -} - server.listen(PORT, IP_ADDRESS, () => { console.log(`@strudel/sampler is now serving audio files from: ${directory} @@ -113,6 +110,6 @@ To use them in the Strudel REPL, run: samples('http://localhost:${PORT}') Or on a machine in the same network: - samples('http://${IP}:${PORT}') + ${IP ? `samples('http://${IP}:${PORT}')` : `Unable to determine server's IP address.`} `); }); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 621a6e59..86c073d0 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -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, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index a2b7828c..61d5d96e 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -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); diff --git a/packages/tidal/package.json b/packages/tidal/package.json index 94507aa9..1758732d 100644 --- a/packages/tidal/package.json +++ b/packages/tidal/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 494d04f0..32fd711e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) @@ -267,6 +270,16 @@ importers: packages/embed: {} + packages/gamepad: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/hs2js: dependencies: web-tree-sitter: @@ -640,6 +653,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../packages/draw + '@strudel/gamepad': + specifier: workspace:* + version: link:../packages/gamepad '@strudel/hydra': specifier: workspace:* version: link:../packages/hydra @@ -1412,6 +1428,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 +1854,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 +2842,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 +4520,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 +4873,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 +6939,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 +7561,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 +8625,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 +8993,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 +10248,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 +12281,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 +12621,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 +15364,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: {} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index b395ed27..2920a7a4 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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 ]", @@ -6298,67 +6452,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/3 → 2/3 | note:eb ]", - "[ 1/3 → 2/3 | note:g2 ]", - "[ 2/3 → 1/1 | note:g ]", - "[ 2/3 → 1/1 | note:c2 ]", - "[ 1/1 → 4/3 | note:c ]", - "[ 1/1 → 4/3 | note:g2 ]", - "[ 4/3 → 5/3 | note:eb ]", - "[ 4/3 → 5/3 | note:c2 ]", - "[ 5/3 → 2/1 | note:g ]", - "[ 5/3 → 2/1 | note:g2 ]", - "[ 2/1 → 7/3 | note:c ]", - "[ 2/1 → 7/3 | note:c2 ]", - "[ 7/3 → 8/3 | note:eb ]", - "[ 7/3 → 8/3 | note:g2 ]", - "[ 8/3 → 3/1 | note:g ]", - "[ 8/3 → 3/1 | note:c2 ]", - "[ 3/1 → 10/3 | note:c ]", - "[ 3/1 → 10/3 | note:g2 ]", - "[ 10/3 → 11/3 | note:eb ]", - "[ 10/3 → 11/3 | note:c2 ]", - "[ 11/3 → 4/1 | note:g ]", - "[ 11/3 → 4/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/4 → 1/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/4 → 3/2 | note:g ]", - "[ 3/2 → 7/4 | note:c ]", - "[ 3/2 → 7/4 | note:e ]", - "[ 7/4 → 2/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/6 → 1/3 | note:eb ]", + "[ 1/6 → 1/3 | note:g2 ]", + "[ 1/3 → 1/2 | note:g ]", + "[ 1/3 → 1/2 | note:c2 ]", + "[ 1/2 → 2/3 | note:c ]", + "[ 1/2 → 2/3 | note:g2 ]", + "[ 2/3 → 5/6 | note:eb ]", + "[ 2/3 → 5/6 | note:c2 ]", + "[ 5/6 → 1/1 | note:g ]", + "[ 5/6 → 1/1 | note:g2 ]", + "[ 1/1 → 7/6 | note:c ]", + "[ 1/1 → 7/6 | note:c2 ]", + "[ 7/6 → 4/3 | note:eb ]", + "[ 7/6 → 4/3 | note:g2 ]", + "[ 4/3 → 3/2 | note:g ]", + "[ 4/3 → 3/2 | note:c2 ]", + "[ 3/2 → 5/3 | note:c ]", + "[ 3/2 → 5/3 | note:g2 ]", + "[ 5/3 → 11/6 | note:eb ]", + "[ 5/3 → 11/6 | note:c2 ]", + "[ 11/6 → 2/1 | note:g ]", + "[ 11/6 → 2/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/6 → 3/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/3 → 7/2 | note:g ]", + "[ 10/3 → 7/2 | note:c2 ]", + "[ 7/2 → 11/3 | note:c ]", + "[ 7/2 → 11/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 ]", ] `; @@ -6773,33 +6914,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 ]", @@ -9309,38 +9423,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 ]", ] `; @@ -9870,6 +9984,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 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index b1657f48..6b75fb3b 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -21,6 +21,9 @@ import '@strudel/xen/xen.mjs'; // import '@strudel/webaudio/webaudio.mjs'; // import '@strudel/serial/serial.mjs'; import '../website/src/repl/piano'; +//import * as motionHelpers from '../packages/motion/index.mjs'; +//import * as geolocationHelpers from '../packages/geolocation/index.mjs'; +import * as gamepadHelpers from '../packages/gamepad/index.mjs'; class MockedNode { chain() { @@ -137,7 +140,7 @@ evalScope( uiHelpersMocked, webaudio, tonalHelpers, - + gamepadHelpers, /* toneHelpers, voicingHelpers, diff --git a/website/package.json b/website/package.json index 411581f8..257ff643 100644 --- a/website/package.json +++ b/website/package.json @@ -29,6 +29,7 @@ "@strudel/csound": "workspace:*", "@strudel/desktopbridge": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/gamepad": "workspace:*", "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/website/public/fonts/CutiePi/Cute_Aurora_demo.ttf b/website/public/fonts/CutiePi/Cute_Aurora_demo.ttf new file mode 100644 index 00000000..2ff9d6cf Binary files /dev/null and b/website/public/fonts/CutiePi/Cute_Aurora_demo.ttf differ diff --git a/website/public/fonts/CutiePi/LICENSE.txt b/website/public/fonts/CutiePi/LICENSE.txt new file mode 100644 index 00000000..dbc024ae --- /dev/null +++ b/website/public/fonts/CutiePi/LICENSE.txt @@ -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 \ No newline at end of file diff --git a/website/src/components/Showcase.jsx b/website/src/components/Showcase.jsx index e4e624a9..a32c944a 100644 --- a/website/src/components/Showcase.jsx +++ b/website/src/components/Showcase.jsx @@ -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); diff --git a/website/src/components/Udels/UdelsEditor.jsx b/website/src/components/Udels/UdelsEditor.jsx index fe7a1ae4..f58ac5fc 100644 --- a/website/src/components/Udels/UdelsEditor.jsx +++ b/website/src/components/Udels/UdelsEditor.jsx @@ -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 ( -
+
diff --git a/website/src/components/Udels/UdelsHeader.jsx b/website/src/components/Udels/UdelsHeader.jsx index d56f3a1d..75471693 100644 --- a/website/src/components/Udels/UdelsHeader.jsx +++ b/website/src/components/Udels/UdelsHeader.jsx @@ -4,7 +4,7 @@ export default function UdelsHeader(Props) { const { numWindows, setNumWindows } = Props; return ( -