From fd0a910bfbcf9127d0f970c8475f445b94082f96 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Wed, 22 Oct 2025 19:38:22 -0400 Subject: [PATCH 01/11] Voicings JSDoc --- packages/core/controls.mjs | 53 +++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a3ca638aa..6f247180d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1750,17 +1750,58 @@ export const { semitone } = registerControl('semitone'); // TODO: synth param export const { voice } = registerControl('voice'); // voicings // https://codeberg.org/uzu/strudel/issues/506 -// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings +/** + * The chord to voice + * @name chord + * @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings + * @example + * chord("").voicing() + **/ export const { chord } = registerControl('chord'); -// which dictionary to use for the voicings +/** + * Which dictionary to use for the voicings. This falls back to the default dictionary if not provided + * + * @name dictionary + * @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use + * @example + * chord("Am C D F Am E Am E").dict('house').voicing + **/ export const { dictionary, dict } = registerControl('dictionary', 'dict'); -// the top note to align the voicing to, defaults to c5 +/** The top note to align the voicing to. Defaults to c5 + * + * @name anchor + * @param {string | Pattern} anchorNote the note to align the voicings to + * @example + * anchor("").chord("C").voicing() + **/ export const { anchor } = registerControl('anchor'); -// how the voicing is offset from the anchored position +/** + * Sets how the voicing is offset from the anchored position + * + * @name offset + * @param {number | Pattern} shift the amount to shift the voicing up or down + * @example + * chord("").offset("<0 1 2 3 4 5>") // alter the voicing each time + **/ export const { offset } = registerControl('offset'); -// how many octaves are voicing steps spread apart, defaults to 1 +/** + * How many octaves are voicing steps spread apart, defaults to 1 + * + * @name octaves + * @param {number | Pattern} count the number of octaves + * @example + * chord("").octaves("<2 4>").voicing() + **/ export const { octaves } = registerControl('octaves'); -// below = anchor note will be removed from the voicing, useful for melody harmonization +/** + * Remove anchor note from the voicing. Useful for melody harmonization + * + * @name mode + * @param {string | Pattern} modeName one of {below | above | duck | root} + * @example + * mode("").chord("C").voicing() + * + **/ export const { mode } = registerControl(['mode', 'anchor']); /** From a8c613b7eb46df2c3bdfa39b2930434306225cc4 Mon Sep 17 00:00:00 2001 From: Ignacio Ortega Date: Thu, 23 Oct 2025 12:45:44 +0200 Subject: [PATCH 02/11] Refactor sound stopping and triggering logic in SoundsTab component --- .../src/repl/components/panel/SoundsTab.jsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 363f6b98a..a9f10bf12 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -62,11 +62,9 @@ export function SoundsTab() { // stop current sound on mouseup useEvent('mouseup', () => { - const t = trigRef.current; + const ref = trigRef.current; trigRef.current = undefined; - t?.then((ref) => { - ref?.stop(getAudioContext().currentTime + 0.01); - }); + ref?.stop?.(getAudioContext().currentTime + 0.01); }); return (
@@ -124,12 +122,19 @@ export function SoundsTab() { duration: 0.5, }; soundPreviewIdx++; - const time = ctx.currentTime + 0.05; const onended = () => trigRef.current?.node?.disconnect(); - trigRef.current = Promise.resolve(onTrigger(time, params, onended)); - trigRef.current.then((ref) => { - connectToDestination(ref?.node); - }); + try { + // Pre-load the sample by calling onTrigger with a future time + // This triggers the loading but schedules playback for later + const time = ctx.currentTime + 0.5; // Give 500ms for loading + const ref = await onTrigger(time, params, onended); + trigRef.current = ref; + if (ref?.node) { + connectToDestination(ref.node); + } + } catch (err) { + console.warn('Failed to trigger sound:', err); + } }} > {' '} From 20bbda30f8aa9d8d56c9cf65c98dc51ccc78f8a2 Mon Sep 17 00:00:00 2001 From: moumar Date: Thu, 23 Oct 2025 23:15:21 +0200 Subject: [PATCH 03/11] Add setting to toggle pattern auto-start on pattern change --- website/src/repl/components/panel/PatternsTab.jsx | 12 ++++++------ website/src/repl/components/panel/SettingsTab.jsx | 6 ++++++ website/src/settings.mjs | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 8e2b75b96..519461442 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -79,13 +79,11 @@ 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 { userPatterns, patternFilter, patternAutoStart } = useSettings(); const viewingPatternID = viewingPatternData?.id; return (
@@ -139,7 +137,7 @@ function UserPatterns({ context }) { updateCodeWindow( context, { ...userPatterns[id], collection: userPattern.collection }, - autoResetPatternOnChange, + patternAutoStart, ) } patterns={userPatterns} @@ -188,6 +186,7 @@ function FeaturedPatterns({ context }) { const examplePatterns = useExamplePatterns(); const collections = examplePatterns.collections; const patterns = collections.get(patternFilterName.featured); + const { patternAutoStart } = useSettings(); return ( { @@ -213,13 +212,14 @@ function LatestPatterns({ context }) { const examplePatterns = useExamplePatterns(); const collections = examplePatterns.collections; const patterns = collections.get(patternFilterName.public); + const { patternAutoStart } = useSettings(); return ( { - updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange); + updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, patternAutoStart); }} paginationOnChange={async (pageNum) => { await loadAndSetPublicPatterns(pageNum - 1); diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 80daef018..c898609d2 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -112,6 +112,7 @@ export function SettingsTab({ started }) { multiChannelOrbits, isTabIndentationEnabled, isMultiCursorEnabled, + patternAutoStart } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -304,6 +305,11 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)} value={isCSSAnimationDisabled} /> + settingsMap.setKey('patternAutoStart', cbEvent.target.checked)} + value={patternAutoStart} + /> Try clicking the logo in the top left! diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 9365a6db5..bbf893551 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -96,6 +96,7 @@ export function useSettings() { isPanelOpen: parseBoolean(state.isPanelOpen), userPatterns: userPatterns, multiChannelOrbits: parseBoolean(state.multiChannelOrbits), + patternAutoStart: isUdels() ? false : parseBoolean(state.patternAutoStart) }; } From 1a362bd8ea640cfc250832db66255d1b95ff0db7 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Fri, 24 Oct 2025 09:19:02 -0400 Subject: [PATCH 04/11] Fix example --- packages/core/controls.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6f247180d..e317af357 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1764,7 +1764,13 @@ export const { chord } = registerControl('chord'); * @name dictionary * @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use * @example - * chord("Am C D F Am E Am E").dict('house').voicing + * addVoicings('house', { +'': ['7 12 16', '0 7 16', '4 7 12'], +'m': ['0 3 7'] +}) +chord("") +.dict('house').anchor(66) +.voicing().room(.5) **/ export const { dictionary, dict } = registerControl('dictionary', 'dict'); /** The top note to align the voicing to. Defaults to c5 From 6d4787ee33df83f83a8ee54ae7338e4c90702e60 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Fri, 24 Oct 2025 09:21:04 -0400 Subject: [PATCH 05/11] Test snapshot --- test/__snapshots__/examples.test.mjs.snap | 123 ++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index c39bb87bf..8ab10e8e0 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -840,6 +840,31 @@ exports[`runs examples > example "amp" example index 0 1`] = ` ] `; +exports[`runs examples > example "anchor" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:E2 ]", + "[ 0/1 → 1/1 | note:C3 ]", + "[ 0/1 → 1/1 | note:E3 ]", + "[ 0/1 → 1/1 | note:G3 ]", + "[ 0/1 → 1/1 | note:C4 ]", + "[ 1/1 → 2/1 | note:C3 ]", + "[ 1/1 → 2/1 | note:G3 ]", + "[ 1/1 → 2/1 | note:C4 ]", + "[ 1/1 → 2/1 | note:E4 ]", + "[ 1/1 → 2/1 | note:G4 ]", + "[ 2/1 → 3/1 | note:E3 ]", + "[ 2/1 → 3/1 | note:C4 ]", + "[ 2/1 → 3/1 | note:E4 ]", + "[ 2/1 → 3/1 | note:G4 ]", + "[ 2/1 → 3/1 | note:C5 ]", + "[ 3/1 → 4/1 | note:C4 ]", + "[ 3/1 → 4/1 | note:G4 ]", + "[ 3/1 → 4/1 | note:C5 ]", + "[ 3/1 → 4/1 | note:E5 ]", + "[ 3/1 → 4/1 | note:G5 ]", +] +`; + exports[`runs examples > example "apply" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:C3 ]", @@ -1847,6 +1872,31 @@ exports[`runs examples > example "chop" example index 0 1`] = ` ] `; +exports[`runs examples > example "chord" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:A3 ]", + "[ 0/1 → 1/1 | note:C4 ]", + "[ 0/1 → 1/1 | note:E4 ]", + "[ 0/1 → 1/1 | note:A4 ]", + "[ 0/1 → 1/1 | note:C5 ]", + "[ 1/1 → 2/1 | note:E3 ]", + "[ 1/1 → 2/1 | note:C4 ]", + "[ 1/1 → 2/1 | note:E4 ]", + "[ 1/1 → 2/1 | note:G4 ]", + "[ 1/1 → 2/1 | note:C5 ]", + "[ 2/1 → 3/1 | note:D3 ]", + "[ 2/1 → 3/1 | note:A3 ]", + "[ 2/1 → 3/1 | note:D4 ]", + "[ 2/1 → 3/1 | note:Gb4 ]", + "[ 2/1 → 3/1 | note:A4 ]", + "[ 3/1 → 4/1 | note:F3 ]", + "[ 3/1 → 4/1 | note:C4 ]", + "[ 3/1 → 4/1 | note:F4 ]", + "[ 3/1 → 4/1 | note:A4 ]", + "[ 3/1 → 4/1 | note:C5 ]", +] +`; + exports[`runs examples > example "chorus" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]", @@ -2739,6 +2789,23 @@ exports[`runs examples > example "detune" example index 0 1`] = ` ] `; +exports[`runs examples > example "dictionary" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:A3 room:0.5 ]", + "[ 0/1 → 1/1 | note:C4 room:0.5 ]", + "[ 0/1 → 1/1 | note:E4 room:0.5 ]", + "[ 1/1 → 2/1 | note:G3 room:0.5 ]", + "[ 1/1 → 2/1 | note:C4 room:0.5 ]", + "[ 1/1 → 2/1 | note:E4 room:0.5 ]", + "[ 2/1 → 3/1 | note:A3 room:0.5 ]", + "[ 2/1 → 3/1 | note:D4 room:0.5 ]", + "[ 2/1 → 3/1 | note:Gb4 room:0.5 ]", + "[ 3/1 → 4/1 | note:A3 room:0.5 ]", + "[ 3/1 → 4/1 | note:C4 room:0.5 ]", + "[ 3/1 → 4/1 | note:F4 room:0.5 ]", +] +`; + exports[`runs examples > example "distort" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh distort:0 ]", @@ -6496,6 +6563,28 @@ exports[`runs examples > example "miditouch" example index 0 1`] = ` ] `; +exports[`runs examples > example "mode" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:E3 ]", + "[ 0/1 → 1/1 | note:C4 ]", + "[ 0/1 → 1/1 | note:E4 ]", + "[ 0/1 → 1/1 | note:G4 ]", + "[ 0/1 → 1/1 | note:C5 ]", + "[ 1/1 → 2/1 | note:C5 ]", + "[ 1/1 → 2/1 | note:G5 ]", + "[ 1/1 → 2/1 | note:C6 ]", + "[ 1/1 → 2/1 | note:E6 ]", + "[ 2/1 → 3/1 | note:E3 ]", + "[ 2/1 → 3/1 | note:C4 ]", + "[ 2/1 → 3/1 | note:E4 ]", + "[ 2/1 → 3/1 | note:G4 ]", + "[ 3/1 → 4/1 | note:C5 ]", + "[ 3/1 → 4/1 | note:G5 ]", + "[ 3/1 → 4/1 | note:C6 ]", + "[ 3/1 → 4/1 | note:E6 ]", +] +`; + exports[`runs examples > example "morph" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", @@ -6814,6 +6903,31 @@ exports[`runs examples > example "octave" example index 0 1`] = ` ] `; +exports[`runs examples > example "octaves" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:A3 ]", + "[ 0/1 → 1/1 | note:C4 ]", + "[ 0/1 → 1/1 | note:E4 ]", + "[ 0/1 → 1/1 | note:A4 ]", + "[ 0/1 → 1/1 | note:C5 ]", + "[ 1/1 → 2/1 | note:E3 ]", + "[ 1/1 → 2/1 | note:C4 ]", + "[ 1/1 → 2/1 | note:E4 ]", + "[ 1/1 → 2/1 | note:G4 ]", + "[ 1/1 → 2/1 | note:C5 ]", + "[ 2/1 → 3/1 | note:D3 ]", + "[ 2/1 → 3/1 | note:A3 ]", + "[ 2/1 → 3/1 | note:D4 ]", + "[ 2/1 → 3/1 | note:Gb4 ]", + "[ 2/1 → 3/1 | note:A4 ]", + "[ 3/1 → 4/1 | note:F3 ]", + "[ 3/1 → 4/1 | note:C4 ]", + "[ 3/1 → 4/1 | note:F4 ]", + "[ 3/1 → 4/1 | note:A4 ]", + "[ 3/1 → 4/1 | note:C5 ]", +] +`; + exports[`runs examples > example "off" example index 0 1`] = ` [ "[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]", @@ -6847,6 +6961,15 @@ exports[`runs examples > example "off" example index 0 1`] = ` ] `; +exports[`runs examples > example "offset" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | chord:Am offset:0 ]", + "[ 1/1 → 2/1 | chord:C offset:1 ]", + "[ 2/1 → 3/1 | chord:D offset:2 ]", + "[ 3/1 → 4/1 | chord:F offset:3 ]", +] +`; + exports[`runs examples > example "often" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh speed:0.5 ]", From fddf2ff2a76eb04f900f187642e6c2e978d97df9 Mon Sep 17 00:00:00 2001 From: moumar Date: Sun, 26 Oct 2025 14:06:31 +0100 Subject: [PATCH 06/11] Set pattern auto-start to true by default --- website/src/settings.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index bbf893551..ef074b785 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -96,7 +96,7 @@ export function useSettings() { isPanelOpen: parseBoolean(state.isPanelOpen), userPatterns: userPatterns, multiChannelOrbits: parseBoolean(state.multiChannelOrbits), - patternAutoStart: isUdels() ? false : parseBoolean(state.patternAutoStart) + patternAutoStart: isUdels() ? false : state.patternAutoStart === undefined ? true : parseBoolean(state.patternAutoStart) }; } From fcd83cce5556541c4fcb72e05a2468ef902af157 Mon Sep 17 00:00:00 2001 From: moumar Date: Sun, 26 Oct 2025 14:07:36 +0100 Subject: [PATCH 07/11] Re-evaluate pattern if activated --- website/src/repl/components/panel/PatternsTab.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 519461442..38b7294de 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -133,13 +133,17 @@ function UserPatterns({ context }) {
{/* {patternFilter === patternFilterName.user && ( */} + onClick={(id) => { updateCodeWindow( context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart, ) - } + + if (context.started && activePattern === id) { + context.handleEvaluate() + } + }} patterns={userPatterns} started={context.started} activePattern={activePattern} From ee43d85f2983cad68a3dc6ae184560038053bdd4 Mon Sep 17 00:00:00 2001 From: Greg Ervin Date: Sun, 26 Oct 2025 13:31:13 -0500 Subject: [PATCH 08/11] Add stick button mappings to gamepad implementation and improve docs --- packages/gamepad/README.md | 6 ++++++ packages/gamepad/docs/gamepad.mdx | 4 ++++ packages/gamepad/gamepad.mjs | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/packages/gamepad/README.md b/packages/gamepad/README.md index 239353fb7..a84177bc2 100644 --- a/packages/gamepad/README.md +++ b/packages/gamepad/README.md @@ -40,6 +40,12 @@ const pattern = sequence([ - D-Pad - `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) - Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`) +- Stick Buttons + - `l3`, `r3` (or `ls`, `rs`) + - Toggle versions: `tglL3`, `tglR3` (or `tglLS`, `tglRS`) +- System Buttons + - `start`, `back` (or uppercase `START`, `BACK`) + - Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) ### Analog Sticks - Left Stick diff --git a/packages/gamepad/docs/gamepad.mdx b/packages/gamepad/docs/gamepad.mdx index e2da594ea..5071e3d58 100644 --- a/packages/gamepad/docs/gamepad.mdx +++ b/packages/gamepad/docs/gamepad.mdx @@ -29,6 +29,10 @@ The gamepad module provides access to buttons and analog sticks as normalized si | | 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`) | +| Stick Buttons | `l3`, 'r3' (or `ls`, `rs`) | +| | Toggle versions: `tglL3`, 'tglR3' (or `tglLs`, `tglRs`) | +| System Buttons | `start`, `back` (or uppercase `START`, `BACK`) | +| | Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) | ### Analog Sticks diff --git a/packages/gamepad/gamepad.mjs b/packages/gamepad/gamepad.mjs index 7667a3620..069c54af1 100644 --- a/packages/gamepad/gamepad.mjs +++ b/packages/gamepad/gamepad.mjs @@ -14,6 +14,10 @@ export const buttonMap = { rt: 7, back: 8, start: 9, + l3: 10, + ls: 10, + r3: 11, + rs: 11, u: 12, up: 12, d: 13, From 91e1cc136e1169b1864b784cdbb02b8e29e14cb5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 23:34:10 -0400 Subject: [PATCH 09/11] working --- packages/midi/midi.mjs | 101 +++++++++++++++++++------------- packages/superdough/helpers.mjs | 5 ++ 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index ae983d200..32f030125 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -5,9 +5,10 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as _WebMidi from 'webmidi'; -import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core'; +import { Pattern, isPattern, logger, ref } from '@strudel/core'; import { noteToMidi, getControlName } from '@strudel/core'; import { Note } from 'webmidi'; +import { scheduleAtTime } from '../superdough/helpers.mjs'; // if you use WebMidi from outside of this package, make sure to import that instance: export const { WebMidi } = _WebMidi; @@ -190,7 +191,7 @@ function mapCC(mapping, value) { } // sends a cc message to the given device on the given channel -function sendCC(ccn, ccv, device, midichan, timeOffsetString) { +function sendCC(ccn, ccv, device, midichan, targetTime) { if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { throw new Error('expected ccv to be a number between 0 and 1'); } @@ -198,19 +199,23 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) { throw new Error('expected ccn to be a number or a string'); } const scaled = Math.round(ccv * 127); - device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); + scheduleAtTime(() => { + device.sendControlChange(ccn, scaled, midichan); + }, targetTime); } // sends a program change message to the given device on the given channel -function sendProgramChange(progNum, device, midichan, timeOffsetString) { +function sendProgramChange(progNum, device, midichan, targetTime) { if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { throw new Error('expected progNum (program change) to be a number between 0 and 127'); } - device.sendProgramChange(progNum, midichan, { time: timeOffsetString }); + scheduleAtTime(() => { + device.sendProgramChange(progNum, midichan); + }, targetTime); } // sends a sysex message to the given device on the given channel -function sendSysex(sysexid, sysexdata, device, timeOffsetString) { +function sendSysex(sysexid, sysexdata, device, targetTime) { if (Array.isArray(sysexid)) { if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { throw new Error('all sysexid bytes must be integers between 0 and 255'); @@ -225,11 +230,13 @@ function sendSysex(sysexid, sysexdata, device, timeOffsetString) { if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { throw new Error('all sysex bytes must be integers between 0 and 255'); } - device.sendSysex(sysexid, sysexdata, { time: timeOffsetString }); + scheduleAtTime(() => { + device.sendSysex(sysexid, sysexdata); + }, targetTime); } // sends a NRPN message to the given device on the given channel -function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) { +function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) { if (Array.isArray(nrpnn)) { if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { throw new Error('all nrpnn bytes must be integers between 0 and 255'); @@ -237,28 +244,34 @@ function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) { } else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) { throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); } - - device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString }); + scheduleAtTime(() => { + device.sendNRPN(nrpnn, nrpv, midichan); + }, targetTime); } // sends a pitch bend message to the given device on the given channel -function sendPitchBend(midibend, device, midichan, timeOffsetString) { +function sendPitchBend(midibend, device, midichan, targetTime) { if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) { throw new Error('expected midibend to be a number between -1 and 1'); } - device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); + scheduleAtTime(() => { + device.sendPitchBend(midibend, midichan); + }, targetTime); } // sends a channel aftertouch message to the given device on the given channel -function sendAftertouch(miditouch, device, midichan, timeOffsetString) { +function sendAftertouch(miditouch, device, midichan, targetTime) { if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) { throw new Error('expected miditouch to be a number between 0 and 1'); } - device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); + + scheduleAtTime(() => { + device.sendChannelAftertouch(miditouch, midichan); + }, targetTime); } // sends a note message to the given device on the given channel -function sendNote(note, velocity, duration, device, midichan, timeOffsetString) { +function sendNote(note, velocity, duration, device, midichan, targetTime) { if (note == null || note === '') { throw new Error('note cannot be null or empty'); } @@ -268,12 +281,12 @@ function sendNote(note, velocity, duration, device, midichan, timeOffsetString) if (duration != null && (typeof duration !== 'number' || duration < 0)) { throw new Error('duration must be a positive number'); } - const midiNumber = typeof note === 'number' ? note : noteToMidi(note); const midiNote = new Note(midiNumber, { attack: velocity, duration }); - device.playNote(midiNote, midichan, { - time: timeOffsetString, - }); + + scheduleAtTime(() => { + device.playNote(midiNote, midichan); + }, targetTime); } /** @@ -309,7 +322,6 @@ Pattern.prototype.midi = function (midiport, options = {}) { let midiConfig = { // Default configuration values isController: false, // Disable sending notes for midi controllers - latencyMs: 34, // Default latency to get audio engine to line up in ms noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms midichannel: 1, // Default MIDI channel velocity: 0.9, // Default velocity @@ -333,18 +345,13 @@ Pattern.prototype.midi = function (midiport, options = {}) { logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), }); - return this.onTrigger((hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, _currentTime, cps, targetTime) => { if (!WebMidi.enabled) { logger('Midi not enabled'); return; } hap.ensureObjectValue(); - //magic number to get audio engine to line up, can probably be calculated somehow - const latencyMs = midiConfig.latencyMs; - // passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output - const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`; - // midi event values from hap with configurable defaults let { note, @@ -380,7 +387,7 @@ Pattern.prototype.midi = function (midiport, options = {}) { // if midimap is set, send a cc messages from defined controls if (midicontrolMap.has(midimap)) { const ccs = mapCC(midicontrolMap.get(midimap), hap.value); - ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString)); + ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime)); } else if (midimap !== 'default') { // Add warning when a non-existent midimap is specified logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`); @@ -392,12 +399,12 @@ Pattern.prototype.midi = function (midiport, options = {}) { // try to prevent glitching by subtracting noteOffsetMs from the duration length const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs; - sendNote(note, velocity, duration, device, midichan, timeOffsetString); + sendNote(note, velocity, duration, device, midichan, targetTime); } // Handle program change if (progNum !== undefined) { - sendProgramChange(progNum, device, midichan, timeOffsetString); + sendProgramChange(progNum, device, midichan, targetTime); } // Handle sysex @@ -407,53 +414,63 @@ Pattern.prototype.midi = function (midiport, options = {}) { // if sysexid is an array the first byte is 0x00 if (sysexid !== undefined && sysexdata !== undefined) { - sendSysex(sysexid, sysexdata, device, timeOffsetString); + sendSysex(sysexid, sysexdata, device, targetTime); } // Handle control change if (ccv !== undefined && ccn !== undefined) { - sendCC(ccn, ccv, device, midichan, timeOffsetString); + sendCC(ccn, ccv, device, midichan, targetTime); } // Handle NRPN non-registered parameter number if (nrpnn !== undefined && nrpv !== undefined) { - sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString); + sendNRPN(nrpnn, nrpv, device, midichan, targetTime); } // Handle midibend if (midibend !== undefined) { - sendPitchBend(midibend, device, midichan, timeOffsetString); + sendPitchBend(midibend, device, midichan, targetTime); } // Handle miditouch if (miditouch !== undefined) { - sendAftertouch(miditouch, device, midichan, timeOffsetString); + sendAftertouch(miditouch, device, midichan, targetTime); } // Handle midicmd if (hap.whole.begin + 0 === 0) { // we need to start here because we have the timing info - device.sendStart({ time: timeOffsetString }); + scheduleAtTime(() => { + device.sendStart(); + }, targetTime); } if (['clock', 'midiClock'].includes(midicmd)) { - device.sendClock({ time: timeOffsetString }); + scheduleAtTime(() => { + device.sendClock(); + }, targetTime); } else if (['start'].includes(midicmd)) { - device.sendStart({ time: timeOffsetString }); + scheduleAtTime(() => { + device.sendStart(); + }, targetTime); } else if (['stop'].includes(midicmd)) { - device.sendStop({ time: timeOffsetString }); + scheduleAtTime(() => { + device.sendStop(); + }, targetTime); } else if (['continue'].includes(midicmd)) { - device.sendContinue({ time: timeOffsetString }); + scheduleAtTime(() => { + device.sendContinue(); + }, targetTime); } else if (Array.isArray(midicmd)) { if (midicmd[0] === 'progNum') { - sendProgramChange(midicmd[1], device, midichan, timeOffsetString); + sendProgramChange(midicmd[1], device, midichan, targetTime); } else if (midicmd[0] === 'cc') { if (midicmd.length === 2) { - sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString); + sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime); } } else if (midicmd[0] === 'sysex') { if (midicmd.length === 3) { const [_, id, data] = midicmd; - sendSysex(id, data, device, timeOffsetString); + sendSysex(id, data, device, targetTime); } } } diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a471da9dd..b06f584a0 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -280,6 +280,11 @@ export function getVibratoOscillator(param, value, t) { return vibratoOscillator; } } + +export function scheduleAtTime(callback, targetTime, audioContext = getAudioContext()) { + const currentTime = audioContext.currentTime; + webAudioTimeout(audioContext, callback, currentTime, targetTime); +} // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // a bit of a hack, but it works very well :) export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { From 809eb7132c43cb4ea6e7c22511f9d314bd403498 Mon Sep 17 00:00:00 2001 From: moumar Date: Sun, 2 Nov 2025 15:20:31 +0100 Subject: [PATCH 10/11] codeformat --- website/src/repl/components/panel/PatternsTab.jsx | 14 +++----------- website/src/repl/components/panel/SettingsTab.jsx | 2 +- website/src/settings.mjs | 6 +++++- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 38b7294de..5e0d50e72 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -134,14 +134,10 @@ function UserPatterns({ context }) { {/* {patternFilter === patternFilterName.user && ( */} { - updateCodeWindow( - context, - { ...userPatterns[id], collection: userPattern.collection }, - patternAutoStart, - ) + updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); if (context.started && activePattern === id) { - context.handleEvaluate() + context.handleEvaluate(); } }} patterns={userPatterns} @@ -197,11 +193,7 @@ function FeaturedPatterns({ context }) { context={context} initialPage={featuredPageNum} patternOnClick={(id) => { - updateCodeWindow( - context, - { ...patterns[id], collection: patternFilterName.featured }, - patternAutoStart, - ); + updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.featured }, patternAutoStart); }} paginationOnChange={async (pageNum) => { await loadAndSetFeaturedPatterns(pageNum - 1); diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index c898609d2..85991f55c 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -112,7 +112,7 @@ export function SettingsTab({ started }) { multiChannelOrbits, isTabIndentationEnabled, isMultiCursorEnabled, - patternAutoStart + patternAutoStart, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; diff --git a/website/src/settings.mjs b/website/src/settings.mjs index ef074b785..7c62b0ded 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -96,7 +96,11 @@ export function useSettings() { isPanelOpen: parseBoolean(state.isPanelOpen), userPatterns: userPatterns, multiChannelOrbits: parseBoolean(state.multiChannelOrbits), - patternAutoStart: isUdels() ? false : state.patternAutoStart === undefined ? true : parseBoolean(state.patternAutoStart) + patternAutoStart: isUdels() + ? false + : state.patternAutoStart === undefined + ? true + : parseBoolean(state.patternAutoStart), }; } From 79875ebc50f73b5adc72c1971be35e008cb3f14a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 5 Nov 2025 01:26:36 -0500 Subject: [PATCH 11/11] fix branch --- packages/xen/tunejs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 5806cf596..9f4253709 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -41,7 +41,6 @@ Tune.prototype.tonicize = function(newTonic) { this.tonic = newTonic } - /* Return data in the mode you are in (freq, ratio, or midi) */ Tune.prototype.note = function(input,octave){