From e3b6884b86f664ed65ed005e7ec8c5952b16f9eb Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Mon, 1 Apr 2024 10:46:32 +0200 Subject: [PATCH 01/88] make soundfont base url configurable --- packages/soundfonts/fontloader.mjs | 17 ++++++++++++++++- packages/soundfonts/index.mjs | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 8cbe3e81a..27f1875a7 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,3 +1,4 @@ +import { persistentMap } from '@nanostores/persistent'; import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core'; import { getAudioContext, @@ -9,6 +10,20 @@ import { } from '@strudel/webaudio'; import gm from './gm.mjs'; +export const defaultFontloaderConfig = { + soundfontUrl: 'https://felixroos.github.io/webaudiofontdata/sound' +} + +export const fontloaderConfigMap = persistentMap('strudel-config-fontloader', defaultFontloaderConfig); + +export function setSoundfontUrl(obj) { + fontloaderConfigMap.setKey('soundfontUrl', JSON.stringify(obj)); +} + +export function getSoundfontUrl() { + return JSON.parse(fontloaderConfigMap.get().soundfontUrl); +} + let loadCache = {}; async function loadFont(name) { if (loadCache[name]) { @@ -16,7 +31,7 @@ async function loadFont(name) { } const load = async () => { // TODO: make soundfont source configurable - const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`; + const url = `${getSoundfontUrl()}/${name}.js`; const preset = await fetch(url).then((res) => res.text()); let [_, data] = preset.split('={'); return eval('{' + data); diff --git a/packages/soundfonts/index.mjs b/packages/soundfonts/index.mjs index c54297b50..31792603c 100644 --- a/packages/soundfonts/index.mjs +++ b/packages/soundfonts/index.mjs @@ -1,6 +1,6 @@ -import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs'; +import { getFontBufferSource, registerSoundfonts, setSoundfontUrl } from './fontloader.mjs'; import * as soundfontList from './list.mjs'; import { startPresetNote } from 'sfumato'; import { loadSoundfont } from './sfumato.mjs'; -export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts }; +export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts, setSoundfontUrl }; From b233c33f56236ebfed2672cc2b1b783048c71f1f Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Mon, 1 Apr 2024 17:14:27 +0200 Subject: [PATCH 02/88] (squash me) save setting in variable, not store --- packages/soundfonts/fontloader.mjs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 27f1875a7..427f57e31 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,4 +1,3 @@ -import { persistentMap } from '@nanostores/persistent'; import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core'; import { getAudioContext, @@ -10,18 +9,11 @@ import { } from '@strudel/webaudio'; import gm from './gm.mjs'; -export const defaultFontloaderConfig = { - soundfontUrl: 'https://felixroos.github.io/webaudiofontdata/sound' -} +let defaultSoundfontUrl = 'https://felixroos.github.io/webaudiofontdata/sound'; +let soundfontUrl = defaultSoundfontUrl; -export const fontloaderConfigMap = persistentMap('strudel-config-fontloader', defaultFontloaderConfig); - -export function setSoundfontUrl(obj) { - fontloaderConfigMap.setKey('soundfontUrl', JSON.stringify(obj)); -} - -export function getSoundfontUrl() { - return JSON.parse(fontloaderConfigMap.get().soundfontUrl); +export function setSoundfontUrl(value) { + soundfontUrl = value; } let loadCache = {}; @@ -31,7 +23,7 @@ async function loadFont(name) { } const load = async () => { // TODO: make soundfont source configurable - const url = `${getSoundfontUrl()}/${name}.js`; + const url = `${soundfontUrl}/${name}.js`; const preset = await fetch(url).then((res) => res.text()); let [_, data] = preset.split('={'); return eval('{' + data); From ec470aa2c6f1400a4f1a14ec68be413850388c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 9 Nov 2024 01:41:59 +0100 Subject: [PATCH 03/88] Feat: midi() command support external instrument parameter mapping This commit adds a second argument to the midi() command: mapping. This argument should be an object containing a key-value map of MIDI controls used by an external synthesizer. If any control is used that matches the mapping, a CC message is sent. --- packages/midi/midi.mjs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 32e66f6c5..92cf8290d 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -89,11 +89,10 @@ if (typeof window !== 'undefined') { }); } -Pattern.prototype.midi = function (output) { +Pattern.prototype.midi = function (output, mapping) { if (isPattern(output)) { throw new Error( - `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${ - WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' + `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } @@ -103,8 +102,7 @@ Pattern.prototype.midi = function (output) { const device = getDevice(output, outputs); const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( - `Midi enabled! Using "${device.name}". ${ - otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : '' + `Midi enabled! Using "${device.name}". ${otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : '' }`, ); }, @@ -137,6 +135,23 @@ Pattern.prototype.midi = function (output) { time: timeOffsetString, }); } + + // Handle mapped parameters if mapping exists + if (mapping && mapping.parameters) { + Object.entries(hap.value).forEach(([param, value]) => { + const mappedParam = mapping.parameters[param]; + if (mappedParam && typeof value === 'number') { + const scaled = Math.round(value * 127); + device.sendControlChange( + mappedParam.cc, + scaled, + mappedParam.channel || midichan, + { time: timeOffsetString } + ); + } + }); + } + if (ccv !== undefined && ccn !== undefined) { if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { throw new Error('expected ccv to be a number between 0 and 1'); @@ -169,8 +184,7 @@ const refs = {}; export async function midin(input) { if (isPattern(input)) { throw new Error( - `midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${ - WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' + `midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } @@ -184,8 +198,7 @@ export async function midin(input) { if (initial) { const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name); logger( - `Midi enabled! Using "${device.name}". ${ - otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' + `Midi enabled! Using "${device.name}". ${otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' }`, ); refs[input] = {}; From 23a4bf66414507d53afad77a1974db3e4974b83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 9 Nov 2024 02:33:56 +0100 Subject: [PATCH 04/88] Getting rid of second argument --- packages/midi/midi.mjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 92cf8290d..83806f911 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -89,17 +89,27 @@ if (typeof window !== 'undefined') { }); } -Pattern.prototype.midi = function (output, mapping) { +Pattern.prototype.midi = function(output) { if (isPattern(output)) { throw new Error( `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } + let portName = output; + let isController = false; + let mapping = {}; + + if (typeof output === 'object') { + const { port, controller = false, ...remainingProps } = output; + portName = port; + isController = controller; + mapping = remainingProps; + } enableWebMidi({ onEnabled: ({ outputs }) => { - const device = getDevice(output, outputs); + const device = getDevice(portName, outputs); const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( `Midi enabled! Using "${device.name}". ${otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : '' @@ -115,7 +125,7 @@ Pattern.prototype.midi = function (output, mapping) { console.log('not enabled'); return; } - const device = getDevice(output, WebMidi.outputs); + const device = getDevice(portName, WebMidi.outputs); hap.ensureObjectValue(); //magic number to get audio engine to line up, can probably be calculated somehow const latencyMs = 34; @@ -128,7 +138,7 @@ Pattern.prototype.midi = function (output, mapping) { // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length const duration = (hap.duration.valueOf() / cps) * 1000 - 10; - if (note != null) { + if (note != null && !isController) { const midiNumber = typeof note === 'number' ? note : noteToMidi(note); const midiNote = new Note(midiNumber, { attack: velocity, duration }); device.playNote(midiNote, midichan, { @@ -137,9 +147,9 @@ Pattern.prototype.midi = function (output, mapping) { } // Handle mapped parameters if mapping exists - if (mapping && mapping.parameters) { + if (mapping) { Object.entries(hap.value).forEach(([param, value]) => { - const mappedParam = mapping.parameters[param]; + const mappedParam = mapping[param]; if (mappedParam && typeof value === 'number') { const scaled = Math.round(value * 127); device.sendControlChange( From bd69ffb4b7e7f244baf9e7b66c9d6f1cc162ec9d Mon Sep 17 00:00:00 2001 From: Matthew Kaney Date: Sat, 16 Nov 2024 14:35:18 -0500 Subject: [PATCH 05/88] Add high-resolution CC option to midi --- packages/midi/midi.mjs | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 83806f911..af8ca297f 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -89,10 +89,11 @@ if (typeof window !== 'undefined') { }); } -Pattern.prototype.midi = function(output) { +Pattern.prototype.midi = function (output) { if (isPattern(output)) { throw new Error( - `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' + `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${ + WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } @@ -112,7 +113,8 @@ Pattern.prototype.midi = function(output) { const device = getDevice(portName, outputs); const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( - `Midi enabled! Using "${device.name}". ${otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : '' + `Midi enabled! Using "${device.name}". ${ + otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : '' }`, ); }, @@ -148,16 +150,20 @@ Pattern.prototype.midi = function(output) { // Handle mapped parameters if mapping exists if (mapping) { - Object.entries(hap.value).forEach(([param, value]) => { - const mappedParam = mapping[param]; - if (mappedParam && typeof value === 'number') { - const scaled = Math.round(value * 127); - device.sendControlChange( - mappedParam.cc, - scaled, - mappedParam.channel || midichan, - { time: timeOffsetString } - ); + Object.entries(mapping).forEach(([name, paramSpec]) => { + if (name in hap.value && typeof hap.value[name] === 'number') { + const value = hap.value[name]; + + // ccnLsb will only exist if this is a high-resolution CC message + const [ccnMsb, ccnLsb] = Array.isArray(paramSpec.cc) ? paramSpec.cc : [paramSpec.cc]; + + const ccvMsb = ccnLsb === undefined ? Math.round(value * 127) : Math.round(value * 16383) >> 7; + device.sendControlChange(ccnMsb, ccvMsb, paramSpec.channel || midichan, { time: timeOffsetString }); + + if (ccnLsb !== undefined) { + const ccvLsb = Math.round(value * 16383) & 0b1111111; + device.sendControlChange(ccnLsb, ccvLsb, paramSpec.channel || midichan, { time: timeOffsetString }); + } } }); } @@ -194,7 +200,8 @@ const refs = {}; export async function midin(input) { if (isPattern(input)) { throw new Error( - `midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' + `midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${ + WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } @@ -208,7 +215,8 @@ export async function midin(input) { if (initial) { const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name); logger( - `Midi enabled! Using "${device.name}". ${otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' + `Midi enabled! Using "${device.name}". ${ + otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' }`, ); refs[input] = {}; From 859f153ec6c343214d56a223fd5842c923ae6cde Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 14 Jan 2025 16:54:53 +0800 Subject: [PATCH 06/88] Add program change(pc) and sysex to midi --- packages/midi/midi.mjs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index af8ca297f..a13ba5842 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -134,7 +134,7 @@ Pattern.prototype.midi = function (output) { // 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}`; // destructure value - let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value; + let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9, pc, sysex } = hap.value; velocity = gain * velocity; @@ -167,7 +167,25 @@ Pattern.prototype.midi = function (output) { } }); } + // Handle program change + if (pc !== undefined) { + if (typeof pc !== 'number' || pc < 0 || pc > 127) { + throw new Error('expected pc (program change) to be a number between 0 and 127'); + } + device.sendProgramChange(pc, midichan, { time: timeOffsetString }); + } + // Handle sysex + if (sysex !== undefined) { + if (!Array.isArray(sysex)) { + throw new Error('expected sysex to be an array of numbers (0-255)'); + } + if (!sysex.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('all sysex bytes must be integers between 0 and 255'); + } + device.sendSysex(undefined, sysex, { time: timeOffsetString }); + } + // Handle control change if (ccv !== undefined && ccn !== undefined) { if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { throw new Error('expected ccv to be a number between 0 and 1'); From c242f5f6252c3c0a883490a9902ed0905074d869 Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 14 Jan 2025 17:11:14 +0800 Subject: [PATCH 07/88] midi mapping to handle program change and sysex --- packages/midi/midi.mjs | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index a13ba5842..a7745620b 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -151,18 +151,36 @@ Pattern.prototype.midi = function (output) { // Handle mapped parameters if mapping exists if (mapping) { Object.entries(mapping).forEach(([name, paramSpec]) => { - if (name in hap.value && typeof hap.value[name] === 'number') { + if (name in hap.value) { const value = hap.value[name]; - // ccnLsb will only exist if this is a high-resolution CC message - const [ccnMsb, ccnLsb] = Array.isArray(paramSpec.cc) ? paramSpec.cc : [paramSpec.cc]; + if (paramSpec.cc) { + if (typeof value !== 'number') { + throw new Error(`Expected ${name} to be a number for CC mapping`); + } + // ccnLsb will only exist if this is a high-resolution CC message + const [ccnMsb, ccnLsb] = Array.isArray(paramSpec.cc) ? paramSpec.cc : [paramSpec.cc]; - const ccvMsb = ccnLsb === undefined ? Math.round(value * 127) : Math.round(value * 16383) >> 7; - device.sendControlChange(ccnMsb, ccvMsb, paramSpec.channel || midichan, { time: timeOffsetString }); + const ccvMsb = ccnLsb === undefined ? Math.round(value * 127) : Math.round(value * 16383) >> 7; + device.sendControlChange(ccnMsb, ccvMsb, paramSpec.channel || midichan, { time: timeOffsetString }); - if (ccnLsb !== undefined) { - const ccvLsb = Math.round(value * 16383) & 0b1111111; - device.sendControlChange(ccnLsb, ccvLsb, paramSpec.channel || midichan, { time: timeOffsetString }); + if (ccnLsb !== undefined) { + const ccvLsb = Math.round(value * 16383) & 0b1111111; + device.sendControlChange(ccnLsb, ccvLsb, paramSpec.channel || midichan, { time: timeOffsetString }); + } + } else if (paramSpec.pc !== undefined) { + if (typeof value !== 'number' || value < 0 || value > 127) { + throw new Error(`Expected ${name} to be a number between 0 and 127 for program change`); + } + device.sendProgramChange(value, paramSpec.channel || midichan, { time: timeOffsetString }); + } else if (paramSpec.sysex) { + if (!Array.isArray(value)) { + throw new Error(`Expected ${name} to be an array of numbers (0-255) for sysex`); + } + if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error(`All sysex bytes in ${name} must be integers between 0 and 255`); + } + device.sendSysex(undefined, value, { time: timeOffsetString }); } } }); From 155ef9e95f89f2b71496618f61387421089a3cd0 Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 14 Jan 2025 22:10:16 +0800 Subject: [PATCH 08/88] register pc and sysex as control keywords --- packages/core/controls.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5a2752944..1bec79a0a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1,5 +1,5 @@ /* -controls.mjs - +controls.mjs - Registers audio controls for pattern manipulation and effects. Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -1513,6 +1513,8 @@ export const { midichan } = registerControl('midichan'); export const { control } = registerControl('control'); export const { ccn } = registerControl('ccn'); export const { ccv } = registerControl('ccv'); +export const { pc } = registerControl('pc'); +export const { sysex } = registerControl('sysex'); export const { polyTouch } = registerControl('polyTouch'); export const { midibend } = registerControl('midibend'); export const { miditouch } = registerControl('miditouch'); From e085819fe224c1c1a643084f0196f0cd0278b38c Mon Sep 17 00:00:00 2001 From: nkymut Date: Wed, 15 Jan 2025 22:43:03 +0800 Subject: [PATCH 09/88] add documentation for pc and sysex --- packages/midi/README.md | 86 ++++++++++++++++++++++++ website/src/pages/learn/input-output.mdx | 38 ++++++++++- 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/packages/midi/README.md b/packages/midi/README.md index 6bb649f2b..0efdd3516 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -7,3 +7,89 @@ This package adds midi functionality to strudel Patterns. ```sh npm i @strudel/midi --save ``` + + +## Available Controls + +The following MIDI controls are available: + +- `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127) +- `midichan` - Sets the MIDI channel (1-16, defaults to 1) +- `velocity` - Sets note velocity (0-1, defaults to 0.9) +- `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1) +- `ccn` - Sets MIDI CC controller number (0-127) +- `ccv` - Sets MIDI CC value (0-1) +- `pc` - Sends MIDI program change messages (0-127) +- `sysex` - Sends MIDI System Exclusive messages (array of bytes 0-255) + +Additional controls can be mapped using the mapping object passed to `.midi()`: + + +## Examples + +### midi(outputName?) + +Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. +If no outputName is given, it uses the first midi output it finds. + +```javascript +$: chord("").voicing().midi() +``` + +In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` + +### midichan(number) + +Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. + +### ccn && ccv + +- `ccn` sets the cc number. Depends on your synths midi mapping +- `ccv` sets the cc value. normalized from 0 to 1. + +```javascript +$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi() +``` + +In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern. +Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default. +But you can also control cc messages separately like this: + +```javascript +$: note("c a f e").midi() +$: ccv(sine.segment(16).slow(4)).ccn(74).midi() +``` + +### pc (Program Change) + +The `pc` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +Program change values should be numbers between 0 and 127. + +```javascript +// Play notes while changing programs +note("c3 e3 g3").pc("<0 1 2>").midi() +``` + +Program change messages are useful for switching between different instrument sounds or presets during a performance. +The exact sound that each program number maps to depends on your MIDI device's configuration. + +## sysex (System Exclusive Message) + +The `sysex` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. +ysEx messages are device-specific commands that allow deeper control over synthesizer parameters. +The value should be an array of numbers between 0-255 representing the SysEx data bytes. + +```javascript +// Send a simple SysEx message +sysex([0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]).midi() +``` + +```javascript +//Send SysEx while playing notes +note("c3 e3 g3") + .sysex("<[0xF0,0x7E,0x7F,0x09,0x01,0xF7] [0xF0,0x7E,0x7F,0x09,0x02,0xF7]>") + .midi() +``` + +The exact format of SysEx messages depends on your MIDI device's specification. +Consult your device's MIDI implementation guide for details on supported SysEx messages. \ No newline at end of file diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 93cbfdc56..17435a81f 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -45,10 +45,42 @@ But you can also control cc messages separately like this: $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`} /> -# OSC/SuperDirt API +## pc (Program Change) -In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. -Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run. +The `pc` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +Program change values should be numbers between 0 and 127. + +").midi() + +// Play notes while changing programs +note("c3 e3 g3").pc("<0 1 2>").midi()`} /> + +Program change messages are useful for switching between different instrument sounds or presets during a performance. +The exact sound that each program number maps to depends on your MIDI device's configuration. + +## sysex (System Exclusive Message) + +The `sysex` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. +ysEx messages are device-specific commands that allow deeper control over synthesizer parameters. +The value should be an array of numbers between 0-255 representing the SysEx data bytes. + +") +.midi()`} /> + +The exact format of SysEx messages depends on your MIDI device's specification. +Consult your device's MIDI implementation guide for details on supported SysEx messages. + +# OSC/SuperDirt/StrudelDirt + +In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software. + +There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt) ## Prequisites From 13a4512601c9e0526d7ff246c5f2cde2c981093d Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 18 Jan 2025 14:31:11 +0800 Subject: [PATCH 10/88] Get sysex working split sysex message into sysexid and sysexdata sysexid is a device identification number or array sysexdata is an array of data to be sent to the device --- packages/core/controls.mjs | 3 +- packages/midi/README.md | 14 ++-- packages/midi/gm.mjs | 130 +++++++++++++++++++++++++++++++++++++ packages/midi/index.mjs | 1 + packages/midi/midi.mjs | 71 ++++++++++++++------ 5 files changed, 190 insertions(+), 29 deletions(-) create mode 100644 packages/midi/gm.mjs diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1bec79a0a..b681db35e 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1514,7 +1514,8 @@ export const { control } = registerControl('control'); export const { ccn } = registerControl('ccn'); export const { ccv } = registerControl('ccv'); export const { pc } = registerControl('pc'); -export const { sysex } = registerControl('sysex'); +export const { sysexid } = registerControl('sysexid'); +export const { sysexdata } = registerControl('sysexdata'); export const { polyTouch } = registerControl('polyTouch'); export const { midibend } = registerControl('midibend'); export const { miditouch } = registerControl('miditouch'); diff --git a/packages/midi/README.md b/packages/midi/README.md index 0efdd3516..d8494728c 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -76,19 +76,17 @@ The exact sound that each program number maps to depends on your MIDI device's c ## sysex (System Exclusive Message) The `sysex` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. -ysEx messages are device-specific commands that allow deeper control over synthesizer parameters. +sysEx messages are device-specific commands that allow deeper control over synthesizer parameters. The value should be an array of numbers between 0-255 representing the SysEx data bytes. ```javascript // Send a simple SysEx message -sysex([0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]).midi() -``` +let id = 0x43; //Yamaha +//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers -```javascript -//Send SysEx while playing notes -note("c3 e3 g3") - .sysex("<[0xF0,0x7E,0x7F,0x09,0x01,0xF7] [0xF0,0x7E,0x7F,0x09,0x02,0xF7]>") - .midi() +let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" + +note("c d e f e d c").sysex(id, data).midi(); ``` The exact format of SysEx messages depends on your MIDI device's specification. diff --git a/packages/midi/gm.mjs b/packages/midi/gm.mjs new file mode 100644 index 000000000..6ba23737a --- /dev/null +++ b/packages/midi/gm.mjs @@ -0,0 +1,130 @@ +export const gm = { + piano: 0, + bright_acoustic_piano: 1, + electric_grand_piano: 2, + honky_tonk_piano: 3, + epiano1: 4, + epiano2: 5, + harpsichord: 6, + clavinet: 7, + celesta: 8, + glockenspiel: 9, + music_box: 10, + vibraphone: 11, + marimba: 12, + xylophone: 13, + tubular_bells: 14, + dulcimer: 15, + drawbar_organ: 16, + percussive_organ: 17, + rock_organ: 18, + church_organ: 19, + reed_organ: 20, + accordion: 21, + harmonica: 22, + bandoneon: 23, + acoustic_guitar_nylon: 24, + acoustic_guitar_steel: 25, + electric_guitar_jazz: 26, + electric_guitar_clean: 27, + electric_guitar_muted: 28, + overdriven_guitar: 29, + distortion_guitar: 30, + guitar_harmonics: 31, + acoustic_bass: 32, + electric_bass_finger: 33, + electric_bass_pick: 34, + fretless_bass: 35, + slap_bass_1: 36, + slap_bass_2: 37, + synth_bass_1: 38, + synth_bass_2: 39, + violin: 40, + viola: 41, + cello: 42, + contrabass: 43, + tremolo_strings: 44, + pizzicato_strings: 45, + orchestral_harp: 46, + timpani: 47, + string_ensemble_1: 48, + string_ensemble_2: 49, + synth_strings_1: 50, + synth_strings_2: 51, + choir_aahs: 52, + voice_oohs: 53, + synth_choir: 54, + orchestra_hit: 55, + trumpet: 56, + trombone: 57, + tuba: 58, + muted_trumpet: 59, + french_horn: 60, + brass_section: 61, + synth_brass_1: 62, + synth_brass_2: 63, + soprano_sax: 64, + alto_sax: 65, + tenor_sax: 66, + baritone_sax: 67, + oboe: 68, + english_horn: 69, + bassoon: 70, + clarinet: 71, + piccolo: 72, + flute: 73, + recorder: 74, + pan_flute: 75, + blown_bottle: 76, + shakuhachi: 77, + whistle: 78, + ocarina: 79, + lead_1_square: 80, + lead_2_sawtooth: 81, + lead_3_calliope: 82, + lead_4_chiff: 83, + lead_5_charang: 84, + lead_6_voice: 85, + lead_7_fifths: 86, + lead_8_bass_lead: 87, + pad_1_new_age: 88, + pad_2_warm: 89, + pad_3_polysynth: 90, + pad_4_choir: 91, + pad_5_bowed: 92, + pad_6_metallic: 93, + pad_7_halo: 94, + pad_8_sweep: 95, + fx_1_rain: 96, + fx_2_soundtrack: 97, + fx_3_crystal: 98, + fx_4_atmosphere: 99, + fx_5_brightness: 100, + fx_6_goblins: 101, + fx_7_echoes: 102, + fx_8_sci_fi: 103, + sitar: 104, + banjo: 105, + shamisen: 106, + koto: 107, + kalimba: 108, + bagpipe: 109, + fiddle: 110, + shanai: 111, + tinkle_bell: 112, + agogo: 113, + steel_drums: 114, + woodblock: 115, + taiko_drum: 116, + melodic_tom: 117, + synth_drum: 118, + reverse_cymbal: 119, + guitar_fret_noise: 120, + breath_noise: 121, + seashore: 122, + bird_tweet: 123, + telephone: 124, + helicopter: 125, + applause: 126, + gunshot: 127, +}; diff --git a/packages/midi/index.mjs b/packages/midi/index.mjs index 399227f73..2a226a538 100644 --- a/packages/midi/index.mjs +++ b/packages/midi/index.mjs @@ -1,3 +1,4 @@ import './midi.mjs'; export * from './midi.mjs'; +export * from './gm.mjs'; diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index a7745620b..2ef266a17 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -8,6 +8,7 @@ import * as _WebMidi from 'webmidi'; import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core'; import { noteToMidi } from '@strudel/core'; import { Note } from 'webmidi'; + // if you use WebMidi from outside of this package, make sure to import that instance: export const { WebMidi } = _WebMidi; @@ -43,13 +44,16 @@ export function enableWebMidi(options = {}) { resolve(WebMidi); return; } - WebMidi.enable((err) => { - if (err) { - reject(err); - } - onReady?.(WebMidi); - resolve(WebMidi); - }); + WebMidi.enable( + (err) => { + if (err) { + reject(err); + } + onReady?.(WebMidi); + resolve(WebMidi); + }, + { sysex: true }, + ); }); } @@ -134,7 +138,20 @@ Pattern.prototype.midi = function (output) { // 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}`; // destructure value - let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9, pc, sysex } = hap.value; + let { + note, + nrpnn, + nrpv, + ccn, + ccv, + midichan = 1, + midicmd, + gain = 1, + velocity = 0.9, + pc, + sysexid, + sysexdata, + } = hap.value; velocity = gain * velocity; @@ -173,15 +190,18 @@ Pattern.prototype.midi = function (output) { throw new Error(`Expected ${name} to be a number between 0 and 127 for program change`); } device.sendProgramChange(value, paramSpec.channel || midichan, { time: timeOffsetString }); - } else if (paramSpec.sysex) { - if (!Array.isArray(value)) { - throw new Error(`Expected ${name} to be an array of numbers (0-255) for sysex`); - } - if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { - throw new Error(`All sysex bytes in ${name} must be integers between 0 and 255`); - } - device.sendSysex(undefined, value, { time: timeOffsetString }); } + // ToDo: support sysex for mapped parameters + // } else if (paramSpec.sysex) { + // if (!Array.isArray(value)) { + // throw new Error(`Expected ${name} to be an array of numbers (0-255) for sysex`); + // } + // if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + // throw new Error(`All sysex bytes in ${name} must be integers between 0 and 255`); + // } + // device.sendSysex(0x43, value, { time: timeOffsetString }); + // //device.sendSysex(0x43, [0x79, 0x09, 0x11, 0x0A, 0x00,0x02], { time: timeOffsetString }); + // } } }); } @@ -193,14 +213,25 @@ Pattern.prototype.midi = function (output) { device.sendProgramChange(pc, midichan, { time: timeOffsetString }); } // Handle sysex - if (sysex !== undefined) { - if (!Array.isArray(sysex)) { + if (sysexid !== undefined && sysexdata !== undefined) { + //console.log('sysex', sysexid, sysexdata); + 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'); + } + } else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) { + throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); + } + + if (!Array.isArray(sysexdata)) { throw new Error('expected sysex to be an array of numbers (0-255)'); } - if (!sysex.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + 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(undefined, sysex, { time: timeOffsetString }); + + device.sendSysex(sysexid, sysexdata, { time: timeOffsetString }); + //device.sendSysex(0x43, [0x79, 0x09, 0x11, 0x0A, 0x00,0x1e], { time: timeOffsetString }); } // Handle control change From 3325a8dbe95c65a79b92338ced90a08611719366 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 18 Jan 2025 14:38:22 +0800 Subject: [PATCH 11/88] Add device specific setting folder 1 - add example sysex data for setting NSX-39(Pocket Miku) voice data. --- packages/midi/device/gakken_nsx-39.mjs | 269 +++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 packages/midi/device/gakken_nsx-39.mjs diff --git a/packages/midi/device/gakken_nsx-39.mjs b/packages/midi/device/gakken_nsx-39.mjs new file mode 100644 index 000000000..6e9299918 --- /dev/null +++ b/packages/midi/device/gakken_nsx-39.mjs @@ -0,0 +1,269 @@ +export const miku = { + あ: '0x79:0x09:0x11:0x0A:0x00:0x00', + い: '0x79:0x09:0x11:0x0A:0x00:0x01', + う: '0x79:0x09:0x11:0x0A:0x00:0x02', + え: '0x79:0x09:0x11:0x0A:0x00:0x03', + お: '0x79:0x09:0x11:0x0A:0x00:0x04', + か: '0x79:0x09:0x11:0x0A:0x00:0x05', + き: '0x79:0x09:0x11:0x0A:0x00:0x06', + く: '0x79:0x09:0x11:0x0A:0x00:0x07', + け: '0x79:0x09:0x11:0x0A:0x00:0x08', + こ: '0x79:0x09:0x11:0x0A:0x00:0x09', + が: '0x79:0x09:0x11:0x0A:0x00:0x0a', + ぎ: '0x79:0x09:0x11:0x0A:0x00:0x0b', + ぐ: '0x79:0x09:0x11:0x0A:0x00:0x0c', + げ: '0x79:0x09:0x11:0x0A:0x00:0x0d', + ご: '0x79:0x09:0x11:0x0A:0x00:0x0e', + きゃ: '0x79:0x09:0x11:0x0A:0x00:0x0f', + きゅ: '0x79:0x09:0x11:0x0A:0x00:0x10', + きょ: '0x79:0x09:0x11:0x0A:0x00:0x11', + ぎゃ: '0x79:0x09:0x11:0x0A:0x00:0x12', + ぎゅ: '0x79:0x09:0x11:0x0A:0x00:0x13', + ぎょ: '0x79:0x09:0x11:0x0A:0x00:0x14', + さ: '0x79:0x09:0x11:0x0A:0x00:0x15', + すぃ: '0x79:0x09:0x11:0x0A:0x00:0x16', + す: '0x79:0x09:0x11:0x0A:0x00:0x17', + せ: '0x79:0x09:0x11:0x0A:0x00:0x18', + そ: '0x79:0x09:0x11:0x0A:0x00:0x19', + ざ: '0x79:0x09:0x11:0x0A:0x00:0x1a', + づぁ: '0x79:0x09:0x11:0x0A:0x00:0x1a', + ずぃ: '0x79:0x09:0x11:0x0A:0x00:0x1b', + づぃ: '0x79:0x09:0x11:0x0A:0x00:0x1b', + ず: '0x79:0x09:0x11:0x0A:0x00:0x1c', + づ: '0x79:0x09:0x11:0x0A:0x00:0x1c', + ぜ: '0x79:0x09:0x11:0x0A:0x00:0x1d', + づぇ: '0x79:0x09:0x11:0x0A:0x00:0x1d', + ぞ: '0x79:0x09:0x11:0x0A:0x00:0x1e', + づぉ: '0x79:0x09:0x11:0x0A:0x00:0x1e', + しゃ: '0x79:0x09:0x11:0x0A:0x00:0x1f', + し: '0x79:0x09:0x11:0x0A:0x00:0x20', + しゅ: '0x79:0x09:0x11:0x0A:0x00:0x21', + しぇ: '0x79:0x09:0x11:0x0A:0x00:0x22', + しょ: '0x79:0x09:0x11:0x0A:0x00:0x23', + じゃ: '0x79:0x09:0x11:0x0A:0x00:0x24', + じ: '0x79:0x09:0x11:0x0A:0x00:0x25', + じゅ: '0x79:0x09:0x11:0x0A:0x00:0x26', + じぇ: '0x79:0x09:0x11:0x0A:0x00:0x27', + じょ: '0x79:0x09:0x11:0x0A:0x00:0x28', + た: '0x79:0x09:0x11:0x0A:0x00:0x29', + てぃ: '0x79:0x09:0x11:0x0A:0x00:0x2a', + とぅ: '0x79:0x09:0x11:0x0A:0x00:0x2b', + て: '0x79:0x09:0x11:0x0A:0x00:0x2c', + と: '0x79:0x09:0x11:0x0A:0x00:0x2d', + だ: '0x79:0x09:0x11:0x0A:0x00:0x2e', + でぃ: '0x79:0x09:0x11:0x0A:0x00:0x2f', + どぅ: '0x79:0x09:0x11:0x0A:0x00:0x30', + で: '0x79:0x09:0x11:0x0A:0x00:0x31', + ど: '0x79:0x09:0x11:0x0A:0x00:0x32', + てゅ: '0x79:0x09:0x11:0x0A:0x00:0x33', + でゅ: '0x79:0x09:0x11:0x0A:0x00:0x34', + ちゃ: '0x79:0x09:0x11:0x0A:0x00:0x35', + ち: '0x79:0x09:0x11:0x0A:0x00:0x36', + ちゅ: '0x79:0x09:0x11:0x0A:0x00:0x37', + ちぇ: '0x79:0x09:0x11:0x0A:0x00:0x38', + ちょ: '0x79:0x09:0x11:0x0A:0x00:0x39', + つぁ: '0x79:0x09:0x11:0x0A:0x00:0x3a', + つぃ: '0x79:0x09:0x11:0x0A:0x00:0x3b', + つ: '0x79:0x09:0x11:0x0A:0x00:0x3c', + つぇ: '0x79:0x09:0x11:0x0A:0x00:0x3d', + つぉ: '0x79:0x09:0x11:0x0A:0x00:0x3e', + な: '0x79:0x09:0x11:0x0A:0x00:0x3f', + に: '0x79:0x09:0x11:0x0A:0x00:0x40', + ぬ: '0x79:0x09:0x11:0x0A:0x00:0x41', + ね: '0x79:0x09:0x11:0x0A:0x00:0x42', + の: '0x79:0x09:0x11:0x0A:0x00:0x43', + にゃ: '0x79:0x09:0x11:0x0A:0x00:0x44', + にゅ: '0x79:0x09:0x11:0x0A:0x00:0x45', + にょ: '0x79:0x09:0x11:0x0A:0x00:0x46', + は: '0x79:0x09:0x11:0x0A:0x00:0x47', + ひ: '0x79:0x09:0x11:0x0A:0x00:0x48', + ふ: '0x79:0x09:0x11:0x0A:0x00:0x49', + へ: '0x79:0x09:0x11:0x0A:0x00:0x4a', + ほ: '0x79:0x09:0x11:0x0A:0x00:0x4b', + ば: '0x79:0x09:0x11:0x0A:0x00:0x4c', + び: '0x79:0x09:0x11:0x0A:0x00:0x4d', + ぶ: '0x79:0x09:0x11:0x0A:0x00:0x4e', + べ: '0x79:0x09:0x11:0x0A:0x00:0x4f', + ぼ: '0x79:0x09:0x11:0x0A:0x00:0x50', + ぱ: '0x79:0x09:0x11:0x0A:0x00:0x51', + ぴ: '0x79:0x09:0x11:0x0A:0x00:0x52', + ぷ: '0x79:0x09:0x11:0x0A:0x00:0x53', + ぺ: '0x79:0x09:0x11:0x0A:0x00:0x54', + ぽ: '0x79:0x09:0x11:0x0A:0x00:0x55', + ひゃ: '0x79:0x09:0x11:0x0A:0x00:0x56', + ひゅ: '0x79:0x09:0x11:0x0A:0x00:0x57', + ひょ: '0x79:0x09:0x11:0x0A:0x00:0x58', + びゃ: '0x79:0x09:0x11:0x0A:0x00:0x59', + びゅ: '0x79:0x09:0x11:0x0A:0x00:0x5a', + びょ: '0x79:0x09:0x11:0x0A:0x00:0x5b', + ぴゃ: '0x79:0x09:0x11:0x0A:0x00:0x5c', + ぴゅ: '0x79:0x09:0x11:0x0A:0x00:0x5d', + ぴょ: '0x79:0x09:0x11:0x0A:0x00:0x5e', + ふぁ: '0x79:0x09:0x11:0x0A:0x00:0x5f', + ふぃ: '0x79:0x09:0x11:0x0A:0x00:0x60', + ふゅ: '0x79:0x09:0x11:0x0A:0x00:0x61', + ふぇ: '0x79:0x09:0x11:0x0A:0x00:0x62', + ふぉ: '0x79:0x09:0x11:0x0A:0x00:0x63', + ま: '0x79:0x09:0x11:0x0A:0x00:0x64', + み: '0x79:0x09:0x11:0x0A:0x00:0x65', + む: '0x79:0x09:0x11:0x0A:0x00:0x66', + め: '0x79:0x09:0x11:0x0A:0x00:0x67', + も: '0x79:0x09:0x11:0x0A:0x00:0x68', + みゃ: '0x79:0x09:0x11:0x0A:0x00:0x69', + みゅ: '0x79:0x09:0x11:0x0A:0x00:0x6a', + みょ: '0x79:0x09:0x11:0x0A:0x00:0x6b', + や: '0x79:0x09:0x11:0x0A:0x00:0x6c', + ゆ: '0x79:0x09:0x11:0x0A:0x00:0x6d', + よ: '0x79:0x09:0x11:0x0A:0x00:0x6e', + ら: '0x79:0x09:0x11:0x0A:0x00:0x6f', + り: '0x79:0x09:0x11:0x0A:0x00:0x70', + る: '0x79:0x09:0x11:0x0A:0x00:0x71', + れ: '0x79:0x09:0x11:0x0A:0x00:0x72', + ろ: '0x79:0x09:0x11:0x0A:0x00:0x73', + りゃ: '0x79:0x09:0x11:0x0A:0x00:0x74', + りゅ: '0x79:0x09:0x11:0x0A:0x00:0x75', + りょ: '0x79:0x09:0x11:0x0A:0x00:0x76', + わ: '0x79:0x09:0x11:0x0A:0x00:0x77', + うぃ: '0x79:0x09:0x11:0x0A:0x00:0x78', + うぇ: '0x79:0x09:0x11:0x0A:0x00:0x79', + うぉ: '0x79:0x09:0x11:0x0A:0x00:0x7a', + ん: '0x79:0x09:0x11:0x0A:0x00:0x7b', + ん1: '0x79:0x09:0x11:0x0A:0x00:0x7c', + ん2: '0x79:0x09:0x11:0x0A:0x00:0x7d', + ん3: '0x79:0x09:0x11:0x0A:0x00:0x7e', + ん4: '0x79:0x09:0x11:0x0A:0x00:0x7f', + + a: '0x79:0x09:0x11:0x0A:0x00:0x00', + i: '0x79:0x09:0x11:0x0A:0x00:0x01', + u: '0x79:0x09:0x11:0x0A:0x00:0x02', + e: '0x79:0x09:0x11:0x0A:0x00:0x03', + o: '0x79:0x09:0x11:0x0A:0x00:0x04', + ka: '0x79:0x09:0x11:0x0A:0x00:0x05', + ki: '0x79:0x09:0x11:0x0A:0x00:0x06', + ku: '0x79:0x09:0x11:0x0A:0x00:0x07', + ke: '0x79:0x09:0x11:0x0A:0x00:0x08', + ko: '0x79:0x09:0x11:0x0A:0x00:0x09', + ga: '0x79:0x09:0x11:0x0A:0x00:0x0a', + gi: '0x79:0x09:0x11:0x0A:0x00:0x0b', + gu: '0x79:0x09:0x11:0x0A:0x00:0x0c', + ge: '0x79:0x09:0x11:0x0A:0x00:0x0d', + go: '0x79:0x09:0x11:0x0A:0x00:0x0e', + kya: '0x79:0x09:0x11:0x0A:0x00:0x0f', + kyu: '0x79:0x09:0x11:0x0A:0x00:0x10', + kyo: '0x79:0x09:0x11:0x0A:0x00:0x11', + gya: '0x79:0x09:0x11:0x0A:0x00:0x12', + gyu: '0x79:0x09:0x11:0x0A:0x00:0x13', + gyo: '0x79:0x09:0x11:0x0A:0x00:0x14', + sa: '0x79:0x09:0x11:0x0A:0x00:0x15', + swi: '0x79:0x09:0x11:0x0A:0x00:0x16', + su: '0x79:0x09:0x11:0x0A:0x00:0x17', + se: '0x79:0x09:0x11:0x0A:0x00:0x18', + so: '0x79:0x09:0x11:0x0A:0x00:0x19', + za: '0x79:0x09:0x11:0x0A:0x00:0x1a', + zwi: '0x79:0x09:0x11:0x0A:0x00:0x1b', + zu: '0x79:0x09:0x11:0x0A:0x00:0x1c', + ze: '0x79:0x09:0x11:0x0A:0x00:0x1d', + zo: '0x79:0x09:0x11:0x0A:0x00:0x1e', + sha: '0x79:0x09:0x11:0x0A:0x00:0x1f', + shi: '0x79:0x09:0x11:0x0A:0x00:0x20', + shu: '0x79:0x09:0x11:0x0A:0x00:0x21', + she: '0x79:0x09:0x11:0x0A:0x00:0x22', + sho: '0x79:0x09:0x11:0x0A:0x00:0x23', + ja: '0x79:0x09:0x11:0x0A:0x00:0x24', + dza: '0x79:0x09:0x11:0x0A:0x00:0x24', + ji: '0x79:0x09:0x11:0x0A:0x00:0x25', + dzi: '0x79:0x09:0x11:0x0A:0x00:0x25', + ju: '0x79:0x09:0x11:0x0A:0x00:0x26', + dzu: '0x79:0x09:0x11:0x0A:0x00:0x26', + je: '0x79:0x09:0x11:0x0A:0x00:0x27', + dze: '0x79:0x09:0x11:0x0A:0x00:0x27', + jo: '0x79:0x09:0x11:0x0A:0x00:0x28', + dzo: '0x79:0x09:0x11:0x0A:0x00:0x28', + ta: '0x79:0x09:0x11:0x0A:0x00:0x29', + ti: '0x79:0x09:0x11:0x0A:0x00:0x2a', + tu: '0x79:0x09:0x11:0x0A:0x00:0x2b', + te: '0x79:0x09:0x11:0x0A:0x00:0x2c', + to: '0x79:0x09:0x11:0x0A:0x00:0x2d', + da: '0x79:0x09:0x11:0x0A:0x00:0x2e', + di: '0x79:0x09:0x11:0x0A:0x00:0x2f', + du: '0x79:0x09:0x11:0x0A:0x00:0x30', + de: '0x79:0x09:0x11:0x0A:0x00:0x31', + do: '0x79:0x09:0x11:0x0A:0x00:0x32', + tyu: '0x79:0x09:0x11:0x0A:0x00:0x33', + dyu: '0x79:0x09:0x11:0x0A:0x00:0x34', + cha: '0x79:0x09:0x11:0x0A:0x00:0x35', + chi: '0x79:0x09:0x11:0x0A:0x00:0x36', + chu: '0x79:0x09:0x11:0x0A:0x00:0x37', + che: '0x79:0x09:0x11:0x0A:0x00:0x38', + cho: '0x79:0x09:0x11:0x0A:0x00:0x39', + tsa: '0x79:0x09:0x11:0x0A:0x00:0x3a', + tsi: '0x79:0x09:0x11:0x0A:0x00:0x3b', + tsu: '0x79:0x09:0x11:0x0A:0x00:0x3c', + tse: '0x79:0x09:0x11:0x0A:0x00:0x3d', + tso: '0x79:0x09:0x11:0x0A:0x00:0x3e', + na: '0x79:0x09:0x11:0x0A:0x00:0x3f', + ni: '0x79:0x09:0x11:0x0A:0x00:0x40', + nu: '0x79:0x09:0x11:0x0A:0x00:0x41', + ne: '0x79:0x09:0x11:0x0A:0x00:0x42', + no: '0x79:0x09:0x11:0x0A:0x00:0x43', + nya: '0x79:0x09:0x11:0x0A:0x00:0x44', + nyu: '0x79:0x09:0x11:0x0A:0x00:0x45', + nyo: '0x79:0x09:0x11:0x0A:0x00:0x46', + ha: '0x79:0x09:0x11:0x0A:0x00:0x47', + hi: '0x79:0x09:0x11:0x0A:0x00:0x48', + fu: '0x79:0x09:0x11:0x0A:0x00:0x49', + he: '0x79:0x09:0x11:0x0A:0x00:0x4a', + ho: '0x79:0x09:0x11:0x0A:0x00:0x4b', + ba: '0x79:0x09:0x11:0x0A:0x00:0x4c', + bi: '0x79:0x09:0x11:0x0A:0x00:0x4d', + bu: '0x79:0x09:0x11:0x0A:0x00:0x4e', + be: '0x79:0x09:0x11:0x0A:0x00:0x4f', + bo: '0x79:0x09:0x11:0x0A:0x00:0x50', + pa: '0x79:0x09:0x11:0x0A:0x00:0x51', + pi: '0x79:0x09:0x11:0x0A:0x00:0x52', + pu: '0x79:0x09:0x11:0x0A:0x00:0x53', + pe: '0x79:0x09:0x11:0x0A:0x00:0x54', + po: '0x79:0x09:0x11:0x0A:0x00:0x55', + hya: '0x79:0x09:0x11:0x0A:0x00:0x56', + hyu: '0x79:0x09:0x11:0x0A:0x00:0x57', + hyo: '0x79:0x09:0x11:0x0A:0x00:0x58', + bya: '0x79:0x09:0x11:0x0A:0x00:0x59', + byu: '0x79:0x09:0x11:0x0A:0x00:0x5a', + byo: '0x79:0x09:0x11:0x0A:0x00:0x5b', + pya: '0x79:0x09:0x11:0x0A:0x00:0x5c', + pyu: '0x79:0x09:0x11:0x0A:0x00:0x5d', + pyo: '0x79:0x09:0x11:0x0A:0x00:0x5e', + fa: '0x79:0x09:0x11:0x0A:0x00:0x5f', + fi: '0x79:0x09:0x11:0x0A:0x00:0x60', + fyu: '0x79:0x09:0x11:0x0A:0x00:0x61', + fe: '0x79:0x09:0x11:0x0A:0x00:0x62', + fo: '0x79:0x09:0x11:0x0A:0x00:0x63', + ma: '0x79:0x09:0x11:0x0A:0x00:0x64', + mi: '0x79:0x09:0x11:0x0A:0x00:0x65', + mu: '0x79:0x09:0x11:0x0A:0x00:0x66', + me: '0x79:0x09:0x11:0x0A:0x00:0x67', + mo: '0x79:0x09:0x11:0x0A:0x00:0x68', + mya: '0x79:0x09:0x11:0x0A:0x00:0x69', + myu: '0x79:0x09:0x11:0x0A:0x00:0x6a', + myo: '0x79:0x09:0x11:0x0A:0x00:0x6b', + ya: '0x79:0x09:0x11:0x0A:0x00:0x6c', + yu: '0x79:0x09:0x11:0x0A:0x00:0x6d', + yo: '0x79:0x09:0x11:0x0A:0x00:0x6e', + ra: '0x79:0x09:0x11:0x0A:0x00:0x6f', + ri: '0x79:0x09:0x11:0x0A:0x00:0x70', + ru: '0x79:0x09:0x11:0x0A:0x00:0x71', + re: '0x79:0x09:0x11:0x0A:0x00:0x72', + ro: '0x79:0x09:0x11:0x0A:0x00:0x73', + rya: '0x79:0x09:0x11:0x0A:0x00:0x74', + ryu: '0x79:0x09:0x11:0x0A:0x00:0x75', + ryo: '0x79:0x09:0x11:0x0A:0x00:0x76', + wa: '0x79:0x09:0x11:0x0A:0x00:0x77', + wi: '0x79:0x09:0x11:0x0A:0x00:0x78', + we: '0x79:0x09:0x11:0x0A:0x00:0x79', + wo: '0x79:0x09:0x11:0x0A:0x00:0x7a', + n: '0x79:0x09:0x11:0x0A:0x00:0x7b', + n1: '0x79:0x09:0x11:0x0A:0x00:0x7c', + n2: '0x79:0x09:0x11:0x0A:0x00:0x7d', + n3: '0x79:0x09:0x11:0x0A:0x00:0x7e', + n4: '0x79:0x09:0x11:0x0A:0x00:0x7f', +}; From a8803784e15eb427279712c8d619ed92a80d7e82 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 18 Jan 2025 14:42:58 +0800 Subject: [PATCH 12/88] prettier! --- packages/midi/device/gakken_nsx-39.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/midi/device/gakken_nsx-39.mjs b/packages/midi/device/gakken_nsx-39.mjs index 6e9299918..b7c1b3216 100644 --- a/packages/midi/device/gakken_nsx-39.mjs +++ b/packages/midi/device/gakken_nsx-39.mjs @@ -26,7 +26,7 @@ export const miku = { せ: '0x79:0x09:0x11:0x0A:0x00:0x18', そ: '0x79:0x09:0x11:0x0A:0x00:0x19', ざ: '0x79:0x09:0x11:0x0A:0x00:0x1a', - づぁ: '0x79:0x09:0x11:0x0A:0x00:0x1a', + づぁ: '0x79:0x09:0x11:0x0A:0x00:0x1a', ずぃ: '0x79:0x09:0x11:0x0A:0x00:0x1b', づぃ: '0x79:0x09:0x11:0x0A:0x00:0x1b', ず: '0x79:0x09:0x11:0x0A:0x00:0x1c', From 268c66cd3dd1bd6647b257f0a703b0b3a3bc150e Mon Sep 17 00:00:00 2001 From: nkymut Date: Mon, 20 Jan 2025 01:11:39 +0800 Subject: [PATCH 13/88] add midicmd documentation - addresses #789 #710 --- packages/midi/README.md | 27 +++++++++++++++++++----- website/src/pages/learn/input-output.mdx | 22 +++++++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/midi/README.md b/packages/midi/README.md index d8494728c..2a1cc020f 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -42,6 +42,24 @@ In the console, you will see a log of the available MIDI devices as soon as you Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. +### midicmd + +`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. + +It supports the following commands: +- `clock`/`midiClock` - Sends MIDI timing clock messages +- `start` - Sends MIDI start message +- `stop` - Sends MIDI stop message +- `continue` - Sends MIDI continue message + +```javascript +// You can control the clock with a pattern and ensure it starts in sync when the repl begins. +// Note: It might act unexpectedly if MIDI isn't set up initially. +stack( + midicmd("clock*24,/2").midi('RHYTHM DESIGNER RD-6') //Trigger start and stop on Behringer RD-6 +) +``` + ### ccn && ccv - `ccn` sets the cc number. Depends on your synths midi mapping @@ -62,7 +80,7 @@ $: ccv(sine.segment(16).slow(4)).ccn(74).midi() ### pc (Program Change) -The `pc` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +`pc` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. Program change values should be numbers between 0 and 127. ```javascript @@ -73,9 +91,9 @@ note("c3 e3 g3").pc("<0 1 2>").midi() Program change messages are useful for switching between different instrument sounds or presets during a performance. The exact sound that each program number maps to depends on your MIDI device's configuration. -## sysex (System Exclusive Message) +## sysexid, sysexdata (System Exclusive Message) -The `sysex` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. +`sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. sysEx messages are device-specific commands that allow deeper control over synthesizer parameters. The value should be an array of numbers between 0-255 representing the SysEx data bytes. @@ -83,10 +101,9 @@ The value should be an array of numbers between 0-255 representing the SysEx dat // Send a simple SysEx message let id = 0x43; //Yamaha //let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers - let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" -note("c d e f e d c").sysex(id, data).midi(); +note("c d e f e d c").sysexid(id).sysexdata(data).midi(); ``` The exact format of SysEx messages depends on your MIDI device's specification. diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 3871216ce..5c4f363d8 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -29,6 +29,24 @@ In the console, you will see a log of the available MIDI devices as soon as you Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. +### midicmd + +`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. + +It supports the following commands: +- `clock`/`midiClock` - Sends MIDI timing clock messages +- `start` - Sends MIDI start message +- `stop` - Sends MIDI stop message +- `continue` - Sends MIDI continue message + +```javascript +// You can control the clock with a pattern and ensure it starts in sync when the repl begins. +// Note: It might act unexpectedly if MIDI isn't set up initially. +stack( + midicmd("clock*24,/2").midi('RHYTHM DESIGNER RD-6') //Trigger start and stop on Behringer RD-6 +) +``` + ## ccn && ccv - `ccn` sets the cc number. Depends on your synths midi mapping @@ -48,7 +66,7 @@ $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`} ## pc (Program Change) -The `pc` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +`pc` sends MIDI program change messages to switch between different presets/patches on your MIDI device. Program change values should be numbers between 0 and 127. Date: Mon, 20 Jan 2025 01:26:41 +0800 Subject: [PATCH 14/88] add midicmd documentation 2 - MiniRepl for input-output.mdx - addresses #789 #710 --- packages/midi/README.md | 2 +- website/src/pages/learn/input-output.mdx | 39 +++++++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/midi/README.md b/packages/midi/README.md index 2a1cc020f..2678ab718 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -56,7 +56,7 @@ It supports the following commands: // You can control the clock with a pattern and ensure it starts in sync when the repl begins. // Note: It might act unexpectedly if MIDI isn't set up initially. stack( - midicmd("clock*24,/2").midi('RHYTHM DESIGNER RD-6') //Trigger start and stop on Behringer RD-6 + midicmd("clock*48,/2").midi('IAC Driver') ) ``` diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 5c4f363d8..7bd8dd09e 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -31,21 +31,24 @@ Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by defa ### midicmd -`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. +`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. It supports the following commands: + - `clock`/`midiClock` - Sends MIDI timing clock messages - `start` - Sends MIDI start message - `stop` - Sends MIDI stop message - `continue` - Sends MIDI continue message -```javascript // You can control the clock with a pattern and ensure it starts in sync when the repl begins. // Note: It might act unexpectedly if MIDI isn't set up initially. -stack( - midicmd("clock*24,/2").midi('RHYTHM DESIGNER RD-6') //Trigger start and stop on Behringer RD-6 -) -``` + +/2").midi('IAC Driver') +)`} +/> ## ccn && ccv @@ -157,8 +160,8 @@ The following example shows how to send a pattern to an MQTT broker: client:only="react" tune={`"hello world" .mqtt(undefined, // username (undefined for open/public servers) - undefined, // password - '/strudel-pattern', // mqtt 'topic' + undefined, // password + '/strudel-pattern', // mqtt 'topic' 'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address 'mystrudel', // MQTT client id - randomly generated if not supplied 0 // latency / delay before sending messages (0 = no delay) @@ -169,12 +172,14 @@ The following example shows how to send a pattern to an MQTT broker: Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools: ``` -> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern" -hello -world -hello -world -... + +> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern" +> hello +> world +> hello +> world +> ... + ``` Control patterns will be encoded as JSON, for example: @@ -194,11 +199,17 @@ Control patterns will be encoded as JSON, for example: Will send messages like the following: ``` + {"s":"sax","speed":2} {"s":"sax","speed":2} {"s":"sax","speed":3} {"s":"sax","speed":2} ... + ``` Libraries for receiving MQTT are available for many programming languages. + +``` + +``` From f95eadab2f8a89fbeec07661deeeee8c97b9e62a Mon Sep 17 00:00:00 2001 From: nkymut Date: Mon, 20 Jan 2025 01:31:26 +0800 Subject: [PATCH 15/88] adjust midicmd heading level --- website/src/pages/learn/input-output.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 7bd8dd09e..ad42c56d5 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -29,7 +29,7 @@ In the console, you will see a log of the available MIDI devices as soon as you Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. -### midicmd +## midicmd(command) `midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. From a4792e29f5662acd68b104e61acdf6e093d8eb60 Mon Sep 17 00:00:00 2001 From: nkymut Date: Wed, 22 Jan 2025 06:48:55 +0800 Subject: [PATCH 16/88] update ProgramChange from `pc` to `progNum` - add progNum keyword handler - update midicmd handler to handle 'progNum' case --- packages/core/controls.mjs | 1 - packages/midi/midi.mjs | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b681db35e..7bfeb1e3d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1513,7 +1513,6 @@ export const { midichan } = registerControl('midichan'); export const { control } = registerControl('control'); export const { ccn } = registerControl('ccn'); export const { ccv } = registerControl('ccv'); -export const { pc } = registerControl('pc'); export const { sysexid } = registerControl('sysexid'); export const { sysexdata } = registerControl('sysexdata'); export const { polyTouch } = registerControl('polyTouch'); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 2ef266a17..5fabb3eaf 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -148,7 +148,7 @@ Pattern.prototype.midi = function (output) { midicmd, gain = 1, velocity = 0.9, - pc, + progNum, sysexid, sysexdata, } = hap.value; @@ -206,11 +206,11 @@ Pattern.prototype.midi = function (output) { }); } // Handle program change - if (pc !== undefined) { - if (typeof pc !== 'number' || pc < 0 || pc > 127) { + if (progNum !== undefined) { + if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { throw new Error('expected pc (program change) to be a number between 0 and 127'); } - device.sendProgramChange(pc, midichan, { time: timeOffsetString }); + device.sendProgramChange(progNum, midichan, { time: timeOffsetString }); } // Handle sysex if (sysexid !== undefined && sysexdata !== undefined) { @@ -245,6 +245,8 @@ Pattern.prototype.midi = function (output) { const scaled = Math.round(ccv * 127); device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } + + // Handle midicmd if (hap.whole.begin + 0 === 0) { // we need to start here because we have the timing info device.sendStart({ time: timeOffsetString }); @@ -257,6 +259,14 @@ Pattern.prototype.midi = function (output) { device.sendStop({ time: timeOffsetString }); } else if (['continue'].includes(midicmd)) { device.sendContinue({ time: timeOffsetString }); + } else if (Array.isArray(midicmd)) { + if (midicmd[0] === 'progNum') { + if (typeof midicmd[1] !== 'number' || midicmd[1] < 0 || midicmd[1] > 127) { + throw new Error('expected pc (program change) to be a number between 0 and 127'); + } else { + device.sendProgramChange(midicmd[1], midichan, { time: timeOffsetString }); + } + } } }); }; From 57c48f0c453a46292ee6a134e55c6a8a2f89931b Mon Sep 17 00:00:00 2001 From: nkymut Date: Thu, 23 Jan 2025 08:30:06 +0800 Subject: [PATCH 17/88] Add 'sysex' control - sysex(id, data) and both arguments are patternable --- packages/core/controls.mjs | 7 +++++++ packages/midi/midi.mjs | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7bfeb1e3d..00d6e7a22 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1616,3 +1616,10 @@ export const ar = register('ar', (t, pat) => { const [attack, release = attack] = t; return pat.set({ attack, release }); }); +export const sysex = register('sysex', (args, pat) => { + if (!Array.isArray(args)) { + throw new Error('sysex expects an array of [id, data]'); + } + const [id, data] = args; + return pat.sysexid(id).sysexdata(data); +}); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 5fabb3eaf..058e7b68c 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -153,6 +153,8 @@ Pattern.prototype.midi = function (output) { sysexdata, } = hap.value; + console.log('hap', hap.value); + velocity = gain * velocity; // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length @@ -213,6 +215,21 @@ Pattern.prototype.midi = function (output) { device.sendProgramChange(progNum, midichan, { time: timeOffsetString }); } // Handle sysex + // sysex data is consist of 2 arrays, first is sysexid, second is sysexdata + // sysexid is a manufacturer id it is either a number or an array of 3 numbers. + // list of manufacturer ids can be found here : https://midi.org/sysexidtable + // if sysexid is an array the first byte is 0x00 + + // if (sysex !== undefined) { + // console.log('sysex', sysex); + // if (Array.isArray(sysex)) { + // if (Array.isArray(sysex[0])) { + // //device.sendSysex(sysex[0], sysex[1], { time: timeOffsetString }); + // } else { + // //device.sendSysex(sysex[0], sysex[1], { time: timeOffsetString }); + // } + // } + // } if (sysexid !== undefined && sysexdata !== undefined) { //console.log('sysex', sysexid, sysexdata); if (Array.isArray(sysexid)) { From d06a75a2cdb112ae92602eb3d4d30ba9503d8e9b Mon Sep 17 00:00:00 2001 From: nkymut Date: Fri, 24 Jan 2025 05:37:11 +0800 Subject: [PATCH 18/88] Add cc to `midicmd`, add API Reference for midi related controls --- packages/core/controls.mjs | 95 +++++++++++++++++++++++++++++++++----- packages/midi/midi.mjs | 39 +++++++++++----- 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 00d6e7a22..4d071a7a6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1509,22 +1509,10 @@ export const { scram } = registerControl('scram'); export const { binshift } = registerControl('binshift'); export const { hbrick } = registerControl('hbrick'); export const { lbrick } = registerControl('lbrick'); -export const { midichan } = registerControl('midichan'); -export const { control } = registerControl('control'); -export const { ccn } = registerControl('ccn'); -export const { ccv } = registerControl('ccv'); -export const { sysexid } = registerControl('sysexid'); -export const { sysexdata } = registerControl('sysexdata'); -export const { polyTouch } = registerControl('polyTouch'); -export const { midibend } = registerControl('midibend'); -export const { miditouch } = registerControl('miditouch'); -export const { ctlNum } = registerControl('ctlNum'); export const { frameRate } = registerControl('frameRate'); export const { frames } = registerControl('frames'); export const { hours } = registerControl('hours'); -export const { midicmd } = registerControl('midicmd'); export const { minutes } = registerControl('minutes'); -export const { progNum } = registerControl('progNum'); export const { seconds } = registerControl('seconds'); export const { songPtr } = registerControl('songPtr'); export const { uid } = registerControl('uid'); @@ -1616,6 +1604,73 @@ export const ar = register('ar', (t, pat) => { const [attack, release = attack] = t; return pat.set({ attack, release }); }); + +//MIDI + +/** + * MIDI channel: Sets the MIDI channel for the event. + * + * @name midichan + * @param {number | Pattern} channel MIDI channel number (0-15) + * @example + * note("c4").midichan(1).midi() + */ +export const { midichan } = registerControl('midichan'); + +export const { midicmd } = registerControl('midicmd'); + +/** + * MIDI control: Sends a MIDI control change message. + * + * @name control + * @param {number | Pattern} MIDI control number (0-127) + * @param {number | Pattern} MIDI controller value (0-127) + */ +export const control = register('control', (args, pat) => { + if (!Array.isArray(args)) { + throw new Error('control expects an array of [ccn, ccv]'); + } + const [_ccn, _ccv] = args; + return pat.ccn(_ccn).ccv(_ccv); +}); + +/** + * MIDI control number: Sends a MIDI control change message. + * + * @name ccn + * @param {number | Pattern} MIDI control number (0-127) + */ +export const { ccn } = registerControl('ccn'); +/** + * MIDI control value: Sends a MIDI control change message. + * + * @name ccv + * @param {number | Pattern} MIDI control value (0-127) + */ +export const { ccv } = registerControl('ccv'); +export const { ctlNum } = registerControl('ctlNum'); +// TODO: ctlVal? + +/** + * MIDI program number: Sends a MIDI program change message. + * + * @name progNum + * @param {number | Pattern} program MIDI program number (0-127) + */ +export const { progNum } = registerControl('progNum'); + +export const { polyTouch } = registerControl('polyTouch'); +export const { midibend } = registerControl('midibend'); +export const { miditouch } = registerControl('miditouch'); + +/** + * MIDI sysex: Sends a MIDI sysex message. + * @name sysex + * @param {number | Pattern} id Sysex ID + * @param {number | Pattern} data Sysex data + * @example + * note("c4").sysex("0x77, "0x01:0x02:0x03:0x04").midichan(1).midi() + */ export const sysex = register('sysex', (args, pat) => { if (!Array.isArray(args)) { throw new Error('sysex expects an array of [id, data]'); @@ -1623,3 +1678,19 @@ export const sysex = register('sysex', (args, pat) => { const [id, data] = args; return pat.sysexid(id).sysexdata(data); }); +/** + * MIDI sysex ID: Sends a MIDI sysex identifier message. + * @name sysexid + * @param {number | Pattern} id Sysex ID + * @example + * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() + */ +export const { sysexid } = registerControl('sysexid'); +/** + * MIDI sysex data: Sends a MIDI sysex message. + * @name sysexdata + * @param {number | Pattern} data Sysex data + * @example + * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() + */ +export const { sysexdata } = registerControl('sysexdata'); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 058e7b68c..fa7b2b6db 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -93,6 +93,12 @@ if (typeof window !== 'undefined') { }); } +/** + * MIDI output: Opens a MIDI output port. + * @param {string | number} output MIDI device name or index defaulting to 0 + * @example + * note("c4").midichan(1).midi("IAC Driver Bus 1") + */ Pattern.prototype.midi = function (output) { if (isPattern(output)) { throw new Error( @@ -105,6 +111,7 @@ Pattern.prototype.midi = function (output) { let isController = false; let mapping = {}; + //TODO: MIDI mapping related if (typeof output === 'object') { const { port, controller = false, ...remainingProps } = output; portName = port; @@ -187,26 +194,16 @@ Pattern.prototype.midi = function (output) { const ccvLsb = Math.round(value * 16383) & 0b1111111; device.sendControlChange(ccnLsb, ccvLsb, paramSpec.channel || midichan, { time: timeOffsetString }); } - } else if (paramSpec.pc !== undefined) { + } else if (paramSpec.progNum !== undefined) { if (typeof value !== 'number' || value < 0 || value > 127) { throw new Error(`Expected ${name} to be a number between 0 and 127 for program change`); } device.sendProgramChange(value, paramSpec.channel || midichan, { time: timeOffsetString }); } - // ToDo: support sysex for mapped parameters - // } else if (paramSpec.sysex) { - // if (!Array.isArray(value)) { - // throw new Error(`Expected ${name} to be an array of numbers (0-255) for sysex`); - // } - // if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { - // throw new Error(`All sysex bytes in ${name} must be integers between 0 and 255`); - // } - // device.sendSysex(0x43, value, { time: timeOffsetString }); - // //device.sendSysex(0x43, [0x79, 0x09, 0x11, 0x0A, 0x00,0x02], { time: timeOffsetString }); - // } } }); } + // Handle program change if (progNum !== undefined) { if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { @@ -283,6 +280,16 @@ Pattern.prototype.midi = function (output) { } else { device.sendProgramChange(midicmd[1], midichan, { time: timeOffsetString }); } + } else if (midicmd[0] === 'cc') { + if (midicmd.length === 2) { + if (typeof midicmd[0] !== 'number' || midicmd[0] < 0 || midicmd[0] > 127) { + throw new Error('expected ccn (control change number) to be a number between 0 and 127'); + } + if (typeof midicmd[1] !== 'number' || midicmd[1] < 0 || midicmd[1] > 127) { + throw new Error('expected ccv (control change value) to be a number between 0 and 127'); + } + device.sendControlChange(midicmd[0], midicmd[1], midichan, { time: timeOffsetString }); + } } } }); @@ -291,6 +298,14 @@ Pattern.prototype.midi = function (output) { let listeners = {}; const refs = {}; +/** + * MIDI input: Opens a MIDI input port to receive MIDI control change messages. + * @param {string | number} input MIDI device name or index defaulting to 0 + * @returns {Function} + * @example + * let cc = await midin("IAC Driver Bus 1") + * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") + */ export async function midin(input) { if (isPattern(input)) { throw new Error( From 393d17a41b014b9faf3be6c959983ed171f44e91 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 25 Jan 2025 01:12:38 +0800 Subject: [PATCH 19/88] fix test error - add mock 'midin' and 'sysex' - typo in sysex example --- packages/core/controls.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 66 +++++++++++++++++++++++ test/runtime.mjs | 15 +++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 4d071a7a6..ca0bdacfa 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1669,7 +1669,7 @@ export const { miditouch } = registerControl('miditouch'); * @param {number | Pattern} id Sysex ID * @param {number | Pattern} data Sysex data * @example - * note("c4").sysex("0x77, "0x01:0x02:0x03:0x04").midichan(1).midi() + * note("c4").sysex(["0x77", "0x01:0x02:0x03:0x04"]).midichan(1).midi() */ export const sysex = register('sysex', (args, pat) => { if (!Array.isArray(args)) { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index c071b5365..0f644ce72 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4669,6 +4669,45 @@ exports[`runs examples > example "mask" example index 0 1`] = ` ] `; +exports[`runs examples > example "midi" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 midichan:1 ]", +] +`; + +exports[`runs examples > example "midichan" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 midichan:1 ]", +] +`; + +exports[`runs examples > example "midin" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 1/4 → 1/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 1/2 → 3/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 3/4 → 1/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", + "[ 1/1 → 5/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 5/4 → 3/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 3/2 → 7/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 7/4 → 2/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", + "[ 2/1 → 9/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 9/4 → 5/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 5/2 → 11/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 11/4 → 3/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", + "[ 3/1 → 13/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", + "[ 13/4 → 7/2 | note:a cutoff:0 resonance:0 s:sawtooth ]", + "[ 7/2 → 15/4 | note:f cutoff:0 resonance:0 s:sawtooth ]", + "[ 15/4 → 4/1 | note:e cutoff:0 resonance:0 s:sawtooth ]", +] +`; + exports[`runs examples > example "mousex" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", @@ -8285,6 +8324,33 @@ exports[`runs examples > example "swingBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "sysex" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", +] +`; + +exports[`runs examples > example "sysexdata" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", +] +`; + +exports[`runs examples > example "sysexid" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]", +] +`; + exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index 18d4a29e8..80a063033 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -11,7 +11,7 @@ import * as webaudio from '@strudel/webaudio'; import { mini, m } from '@strudel/mini/mini.mjs'; // import * as voicingHelpers from '@strudel/tonal/voicings.mjs'; // import euclid from '@strudel/core/euclid.mjs'; -// import '@strudel/midi/midi.mjs'; +//import '@strudel/midi/midi.mjs'; import * as tonalHelpers from '@strudel/tonal'; import '@strudel/xen/xen.mjs'; // import '@strudel/xen/tune.mjs'; @@ -122,6 +122,10 @@ strudel.Pattern.prototype.midi = function () { return this; }; +strudel.Pattern.prototype.midin = function () { + return this; +}; + strudel.Pattern.prototype._scope = function () { return this; }; @@ -164,6 +168,12 @@ const loadCsound = () => {}; const loadCSound = () => {}; const loadcsound = () => {}; +const midin = () => { + return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0 +}; + +const sysex = ([id, data]) => {}; + // TODO: refactor to evalScope evalScope( // Tone, @@ -172,6 +182,7 @@ evalScope( uiHelpersMocked, webaudio, tonalHelpers, + /* toneHelpers, voicingHelpers, @@ -179,6 +190,8 @@ evalScope( uiHelpers, */ { + midin, + sysex, // gist, // euclid, csound: id, From b8b999eab58e2eb11a75896669d0f1723bc0c279 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 25 Jan 2025 02:28:56 +0800 Subject: [PATCH 20/88] add `midibend`, `miditouch` --- packages/core/controls.mjs | 44 ++++++++++++++++++++++++++++++++++---- packages/midi/midi.mjs | 43 +++++++++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index ca0bdacfa..1e1a1afb9 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1651,18 +1651,33 @@ export const { ccv } = registerControl('ccv'); export const { ctlNum } = registerControl('ctlNum'); // TODO: ctlVal? +/** + * MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message. + * @name nrpnn + * @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127) + * @example + * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() + */ +export const { nrpnn } = registerControl('nrpnn'); +/** + * MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message. + * @name nrpv + * @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127) + * @example + * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() + */ +export const { nrpv } = registerControl('nrpv'); + /** * MIDI program number: Sends a MIDI program change message. * * @name progNum * @param {number | Pattern} program MIDI program number (0-127) + * @example + * note("c4").progNum(10).midichan(1).midi() */ export const { progNum } = registerControl('progNum'); -export const { polyTouch } = registerControl('polyTouch'); -export const { midibend } = registerControl('midibend'); -export const { miditouch } = registerControl('miditouch'); - /** * MIDI sysex: Sends a MIDI sysex message. * @name sysex @@ -1694,3 +1709,24 @@ export const { sysexid } = registerControl('sysexid'); * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() */ export const { sysexdata } = registerControl('sysexdata'); + + +/** + * MIDI pitch bend: Sends a MIDI pitch bend message. + * @name midibend + * @param {number | Pattern} midibend MIDI pitch bend (-1 - 1) + * @example + * note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi() + */ +export const { midibend } = registerControl('midibend'); +/** + * MIDI key after touch: Sends a MIDI key after touch message. + * @name miditouch + * @param {number | Pattern} miditouch MIDI key after touch (0-1) + * @example + * note("c4").miditouch(sine.slow(4).range(0,1)).midi() + */ +export const { miditouch } = registerControl('miditouch'); + +// TODO: what is this? +export const { polyTouch } = registerControl('polyTouch'); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index fa7b2b6db..01b663956 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -153,6 +153,9 @@ Pattern.prototype.midi = function (output) { ccv, midichan = 1, midicmd, + midibend, + miditouch, + polyTouch, //?? gain = 1, velocity = 0.9, progNum, @@ -217,18 +220,7 @@ Pattern.prototype.midi = function (output) { // list of manufacturer ids can be found here : https://midi.org/sysexidtable // if sysexid is an array the first byte is 0x00 - // if (sysex !== undefined) { - // console.log('sysex', sysex); - // if (Array.isArray(sysex)) { - // if (Array.isArray(sysex[0])) { - // //device.sendSysex(sysex[0], sysex[1], { time: timeOffsetString }); - // } else { - // //device.sendSysex(sysex[0], sysex[1], { time: timeOffsetString }); - // } - // } - // } if (sysexid !== undefined && sysexdata !== undefined) { - //console.log('sysex', sysexid, sysexdata); 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'); @@ -260,6 +252,35 @@ Pattern.prototype.midi = function (output) { device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } + // Handle NRPN non-registered parameter number + if (nrpnn !== undefined && nrpv !== undefined) { + 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'); + } + } 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 }); + } + + // Handle midibend + if (midibend !== undefined) { + 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 }); + } + + // Handle miditouch + if (miditouch !== undefined) { + if (typeof miditouch !== 'number' || miditouch < 1 || miditouch > 0) { + throw new Error('expected miditouch to be a number between 1 and 0'); + } + device.sendKeyAfterTouch(miditouch, midichan, { time: timeOffsetString }); + } + // Handle midicmd if (hap.whole.begin + 0 === 0) { // we need to start here because we have the timing info From e19d059c0f6eaf4cd6684dc670a104191b322425 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 25 Jan 2025 02:29:44 +0800 Subject: [PATCH 21/88] update documents --- packages/midi/README.md | 60 ++++++++++++++++++------ website/src/pages/learn/input-output.mdx | 40 +++++++++++----- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/packages/midi/README.md b/packages/midi/README.md index 2678ab718..524baa17f 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -8,23 +8,37 @@ This package adds midi functionality to strudel Patterns. npm i @strudel/midi --save ``` - ## Available Controls The following MIDI controls are available: +OUTPUT: + +- `midi` - opens a midi output device. - `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127) - `midichan` - Sets the MIDI channel (1-16, defaults to 1) - `velocity` - Sets note velocity (0-1, defaults to 0.9) - `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1) +- `control` - Sets MIDI control change messages - `ccn` - Sets MIDI CC controller number (0-127) - `ccv` - Sets MIDI CC value (0-1) -- `pc` - Sends MIDI program change messages (0-127) -- `sysex` - Sends MIDI System Exclusive messages (array of bytes 0-255) +- `progNum` - Sends MIDI program change messages (0-127) +- `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127) +- `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127) +- `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127) +- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127) +- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127) +- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices. +- `midibend` - Sets MIDI pitch bend (-1 - 1) +- `miditouch` - Sets MIDI key after touch (0-1) + + +INPUT: + +- `midin` - Opens a MIDI input port to receive MIDI control change messages. Additional controls can be mapped using the mapping object passed to `.midi()`: - ## Examples ### midi(outputName?) @@ -33,7 +47,7 @@ Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (L If no outputName is given, it uses the first midi output it finds. ```javascript -$: chord("").voicing().midi() +$: chord("").voicing().midi('IAC Driver') ``` In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` @@ -44,9 +58,10 @@ Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by defa ### midicmd -`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. +`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. It supports the following commands: + - `clock`/`midiClock` - Sends MIDI timing clock messages - `start` - Sends MIDI start message - `stop` - Sends MIDI stop message @@ -60,12 +75,15 @@ stack( ) ``` -### ccn && ccv +### control, ccn && ccv + +`control` sends MIDI control change messages to your MIDI device. - `ccn` sets the cc number. Depends on your synths midi mapping - `ccv` sets the cc value. normalized from 0 to 1. ```javascript +$: note("c a f e").control([74, sine.slow(4)]).midi() $: note("c a f e").ccn(74).ccv(sine.slow(4)).midi() ``` @@ -78,9 +96,9 @@ $: note("c a f e").midi() $: ccv(sine.segment(16).slow(4)).ccn(74).midi() ``` -### pc (Program Change) +### progNum (Program Change) -`pc` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. Program change values should be numbers between 0 and 127. ```javascript @@ -91,9 +109,9 @@ note("c3 e3 g3").pc("<0 1 2>").midi() Program change messages are useful for switching between different instrument sounds or presets during a performance. The exact sound that each program number maps to depends on your MIDI device's configuration. -## sysexid, sysexdata (System Exclusive Message) +## sysex, sysexid && sysexdata (System Exclusive Message) -`sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. +`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. sysEx messages are device-specific commands that allow deeper control over synthesizer parameters. The value should be an array of numbers between 0-255 representing the SysEx data bytes. @@ -102,9 +120,21 @@ The value should be an array of numbers between 0-255 representing the SysEx dat let id = 0x43; //Yamaha //let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" - -note("c d e f e d c").sysexid(id).sysexdata(data).midi(); +$: note("c d e f e d c").sysex(id, data).midi(); +$: note("c d e f e d c").sysexid(id).sysexdata(data).midi(); ``` -The exact format of SysEx messages depends on your MIDI device's specification. -Consult your device's MIDI implementation guide for details on supported SysEx messages. \ No newline at end of file +The exact format of SysEx messages depends on your MIDI device's specification. +Consult your device's MIDI implementation guide for details on supported SysEx messages. + +### midibend && miditouch + +`midibend` sets MIDI pitch bend (-1 - 1) +`miditouch` sets MIDI key after touch (0-1) + +```javascript + +$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi(); +$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi(); + +``` \ No newline at end of file diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index ad42c56d5..aab4be490 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -50,11 +50,14 @@ It supports the following commands: )`} /> -## ccn && ccv +## control, ccn && ccv +- `control` sends MIDI control change messages to your MIDI device. - `ccn` sets the cc number. Depends on your synths midi mapping - `ccv` sets the cc value. normalized from 0 to 1. + + In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern. @@ -67,37 +70,48 @@ But you can also control cc messages separately like this: $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`} /> -## pc (Program Change) +## progNum (Program Change) -`pc` sends MIDI program change messages to switch between different presets/patches on your MIDI device. +`progNum` sends MIDI program change messages to switch between different presets/patches on your MIDI device. Program change values should be numbers between 0 and 127. ").midi() +progNum("<0 1>").midi() // Play notes while changing programs -note("c3 e3 g3").pc("<0 1 2>").midi()`} /> +note("c3 e3 g3").progNum("<0 1 2>").midi()`} /> Program change messages are useful for switching between different instrument sounds or presets during a performance. The exact sound that each program number maps to depends on your MIDI device's configuration. -## sysex (System Exclusive Message) +## sysex, sysexid && sysexdata (System Exclusive Message) `sysex` sends MIDI System Exclusive (SysEx) messages to your MIDI device. ysEx messages are device-specific commands that allow deeper control over synthesizer parameters. The value should be an array of numbers between 0-255 representing the SysEx data bytes. -") -.midi()`} /> + The exact format of SysEx messages depends on your MIDI device's specification. Consult your device's MIDI implementation guide for details on supported SysEx messages. +## midibend && miditouch + +`midibend` sets MIDI pitch bend (-1 - 1) +`miditouch` sets MIDI key after touch (0-1) + + + + + # OSC/SuperDirt/StrudelDirt In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software. From 3189b365c8d1afef922f619e7f640010bc5c49c4 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 25 Jan 2025 03:27:46 +0800 Subject: [PATCH 22/88] fix midibend and miditouch --- packages/core/controls.mjs | 3 +-- packages/midi/midi.mjs | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1e1a1afb9..1b4feb60d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1710,14 +1710,13 @@ export const { sysexid } = registerControl('sysexid'); */ export const { sysexdata } = registerControl('sysexdata'); - /** * MIDI pitch bend: Sends a MIDI pitch bend message. * @name midibend * @param {number | Pattern} midibend MIDI pitch bend (-1 - 1) * @example * note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi() - */ + */ export const { midibend } = registerControl('midibend'); /** * MIDI key after touch: Sends a MIDI key after touch message. diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 01b663956..b1d7e7151 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -163,8 +163,6 @@ Pattern.prototype.midi = function (output) { sysexdata, } = hap.value; - console.log('hap', hap.value); - velocity = gain * velocity; // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length @@ -267,18 +265,20 @@ Pattern.prototype.midi = function (output) { // Handle midibend if (midibend !== undefined) { - if (typeof midibend !== 'number' || midibend < 1 || midibend > -1) { + if (typeof midibend == 'number' || midibend < 1 || midibend > -1) { + device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); + }else{ throw new Error('expected midibend to be a number between 1 and -1'); } - device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); } // Handle miditouch if (miditouch !== undefined) { - if (typeof miditouch !== 'number' || miditouch < 1 || miditouch > 0) { + if (typeof miditouch == 'number' || miditouch < 1 || miditouch > 0) { + device.sendKeyAfterTouch(miditouch, midichan, { time: timeOffsetString }); + }else{ throw new Error('expected miditouch to be a number between 1 and 0'); } - device.sendKeyAfterTouch(miditouch, midichan, { time: timeOffsetString }); } // Handle midicmd From 3ffe3957ba66de0722d8f84f65e0ff01d5fdf6cd Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 25 Jan 2025 08:00:49 +0800 Subject: [PATCH 23/88] Prettier! --- packages/midi/midi.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index b1d7e7151..aee54202f 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -267,7 +267,7 @@ Pattern.prototype.midi = function (output) { if (midibend !== undefined) { if (typeof midibend == 'number' || midibend < 1 || midibend > -1) { device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); - }else{ + } else { throw new Error('expected midibend to be a number between 1 and -1'); } } @@ -276,7 +276,7 @@ Pattern.prototype.midi = function (output) { if (miditouch !== undefined) { if (typeof miditouch == 'number' || miditouch < 1 || miditouch > 0) { device.sendKeyAfterTouch(miditouch, midichan, { time: timeOffsetString }); - }else{ + } else { throw new Error('expected miditouch to be a number between 1 and 0'); } } From c5d7f95441f067a35890ee4e674ddbc8bec8340d Mon Sep 17 00:00:00 2001 From: nkymut Date: Sat, 25 Jan 2025 10:01:58 +0800 Subject: [PATCH 24/88] add testsnapshot --- test/__snapshots__/examples.test.mjs.snap | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 0f644ce72..9382bce73 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4678,6 +4678,15 @@ exports[`runs examples > example "midi" example index 0 1`] = ` ] `; +exports[`runs examples > example "midibend" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midibend:0.282842712474619 ]", + "[ 1/1 → 2/1 | note:c4 midibend:0.282842712474619 ]", + "[ 2/1 → 3/1 | note:c4 midibend:-0.282842712474619 ]", + "[ 3/1 → 4/1 | note:c4 midibend:-0.2828427124746191 ]", +] +`; + exports[`runs examples > example "midichan" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 midichan:1 ]", @@ -4708,6 +4717,15 @@ exports[`runs examples > example "midin" example index 0 1`] = ` ] `; +exports[`runs examples > example "miditouch" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 miditouch:0.8535533905932737 ]", + "[ 1/1 → 2/1 | note:c4 miditouch:0.8535533905932737 ]", + "[ 2/1 → 3/1 | note:c4 miditouch:0.14644660940672627 ]", + "[ 3/1 → 4/1 | note:c4 miditouch:0.14644660940672616 ]", +] +`; + exports[`runs examples > example "mousex" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", @@ -4928,6 +4946,24 @@ exports[`runs examples > example "note" example index 2 1`] = ` ] `; +exports[`runs examples > example "nrpnn" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", +] +`; + +exports[`runs examples > example "nrpv" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", +] +`; + exports[`runs examples > example "octave" example index 0 1`] = ` [ "[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]", @@ -5761,6 +5797,15 @@ exports[`runs examples > example "pressBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "progNum" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 progNum:10 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 progNum:10 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 progNum:10 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 progNum:10 midichan:1 ]", +] +`; + exports[`runs examples > example "pure" example index 0 1`] = ` [ "[ 0/1 → 1/1 | e4 ]", From 4636d824fa2d49aeb8c065c6650d973409957580 Mon Sep 17 00:00:00 2001 From: nkymut Date: Mon, 3 Feb 2025 08:40:39 +0800 Subject: [PATCH 25/88] remove experimental code --- packages/midi/device/gakken_nsx-39.mjs | 269 ------------------------- packages/midi/gm.mjs | 130 ------------ packages/midi/index.mjs | 2 +- 3 files changed, 1 insertion(+), 400 deletions(-) delete mode 100644 packages/midi/device/gakken_nsx-39.mjs delete mode 100644 packages/midi/gm.mjs diff --git a/packages/midi/device/gakken_nsx-39.mjs b/packages/midi/device/gakken_nsx-39.mjs deleted file mode 100644 index b7c1b3216..000000000 --- a/packages/midi/device/gakken_nsx-39.mjs +++ /dev/null @@ -1,269 +0,0 @@ -export const miku = { - あ: '0x79:0x09:0x11:0x0A:0x00:0x00', - い: '0x79:0x09:0x11:0x0A:0x00:0x01', - う: '0x79:0x09:0x11:0x0A:0x00:0x02', - え: '0x79:0x09:0x11:0x0A:0x00:0x03', - お: '0x79:0x09:0x11:0x0A:0x00:0x04', - か: '0x79:0x09:0x11:0x0A:0x00:0x05', - き: '0x79:0x09:0x11:0x0A:0x00:0x06', - く: '0x79:0x09:0x11:0x0A:0x00:0x07', - け: '0x79:0x09:0x11:0x0A:0x00:0x08', - こ: '0x79:0x09:0x11:0x0A:0x00:0x09', - が: '0x79:0x09:0x11:0x0A:0x00:0x0a', - ぎ: '0x79:0x09:0x11:0x0A:0x00:0x0b', - ぐ: '0x79:0x09:0x11:0x0A:0x00:0x0c', - げ: '0x79:0x09:0x11:0x0A:0x00:0x0d', - ご: '0x79:0x09:0x11:0x0A:0x00:0x0e', - きゃ: '0x79:0x09:0x11:0x0A:0x00:0x0f', - きゅ: '0x79:0x09:0x11:0x0A:0x00:0x10', - きょ: '0x79:0x09:0x11:0x0A:0x00:0x11', - ぎゃ: '0x79:0x09:0x11:0x0A:0x00:0x12', - ぎゅ: '0x79:0x09:0x11:0x0A:0x00:0x13', - ぎょ: '0x79:0x09:0x11:0x0A:0x00:0x14', - さ: '0x79:0x09:0x11:0x0A:0x00:0x15', - すぃ: '0x79:0x09:0x11:0x0A:0x00:0x16', - す: '0x79:0x09:0x11:0x0A:0x00:0x17', - せ: '0x79:0x09:0x11:0x0A:0x00:0x18', - そ: '0x79:0x09:0x11:0x0A:0x00:0x19', - ざ: '0x79:0x09:0x11:0x0A:0x00:0x1a', - づぁ: '0x79:0x09:0x11:0x0A:0x00:0x1a', - ずぃ: '0x79:0x09:0x11:0x0A:0x00:0x1b', - づぃ: '0x79:0x09:0x11:0x0A:0x00:0x1b', - ず: '0x79:0x09:0x11:0x0A:0x00:0x1c', - づ: '0x79:0x09:0x11:0x0A:0x00:0x1c', - ぜ: '0x79:0x09:0x11:0x0A:0x00:0x1d', - づぇ: '0x79:0x09:0x11:0x0A:0x00:0x1d', - ぞ: '0x79:0x09:0x11:0x0A:0x00:0x1e', - づぉ: '0x79:0x09:0x11:0x0A:0x00:0x1e', - しゃ: '0x79:0x09:0x11:0x0A:0x00:0x1f', - し: '0x79:0x09:0x11:0x0A:0x00:0x20', - しゅ: '0x79:0x09:0x11:0x0A:0x00:0x21', - しぇ: '0x79:0x09:0x11:0x0A:0x00:0x22', - しょ: '0x79:0x09:0x11:0x0A:0x00:0x23', - じゃ: '0x79:0x09:0x11:0x0A:0x00:0x24', - じ: '0x79:0x09:0x11:0x0A:0x00:0x25', - じゅ: '0x79:0x09:0x11:0x0A:0x00:0x26', - じぇ: '0x79:0x09:0x11:0x0A:0x00:0x27', - じょ: '0x79:0x09:0x11:0x0A:0x00:0x28', - た: '0x79:0x09:0x11:0x0A:0x00:0x29', - てぃ: '0x79:0x09:0x11:0x0A:0x00:0x2a', - とぅ: '0x79:0x09:0x11:0x0A:0x00:0x2b', - て: '0x79:0x09:0x11:0x0A:0x00:0x2c', - と: '0x79:0x09:0x11:0x0A:0x00:0x2d', - だ: '0x79:0x09:0x11:0x0A:0x00:0x2e', - でぃ: '0x79:0x09:0x11:0x0A:0x00:0x2f', - どぅ: '0x79:0x09:0x11:0x0A:0x00:0x30', - で: '0x79:0x09:0x11:0x0A:0x00:0x31', - ど: '0x79:0x09:0x11:0x0A:0x00:0x32', - てゅ: '0x79:0x09:0x11:0x0A:0x00:0x33', - でゅ: '0x79:0x09:0x11:0x0A:0x00:0x34', - ちゃ: '0x79:0x09:0x11:0x0A:0x00:0x35', - ち: '0x79:0x09:0x11:0x0A:0x00:0x36', - ちゅ: '0x79:0x09:0x11:0x0A:0x00:0x37', - ちぇ: '0x79:0x09:0x11:0x0A:0x00:0x38', - ちょ: '0x79:0x09:0x11:0x0A:0x00:0x39', - つぁ: '0x79:0x09:0x11:0x0A:0x00:0x3a', - つぃ: '0x79:0x09:0x11:0x0A:0x00:0x3b', - つ: '0x79:0x09:0x11:0x0A:0x00:0x3c', - つぇ: '0x79:0x09:0x11:0x0A:0x00:0x3d', - つぉ: '0x79:0x09:0x11:0x0A:0x00:0x3e', - な: '0x79:0x09:0x11:0x0A:0x00:0x3f', - に: '0x79:0x09:0x11:0x0A:0x00:0x40', - ぬ: '0x79:0x09:0x11:0x0A:0x00:0x41', - ね: '0x79:0x09:0x11:0x0A:0x00:0x42', - の: '0x79:0x09:0x11:0x0A:0x00:0x43', - にゃ: '0x79:0x09:0x11:0x0A:0x00:0x44', - にゅ: '0x79:0x09:0x11:0x0A:0x00:0x45', - にょ: '0x79:0x09:0x11:0x0A:0x00:0x46', - は: '0x79:0x09:0x11:0x0A:0x00:0x47', - ひ: '0x79:0x09:0x11:0x0A:0x00:0x48', - ふ: '0x79:0x09:0x11:0x0A:0x00:0x49', - へ: '0x79:0x09:0x11:0x0A:0x00:0x4a', - ほ: '0x79:0x09:0x11:0x0A:0x00:0x4b', - ば: '0x79:0x09:0x11:0x0A:0x00:0x4c', - び: '0x79:0x09:0x11:0x0A:0x00:0x4d', - ぶ: '0x79:0x09:0x11:0x0A:0x00:0x4e', - べ: '0x79:0x09:0x11:0x0A:0x00:0x4f', - ぼ: '0x79:0x09:0x11:0x0A:0x00:0x50', - ぱ: '0x79:0x09:0x11:0x0A:0x00:0x51', - ぴ: '0x79:0x09:0x11:0x0A:0x00:0x52', - ぷ: '0x79:0x09:0x11:0x0A:0x00:0x53', - ぺ: '0x79:0x09:0x11:0x0A:0x00:0x54', - ぽ: '0x79:0x09:0x11:0x0A:0x00:0x55', - ひゃ: '0x79:0x09:0x11:0x0A:0x00:0x56', - ひゅ: '0x79:0x09:0x11:0x0A:0x00:0x57', - ひょ: '0x79:0x09:0x11:0x0A:0x00:0x58', - びゃ: '0x79:0x09:0x11:0x0A:0x00:0x59', - びゅ: '0x79:0x09:0x11:0x0A:0x00:0x5a', - びょ: '0x79:0x09:0x11:0x0A:0x00:0x5b', - ぴゃ: '0x79:0x09:0x11:0x0A:0x00:0x5c', - ぴゅ: '0x79:0x09:0x11:0x0A:0x00:0x5d', - ぴょ: '0x79:0x09:0x11:0x0A:0x00:0x5e', - ふぁ: '0x79:0x09:0x11:0x0A:0x00:0x5f', - ふぃ: '0x79:0x09:0x11:0x0A:0x00:0x60', - ふゅ: '0x79:0x09:0x11:0x0A:0x00:0x61', - ふぇ: '0x79:0x09:0x11:0x0A:0x00:0x62', - ふぉ: '0x79:0x09:0x11:0x0A:0x00:0x63', - ま: '0x79:0x09:0x11:0x0A:0x00:0x64', - み: '0x79:0x09:0x11:0x0A:0x00:0x65', - む: '0x79:0x09:0x11:0x0A:0x00:0x66', - め: '0x79:0x09:0x11:0x0A:0x00:0x67', - も: '0x79:0x09:0x11:0x0A:0x00:0x68', - みゃ: '0x79:0x09:0x11:0x0A:0x00:0x69', - みゅ: '0x79:0x09:0x11:0x0A:0x00:0x6a', - みょ: '0x79:0x09:0x11:0x0A:0x00:0x6b', - や: '0x79:0x09:0x11:0x0A:0x00:0x6c', - ゆ: '0x79:0x09:0x11:0x0A:0x00:0x6d', - よ: '0x79:0x09:0x11:0x0A:0x00:0x6e', - ら: '0x79:0x09:0x11:0x0A:0x00:0x6f', - り: '0x79:0x09:0x11:0x0A:0x00:0x70', - る: '0x79:0x09:0x11:0x0A:0x00:0x71', - れ: '0x79:0x09:0x11:0x0A:0x00:0x72', - ろ: '0x79:0x09:0x11:0x0A:0x00:0x73', - りゃ: '0x79:0x09:0x11:0x0A:0x00:0x74', - りゅ: '0x79:0x09:0x11:0x0A:0x00:0x75', - りょ: '0x79:0x09:0x11:0x0A:0x00:0x76', - わ: '0x79:0x09:0x11:0x0A:0x00:0x77', - うぃ: '0x79:0x09:0x11:0x0A:0x00:0x78', - うぇ: '0x79:0x09:0x11:0x0A:0x00:0x79', - うぉ: '0x79:0x09:0x11:0x0A:0x00:0x7a', - ん: '0x79:0x09:0x11:0x0A:0x00:0x7b', - ん1: '0x79:0x09:0x11:0x0A:0x00:0x7c', - ん2: '0x79:0x09:0x11:0x0A:0x00:0x7d', - ん3: '0x79:0x09:0x11:0x0A:0x00:0x7e', - ん4: '0x79:0x09:0x11:0x0A:0x00:0x7f', - - a: '0x79:0x09:0x11:0x0A:0x00:0x00', - i: '0x79:0x09:0x11:0x0A:0x00:0x01', - u: '0x79:0x09:0x11:0x0A:0x00:0x02', - e: '0x79:0x09:0x11:0x0A:0x00:0x03', - o: '0x79:0x09:0x11:0x0A:0x00:0x04', - ka: '0x79:0x09:0x11:0x0A:0x00:0x05', - ki: '0x79:0x09:0x11:0x0A:0x00:0x06', - ku: '0x79:0x09:0x11:0x0A:0x00:0x07', - ke: '0x79:0x09:0x11:0x0A:0x00:0x08', - ko: '0x79:0x09:0x11:0x0A:0x00:0x09', - ga: '0x79:0x09:0x11:0x0A:0x00:0x0a', - gi: '0x79:0x09:0x11:0x0A:0x00:0x0b', - gu: '0x79:0x09:0x11:0x0A:0x00:0x0c', - ge: '0x79:0x09:0x11:0x0A:0x00:0x0d', - go: '0x79:0x09:0x11:0x0A:0x00:0x0e', - kya: '0x79:0x09:0x11:0x0A:0x00:0x0f', - kyu: '0x79:0x09:0x11:0x0A:0x00:0x10', - kyo: '0x79:0x09:0x11:0x0A:0x00:0x11', - gya: '0x79:0x09:0x11:0x0A:0x00:0x12', - gyu: '0x79:0x09:0x11:0x0A:0x00:0x13', - gyo: '0x79:0x09:0x11:0x0A:0x00:0x14', - sa: '0x79:0x09:0x11:0x0A:0x00:0x15', - swi: '0x79:0x09:0x11:0x0A:0x00:0x16', - su: '0x79:0x09:0x11:0x0A:0x00:0x17', - se: '0x79:0x09:0x11:0x0A:0x00:0x18', - so: '0x79:0x09:0x11:0x0A:0x00:0x19', - za: '0x79:0x09:0x11:0x0A:0x00:0x1a', - zwi: '0x79:0x09:0x11:0x0A:0x00:0x1b', - zu: '0x79:0x09:0x11:0x0A:0x00:0x1c', - ze: '0x79:0x09:0x11:0x0A:0x00:0x1d', - zo: '0x79:0x09:0x11:0x0A:0x00:0x1e', - sha: '0x79:0x09:0x11:0x0A:0x00:0x1f', - shi: '0x79:0x09:0x11:0x0A:0x00:0x20', - shu: '0x79:0x09:0x11:0x0A:0x00:0x21', - she: '0x79:0x09:0x11:0x0A:0x00:0x22', - sho: '0x79:0x09:0x11:0x0A:0x00:0x23', - ja: '0x79:0x09:0x11:0x0A:0x00:0x24', - dza: '0x79:0x09:0x11:0x0A:0x00:0x24', - ji: '0x79:0x09:0x11:0x0A:0x00:0x25', - dzi: '0x79:0x09:0x11:0x0A:0x00:0x25', - ju: '0x79:0x09:0x11:0x0A:0x00:0x26', - dzu: '0x79:0x09:0x11:0x0A:0x00:0x26', - je: '0x79:0x09:0x11:0x0A:0x00:0x27', - dze: '0x79:0x09:0x11:0x0A:0x00:0x27', - jo: '0x79:0x09:0x11:0x0A:0x00:0x28', - dzo: '0x79:0x09:0x11:0x0A:0x00:0x28', - ta: '0x79:0x09:0x11:0x0A:0x00:0x29', - ti: '0x79:0x09:0x11:0x0A:0x00:0x2a', - tu: '0x79:0x09:0x11:0x0A:0x00:0x2b', - te: '0x79:0x09:0x11:0x0A:0x00:0x2c', - to: '0x79:0x09:0x11:0x0A:0x00:0x2d', - da: '0x79:0x09:0x11:0x0A:0x00:0x2e', - di: '0x79:0x09:0x11:0x0A:0x00:0x2f', - du: '0x79:0x09:0x11:0x0A:0x00:0x30', - de: '0x79:0x09:0x11:0x0A:0x00:0x31', - do: '0x79:0x09:0x11:0x0A:0x00:0x32', - tyu: '0x79:0x09:0x11:0x0A:0x00:0x33', - dyu: '0x79:0x09:0x11:0x0A:0x00:0x34', - cha: '0x79:0x09:0x11:0x0A:0x00:0x35', - chi: '0x79:0x09:0x11:0x0A:0x00:0x36', - chu: '0x79:0x09:0x11:0x0A:0x00:0x37', - che: '0x79:0x09:0x11:0x0A:0x00:0x38', - cho: '0x79:0x09:0x11:0x0A:0x00:0x39', - tsa: '0x79:0x09:0x11:0x0A:0x00:0x3a', - tsi: '0x79:0x09:0x11:0x0A:0x00:0x3b', - tsu: '0x79:0x09:0x11:0x0A:0x00:0x3c', - tse: '0x79:0x09:0x11:0x0A:0x00:0x3d', - tso: '0x79:0x09:0x11:0x0A:0x00:0x3e', - na: '0x79:0x09:0x11:0x0A:0x00:0x3f', - ni: '0x79:0x09:0x11:0x0A:0x00:0x40', - nu: '0x79:0x09:0x11:0x0A:0x00:0x41', - ne: '0x79:0x09:0x11:0x0A:0x00:0x42', - no: '0x79:0x09:0x11:0x0A:0x00:0x43', - nya: '0x79:0x09:0x11:0x0A:0x00:0x44', - nyu: '0x79:0x09:0x11:0x0A:0x00:0x45', - nyo: '0x79:0x09:0x11:0x0A:0x00:0x46', - ha: '0x79:0x09:0x11:0x0A:0x00:0x47', - hi: '0x79:0x09:0x11:0x0A:0x00:0x48', - fu: '0x79:0x09:0x11:0x0A:0x00:0x49', - he: '0x79:0x09:0x11:0x0A:0x00:0x4a', - ho: '0x79:0x09:0x11:0x0A:0x00:0x4b', - ba: '0x79:0x09:0x11:0x0A:0x00:0x4c', - bi: '0x79:0x09:0x11:0x0A:0x00:0x4d', - bu: '0x79:0x09:0x11:0x0A:0x00:0x4e', - be: '0x79:0x09:0x11:0x0A:0x00:0x4f', - bo: '0x79:0x09:0x11:0x0A:0x00:0x50', - pa: '0x79:0x09:0x11:0x0A:0x00:0x51', - pi: '0x79:0x09:0x11:0x0A:0x00:0x52', - pu: '0x79:0x09:0x11:0x0A:0x00:0x53', - pe: '0x79:0x09:0x11:0x0A:0x00:0x54', - po: '0x79:0x09:0x11:0x0A:0x00:0x55', - hya: '0x79:0x09:0x11:0x0A:0x00:0x56', - hyu: '0x79:0x09:0x11:0x0A:0x00:0x57', - hyo: '0x79:0x09:0x11:0x0A:0x00:0x58', - bya: '0x79:0x09:0x11:0x0A:0x00:0x59', - byu: '0x79:0x09:0x11:0x0A:0x00:0x5a', - byo: '0x79:0x09:0x11:0x0A:0x00:0x5b', - pya: '0x79:0x09:0x11:0x0A:0x00:0x5c', - pyu: '0x79:0x09:0x11:0x0A:0x00:0x5d', - pyo: '0x79:0x09:0x11:0x0A:0x00:0x5e', - fa: '0x79:0x09:0x11:0x0A:0x00:0x5f', - fi: '0x79:0x09:0x11:0x0A:0x00:0x60', - fyu: '0x79:0x09:0x11:0x0A:0x00:0x61', - fe: '0x79:0x09:0x11:0x0A:0x00:0x62', - fo: '0x79:0x09:0x11:0x0A:0x00:0x63', - ma: '0x79:0x09:0x11:0x0A:0x00:0x64', - mi: '0x79:0x09:0x11:0x0A:0x00:0x65', - mu: '0x79:0x09:0x11:0x0A:0x00:0x66', - me: '0x79:0x09:0x11:0x0A:0x00:0x67', - mo: '0x79:0x09:0x11:0x0A:0x00:0x68', - mya: '0x79:0x09:0x11:0x0A:0x00:0x69', - myu: '0x79:0x09:0x11:0x0A:0x00:0x6a', - myo: '0x79:0x09:0x11:0x0A:0x00:0x6b', - ya: '0x79:0x09:0x11:0x0A:0x00:0x6c', - yu: '0x79:0x09:0x11:0x0A:0x00:0x6d', - yo: '0x79:0x09:0x11:0x0A:0x00:0x6e', - ra: '0x79:0x09:0x11:0x0A:0x00:0x6f', - ri: '0x79:0x09:0x11:0x0A:0x00:0x70', - ru: '0x79:0x09:0x11:0x0A:0x00:0x71', - re: '0x79:0x09:0x11:0x0A:0x00:0x72', - ro: '0x79:0x09:0x11:0x0A:0x00:0x73', - rya: '0x79:0x09:0x11:0x0A:0x00:0x74', - ryu: '0x79:0x09:0x11:0x0A:0x00:0x75', - ryo: '0x79:0x09:0x11:0x0A:0x00:0x76', - wa: '0x79:0x09:0x11:0x0A:0x00:0x77', - wi: '0x79:0x09:0x11:0x0A:0x00:0x78', - we: '0x79:0x09:0x11:0x0A:0x00:0x79', - wo: '0x79:0x09:0x11:0x0A:0x00:0x7a', - n: '0x79:0x09:0x11:0x0A:0x00:0x7b', - n1: '0x79:0x09:0x11:0x0A:0x00:0x7c', - n2: '0x79:0x09:0x11:0x0A:0x00:0x7d', - n3: '0x79:0x09:0x11:0x0A:0x00:0x7e', - n4: '0x79:0x09:0x11:0x0A:0x00:0x7f', -}; diff --git a/packages/midi/gm.mjs b/packages/midi/gm.mjs deleted file mode 100644 index 6ba23737a..000000000 --- a/packages/midi/gm.mjs +++ /dev/null @@ -1,130 +0,0 @@ -export const gm = { - piano: 0, - bright_acoustic_piano: 1, - electric_grand_piano: 2, - honky_tonk_piano: 3, - epiano1: 4, - epiano2: 5, - harpsichord: 6, - clavinet: 7, - celesta: 8, - glockenspiel: 9, - music_box: 10, - vibraphone: 11, - marimba: 12, - xylophone: 13, - tubular_bells: 14, - dulcimer: 15, - drawbar_organ: 16, - percussive_organ: 17, - rock_organ: 18, - church_organ: 19, - reed_organ: 20, - accordion: 21, - harmonica: 22, - bandoneon: 23, - acoustic_guitar_nylon: 24, - acoustic_guitar_steel: 25, - electric_guitar_jazz: 26, - electric_guitar_clean: 27, - electric_guitar_muted: 28, - overdriven_guitar: 29, - distortion_guitar: 30, - guitar_harmonics: 31, - acoustic_bass: 32, - electric_bass_finger: 33, - electric_bass_pick: 34, - fretless_bass: 35, - slap_bass_1: 36, - slap_bass_2: 37, - synth_bass_1: 38, - synth_bass_2: 39, - violin: 40, - viola: 41, - cello: 42, - contrabass: 43, - tremolo_strings: 44, - pizzicato_strings: 45, - orchestral_harp: 46, - timpani: 47, - string_ensemble_1: 48, - string_ensemble_2: 49, - synth_strings_1: 50, - synth_strings_2: 51, - choir_aahs: 52, - voice_oohs: 53, - synth_choir: 54, - orchestra_hit: 55, - trumpet: 56, - trombone: 57, - tuba: 58, - muted_trumpet: 59, - french_horn: 60, - brass_section: 61, - synth_brass_1: 62, - synth_brass_2: 63, - soprano_sax: 64, - alto_sax: 65, - tenor_sax: 66, - baritone_sax: 67, - oboe: 68, - english_horn: 69, - bassoon: 70, - clarinet: 71, - piccolo: 72, - flute: 73, - recorder: 74, - pan_flute: 75, - blown_bottle: 76, - shakuhachi: 77, - whistle: 78, - ocarina: 79, - lead_1_square: 80, - lead_2_sawtooth: 81, - lead_3_calliope: 82, - lead_4_chiff: 83, - lead_5_charang: 84, - lead_6_voice: 85, - lead_7_fifths: 86, - lead_8_bass_lead: 87, - pad_1_new_age: 88, - pad_2_warm: 89, - pad_3_polysynth: 90, - pad_4_choir: 91, - pad_5_bowed: 92, - pad_6_metallic: 93, - pad_7_halo: 94, - pad_8_sweep: 95, - fx_1_rain: 96, - fx_2_soundtrack: 97, - fx_3_crystal: 98, - fx_4_atmosphere: 99, - fx_5_brightness: 100, - fx_6_goblins: 101, - fx_7_echoes: 102, - fx_8_sci_fi: 103, - sitar: 104, - banjo: 105, - shamisen: 106, - koto: 107, - kalimba: 108, - bagpipe: 109, - fiddle: 110, - shanai: 111, - tinkle_bell: 112, - agogo: 113, - steel_drums: 114, - woodblock: 115, - taiko_drum: 116, - melodic_tom: 117, - synth_drum: 118, - reverse_cymbal: 119, - guitar_fret_noise: 120, - breath_noise: 121, - seashore: 122, - bird_tweet: 123, - telephone: 124, - helicopter: 125, - applause: 126, - gunshot: 127, -}; diff --git a/packages/midi/index.mjs b/packages/midi/index.mjs index 2a226a538..63efd4cfb 100644 --- a/packages/midi/index.mjs +++ b/packages/midi/index.mjs @@ -1,4 +1,4 @@ import './midi.mjs'; export * from './midi.mjs'; -export * from './gm.mjs'; + From ed7dc4ef6ec3e197c29599d325fac665332fd23a Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 4 Feb 2025 02:49:47 +0800 Subject: [PATCH 26/88] codeformat --- packages/midi/index.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/midi/index.mjs b/packages/midi/index.mjs index 63efd4cfb..399227f73 100644 --- a/packages/midi/index.mjs +++ b/packages/midi/index.mjs @@ -1,4 +1,3 @@ import './midi.mjs'; export * from './midi.mjs'; - From 52d1443cf8d467b44c24259bd1193f038a8a42c0 Mon Sep 17 00:00:00 2001 From: nkymut Date: Wed, 5 Feb 2025 07:41:24 +0800 Subject: [PATCH 27/88] Add midicmd JSdoc --- packages/core/controls.mjs | 8 + test/__snapshots__/examples.test.mjs.snap | 199 ++++++++++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1b4feb60d..37f37c982 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1617,6 +1617,14 @@ export const ar = register('ar', (t, pat) => { */ export const { midichan } = registerControl('midichan'); +/** + * MIDI command: Sends a MIDI command message. + * + * @name midicmd + * @param {number | Pattern} command MIDI command + * @example + * midicmd("clock*48,/2").midi() + */ export const { midicmd } = registerControl('midicmd'); /** diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 9382bce73..4cdafb787 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4696,6 +4696,205 @@ exports[`runs examples > example "midichan" example index 0 1`] = ` ] `; +exports[`runs examples > example "midicmd" example index 0 1`] = ` +[ + "[ 0/1 → 1/48 | midicmd:clock ]", + "[ 0/1 → 2/1 | midicmd:start ]", + "[ 1/48 → 1/24 | midicmd:clock ]", + "[ 1/24 → 1/16 | midicmd:clock ]", + "[ 1/16 → 1/12 | midicmd:clock ]", + "[ 1/12 → 5/48 | midicmd:clock ]", + "[ 5/48 → 1/8 | midicmd:clock ]", + "[ 1/8 → 7/48 | midicmd:clock ]", + "[ 7/48 → 1/6 | midicmd:clock ]", + "[ 1/6 → 3/16 | midicmd:clock ]", + "[ 3/16 → 5/24 | midicmd:clock ]", + "[ 5/24 → 11/48 | midicmd:clock ]", + "[ 11/48 → 1/4 | midicmd:clock ]", + "[ 1/4 → 13/48 | midicmd:clock ]", + "[ 13/48 → 7/24 | midicmd:clock ]", + "[ 7/24 → 5/16 | midicmd:clock ]", + "[ 5/16 → 1/3 | midicmd:clock ]", + "[ 1/3 → 17/48 | midicmd:clock ]", + "[ 17/48 → 3/8 | midicmd:clock ]", + "[ 3/8 → 19/48 | midicmd:clock ]", + "[ 19/48 → 5/12 | midicmd:clock ]", + "[ 5/12 → 7/16 | midicmd:clock ]", + "[ 7/16 → 11/24 | midicmd:clock ]", + "[ 11/24 → 23/48 | midicmd:clock ]", + "[ 23/48 → 1/2 | midicmd:clock ]", + "[ 1/2 → 25/48 | midicmd:clock ]", + "[ 25/48 → 13/24 | midicmd:clock ]", + "[ 13/24 → 9/16 | midicmd:clock ]", + "[ 9/16 → 7/12 | midicmd:clock ]", + "[ 7/12 → 29/48 | midicmd:clock ]", + "[ 29/48 → 5/8 | midicmd:clock ]", + "[ 5/8 → 31/48 | midicmd:clock ]", + "[ 31/48 → 2/3 | midicmd:clock ]", + "[ 2/3 → 11/16 | midicmd:clock ]", + "[ 11/16 → 17/24 | midicmd:clock ]", + "[ 17/24 → 35/48 | midicmd:clock ]", + "[ 35/48 → 3/4 | midicmd:clock ]", + "[ 3/4 → 37/48 | midicmd:clock ]", + "[ 37/48 → 19/24 | midicmd:clock ]", + "[ 19/24 → 13/16 | midicmd:clock ]", + "[ 13/16 → 5/6 | midicmd:clock ]", + "[ 5/6 → 41/48 | midicmd:clock ]", + "[ 41/48 → 7/8 | midicmd:clock ]", + "[ 7/8 → 43/48 | midicmd:clock ]", + "[ 43/48 → 11/12 | midicmd:clock ]", + "[ 11/12 → 15/16 | midicmd:clock ]", + "[ 15/16 → 23/24 | midicmd:clock ]", + "[ 23/24 → 47/48 | midicmd:clock ]", + "[ 47/48 → 1/1 | midicmd:clock ]", + "[ 1/1 → 49/48 | midicmd:clock ]", + "[ 49/48 → 25/24 | midicmd:clock ]", + "[ 25/24 → 17/16 | midicmd:clock ]", + "[ 17/16 → 13/12 | midicmd:clock ]", + "[ 13/12 → 53/48 | midicmd:clock ]", + "[ 53/48 → 9/8 | midicmd:clock ]", + "[ 9/8 → 55/48 | midicmd:clock ]", + "[ 55/48 → 7/6 | midicmd:clock ]", + "[ 7/6 → 19/16 | midicmd:clock ]", + "[ 19/16 → 29/24 | midicmd:clock ]", + "[ 29/24 → 59/48 | midicmd:clock ]", + "[ 59/48 → 5/4 | midicmd:clock ]", + "[ 5/4 → 61/48 | midicmd:clock ]", + "[ 61/48 → 31/24 | midicmd:clock ]", + "[ 31/24 → 21/16 | midicmd:clock ]", + "[ 21/16 → 4/3 | midicmd:clock ]", + "[ 4/3 → 65/48 | midicmd:clock ]", + "[ 65/48 → 11/8 | midicmd:clock ]", + "[ 11/8 → 67/48 | midicmd:clock ]", + "[ 67/48 → 17/12 | midicmd:clock ]", + "[ 17/12 → 23/16 | midicmd:clock ]", + "[ 23/16 → 35/24 | midicmd:clock ]", + "[ 35/24 → 71/48 | midicmd:clock ]", + "[ 71/48 → 3/2 | midicmd:clock ]", + "[ 3/2 → 73/48 | midicmd:clock ]", + "[ 73/48 → 37/24 | midicmd:clock ]", + "[ 37/24 → 25/16 | midicmd:clock ]", + "[ 25/16 → 19/12 | midicmd:clock ]", + "[ 19/12 → 77/48 | midicmd:clock ]", + "[ 77/48 → 13/8 | midicmd:clock ]", + "[ 13/8 → 79/48 | midicmd:clock ]", + "[ 79/48 → 5/3 | midicmd:clock ]", + "[ 5/3 → 27/16 | midicmd:clock ]", + "[ 27/16 → 41/24 | midicmd:clock ]", + "[ 41/24 → 83/48 | midicmd:clock ]", + "[ 83/48 → 7/4 | midicmd:clock ]", + "[ 7/4 → 85/48 | midicmd:clock ]", + "[ 85/48 → 43/24 | midicmd:clock ]", + "[ 43/24 → 29/16 | midicmd:clock ]", + "[ 29/16 → 11/6 | midicmd:clock ]", + "[ 11/6 → 89/48 | midicmd:clock ]", + "[ 89/48 → 15/8 | midicmd:clock ]", + "[ 15/8 → 91/48 | midicmd:clock ]", + "[ 91/48 → 23/12 | midicmd:clock ]", + "[ 23/12 → 31/16 | midicmd:clock ]", + "[ 31/16 → 47/24 | midicmd:clock ]", + "[ 47/24 → 95/48 | midicmd:clock ]", + "[ 95/48 → 2/1 | midicmd:clock ]", + "[ 2/1 → 97/48 | midicmd:clock ]", + "[ 2/1 → 4/1 | midicmd:stop ]", + "[ 97/48 → 49/24 | midicmd:clock ]", + "[ 49/24 → 33/16 | midicmd:clock ]", + "[ 33/16 → 25/12 | midicmd:clock ]", + "[ 25/12 → 101/48 | midicmd:clock ]", + "[ 101/48 → 17/8 | midicmd:clock ]", + "[ 17/8 → 103/48 | midicmd:clock ]", + "[ 103/48 → 13/6 | midicmd:clock ]", + "[ 13/6 → 35/16 | midicmd:clock ]", + "[ 35/16 → 53/24 | midicmd:clock ]", + "[ 53/24 → 107/48 | midicmd:clock ]", + "[ 107/48 → 9/4 | midicmd:clock ]", + "[ 9/4 → 109/48 | midicmd:clock ]", + "[ 109/48 → 55/24 | midicmd:clock ]", + "[ 55/24 → 37/16 | midicmd:clock ]", + "[ 37/16 → 7/3 | midicmd:clock ]", + "[ 7/3 → 113/48 | midicmd:clock ]", + "[ 113/48 → 19/8 | midicmd:clock ]", + "[ 19/8 → 115/48 | midicmd:clock ]", + "[ 115/48 → 29/12 | midicmd:clock ]", + "[ 29/12 → 39/16 | midicmd:clock ]", + "[ 39/16 → 59/24 | midicmd:clock ]", + "[ 59/24 → 119/48 | midicmd:clock ]", + "[ 119/48 → 5/2 | midicmd:clock ]", + "[ 5/2 → 121/48 | midicmd:clock ]", + "[ 121/48 → 61/24 | midicmd:clock ]", + "[ 61/24 → 41/16 | midicmd:clock ]", + "[ 41/16 → 31/12 | midicmd:clock ]", + "[ 31/12 → 125/48 | midicmd:clock ]", + "[ 125/48 → 21/8 | midicmd:clock ]", + "[ 21/8 → 127/48 | midicmd:clock ]", + "[ 127/48 → 8/3 | midicmd:clock ]", + "[ 8/3 → 43/16 | midicmd:clock ]", + "[ 43/16 → 65/24 | midicmd:clock ]", + "[ 65/24 → 131/48 | midicmd:clock ]", + "[ 131/48 → 11/4 | midicmd:clock ]", + "[ 11/4 → 133/48 | midicmd:clock ]", + "[ 133/48 → 67/24 | midicmd:clock ]", + "[ 67/24 → 45/16 | midicmd:clock ]", + "[ 45/16 → 17/6 | midicmd:clock ]", + "[ 17/6 → 137/48 | midicmd:clock ]", + "[ 137/48 → 23/8 | midicmd:clock ]", + "[ 23/8 → 139/48 | midicmd:clock ]", + "[ 139/48 → 35/12 | midicmd:clock ]", + "[ 35/12 → 47/16 | midicmd:clock ]", + "[ 47/16 → 71/24 | midicmd:clock ]", + "[ 71/24 → 143/48 | midicmd:clock ]", + "[ 143/48 → 3/1 | midicmd:clock ]", + "[ 3/1 → 145/48 | midicmd:clock ]", + "[ 145/48 → 73/24 | midicmd:clock ]", + "[ 73/24 → 49/16 | midicmd:clock ]", + "[ 49/16 → 37/12 | midicmd:clock ]", + "[ 37/12 → 149/48 | midicmd:clock ]", + "[ 149/48 → 25/8 | midicmd:clock ]", + "[ 25/8 → 151/48 | midicmd:clock ]", + "[ 151/48 → 19/6 | midicmd:clock ]", + "[ 19/6 → 51/16 | midicmd:clock ]", + "[ 51/16 → 77/24 | midicmd:clock ]", + "[ 77/24 → 155/48 | midicmd:clock ]", + "[ 155/48 → 13/4 | midicmd:clock ]", + "[ 13/4 → 157/48 | midicmd:clock ]", + "[ 157/48 → 79/24 | midicmd:clock ]", + "[ 79/24 → 53/16 | midicmd:clock ]", + "[ 53/16 → 10/3 | midicmd:clock ]", + "[ 10/3 → 161/48 | midicmd:clock ]", + "[ 161/48 → 27/8 | midicmd:clock ]", + "[ 27/8 → 163/48 | midicmd:clock ]", + "[ 163/48 → 41/12 | midicmd:clock ]", + "[ 41/12 → 55/16 | midicmd:clock ]", + "[ 55/16 → 83/24 | midicmd:clock ]", + "[ 83/24 → 167/48 | midicmd:clock ]", + "[ 167/48 → 7/2 | midicmd:clock ]", + "[ 7/2 → 169/48 | midicmd:clock ]", + "[ 169/48 → 85/24 | midicmd:clock ]", + "[ 85/24 → 57/16 | midicmd:clock ]", + "[ 57/16 → 43/12 | midicmd:clock ]", + "[ 43/12 → 173/48 | midicmd:clock ]", + "[ 173/48 → 29/8 | midicmd:clock ]", + "[ 29/8 → 175/48 | midicmd:clock ]", + "[ 175/48 → 11/3 | midicmd:clock ]", + "[ 11/3 → 59/16 | midicmd:clock ]", + "[ 59/16 → 89/24 | midicmd:clock ]", + "[ 89/24 → 179/48 | midicmd:clock ]", + "[ 179/48 → 15/4 | midicmd:clock ]", + "[ 15/4 → 181/48 | midicmd:clock ]", + "[ 181/48 → 91/24 | midicmd:clock ]", + "[ 91/24 → 61/16 | midicmd:clock ]", + "[ 61/16 → 23/6 | midicmd:clock ]", + "[ 23/6 → 185/48 | midicmd:clock ]", + "[ 185/48 → 31/8 | midicmd:clock ]", + "[ 31/8 → 187/48 | midicmd:clock ]", + "[ 187/48 → 47/12 | midicmd:clock ]", + "[ 47/12 → 63/16 | midicmd:clock ]", + "[ 63/16 → 95/24 | midicmd:clock ]", + "[ 95/24 → 191/48 | midicmd:clock ]", + "[ 191/48 → 4/1 | midicmd:clock ]", +] +`; + exports[`runs examples > example "midin" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]", From 20dcae68c14e8737a6d333309baffb9702b118db Mon Sep 17 00:00:00 2001 From: nkymut Date: Wed, 5 Feb 2025 07:42:06 +0800 Subject: [PATCH 28/88] add sysex handler to midicmd --- packages/midi/midi.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index aee54202f..2c46d61c9 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -311,6 +311,21 @@ Pattern.prototype.midi = function (output) { } device.sendControlChange(midicmd[0], midicmd[1], midichan, { time: timeOffsetString }); } + } else if (midicmd[0] === 'sysex') { + if (midicmd.length === 3) { + const [_, id, data] = midicmd; + if (Array.isArray(id)) { + if (!id.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('all sysex id bytes must be integers between 0 and 255'); + } + } else if (!Number.isInteger(id) || id < 0 || id > 255) { + throw new Error('sysex id must be a number between 0 and 255 or an array of such integers'); + } + if (!Array.isArray(data) || !data.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('sysex data must be an array of integers between 0 and 255'); + } + device.sendSysex(id, data, { time: timeOffsetString }); + } } } }); From 9b279ff671cc0ee342324680b130006837921a52 Mon Sep 17 00:00:00 2001 From: nkymut Date: Wed, 5 Feb 2025 08:11:39 +0800 Subject: [PATCH 29/88] update 'midicmd' in README.md --- packages/midi/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/midi/README.md b/packages/midi/README.md index 524baa17f..faf9993f7 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -75,6 +75,23 @@ stack( ) ``` +`midicmd` also supports sending control change, program change and sysex messages. + +- `cc` - sends MIDI control change messages. +- `progNum` - sends MIDI program change messages. +- `sysex` - sends MIDI system exclusive messages. + +```javascript +stack( + // "cc:ccn:ccv" + midicmd("cc:74:1").midi('IAC Driver'), + // "progNum:progNum" + midicmd("progNum:1").midi('IAC Driver'), + // "sysex:[sysexid]:[sysexdata]" + midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver') +) +``` + ### control, ccn && ccv `control` sends MIDI control change messages to your MIDI device. From 54c54543062941f65a41bbba0605bb7732e82065 Mon Sep 17 00:00:00 2001 From: nkymut Date: Wed, 5 Feb 2025 08:17:42 +0800 Subject: [PATCH 30/88] edit README.md --- packages/midi/README.md | 134 ++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/packages/midi/README.md b/packages/midi/README.md index faf9993f7..cefe40ab3 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -26,11 +26,11 @@ OUTPUT: - `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127) - `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127) - `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127) -- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127) -- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127) -- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices. - `midibend` - Sets MIDI pitch bend (-1 - 1) - `miditouch` - Sets MIDI key after touch (0-1) +- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices. +- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127) +- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127) INPUT: @@ -56,6 +56,70 @@ In the console, you will see a log of the available MIDI devices as soon as you Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. +### control, ccn && ccv + +`control` sends MIDI control change messages to your MIDI device. + +- `ccn` sets the cc number. Depends on your synths midi mapping +- `ccv` sets the cc value. normalized from 0 to 1. + +```javascript +$: note("c a f e").control([74, sine.slow(4)]).midi() +$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi() +``` + +In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern. +Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default. +But you can also control cc messages separately like this: + +```javascript +$: note("c a f e").midi() +$: ccv(sine.segment(16).slow(4)).ccn(74).midi() +``` + +### progNum (Program Change) + +`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. +Program change values should be numbers between 0 and 127. + +```javascript +// Play notes while changing programs +note("c3 e3 g3").progNum("<0 1 2>").midi() +``` + +Program change messages are useful for switching between different instrument sounds or presets during a performance. +The exact sound that each program number maps to depends on your MIDI device's configuration. + +## sysex, sysexid && sysexdata (System Exclusive Message) + +`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. +sysEx messages are device-specific commands that allow deeper control over synthesizer parameters. +The value should be an array of numbers between 0-255 representing the SysEx data bytes. + +```javascript +// Send a simple SysEx message +let id = 0x43; //Yamaha +//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers +let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" +$: note("c d e f e d c").sysex(id, data).midi(); +$: note("c d e f e d c").sysexid(id).sysexdata(data).midi(); +``` + +The exact format of SysEx messages depends on your MIDI device's specification. +Consult your device's MIDI implementation guide for details on supported SysEx messages. + +### midibend && miditouch + +`midibend` sets MIDI pitch bend (-1 - 1) +`miditouch` sets MIDI key after touch (0-1) + +```javascript + +$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi(); +$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi(); + +``` + ### midicmd `midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices. @@ -90,68 +154,4 @@ stack( // "sysex:[sysexid]:[sysexdata]" midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver') ) -``` - -### control, ccn && ccv - -`control` sends MIDI control change messages to your MIDI device. - -- `ccn` sets the cc number. Depends on your synths midi mapping -- `ccv` sets the cc value. normalized from 0 to 1. - -```javascript -$: note("c a f e").control([74, sine.slow(4)]).midi() -$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi() -``` - -In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern. -Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default. -But you can also control cc messages separately like this: - -```javascript -$: note("c a f e").midi() -$: ccv(sine.segment(16).slow(4)).ccn(74).midi() -``` - -### progNum (Program Change) - -`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device. -Program change values should be numbers between 0 and 127. - -```javascript -// Play notes while changing programs -note("c3 e3 g3").pc("<0 1 2>").midi() -``` - -Program change messages are useful for switching between different instrument sounds or presets during a performance. -The exact sound that each program number maps to depends on your MIDI device's configuration. - -## sysex, sysexid && sysexdata (System Exclusive Message) - -`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. -sysEx messages are device-specific commands that allow deeper control over synthesizer parameters. -The value should be an array of numbers between 0-255 representing the SysEx data bytes. - -```javascript -// Send a simple SysEx message -let id = 0x43; //Yamaha -//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers -let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" -$: note("c d e f e d c").sysex(id, data).midi(); -$: note("c d e f e d c").sysexid(id).sysexdata(data).midi(); -``` - -The exact format of SysEx messages depends on your MIDI device's specification. -Consult your device's MIDI implementation guide for details on supported SysEx messages. - -### midibend && miditouch - -`midibend` sets MIDI pitch bend (-1 - 1) -`miditouch` sets MIDI key after touch (0-1) - -```javascript - -$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi(); -$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi(); - ``` \ No newline at end of file From 5ff1d35272f3f021a60fd5edcd852f24fb40116f Mon Sep 17 00:00:00 2001 From: nkymut Date: Fri, 7 Feb 2025 00:08:31 +0800 Subject: [PATCH 31/88] 'miditouch' change sendKeyAfterTouch to sendChannelAfterTouch --- packages/midi/midi.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 2c46d61c9..b2b96e305 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -275,7 +275,7 @@ Pattern.prototype.midi = function (output) { // Handle miditouch if (miditouch !== undefined) { if (typeof miditouch == 'number' || miditouch < 1 || miditouch > 0) { - device.sendKeyAfterTouch(miditouch, midichan, { time: timeOffsetString }); + device.sendChannelAfterTouch(miditouch, midichan, { time: timeOffsetString }); } else { throw new Error('expected miditouch to be a number between 1 and 0'); } From 451cdcc3a900d822832713971ff081d09810405a Mon Sep 17 00:00:00 2001 From: nkymut Date: Fri, 7 Feb 2025 04:39:24 +0800 Subject: [PATCH 32/88] fix sendChannelAfterTouch -> sendChannelAftertouch --- packages/midi/midi.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index b2b96e305..24b7e3821 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -275,7 +275,7 @@ Pattern.prototype.midi = function (output) { // Handle miditouch if (miditouch !== undefined) { if (typeof miditouch == 'number' || miditouch < 1 || miditouch > 0) { - device.sendChannelAfterTouch(miditouch, midichan, { time: timeOffsetString }); + device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); } else { throw new Error('expected miditouch to be a number between 1 and 0'); } From f802f18d68f761943f7baca5013b1da3bddaeb87 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sun, 9 Feb 2025 10:56:51 +0800 Subject: [PATCH 33/88] refactor(midi): extract MIDI message handlers into dedicated functions - Move MIDI API logic into separate functions following #1274 (sendCC, sendProgramChange, sendPitchBend, etc.) --- packages/core/controls.mjs | 2 - packages/midi/midi.mjs | 261 +++++----------------- test/__snapshots__/examples.test.mjs.snap | 16 +- website/src/pages/learn/input-output.mdx | 1 - 4 files changed, 59 insertions(+), 221 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index ae3a71ca5..b4f65fbfb 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1514,8 +1514,6 @@ export const { binshift } = registerControl('binshift'); export const { hbrick } = registerControl('hbrick'); export const { lbrick } = registerControl('lbrick'); - - export const { frameRate } = registerControl('frameRate'); export const { frames } = registerControl('frames'); export const { hours } = registerControl('hours'); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index a39e8e1da..4606dbb5d 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -200,111 +200,60 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) { device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } -// registry for midi mappings, converting control names to cc messages -export const midicontrolMap = new Map(); - -// takes midimap and converts each control key to the main control name -function unifyMapping(mapping) { - return Object.fromEntries( - Object.entries(mapping).map(([key, mapping]) => { - if (typeof mapping === 'number') { - mapping = { ccn: mapping }; - } - return [getControlName(key), mapping]; - }), - ); -} - -function githubPath(base, subpath = '') { - if (!base.startsWith('github:')) { - throw new Error('expected "github:" at the start of pseudoUrl'); +// sends a program change message to the given device on the given channel +function sendProgramChange(progNum, device, midichan, timeOffsetString) { + if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { + throw new Error('expected progNum (program change) to be a number between 0 and 127'); } - let [_, path] = base.split('github:'); - path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; - } - return `https://raw.githubusercontent.com/${path}/${subpath}`; + device.sendProgramChange(progNum, midichan, { time: timeOffsetString }); } -/** - * configures the default midimap, which is used when no "midimap" port is set - * @example - * defaultmidimap({ lpf: 74 }) - * $: note("c a f e").midi(); - * $: lpf(sine.slow(4).segment(16)).midi(); - */ -export function defaultmidimap(mapping) { - midicontrolMap.set('default', unifyMapping(mapping)); -} - -let loadCache = {}; - -/** - * Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers. - * @example - * midimaps({ mymap: { lpf: 74 } }) - * $: note("c a f e") - * .lpf(sine.slow(4)) - * .midimap('mymap') - * .midi() - * @example - * midimaps({ mymap: { - * lpf: { ccn: 74, min: 0, max: 20000, exp: 0.5 } - * }}) - * $: note("c a f e") - * .lpf(sine.slow(2).range(400,2000)) - * .midimap('mymap') - * .midi() - */ -export async function midimaps(map) { - if (typeof map === 'string') { - if (map.startsWith('github:')) { - map = githubPath(map, 'midimap.json'); +// sends a sysex message to the given device on the given channel +function sendSysex(sysexid, sysexdata, device, timeOffsetString) { + 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'); } - if (!loadCache[map]) { - loadCache[map] = fetch(map).then((res) => res.json()); + } else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) { + throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); + } + + if (!Array.isArray(sysexdata)) { + throw new Error('expected sysex to be an array of numbers (0-255)'); + } + 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 }); +} + +// sends a NRPN message to the given device on the given channel +function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) { + 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'); } - map = await loadCache[map]; - } - if (typeof map === 'object') { - Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping))); + } 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 }); } -// registry for midi sounds, converting sound names to controls -export const midisoundMap = new Map(); - -// normalizes the given value from the given range and exponent -function normalize(value = 0, min = 0, max = 1, exp = 1) { - if (min === max) { - throw new Error('min and max cannot be the same value'); +// sends a pitch bend message to the given device on the given channel +function sendPitchBend(midibend, device, midichan, timeOffsetString) { + if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) { + throw new Error('expected midibend to be a number between -1 and 1'); } - let normalized = (value - min) / (max - min); - normalized = Math.min(1, Math.max(0, normalized)); - return Math.pow(normalized, exp); -} -function mapCC(mapping, value) { - return Object.keys(value) - .filter((key) => !!mapping[getControlName(key)]) - .map((key) => { - const { ccn, min = 0, max = 1, exp = 1 } = mapping[key]; - const ccv = normalize(value[key], min, max, exp); - return { ccn, ccv }; - }); + device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); } -// sends a cc message to the given device on the given channel -function sendCC(ccn, ccv, device, midichan, timeOffsetString) { - if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { - throw new Error('expected ccv to be a number between 0 and 1'); +// sends a channel aftertouch message to the given device on the given channel +function sendAftertouch(miditouch, device, midichan, timeOffsetString) { + if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) { + throw new Error('expected miditouch to be a number between 0 and 1'); } - if (!['string', 'number'].includes(typeof ccn)) { - 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 }); + device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); } /** @@ -404,43 +353,11 @@ Pattern.prototype.midi = function (output) { }); } - // Handle mapped parameters if mapping exists - if (mapping) { - Object.entries(mapping).forEach(([name, paramSpec]) => { - if (name in hap.value) { - const value = hap.value[name]; - - if (paramSpec.cc) { - if (typeof value !== 'number') { - throw new Error(`Expected ${name} to be a number for CC mapping`); - } - // ccnLsb will only exist if this is a high-resolution CC message - const [ccnMsb, ccnLsb] = Array.isArray(paramSpec.cc) ? paramSpec.cc : [paramSpec.cc]; - - const ccvMsb = ccnLsb === undefined ? Math.round(value * 127) : Math.round(value * 16383) >> 7; - device.sendControlChange(ccnMsb, ccvMsb, paramSpec.channel || midichan, { time: timeOffsetString }); - - if (ccnLsb !== undefined) { - const ccvLsb = Math.round(value * 16383) & 0b1111111; - device.sendControlChange(ccnLsb, ccvLsb, paramSpec.channel || midichan, { time: timeOffsetString }); - } - } else if (paramSpec.progNum !== undefined) { - if (typeof value !== 'number' || value < 0 || value > 127) { - throw new Error(`Expected ${name} to be a number between 0 and 127 for program change`); - } - device.sendProgramChange(value, paramSpec.channel || midichan, { time: timeOffsetString }); - } - } - }); - } - // Handle program change if (progNum !== undefined) { - if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { - throw new Error('expected pc (program change) to be a number between 0 and 127'); - } - device.sendProgramChange(progNum, midichan, { time: timeOffsetString }); + sendProgramChange(progNum, device, midichan, timeOffsetString); } + // Handle sysex // sysex data is consist of 2 arrays, first is sysexid, second is sysexdata // sysexid is a manufacturer id it is either a number or an array of 3 numbers. @@ -448,23 +365,7 @@ Pattern.prototype.midi = function (output) { // if sysexid is an array the first byte is 0x00 if (sysexid !== undefined && sysexdata !== undefined) { - 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'); - } - } else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) { - throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); - } - - if (!Array.isArray(sysexdata)) { - throw new Error('expected sysex to be an array of numbers (0-255)'); - } - 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 }); - //device.sendSysex(0x43, [0x79, 0x09, 0x11, 0x0A, 0x00,0x1e], { time: timeOffsetString }); + sendSysex(sysexid, sysexdata, device, timeOffsetString); } // Handle control change @@ -474,57 +375,17 @@ Pattern.prototype.midi = function (output) { // Handle NRPN non-registered parameter number if (nrpnn !== undefined && nrpv !== undefined) { - 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'); - } - } 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 }); + sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString); } // Handle midibend if (midibend !== undefined) { - if (typeof midibend == 'number' || midibend < 1 || midibend > -1) { - device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); - } else { - throw new Error('expected midibend to be a number between 1 and -1'); - } - const scaled = Math.round(ccv * 127); - device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); - } - - // Handle NRPN non-registered parameter number - if (nrpnn !== undefined && nrpv !== undefined) { - 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'); - } - } 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 }); - } - - // Handle midibend - if (midibend !== undefined) { - if (typeof midibend == 'number' || midibend < 1 || midibend > -1) { - device.sendPitchBend(midibend, midichan, { time: timeOffsetString }); - } else { - throw new Error('expected midibend to be a number between 1 and -1'); - } + sendPitchBend(midibend, device, midichan, timeOffsetString); } // Handle miditouch if (miditouch !== undefined) { - if (typeof miditouch == 'number' || miditouch < 1 || miditouch > 0) { - device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); - } else { - throw new Error('expected miditouch to be a number between 1 and 0'); - } + sendAftertouch(miditouch, device, midichan, timeOffsetString); } // Handle midicmd @@ -542,35 +403,15 @@ Pattern.prototype.midi = function (output) { device.sendContinue({ time: timeOffsetString }); } else if (Array.isArray(midicmd)) { if (midicmd[0] === 'progNum') { - if (typeof midicmd[1] !== 'number' || midicmd[1] < 0 || midicmd[1] > 127) { - throw new Error('expected pc (program change) to be a number between 0 and 127'); - } else { - device.sendProgramChange(midicmd[1], midichan, { time: timeOffsetString }); - } + sendProgramChange(midicmd[1], device, midichan, timeOffsetString); } else if (midicmd[0] === 'cc') { if (midicmd.length === 2) { - if (typeof midicmd[0] !== 'number' || midicmd[0] < 0 || midicmd[0] > 127) { - throw new Error('expected ccn (control change number) to be a number between 0 and 127'); - } - if (typeof midicmd[1] !== 'number' || midicmd[1] < 0 || midicmd[1] > 127) { - throw new Error('expected ccv (control change value) to be a number between 0 and 127'); - } - device.sendControlChange(midicmd[0], midicmd[1], midichan, { time: timeOffsetString }); + sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString); } } else if (midicmd[0] === 'sysex') { if (midicmd.length === 3) { const [_, id, data] = midicmd; - if (Array.isArray(id)) { - if (!id.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { - throw new Error('all sysex id bytes must be integers between 0 and 255'); - } - } else if (!Number.isInteger(id) || id < 0 || id > 255) { - throw new Error('sysex id must be a number between 0 and 255 or an array of such integers'); - } - if (!Array.isArray(data) || !data.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { - throw new Error('sysex data must be an array of integers between 0 and 255'); - } - device.sendSysex(id, data, { time: timeOffsetString }); + sendSysex(id, data, device, timeOffsetString); } } } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 9eade34ef..b395ed273 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5063,10 +5063,10 @@ exports[`runs examples > example "midi" example index 0 1`] = ` exports[`runs examples > example "midibend" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | note:c4 midibend:0.282842712474619 ]", - "[ 1/1 → 2/1 | note:c4 midibend:0.282842712474619 ]", - "[ 2/1 → 3/1 | note:c4 midibend:-0.282842712474619 ]", - "[ 3/1 → 4/1 | note:c4 midibend:-0.2828427124746191 ]", + "[ 0/1 → 1/1 | note:c4 midibend:0 ]", + "[ 1/1 → 2/1 | note:c4 midibend:0.4 ]", + "[ 2/1 → 3/1 | note:c4 midibend:1.1102230246251565e-16 ]", + "[ 3/1 → 4/1 | note:c4 midibend:-0.4 ]", ] `; @@ -5301,10 +5301,10 @@ exports[`runs examples > example "midin" example index 0 1`] = ` exports[`runs examples > example "miditouch" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | note:c4 miditouch:0.8535533905932737 ]", - "[ 1/1 → 2/1 | note:c4 miditouch:0.8535533905932737 ]", - "[ 2/1 → 3/1 | note:c4 miditouch:0.14644660940672627 ]", - "[ 3/1 → 4/1 | note:c4 miditouch:0.14644660940672616 ]", + "[ 0/1 → 1/1 | note:c4 miditouch:0.5 ]", + "[ 1/1 → 2/1 | note:c4 miditouch:1 ]", + "[ 2/1 → 3/1 | note:c4 miditouch:0.5000000000000001 ]", + "[ 3/1 → 4/1 | note:c4 miditouch:0 ]", ] `; diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index e0118f611..15d31f2a5 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -122,7 +122,6 @@ Consult your device's MIDI implementation guide for details on supported SysEx m - # OSC/SuperDirt/StrudelDirt In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software. From b98ebc696d7d391aeab6fade3e63fcf6a096b764 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sun, 9 Feb 2025 12:19:30 +0800 Subject: [PATCH 34/88] remove midimap JSDoc --- packages/core/controls.mjs | 7 +------ packages/midi/midi.mjs | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b4f65fbfb..0c89ff6c8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1622,12 +1622,7 @@ export const ar = register('ar', (t, pat) => { */ export const { midichan } = registerControl('midichan'); -/** - * MIDI map: Sets the MIDI map for the event. - * - * @name midimap - * @param {Object} map MIDI map - */ + export const { midimap } = registerControl('midimap'); /** diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 4606dbb5d..9ce933969 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -178,6 +178,7 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) { normalized = Math.min(1, Math.max(0, normalized)); return Math.pow(normalized, exp); } + function mapCC(mapping, value) { return Object.keys(value) .filter((key) => !!mapping[getControlName(key)]) From 43523b73c4014c3ef8b0312f5899d08272a2c552 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sun, 9 Feb 2025 15:12:14 +0800 Subject: [PATCH 35/88] codeformat --- packages/core/controls.mjs | 1 - packages/midi/midi.mjs | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 0c89ff6c8..967c09044 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1622,7 +1622,6 @@ export const ar = register('ar', (t, pat) => { */ export const { midichan } = registerControl('midichan'); - export const { midimap } = registerControl('midimap'); /** diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 9ce933969..8de6c447f 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -180,6 +180,8 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) { } function mapCC(mapping, value) { + console.log('mapping', mapping); + console.log('value', value); return Object.keys(value) .filter((key) => !!mapping[getControlName(key)]) .map((key) => { @@ -340,6 +342,7 @@ Pattern.prototype.midi = function (output) { velocity = gain * velocity; // if midimap is set, send a cc messages from defined controls if (midicontrolMap.has(midimap)) { + console.log('midimap', midimap); const ccs = mapCC(midicontrolMap.get(midimap), hap.value); ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString)); } From c639e44ca7a65aeaa77599d37747d097fd5c82c3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Feb 2025 23:07:45 +0100 Subject: [PATCH 36/88] fix: dont bail when ip cannot be determined (happens when no wifi) --- packages/sampler/sample-server.mjs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 12520992d..7039a8ce1 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -99,12 +99,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 +107,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.`} `); }); From b016720696db3f62f30da35107d346c07f7a3e56 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 20 Feb 2025 23:37:59 +0100 Subject: [PATCH 37/88] sampler: support serving any file (like strudel.json) --- packages/sampler/sample-server.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 7039a8ce1..d1e56108f 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; From 3c2692bdda372915fd804f7332ec4ef272a63f78 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sun, 9 Mar 2025 06:06:59 +0800 Subject: [PATCH 38/88] remove debugging artifacts --- packages/midi/midi.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 8de6c447f..4f518e0d4 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -180,8 +180,6 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) { } function mapCC(mapping, value) { - console.log('mapping', mapping); - console.log('value', value); return Object.keys(value) .filter((key) => !!mapping[getControlName(key)]) .map((key) => { @@ -301,7 +299,7 @@ Pattern.prototype.midi = function (output) { return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { if (!WebMidi.enabled) { - console.log('not enabled'); + logger('Midi not enabled'); return; } hap.ensureObjectValue(); @@ -342,7 +340,6 @@ Pattern.prototype.midi = function (output) { velocity = gain * velocity; // if midimap is set, send a cc messages from defined controls if (midicontrolMap.has(midimap)) { - console.log('midimap', midimap); const ccs = mapCC(midicontrolMap.get(midimap), hap.value); ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString)); } From 2ccb95aec05d9ce32092dcb72e7814fe4d9abb33 Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 11 Mar 2025 23:29:37 +0800 Subject: [PATCH 39/88] Refactor: Consolidate configuration variables into midiConfig object - add options argument to .midi - add midiConfig object with properties --- packages/midi/midi.mjs | 105 ++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 4f518e0d4..45f93d1dc 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -257,35 +257,71 @@ function sendAftertouch(miditouch, device, midichan, timeOffsetString) { device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); } +// sends a note message to the given device on the given channel +function sendNote(note, velocity, duration, device, midichan, timeOffsetString) { + if (note == null || note === '') { + throw new Error('note cannot be null or empty'); + } + if (velocity != null && (typeof velocity !== 'number' || velocity < 0 || velocity > 1)) { + throw new Error('velocity must be a number between 0 and 1'); + } + 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, + }); +} + /** * MIDI output: Opens a MIDI output port. - * @param {string | number} output MIDI device name or index defaulting to 0 + * @param {string | number} midiport MIDI device name or index defaulting to 0 + * @param {object} options Additional MIDI configuration options * @example - * note("c4").midichan(1).midi("IAC Driver Bus 1") + * note("c4").midichan(1).midi('IAC Driver Bus 1') + * @example + * note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 }) */ -Pattern.prototype.midi = function (output) { - if (isPattern(output)) { + +Pattern.prototype.midi = function (midiport, options = {}) { + if (isPattern(midiport)) { throw new Error( - `.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${ + `.midi does not accept Pattern input for midiport. Make sure to pass device name with single quotes. Example: .midi('${ WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1' }')`, ); } - let portName = output; - let isController = false; - let mapping = {}; - //TODO: MIDI mapping related - if (typeof output === 'object') { - const { port, controller = false, ...remainingProps } = output; - portName = port; - isController = controller; - mapping = remainingProps; + // For backward compatibility + if (typeof midiport === 'object') { + const { port, isController = false, ...configOptions } = midiport; + options = { + isController, + ...configOptions, + ...options, // Keep any options passed separately + }; + midiport = port; } + 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 + gain: 1, // Default gain + midimap: 'default', // Default MIDI map + midiport: midiport, // Store the port in the config + ...options, // Override defaults with provided options + }; + enableWebMidi({ onEnabled: ({ outputs }) => { - const device = getDevice(portName, outputs); + const device = getDevice(midiConfig.midiport, outputs); const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( `Midi enabled! Using "${device.name}". ${ @@ -303,30 +339,31 @@ Pattern.prototype.midi = function (output) { return; } hap.ensureObjectValue(); + //magic number to get audio engine to line up, can probably be calculated somehow - const latencyMs = 34; + 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}`; - // destructure value + // midi event values from hap with configurable defaults let { note, nrpnn, nrpv, ccn, ccv, - midichan = 1, + midichan = midiConfig.midichannel, midicmd, midibend, miditouch, - polyTouch, //?? - gain = 1, - velocity = 0.9, + polyTouch, + gain = midiConfig.gain, + velocity = midiConfig.velocity, progNum, sysexid, sysexdata, - midimap = 'default', - midiport = output, + midimap = midiConfig.midimap, + midiport = midiConfig.midiport, } = hap.value; const device = getDevice(midiport, WebMidi.outputs); @@ -338,20 +375,24 @@ Pattern.prototype.midi = function (output) { } velocity = gain * velocity; + + // Handle midimap // 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)); + } else if (midimap !== 'default') { + // Add warning when a non-existent midimap is specified + logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`); } - // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length - const duration = (hap.duration.valueOf() / cps) * 1000 - 10; - if (note != null && !isController) { - const midiNumber = typeof note === 'number' ? note : noteToMidi(note); - const midiNote = new Note(midiNumber, { attack: velocity, duration }); - device.playNote(midiNote, midichan, { - time: timeOffsetString, - }); + // Handle note + if (note !== undefined && !midiConfig.isController) { + // note off messages will often a few ms arrive late, + // 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); } // Handle program change @@ -427,7 +468,7 @@ const refs = {}; * @param {string | number} input MIDI device name or index defaulting to 0 * @returns {Function} * @example - * let cc = await midin("IAC Driver Bus 1") + * let cc = await midin('IAC Driver Bus 1') * note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth") */ export async function midin(input) { From 17df418935a4e28bba4e987ad2811373ab7d5530 Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 11 Mar 2025 23:30:21 +0800 Subject: [PATCH 40/88] Update Documentation --- packages/core/controls.mjs | 2 + packages/midi/README.md | 38 +++++++++++++- test/__snapshots__/examples.test.mjs.snap | 30 +++++++++++ website/src/pages/learn/input-output.mdx | 63 ++++++++++++++++++++--- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 967c09044..82330bb85 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1629,6 +1629,8 @@ export const { midimap } = registerControl('midimap'); * * @name midiport * @param {number | Pattern} port MIDI port + * @example + * note("c a f e").midiport("<0 1 2 3>").midi() */ export const { midiport } = registerControl('midiport'); diff --git a/packages/midi/README.md b/packages/midi/README.md index cefe40ab3..aa90992da 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -41,7 +41,7 @@ Additional controls can be mapped using the mapping object passed to `.midi()`: ## Examples -### midi(outputName?) +### midi(outputName?, options?) Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. If no outputName is given, it uses the first midi output it finds. @@ -52,6 +52,42 @@ $: chord("").voicing().midi('IAC Driver') In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` +### Options + +The `.midi()` function accepts an options object with the following properties: + +```javascript +$: note("c a f e").midi('IAC Driver', { isController: true, midimap: 'default'}) +``` + +
+Available Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers | +| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine | +| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching | +| midichannel | number | 1 | Default MIDI channel (1-16) | +| velocity | number | 0.9 | Default note velocity (0-1) | +| gain | number | 1 | Default gain multiplier for velocity (0-1) | +| midimap | string | 'default' | Name of MIDI mapping to use for control changes | +| midiport | string/number | - | MIDI device name or index | + +
+ + + + +### midiport(outputName) + +Selects the MIDI output device to use, pattern can be used to switch between devices. + +```javascript +$: midiport('IAC Driver') +$: note("c a f e").midiport("<0 1 2 3>").midi() +``` + ### midichan(number) Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 2920a7a40..ebd1c209d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5215,6 +5215,15 @@ exports[`runs examples > example "midi" example index 0 1`] = ` ] `; +exports[`runs examples > example "midi" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 midichan:1 ]", + "[ 1/1 → 2/1 | note:c4 midichan:1 ]", + "[ 2/1 → 3/1 | note:c4 midichan:1 ]", + "[ 3/1 → 4/1 | note:c4 midichan:1 ]", +] +`; + exports[`runs examples > example "midibend" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 midibend:0 ]", @@ -5453,6 +5462,27 @@ exports[`runs examples > example "midin" example index 0 1`] = ` ] `; +exports[`runs examples > example "midiport" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c midiport:0 ]", + "[ 1/4 → 1/2 | note:a midiport:0 ]", + "[ 1/2 → 3/4 | note:f midiport:0 ]", + "[ 3/4 → 1/1 | note:e midiport:0 ]", + "[ 1/1 → 5/4 | note:c midiport:1 ]", + "[ 5/4 → 3/2 | note:a midiport:1 ]", + "[ 3/2 → 7/4 | note:f midiport:1 ]", + "[ 7/4 → 2/1 | note:e midiport:1 ]", + "[ 2/1 → 9/4 | note:c midiport:2 ]", + "[ 9/4 → 5/2 | note:a midiport:2 ]", + "[ 5/2 → 11/4 | note:f midiport:2 ]", + "[ 11/4 → 3/1 | note:e midiport:2 ]", + "[ 3/1 → 13/4 | note:c midiport:3 ]", + "[ 13/4 → 7/2 | note:a midiport:3 ]", + "[ 7/2 → 15/4 | note:f midiport:3 ]", + "[ 15/4 → 4/1 | note:e midiport:3 ]", +] +`; + exports[`runs examples > example "miditouch" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 miditouch:0.5 ]", diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 15d31f2a5..92379aa6f 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -16,14 +16,63 @@ It is also possible to pattern other things with Strudel, such as software and h Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern: -## midi(outputName?) +## midiin(inputName?) + + + +## midi(outputName?,options?) Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. If no outputName is given, it uses the first midi output it finds. -").voicing().midi()`} /> +").voicing().midi('IAC Driver') +`} +/> -In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` +In the console, you will see a log of the available MIDI devices as soon as you run the code, +e.g. + +``` + `Midi connected! Using "Midi Through Port-0".` +``` + +The `.midi()` function accepts an options object with the following properties: + + + +
+Available Options + +| Option | Type | Default | Description | +| ------------ | ------------- | --------- | ---------------------------------------------------------------------- | +| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers | +| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine | +| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching | +| midichannel | number | 1 | Default MIDI channel (1-16) | +| velocity | number | 0.9 | Default note velocity (0-1) | +| gain | number | 1 | Default gain multiplier for velocity (0-1) | +| midimap | string | 'default' | Name of MIDI mapping to use for control changes | +| midiport | string/number | - | MIDI device name or index | + +
+ +### midiport(outputName) + +Selects the MIDI output device to use, pattern can be used to switch between devices. + +```javascript +$: midiport('IAC Driver'); +$: note('c a f e').midiport('<0 1 2 3>').midi(); +``` + + ## midichan(number) @@ -106,8 +155,8 @@ The value should be an array of numbers between 0-255 representing the SysEx dat let id = 0x43; //Yamaha //let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa" -$: note("c d e f e d c").sysex(id, data).midi(); -$: note("c d e f e d c").sysexid(id).sysexdata(data).midi();`} +$: note("c a f e").sysex(id, data).midi(); +$: note("c a f e").sysexid(id).sysexdata(data).midi();`} /> The exact format of SysEx messages depends on your MIDI device's specification. @@ -118,9 +167,9 @@ Consult your device's MIDI implementation guide for details on supported SysEx m `midibend` sets MIDI pitch bend (-1 - 1) `miditouch` sets MIDI key after touch (0-1) - + - + # OSC/SuperDirt/StrudelDirt From 5fcb96f73c5d19c5b7ad121024e9e91154aa5d4f Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 11 Mar 2025 23:33:45 +0800 Subject: [PATCH 41/88] codeformat --- packages/midi/midi.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 45f93d1dc..ce7cdb0e2 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -309,7 +309,7 @@ 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 + 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 From b27c9dbf372e3354354a3828e0469746c6c7bffe Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Mon, 17 Mar 2025 09:38:54 +0000 Subject: [PATCH 42/88] add num samples (edited numbers) (#1309) --- website/src/repl/prebake.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 8ecfef3ac..da857d2d3 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -115,6 +115,17 @@ export async function prebake() { 'numbers/7.wav', 'numbers/8.wav', ], + num: [ + 'num/0.wav', + 'num/1.wav', + 'num/2.wav', + 'num/3.wav', + 'num/4.wav', + 'num/5.wav', + 'num/6.wav', + 'num/7.wav', + 'num/8.wav', + ], }, 'github:tidalcycles/dirt-samples', { From f5bb88a696edf39a2f0d548ea83190edc746c8ba Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Tue, 18 Mar 2025 19:07:08 +0000 Subject: [PATCH 43/88] Add num samples from 0 up to 20 (#1310) * re-edited numbers * num samples up to 20 * add freesound license --- website/src/repl/prebake.mjs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index da857d2d3..68befdd96 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -116,15 +116,27 @@ export async function prebake() { 'numbers/8.wav', ], num: [ - 'num/0.wav', - 'num/1.wav', - 'num/2.wav', - 'num/3.wav', - 'num/4.wav', - 'num/5.wav', - 'num/6.wav', - 'num/7.wav', - 'num/8.wav', + 'num/00.wav', + 'num/01.wav', + 'num/02.wav', + 'num/03.wav', + 'num/04.wav', + 'num/05.wav', + 'num/06.wav', + 'num/07.wav', + 'num/08.wav', + 'num/09.wav', + 'num/10.wav', + 'num/11.wav', + 'num/12.wav', + 'num/13.wav', + 'num/14.wav', + 'num/15.wav', + 'num/16.wav', + 'num/17.wav', + 'num/18.wav', + 'num/19.wav', + 'num/20.wav', ], }, 'github:tidalcycles/dirt-samples', From 38876e135b8e8f0fa1d47803ca6c68acc1445cf5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Mar 2025 11:54:10 -0400 Subject: [PATCH 44/88] fixed --- packages/core/controls.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1f2d3e703..b404ccb1e 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1632,11 +1632,14 @@ export const getControlName = (alias) => { * Sets properties in a batch. * * @name as - * @param {Array} mapping the control names that are set + * @param {String | Array} mapping the control names that are set * @example * "c:.5 a:1 f:.25 e:.8".as("note:clip") + * @example + * "{0@2 0.25 0 0.5 .3 .5}%8".as("begin").s("sax_vib").clip(1) */ export const as = register('as', (mapping, pat) => { + mapping = Array.isArray(mapping) ? mapping : [mapping] return pat.fmap((v) => { v = Array.isArray(v) ? v : [v]; v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]])); From dccfefacd76ea91a9621d0d242637de214175262 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Mar 2025 12:06:43 -0400 Subject: [PATCH 45/88] test --- packages/core/controls.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 33 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b404ccb1e..e0256fa51 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1639,7 +1639,7 @@ export const getControlName = (alias) => { * "{0@2 0.25 0 0.5 .3 .5}%8".as("begin").s("sax_vib").clip(1) */ export const as = register('as', (mapping, pat) => { - mapping = Array.isArray(mapping) ? mapping : [mapping] + mapping = Array.isArray(mapping) ? mapping : [mapping]; return pat.fmap((v) => { v = Array.isArray(v) ? v : [v]; v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]])); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 96a11d49b..ce9712e97 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -933,6 +933,39 @@ exports[`runs examples > example "as" example index 0 1`] = ` ] `; +exports[`runs examples > example "as" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | begin:0 s:sax_vib clip:1 ]", + "[ 1/4 → 3/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 3/8 → 1/2 | begin:0 s:sax_vib clip:1 ]", + "[ 1/2 → 5/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 5/8 → 3/4 | begin:0.3 s:sax_vib clip:1 ]", + "[ 3/4 → 7/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ (7/8 → 1/1) ⇝ 9/8 | begin:0 s:sax_vib clip:1 ]", + "[ 7/8 ⇜ (1/1 → 9/8) | begin:0 s:sax_vib clip:1 ]", + "[ 9/8 → 5/4 | begin:0.25 s:sax_vib clip:1 ]", + "[ 5/4 → 11/8 | begin:0 s:sax_vib clip:1 ]", + "[ 11/8 → 3/2 | begin:0.5 s:sax_vib clip:1 ]", + "[ 3/2 → 13/8 | begin:0.3 s:sax_vib clip:1 ]", + "[ 13/8 → 7/4 | begin:0.5 s:sax_vib clip:1 ]", + "[ 7/4 → 2/1 | begin:0 s:sax_vib clip:1 ]", + "[ 2/1 → 17/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 17/8 → 9/4 | begin:0 s:sax_vib clip:1 ]", + "[ 9/4 → 19/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 19/8 → 5/2 | begin:0.3 s:sax_vib clip:1 ]", + "[ 5/2 → 21/8 | begin:0.5 s:sax_vib clip:1 ]", + "[ 21/8 → 23/8 | begin:0 s:sax_vib clip:1 ]", + "[ 23/8 → 3/1 | begin:0.25 s:sax_vib clip:1 ]", + "[ 3/1 → 25/8 | begin:0 s:sax_vib clip:1 ]", + "[ 25/8 → 13/4 | begin:0.5 s:sax_vib clip:1 ]", + "[ 13/4 → 27/8 | begin:0.3 s:sax_vib clip:1 ]", + "[ 27/8 → 7/2 | begin:0.5 s:sax_vib clip:1 ]", + "[ 7/2 → 15/4 | begin:0 s:sax_vib clip:1 ]", + "[ 15/4 → 31/8 | begin:0.25 s:sax_vib clip:1 ]", + "[ 31/8 → 4/1 | begin:0 s:sax_vib clip:1 ]", +] +`; + exports[`runs examples > example "attack" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 attack:0 ]", From cbd448e9347f2e30c3060b277882ec122a17086f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 00:09:46 -0400 Subject: [PATCH 46/88] update docs --- packages/core/pattern.mjs | 16 +++++++++++----- packages/core/signal.mjs | 3 +++ .../src/repl/components/panel/SettingsTab.jsx | 18 ------------------ 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c1e95211d..4adb9bd61 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2106,10 +2106,11 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. * @param {number} segments number of segments per cycle + * @synonyms seg * @example * note(saw.range(40,52).segment(24)) */ -export const segment = register('segment', function (rate, pat) { +export const {segment, seg} = register(['segment', 'seg'], function (rate, pat) { return pat.struct(pure(true)._fast(rate)).setSteps(rate); }); @@ -2485,16 +2486,21 @@ export const bypass = register( ); /** - * Loops the pattern inside at `offset` for `cycles`. + * Loops the pattern inside an `offset` for `cycles`. + * If you think of the entire span of time in cycles as a ribbon, you can grab a single piece and loop it. + * @synonym rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles * @example - * note("").ribbon(1, 2).fast(2) + * note("").ribbon(1, 2) * @example * // Looping a portion of randomness - * note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2) + * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2) + * @example + * // rhythm generator + * s("bd!16?").ribbon(29,.5)._punchcard() */ -export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); +export const {ribbon, rib} = register(['ribbon', 'rib'], (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 5348173d3..f09c49661 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -520,12 +520,15 @@ export const degradeByWith = register( * * @name degradeBy * @memberof Pattern + * @synonym * @param {number} amount - a number between 0 and 1 * @returns Pattern * @example * s("hh*8").degradeBy(0.2) * @example * s("[hh?0.2]*8") + * @example + * s("bd").segment(16).degradeBy(.5).ribbon(16,1)._punchcard() */ export const degradeBy = register( 'degradeBy', diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 4ad3cc9ac..3a44cd0ac 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -179,25 +179,7 @@ export function SettingsTab({ started }) { value={togglePanelTrigger} onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)} items={{ click: 'Click', hover: 'Hover' }} - > - {/* { - if (cbEvent.target.checked) { - settingsMap.setKey('togglePanelTrigger', 'click'); - } - }} - value={togglePanelTrigger != 'hover'} /> - { - if (cbEvent.target.checked) { - settingsMap.setKey('togglePanelTrigger', 'hover'); - } - }} - value={togglePanelTrigger == 'hover'} - /> */} Date: Mon, 24 Mar 2025 00:15:36 -0400 Subject: [PATCH 47/88] update test --- packages/core/pattern.mjs | 8 +- packages/core/signal.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 344 +++++++++++++--------- 3 files changed, 215 insertions(+), 139 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4adb9bd61..49caeaf0e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2110,7 +2110,7 @@ export const linger = register( * @example * note(saw.range(40,52).segment(24)) */ -export const {segment, seg} = register(['segment', 'seg'], function (rate, pat) { +export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) { return pat.struct(pure(true)._fast(rate)).setSteps(rate); }); @@ -2498,9 +2498,11 @@ export const bypass = register( * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2) * @example * // rhythm generator - * s("bd!16?").ribbon(29,.5)._punchcard() + * s("bd!16?").ribbon(29,.5) */ -export const {ribbon, rib} = register(['ribbon', 'rib'], (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles))); +export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) => + pat.early(offset).restart(pure(1).slow(cycles)), +); export const hsla = register('hsla', (h, s, l, a, pat) => { return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index f09c49661..22738ea98 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -528,7 +528,7 @@ export const degradeByWith = register( * @example * s("[hh?0.2]*8") * @example - * s("bd").segment(16).degradeBy(.5).ribbon(16,1)._punchcard() + * s("bd").segment(16).degradeBy(.5).ribbon(16,1) */ export const degradeBy = register( 'degradeBy', diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 71a26c813..80ee7344c 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1,5 +1,181 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`runs examples > example "" example index 0 1`] = ` +[ + "[ 0/1 → 1/24 | note:40 ]", + "[ 1/24 → 1/12 | note:40.5 ]", + "[ 1/12 → 1/8 | note:41 ]", + "[ 1/8 → 1/6 | note:41.5 ]", + "[ 1/6 → 5/24 | note:42 ]", + "[ 5/24 → 1/4 | note:42.5 ]", + "[ 1/4 → 7/24 | note:43 ]", + "[ 7/24 → 1/3 | note:43.5 ]", + "[ 1/3 → 3/8 | note:44 ]", + "[ 3/8 → 5/12 | note:44.5 ]", + "[ 5/12 → 11/24 | note:45 ]", + "[ 11/24 → 1/2 | note:45.5 ]", + "[ 1/2 → 13/24 | note:46 ]", + "[ 13/24 → 7/12 | note:46.5 ]", + "[ 7/12 → 5/8 | note:47 ]", + "[ 5/8 → 2/3 | note:47.5 ]", + "[ 2/3 → 17/24 | note:48 ]", + "[ 17/24 → 3/4 | note:48.5 ]", + "[ 3/4 → 19/24 | note:49 ]", + "[ 19/24 → 5/6 | note:49.5 ]", + "[ 5/6 → 7/8 | note:50 ]", + "[ 7/8 → 11/12 | note:50.5 ]", + "[ 11/12 → 23/24 | note:51 ]", + "[ 23/24 → 1/1 | note:51.5 ]", + "[ 1/1 → 25/24 | note:40 ]", + "[ 25/24 → 13/12 | note:40.5 ]", + "[ 13/12 → 9/8 | note:41 ]", + "[ 9/8 → 7/6 | note:41.5 ]", + "[ 7/6 → 29/24 | note:42 ]", + "[ 29/24 → 5/4 | note:42.5 ]", + "[ 5/4 → 31/24 | note:43 ]", + "[ 31/24 → 4/3 | note:43.5 ]", + "[ 4/3 → 11/8 | note:44 ]", + "[ 11/8 → 17/12 | note:44.5 ]", + "[ 17/12 → 35/24 | note:45 ]", + "[ 35/24 → 3/2 | note:45.5 ]", + "[ 3/2 → 37/24 | note:46 ]", + "[ 37/24 → 19/12 | note:46.5 ]", + "[ 19/12 → 13/8 | note:47 ]", + "[ 13/8 → 5/3 | note:47.5 ]", + "[ 5/3 → 41/24 | note:48 ]", + "[ 41/24 → 7/4 | note:48.5 ]", + "[ 7/4 → 43/24 | note:49 ]", + "[ 43/24 → 11/6 | note:49.5 ]", + "[ 11/6 → 15/8 | note:50 ]", + "[ 15/8 → 23/12 | note:50.5 ]", + "[ 23/12 → 47/24 | note:51 ]", + "[ 47/24 → 2/1 | note:51.5 ]", + "[ 2/1 → 49/24 | note:40 ]", + "[ 49/24 → 25/12 | note:40.5 ]", + "[ 25/12 → 17/8 | note:41 ]", + "[ 17/8 → 13/6 | note:41.5 ]", + "[ 13/6 → 53/24 | note:42 ]", + "[ 53/24 → 9/4 | note:42.5 ]", + "[ 9/4 → 55/24 | note:43 ]", + "[ 55/24 → 7/3 | note:43.5 ]", + "[ 7/3 → 19/8 | note:44 ]", + "[ 19/8 → 29/12 | note:44.5 ]", + "[ 29/12 → 59/24 | note:45 ]", + "[ 59/24 → 5/2 | note:45.5 ]", + "[ 5/2 → 61/24 | note:46 ]", + "[ 61/24 → 31/12 | note:46.5 ]", + "[ 31/12 → 21/8 | note:47 ]", + "[ 21/8 → 8/3 | note:47.5 ]", + "[ 8/3 → 65/24 | note:48 ]", + "[ 65/24 → 11/4 | note:48.5 ]", + "[ 11/4 → 67/24 | note:49 ]", + "[ 67/24 → 17/6 | note:49.5 ]", + "[ 17/6 → 23/8 | note:50 ]", + "[ 23/8 → 35/12 | note:50.5 ]", + "[ 35/12 → 71/24 | note:51 ]", + "[ 71/24 → 3/1 | note:51.5 ]", + "[ 3/1 → 73/24 | note:40 ]", + "[ 73/24 → 37/12 | note:40.5 ]", + "[ 37/12 → 25/8 | note:41 ]", + "[ 25/8 → 19/6 | note:41.5 ]", + "[ 19/6 → 77/24 | note:42 ]", + "[ 77/24 → 13/4 | note:42.5 ]", + "[ 13/4 → 79/24 | note:43 ]", + "[ 79/24 → 10/3 | note:43.5 ]", + "[ 10/3 → 27/8 | note:44 ]", + "[ 27/8 → 41/12 | note:44.5 ]", + "[ 41/12 → 83/24 | note:45 ]", + "[ 83/24 → 7/2 | note:45.5 ]", + "[ 7/2 → 85/24 | note:46 ]", + "[ 85/24 → 43/12 | note:46.5 ]", + "[ 43/12 → 29/8 | note:47 ]", + "[ 29/8 → 11/3 | note:47.5 ]", + "[ 11/3 → 89/24 | note:48 ]", + "[ 89/24 → 15/4 | note:48.5 ]", + "[ 15/4 → 91/24 | note:49 ]", + "[ 91/24 → 23/6 | note:49.5 ]", + "[ 23/6 → 31/8 | note:50 ]", + "[ 31/8 → 47/12 | note:50.5 ]", + "[ 47/12 → 95/24 | note:51 ]", + "[ 95/24 → 4/1 | note:51.5 ]", +] +`; + +exports[`runs examples > example "" example index 0 2`] = ` +[ + "[ 0/1 → 1/1 | note:d ]", + "[ 1/1 → 2/1 | note:e ]", + "[ 2/1 → 3/1 | note:d ]", + "[ 3/1 → 4/1 | note:e ]", +] +`; + +exports[`runs examples > example "" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | note:A3 ]", + "[ 1/4 → 1/2 | note:C3 ]", + "[ 1/2 → 3/4 | note:D3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", + "[ 9/4 → 5/2 | note:C3 ]", + "[ 5/2 → 11/4 | note:D3 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", +] +`; + +exports[`runs examples > example "" example index 2 1`] = ` +[ + "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", +] +`; + exports[`runs examples > example "_euclidRot" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c3 ]", @@ -2322,6 +2498,39 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = ` ] `; +exports[`runs examples > example "degradeBy" example index 2 1`] = ` +[ + "[ 1/8 → 3/16 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 5/16 → 3/8 | s:bd ]", + "[ 1/2 → 9/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 15/16 → 1/1 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 21/16 → 11/8 | s:bd ]", + "[ 3/2 → 25/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 31/16 → 2/1 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 37/16 → 19/8 | s:bd ]", + "[ 5/2 → 41/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 47/16 → 3/1 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 53/16 → 27/8 | s:bd ]", + "[ 7/2 → 57/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 63/16 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "delay" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0 ]", @@ -7121,40 +7330,6 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; -exports[`runs examples > example "ribbon" example index 0 1`] = ` -[ - "[ 0/1 → 1/2 | note:d ]", - "[ 1/2 → 1/1 | note:e ]", - "[ 1/1 → 3/2 | note:d ]", - "[ 3/2 → 2/1 | note:e ]", - "[ 2/1 → 5/2 | note:d ]", - "[ 5/2 → 3/1 | note:e ]", - "[ 3/1 → 7/2 | note:d ]", - "[ 7/2 → 4/1 | note:e ]", -] -`; - -exports[`runs examples > example "ribbon" example index 1 1`] = ` -[ - "[ 0/1 → 1/4 | note:G3 ]", - "[ 1/4 → 1/2 | note:C3 ]", - "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:F3 ]", - "[ 1/1 → 5/4 | note:Eb3 ]", - "[ 5/4 → 3/2 | note:Ab3 ]", - "[ 3/2 → 7/4 | note:G3 ]", - "[ 7/4 → 2/1 | note:C4 ]", - "[ 2/1 → 9/4 | note:G3 ]", - "[ 9/4 → 5/2 | note:C3 ]", - "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:F3 ]", - "[ 3/1 → 13/4 | note:Eb3 ]", - "[ 13/4 → 7/2 | note:Ab3 ]", - "[ 7/2 → 15/4 | note:G3 ]", - "[ 15/4 → 4/1 | note:C4 ]", -] -`; - exports[`runs examples > example "room" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0 ]", @@ -7797,107 +7972,6 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; -exports[`runs examples > example "segment" example index 0 1`] = ` -[ - "[ 0/1 → 1/24 | note:40 ]", - "[ 1/24 → 1/12 | note:40.5 ]", - "[ 1/12 → 1/8 | note:41 ]", - "[ 1/8 → 1/6 | note:41.5 ]", - "[ 1/6 → 5/24 | note:42 ]", - "[ 5/24 → 1/4 | note:42.5 ]", - "[ 1/4 → 7/24 | note:43 ]", - "[ 7/24 → 1/3 | note:43.5 ]", - "[ 1/3 → 3/8 | note:44 ]", - "[ 3/8 → 5/12 | note:44.5 ]", - "[ 5/12 → 11/24 | note:45 ]", - "[ 11/24 → 1/2 | note:45.5 ]", - "[ 1/2 → 13/24 | note:46 ]", - "[ 13/24 → 7/12 | note:46.5 ]", - "[ 7/12 → 5/8 | note:47 ]", - "[ 5/8 → 2/3 | note:47.5 ]", - "[ 2/3 → 17/24 | note:48 ]", - "[ 17/24 → 3/4 | note:48.5 ]", - "[ 3/4 → 19/24 | note:49 ]", - "[ 19/24 → 5/6 | note:49.5 ]", - "[ 5/6 → 7/8 | note:50 ]", - "[ 7/8 → 11/12 | note:50.5 ]", - "[ 11/12 → 23/24 | note:51 ]", - "[ 23/24 → 1/1 | note:51.5 ]", - "[ 1/1 → 25/24 | note:40 ]", - "[ 25/24 → 13/12 | note:40.5 ]", - "[ 13/12 → 9/8 | note:41 ]", - "[ 9/8 → 7/6 | note:41.5 ]", - "[ 7/6 → 29/24 | note:42 ]", - "[ 29/24 → 5/4 | note:42.5 ]", - "[ 5/4 → 31/24 | note:43 ]", - "[ 31/24 → 4/3 | note:43.5 ]", - "[ 4/3 → 11/8 | note:44 ]", - "[ 11/8 → 17/12 | note:44.5 ]", - "[ 17/12 → 35/24 | note:45 ]", - "[ 35/24 → 3/2 | note:45.5 ]", - "[ 3/2 → 37/24 | note:46 ]", - "[ 37/24 → 19/12 | note:46.5 ]", - "[ 19/12 → 13/8 | note:47 ]", - "[ 13/8 → 5/3 | note:47.5 ]", - "[ 5/3 → 41/24 | note:48 ]", - "[ 41/24 → 7/4 | note:48.5 ]", - "[ 7/4 → 43/24 | note:49 ]", - "[ 43/24 → 11/6 | note:49.5 ]", - "[ 11/6 → 15/8 | note:50 ]", - "[ 15/8 → 23/12 | note:50.5 ]", - "[ 23/12 → 47/24 | note:51 ]", - "[ 47/24 → 2/1 | note:51.5 ]", - "[ 2/1 → 49/24 | note:40 ]", - "[ 49/24 → 25/12 | note:40.5 ]", - "[ 25/12 → 17/8 | note:41 ]", - "[ 17/8 → 13/6 | note:41.5 ]", - "[ 13/6 → 53/24 | note:42 ]", - "[ 53/24 → 9/4 | note:42.5 ]", - "[ 9/4 → 55/24 | note:43 ]", - "[ 55/24 → 7/3 | note:43.5 ]", - "[ 7/3 → 19/8 | note:44 ]", - "[ 19/8 → 29/12 | note:44.5 ]", - "[ 29/12 → 59/24 | note:45 ]", - "[ 59/24 → 5/2 | note:45.5 ]", - "[ 5/2 → 61/24 | note:46 ]", - "[ 61/24 → 31/12 | note:46.5 ]", - "[ 31/12 → 21/8 | note:47 ]", - "[ 21/8 → 8/3 | note:47.5 ]", - "[ 8/3 → 65/24 | note:48 ]", - "[ 65/24 → 11/4 | note:48.5 ]", - "[ 11/4 → 67/24 | note:49 ]", - "[ 67/24 → 17/6 | note:49.5 ]", - "[ 17/6 → 23/8 | note:50 ]", - "[ 23/8 → 35/12 | note:50.5 ]", - "[ 35/12 → 71/24 | note:51 ]", - "[ 71/24 → 3/1 | note:51.5 ]", - "[ 3/1 → 73/24 | note:40 ]", - "[ 73/24 → 37/12 | note:40.5 ]", - "[ 37/12 → 25/8 | note:41 ]", - "[ 25/8 → 19/6 | note:41.5 ]", - "[ 19/6 → 77/24 | note:42 ]", - "[ 77/24 → 13/4 | note:42.5 ]", - "[ 13/4 → 79/24 | note:43 ]", - "[ 79/24 → 10/3 | note:43.5 ]", - "[ 10/3 → 27/8 | note:44 ]", - "[ 27/8 → 41/12 | note:44.5 ]", - "[ 41/12 → 83/24 | note:45 ]", - "[ 83/24 → 7/2 | note:45.5 ]", - "[ 7/2 → 85/24 | note:46 ]", - "[ 85/24 → 43/12 | note:46.5 ]", - "[ 43/12 → 29/8 | note:47 ]", - "[ 29/8 → 11/3 | note:47.5 ]", - "[ 11/3 → 89/24 | note:48 ]", - "[ 89/24 → 15/4 | note:48.5 ]", - "[ 15/4 → 91/24 | note:49 ]", - "[ 91/24 → 23/6 | note:49.5 ]", - "[ 23/6 → 31/8 | note:50 ]", - "[ 31/8 → 47/12 | note:50.5 ]", - "[ 47/12 → 95/24 | note:51 ]", - "[ 95/24 → 4/1 | note:51.5 ]", -] -`; - exports[`runs examples > example "seq" example index 0 1`] = ` [ "[ 0/1 → 1/3 | note:e5 ]", From bfbd920b1d3d2e90c15fd3a0b8ebc8df5fe994e5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 00:18:26 -0400 Subject: [PATCH 48/88] fix doc --- packages/core/signal.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 22738ea98..fdd7476fd 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -520,7 +520,6 @@ export const degradeByWith = register( * * @name degradeBy * @memberof Pattern - * @synonym * @param {number} amount - a number between 0 and 1 * @returns Pattern * @example From 3eea908636bce6936b410fa3053dcf70bd5f5252 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 00:33:00 -0400 Subject: [PATCH 49/88] fix doc --- packages/core/pattern.mjs | 6 ++++-- packages/core/signal.mjs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 49caeaf0e..6e294129e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2105,8 +2105,9 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. - * @param {number} segments number of segments per cycle + * @name segment * @synonyms seg + * @param {number} segments number of segments per cycle * @example * note(saw.range(40,52).segment(24)) */ @@ -2487,7 +2488,8 @@ export const bypass = register( /** * Loops the pattern inside an `offset` for `cycles`. - * If you think of the entire span of time in cycles as a ribbon, you can grab a single piece and loop it. + * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it. + * @name ribbon * @synonym rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index fdd7476fd..66ac8a5a6 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -527,6 +527,7 @@ export const degradeByWith = register( * @example * s("[hh?0.2]*8") * @example + * //beat generator * s("bd").segment(16).degradeBy(.5).ribbon(16,1) */ export const degradeBy = register( From 282180ef1cec109f270736ef2712ee06fa990fc6 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 01:28:31 -0400 Subject: [PATCH 50/88] style update --- website/src/repl/components/UserFacingErrorMessage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx index ec8d8be33..9ccfacd88 100644 --- a/website/src/repl/components/UserFacingErrorMessage.jsx +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -4,5 +4,5 @@ export default function UserFacingErrorMessage(Props) { if (error == null) { return; } - return
{error.message || 'Unknown Error :-/'}
; + return
Error: {error.message || 'Unknown Error :-/'}
; } From dc8e5d53946612cb97b6fcb666e3525193a77738 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 01:28:43 -0400 Subject: [PATCH 51/88] lint --- website/src/repl/components/UserFacingErrorMessage.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx index 9ccfacd88..1d9de037d 100644 --- a/website/src/repl/components/UserFacingErrorMessage.jsx +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -4,5 +4,9 @@ export default function UserFacingErrorMessage(Props) { if (error == null) { return; } - return
Error: {error.message || 'Unknown Error :-/'}
; + return ( +
+ Error: {error.message || 'Unknown Error :-/'} +
+ ); } From dee75bc75954f0c5e00fddd563d8fee51c9b43ae Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 24 Mar 2025 01:32:48 -0400 Subject: [PATCH 52/88] update tests --- test/__snapshots__/examples.test.mjs.snap | 352 +++++++++++----------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 80ee7344c..b9da0f538 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1,181 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`runs examples > example "" example index 0 1`] = ` -[ - "[ 0/1 → 1/24 | note:40 ]", - "[ 1/24 → 1/12 | note:40.5 ]", - "[ 1/12 → 1/8 | note:41 ]", - "[ 1/8 → 1/6 | note:41.5 ]", - "[ 1/6 → 5/24 | note:42 ]", - "[ 5/24 → 1/4 | note:42.5 ]", - "[ 1/4 → 7/24 | note:43 ]", - "[ 7/24 → 1/3 | note:43.5 ]", - "[ 1/3 → 3/8 | note:44 ]", - "[ 3/8 → 5/12 | note:44.5 ]", - "[ 5/12 → 11/24 | note:45 ]", - "[ 11/24 → 1/2 | note:45.5 ]", - "[ 1/2 → 13/24 | note:46 ]", - "[ 13/24 → 7/12 | note:46.5 ]", - "[ 7/12 → 5/8 | note:47 ]", - "[ 5/8 → 2/3 | note:47.5 ]", - "[ 2/3 → 17/24 | note:48 ]", - "[ 17/24 → 3/4 | note:48.5 ]", - "[ 3/4 → 19/24 | note:49 ]", - "[ 19/24 → 5/6 | note:49.5 ]", - "[ 5/6 → 7/8 | note:50 ]", - "[ 7/8 → 11/12 | note:50.5 ]", - "[ 11/12 → 23/24 | note:51 ]", - "[ 23/24 → 1/1 | note:51.5 ]", - "[ 1/1 → 25/24 | note:40 ]", - "[ 25/24 → 13/12 | note:40.5 ]", - "[ 13/12 → 9/8 | note:41 ]", - "[ 9/8 → 7/6 | note:41.5 ]", - "[ 7/6 → 29/24 | note:42 ]", - "[ 29/24 → 5/4 | note:42.5 ]", - "[ 5/4 → 31/24 | note:43 ]", - "[ 31/24 → 4/3 | note:43.5 ]", - "[ 4/3 → 11/8 | note:44 ]", - "[ 11/8 → 17/12 | note:44.5 ]", - "[ 17/12 → 35/24 | note:45 ]", - "[ 35/24 → 3/2 | note:45.5 ]", - "[ 3/2 → 37/24 | note:46 ]", - "[ 37/24 → 19/12 | note:46.5 ]", - "[ 19/12 → 13/8 | note:47 ]", - "[ 13/8 → 5/3 | note:47.5 ]", - "[ 5/3 → 41/24 | note:48 ]", - "[ 41/24 → 7/4 | note:48.5 ]", - "[ 7/4 → 43/24 | note:49 ]", - "[ 43/24 → 11/6 | note:49.5 ]", - "[ 11/6 → 15/8 | note:50 ]", - "[ 15/8 → 23/12 | note:50.5 ]", - "[ 23/12 → 47/24 | note:51 ]", - "[ 47/24 → 2/1 | note:51.5 ]", - "[ 2/1 → 49/24 | note:40 ]", - "[ 49/24 → 25/12 | note:40.5 ]", - "[ 25/12 → 17/8 | note:41 ]", - "[ 17/8 → 13/6 | note:41.5 ]", - "[ 13/6 → 53/24 | note:42 ]", - "[ 53/24 → 9/4 | note:42.5 ]", - "[ 9/4 → 55/24 | note:43 ]", - "[ 55/24 → 7/3 | note:43.5 ]", - "[ 7/3 → 19/8 | note:44 ]", - "[ 19/8 → 29/12 | note:44.5 ]", - "[ 29/12 → 59/24 | note:45 ]", - "[ 59/24 → 5/2 | note:45.5 ]", - "[ 5/2 → 61/24 | note:46 ]", - "[ 61/24 → 31/12 | note:46.5 ]", - "[ 31/12 → 21/8 | note:47 ]", - "[ 21/8 → 8/3 | note:47.5 ]", - "[ 8/3 → 65/24 | note:48 ]", - "[ 65/24 → 11/4 | note:48.5 ]", - "[ 11/4 → 67/24 | note:49 ]", - "[ 67/24 → 17/6 | note:49.5 ]", - "[ 17/6 → 23/8 | note:50 ]", - "[ 23/8 → 35/12 | note:50.5 ]", - "[ 35/12 → 71/24 | note:51 ]", - "[ 71/24 → 3/1 | note:51.5 ]", - "[ 3/1 → 73/24 | note:40 ]", - "[ 73/24 → 37/12 | note:40.5 ]", - "[ 37/12 → 25/8 | note:41 ]", - "[ 25/8 → 19/6 | note:41.5 ]", - "[ 19/6 → 77/24 | note:42 ]", - "[ 77/24 → 13/4 | note:42.5 ]", - "[ 13/4 → 79/24 | note:43 ]", - "[ 79/24 → 10/3 | note:43.5 ]", - "[ 10/3 → 27/8 | note:44 ]", - "[ 27/8 → 41/12 | note:44.5 ]", - "[ 41/12 → 83/24 | note:45 ]", - "[ 83/24 → 7/2 | note:45.5 ]", - "[ 7/2 → 85/24 | note:46 ]", - "[ 85/24 → 43/12 | note:46.5 ]", - "[ 43/12 → 29/8 | note:47 ]", - "[ 29/8 → 11/3 | note:47.5 ]", - "[ 11/3 → 89/24 | note:48 ]", - "[ 89/24 → 15/4 | note:48.5 ]", - "[ 15/4 → 91/24 | note:49 ]", - "[ 91/24 → 23/6 | note:49.5 ]", - "[ 23/6 → 31/8 | note:50 ]", - "[ 31/8 → 47/12 | note:50.5 ]", - "[ 47/12 → 95/24 | note:51 ]", - "[ 95/24 → 4/1 | note:51.5 ]", -] -`; - -exports[`runs examples > example "" example index 0 2`] = ` -[ - "[ 0/1 → 1/1 | note:d ]", - "[ 1/1 → 2/1 | note:e ]", - "[ 2/1 → 3/1 | note:d ]", - "[ 3/1 → 4/1 | note:e ]", -] -`; - -exports[`runs examples > example "" example index 1 1`] = ` -[ - "[ 0/1 → 1/4 | note:A3 ]", - "[ 1/4 → 1/2 | note:C3 ]", - "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:G3 ]", - "[ 1/1 → 5/4 | note:E3 ]", - "[ 5/4 → 3/2 | note:C4 ]", - "[ 3/2 → 7/4 | note:A3 ]", - "[ 7/4 → 2/1 | note:E4 ]", - "[ 2/1 → 9/4 | note:A3 ]", - "[ 9/4 → 5/2 | note:C3 ]", - "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:G3 ]", - "[ 3/1 → 13/4 | note:E3 ]", - "[ 13/4 → 7/2 | note:C4 ]", - "[ 7/2 → 15/4 | note:A3 ]", - "[ 15/4 → 4/1 | note:E4 ]", -] -`; - -exports[`runs examples > example "" example index 2 1`] = ` -[ - "[ 1/16 → 1/8 | s:bd ]", - "[ 1/8 → 3/16 | s:bd ]", - "[ 3/16 → 1/4 | s:bd ]", - "[ 1/4 → 5/16 | s:bd ]", - "[ 3/8 → 7/16 | s:bd ]", - "[ 9/16 → 5/8 | s:bd ]", - "[ 5/8 → 11/16 | s:bd ]", - "[ 11/16 → 3/4 | s:bd ]", - "[ 3/4 → 13/16 | s:bd ]", - "[ 7/8 → 15/16 | s:bd ]", - "[ 17/16 → 9/8 | s:bd ]", - "[ 9/8 → 19/16 | s:bd ]", - "[ 19/16 → 5/4 | s:bd ]", - "[ 5/4 → 21/16 | s:bd ]", - "[ 11/8 → 23/16 | s:bd ]", - "[ 25/16 → 13/8 | s:bd ]", - "[ 13/8 → 27/16 | s:bd ]", - "[ 27/16 → 7/4 | s:bd ]", - "[ 7/4 → 29/16 | s:bd ]", - "[ 15/8 → 31/16 | s:bd ]", - "[ 33/16 → 17/8 | s:bd ]", - "[ 17/8 → 35/16 | s:bd ]", - "[ 35/16 → 9/4 | s:bd ]", - "[ 9/4 → 37/16 | s:bd ]", - "[ 19/8 → 39/16 | s:bd ]", - "[ 41/16 → 21/8 | s:bd ]", - "[ 21/8 → 43/16 | s:bd ]", - "[ 43/16 → 11/4 | s:bd ]", - "[ 11/4 → 45/16 | s:bd ]", - "[ 23/8 → 47/16 | s:bd ]", - "[ 49/16 → 25/8 | s:bd ]", - "[ 25/8 → 51/16 | s:bd ]", - "[ 51/16 → 13/4 | s:bd ]", - "[ 13/4 → 53/16 | s:bd ]", - "[ 27/8 → 55/16 | s:bd ]", - "[ 57/16 → 29/8 | s:bd ]", - "[ 29/8 → 59/16 | s:bd ]", - "[ 59/16 → 15/4 | s:bd ]", - "[ 15/4 → 61/16 | s:bd ]", - "[ 31/8 → 63/16 | s:bd ]", -] -`; - exports[`runs examples > example "_euclidRot" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c3 ]", @@ -7330,6 +7154,81 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; +exports[`runs examples > example "ribbon" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:d ]", + "[ 1/1 → 2/1 | note:e ]", + "[ 2/1 → 3/1 | note:d ]", + "[ 3/1 → 4/1 | note:e ]", +] +`; + +exports[`runs examples > example "ribbon" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | note:A3 ]", + "[ 1/4 → 1/2 | note:C3 ]", + "[ 1/2 → 3/4 | note:D3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", + "[ 9/4 → 5/2 | note:C3 ]", + "[ 5/2 → 11/4 | note:D3 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", +] +`; + +exports[`runs examples > example "ribbon" example index 2 1`] = ` +[ + "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", +] +`; + exports[`runs examples > example "room" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0 ]", @@ -7972,6 +7871,107 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; +exports[`runs examples > example "segment" example index 0 1`] = ` +[ + "[ 0/1 → 1/24 | note:40 ]", + "[ 1/24 → 1/12 | note:40.5 ]", + "[ 1/12 → 1/8 | note:41 ]", + "[ 1/8 → 1/6 | note:41.5 ]", + "[ 1/6 → 5/24 | note:42 ]", + "[ 5/24 → 1/4 | note:42.5 ]", + "[ 1/4 → 7/24 | note:43 ]", + "[ 7/24 → 1/3 | note:43.5 ]", + "[ 1/3 → 3/8 | note:44 ]", + "[ 3/8 → 5/12 | note:44.5 ]", + "[ 5/12 → 11/24 | note:45 ]", + "[ 11/24 → 1/2 | note:45.5 ]", + "[ 1/2 → 13/24 | note:46 ]", + "[ 13/24 → 7/12 | note:46.5 ]", + "[ 7/12 → 5/8 | note:47 ]", + "[ 5/8 → 2/3 | note:47.5 ]", + "[ 2/3 → 17/24 | note:48 ]", + "[ 17/24 → 3/4 | note:48.5 ]", + "[ 3/4 → 19/24 | note:49 ]", + "[ 19/24 → 5/6 | note:49.5 ]", + "[ 5/6 → 7/8 | note:50 ]", + "[ 7/8 → 11/12 | note:50.5 ]", + "[ 11/12 → 23/24 | note:51 ]", + "[ 23/24 → 1/1 | note:51.5 ]", + "[ 1/1 → 25/24 | note:40 ]", + "[ 25/24 → 13/12 | note:40.5 ]", + "[ 13/12 → 9/8 | note:41 ]", + "[ 9/8 → 7/6 | note:41.5 ]", + "[ 7/6 → 29/24 | note:42 ]", + "[ 29/24 → 5/4 | note:42.5 ]", + "[ 5/4 → 31/24 | note:43 ]", + "[ 31/24 → 4/3 | note:43.5 ]", + "[ 4/3 → 11/8 | note:44 ]", + "[ 11/8 → 17/12 | note:44.5 ]", + "[ 17/12 → 35/24 | note:45 ]", + "[ 35/24 → 3/2 | note:45.5 ]", + "[ 3/2 → 37/24 | note:46 ]", + "[ 37/24 → 19/12 | note:46.5 ]", + "[ 19/12 → 13/8 | note:47 ]", + "[ 13/8 → 5/3 | note:47.5 ]", + "[ 5/3 → 41/24 | note:48 ]", + "[ 41/24 → 7/4 | note:48.5 ]", + "[ 7/4 → 43/24 | note:49 ]", + "[ 43/24 → 11/6 | note:49.5 ]", + "[ 11/6 → 15/8 | note:50 ]", + "[ 15/8 → 23/12 | note:50.5 ]", + "[ 23/12 → 47/24 | note:51 ]", + "[ 47/24 → 2/1 | note:51.5 ]", + "[ 2/1 → 49/24 | note:40 ]", + "[ 49/24 → 25/12 | note:40.5 ]", + "[ 25/12 → 17/8 | note:41 ]", + "[ 17/8 → 13/6 | note:41.5 ]", + "[ 13/6 → 53/24 | note:42 ]", + "[ 53/24 → 9/4 | note:42.5 ]", + "[ 9/4 → 55/24 | note:43 ]", + "[ 55/24 → 7/3 | note:43.5 ]", + "[ 7/3 → 19/8 | note:44 ]", + "[ 19/8 → 29/12 | note:44.5 ]", + "[ 29/12 → 59/24 | note:45 ]", + "[ 59/24 → 5/2 | note:45.5 ]", + "[ 5/2 → 61/24 | note:46 ]", + "[ 61/24 → 31/12 | note:46.5 ]", + "[ 31/12 → 21/8 | note:47 ]", + "[ 21/8 → 8/3 | note:47.5 ]", + "[ 8/3 → 65/24 | note:48 ]", + "[ 65/24 → 11/4 | note:48.5 ]", + "[ 11/4 → 67/24 | note:49 ]", + "[ 67/24 → 17/6 | note:49.5 ]", + "[ 17/6 → 23/8 | note:50 ]", + "[ 23/8 → 35/12 | note:50.5 ]", + "[ 35/12 → 71/24 | note:51 ]", + "[ 71/24 → 3/1 | note:51.5 ]", + "[ 3/1 → 73/24 | note:40 ]", + "[ 73/24 → 37/12 | note:40.5 ]", + "[ 37/12 → 25/8 | note:41 ]", + "[ 25/8 → 19/6 | note:41.5 ]", + "[ 19/6 → 77/24 | note:42 ]", + "[ 77/24 → 13/4 | note:42.5 ]", + "[ 13/4 → 79/24 | note:43 ]", + "[ 79/24 → 10/3 | note:43.5 ]", + "[ 10/3 → 27/8 | note:44 ]", + "[ 27/8 → 41/12 | note:44.5 ]", + "[ 41/12 → 83/24 | note:45 ]", + "[ 83/24 → 7/2 | note:45.5 ]", + "[ 7/2 → 85/24 | note:46 ]", + "[ 85/24 → 43/12 | note:46.5 ]", + "[ 43/12 → 29/8 | note:47 ]", + "[ 29/8 → 11/3 | note:47.5 ]", + "[ 11/3 → 89/24 | note:48 ]", + "[ 89/24 → 15/4 | note:48.5 ]", + "[ 15/4 → 91/24 | note:49 ]", + "[ 91/24 → 23/6 | note:49.5 ]", + "[ 23/6 → 31/8 | note:50 ]", + "[ 31/8 → 47/12 | note:50.5 ]", + "[ 47/12 → 95/24 | note:51 ]", + "[ 95/24 → 4/1 | note:51.5 ]", +] +`; + exports[`runs examples > example "seq" example index 0 1`] = ` [ "[ 0/1 → 1/3 | note:e5 ]", From 6d355a803ae490e0940ec24c00a91da75776f8d4 Mon Sep 17 00:00:00 2001 From: Rene Nyffenegger Date: Wed, 26 Mar 2025 14:54:15 +0100 Subject: [PATCH 53/88] Fix typo pattnr (#1316) --- website/src/pages/learn/stepwise.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/stepwise.mdx b/website/src/pages/learn/stepwise.mdx index d3ca334af..e632ee7ec 100644 --- a/website/src/pages/learn/stepwise.mdx +++ b/website/src/pages/learn/stepwise.mdx @@ -24,7 +24,7 @@ With the new stepwise `stepcat` function, the steps of the two patterns will be By default, steps are counted according to the 'top level' in mini-notation. For example `"a [b c] d e"` has five events in it per cycle, but is counted as four steps, where `[b c]` is counted as a single step. -However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattenr are twice as long, and so counted as two steps each. +However, you can mark a different metrical level to count steps relative to, using a `^` at the start of a sub-pattern. If we do this to the subpattern in our example: `"a [^b c] d e"`, then the pattern is now counted as having _eight_ steps. This is because 'b' and 'c' are each counted as single steps, and the events in the pattern are twice as long, and so counted as two steps each. ## Pacing the steps From e55b7c6ce338302691cbe16e1666e928ec8c7732 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 00:55:12 -0400 Subject: [PATCH 54/88] mostly working --- packages/superdough/sampler.mjs | 1 + packages/superdough/superdough.mjs | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f5e46d6bb..45f6b33e1 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -278,6 +278,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; + export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..c6daabbfc 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -374,6 +374,9 @@ export function resetGlobalEffects() { analysersData = {}; } +let allAudioNodeChains = []; +let maxPolyphony = 12; + export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; @@ -472,14 +475,30 @@ export const superdough = async (value, t, hapDuration) => { } = value; gain = nanFallback(gain, 1); + console.info(allAudioNodeChains.length) + + for (let i = 0; i <= allAudioNodeChains.length - maxPolyphony; i++) { + const chain = allAudioNodeChains.shift() + chain.forEach(node => node?.disconnect()) + } + // if (allAudioNodeChains.length > maxPolyphony) { + + + // allAudioNodeChains.slice(0, allAudioNodeChains.length - maxPolyphony).flat().forEach((node) => { + // node.disconnect(); + // }); + // console.info(allAudioNodes.length) + // } //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future - let toDisconnect = []; // audio nodes that will be disconnected when the source has ended + let audioNodes = []; + // audio nodes that will be disconnected when the source has ended + const onended = () => { - toDisconnect.forEach((n) => n?.disconnect()); + audioNodes.forEach((n) => n?.disconnect()); }; if (bank && s) { s = `${bank}_${s}`; @@ -654,9 +673,10 @@ export const superdough = async (value, t, hapDuration) => { // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - // toDisconnect = all the node that should be disconnected in onended callback + // audioNodes = all the node that should be disconnected in onended callback // this is crucial for performance - toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]); + audioNodes = chain.concat([delaySend, reverbSend, analyserSend]); + allAudioNodeChains.push(audioNodes) }; export const superdoughTrigger = (t, hap, ct, cps) => { From 8962e5a4f01b289dabe4b5f3e8dac8c555db3cb5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 01:35:25 -0400 Subject: [PATCH 55/88] max poly --- packages/superdough/superdough.mjs | 46 ++++++++++++++--------------- website/src/repl/useReplContext.jsx | 4 +-- website/src/settings.mjs | 1 + 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c6daabbfc..c306f54ec 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -14,6 +14,10 @@ import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +let maxPolyphony = 128; +export function setMaxPolyphony(polyphony) { + maxPolyphony = polyphony; +} export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { @@ -162,7 +166,8 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false } = options; + const { disableWorklets = false, polyphony = maxPolyphony } = options; + setMaxPolyphony(polyphony); if (typeof window === 'undefined') { return; } @@ -374,8 +379,7 @@ export function resetGlobalEffects() { analysersData = {}; } -let allAudioNodeChains = []; -let maxPolyphony = 12; +let allAudioNodeChains = new Map(); export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); @@ -475,30 +479,30 @@ export const superdough = async (value, t, hapDuration) => { } = value; gain = nanFallback(gain, 1); - console.info(allAudioNodeChains.length) - for (let i = 0; i <= allAudioNodeChains.length - maxPolyphony; i++) { - const chain = allAudioNodeChains.shift() - chain.forEach(node => node?.disconnect()) - } - // if (allAudioNodeChains.length > maxPolyphony) { + const chainID = Math.round(Math.random() * 10000); - - // allAudioNodeChains.slice(0, allAudioNodeChains.length - maxPolyphony).flat().forEach((node) => { - // node.disconnect(); - // }); - // console.info(allAudioNodes.length) - // } + // oldest audio nodes will be removed if maximum polyphony is exceeded + for (let i = 0; i <= allAudioNodeChains.size - maxPolyphony; i++) { + const ch = allAudioNodeChains.entries().next(); + const key = ch.value[0]; + const chain = ch.value[1]; + if (key == null) { + continue; + } + chain?.forEach((node) => node?.disconnect()); + allAudioNodeChains.delete(key); + } //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future - let audioNodes = []; - // audio nodes that will be disconnected when the source has ended const onended = () => { - audioNodes.forEach((n) => n?.disconnect()); + const chain = allAudioNodeChains.get(chainID); + chain?.forEach((n) => n?.disconnect()); + allAudioNodeChains.delete(chainID); }; if (bank && s) { s = `${bank}_${s}`; @@ -672,11 +676,7 @@ export const superdough = async (value, t, hapDuration) => { // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - - // audioNodes = all the node that should be disconnected in onended callback - // this is crucial for performance - audioNodes = chain.concat([delaySend, reverbSend, analyserSend]); - allAudioNodeChains.push(audioNodes) + allAudioNodeChains.set(chainID, [...chain, delaySend, reverbSend, analyserSend]); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index ba8616272..801a0886d 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -36,11 +36,11 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode } = settingsMap.get(); +const { latestCode, maxPolyphony } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick(); + audioReady = initAudioOnFirstClick({ maxPolyphony }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index cd3d89366..132d84bd5 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -40,6 +40,7 @@ export const defaultSettings = { audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, + maxPolyphony: 128, }; let search = null; From 6f9bcc53dc43c3166564e0eca0c3ccae20a871ee Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 16:33:47 -0400 Subject: [PATCH 56/88] ramp cut audio node --- packages/superdough/helpers.mjs | 1 + packages/superdough/sampler.mjs | 4 ++- packages/superdough/superdough.mjs | 45 ++++++++++++++++-------------- packages/superdough/synth.mjs | 30 +++++++++++++------- packages/superdough/worklets.mjs | 3 ++ 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e51156fec..dfa8660f4 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.onended = () => { onComplete(); }; + return constantNode } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 45f6b33e1..f8eddffc2 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -345,7 +345,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { }; let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); - const stop = (endTime, playWholeBuffer) => {}; + const stop = (endTime) => { + bufferSource.stop(endTime) + }; const handle = { node: out, bufferSource, stop }; // cut groups diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c306f54ec..3cb5349aa 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -379,7 +379,7 @@ export function resetGlobalEffects() { analysersData = {}; } -let allAudioNodeChains = new Map(); +let activeSoundSources = new Map(); export const superdough = async (value, t, hapDuration) => { const ac = getAudioContext(); @@ -480,30 +480,24 @@ export const superdough = async (value, t, hapDuration) => { gain = nanFallback(gain, 1); - const chainID = Math.round(Math.random() * 10000); + const chainID = Math.round(Math.random() * 1000000); - // oldest audio nodes will be removed if maximum polyphony is exceeded - for (let i = 0; i <= allAudioNodeChains.size - maxPolyphony; i++) { - const ch = allAudioNodeChains.entries().next(); - const key = ch.value[0]; - const chain = ch.value[1]; - if (key == null) { - continue; - } - chain?.forEach((node) => node?.disconnect()); - allAudioNodeChains.delete(key); + // oldest audio nodes will be destroyed if maximum polyphony is exceeded + for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) { + const ch = activeSoundSources.entries().next(); + const source = ch.value[1]; + const chainID = ch.value[0]; + const endTime = t + .25; + source?.node?.gain?.linearRampToValueAtTime(0, endTime); + source?.stop?.(endTime); + activeSoundSources.delete(chainID); } //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); - gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future + let audioNodes = []; - const onended = () => { - const chain = allAudioNodeChains.get(chainID); - chain?.forEach((n) => n?.disconnect()); - allAudioNodeChains.delete(chainID); - }; if (bank && s) { s = `${bank}_${s}`; value.s = s; @@ -515,10 +509,15 @@ export const superdough = async (value, t, hapDuration) => { sourceNode = source(t, value, hapDuration); } else if (getSound(s)) { const { onTrigger } = getSound(s); - const soundHandle = await onTrigger(t, value, onended); + const onEnded = () => { + audioNodes.forEach((n) => n?.disconnect()); + activeSoundSources.delete(chainID); + }; + const soundHandle = await onTrigger(t, value, onEnded); + if (soundHandle) { sourceNode = soundHandle.node; - soundHandle.stop(t + hapDuration); + activeSoundSources.set(chainID, soundHandle); } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -648,6 +647,7 @@ export const superdough = async (value, t, hapDuration) => { if (delay > 0 && delaytime > 0 && delayfeedback > 0) { const delyNode = getDelay(orbit, delaytime, delayfeedback, t); delaySend = effectSend(post, delyNode, delay); + audioNodes.push(delaySend); } // reverb let reverbSend; @@ -665,6 +665,7 @@ export const superdough = async (value, t, hapDuration) => { } const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR); reverbSend = effectSend(post, reverbNode, room); + audioNodes.push(reverbSend); } // analyser @@ -672,11 +673,13 @@ export const superdough = async (value, t, hapDuration) => { if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); analyserSend = effectSend(post, analyserNode, 1); + audioNodes.push(analyserSend); } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - allAudioNodeChains.set(chainID, [...chain, delaySend, reverbSend, analyserSend]); + audioNodes = audioNodes.concat(chain); + // activeSoundSources.set(chainID, [...chain, delaySend, reverbSend, analyserSend].filter(Boolean)); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 86c073d0b..f40b40c6d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -63,7 +63,9 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime); + }, }; }, { type: 'synth', prebake: true }, @@ -110,7 +112,9 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { o.disconnect(); @@ -123,11 +127,11 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear'); - return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -169,7 +173,10 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - webAudioTimeout( + + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + + let timeoutNode = webAudioTimeout( ac, () => { o.disconnect(); @@ -182,11 +189,11 @@ export function registerSynthSounds() { end, ); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - return { node: envGain, - stop: (time) => {}, + stop: (time) => { + timeoutNode.stop(time); + }, }; }, { prebake: true, type: 'synth' }, @@ -229,7 +236,9 @@ export function registerSynthSounds() { stop(envEnd); return { node, - stop: (releaseTime) => {}, + stop: (endTime) => { + stop(endTime) + }, }; }, { type: 'synth', prebake: true }, @@ -299,6 +308,7 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, stop: (time) => { + // console.info(time) fmModulator.stop(time); vibratoOscillator?.stop(time); noiseMix?.stop(time); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 61d5d96e9..286d8c039 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -663,6 +663,9 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); class PulseOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); + // this.port.onmessage = (event) => { + // console.info(event) + // }; this.pi = _PI; this.phi = -this.pi; // phase this.Y0 = 0; // feedback memories From d8e4055c6e5dd037386763252e83836e9edc842e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 20:22:24 -0400 Subject: [PATCH 57/88] settings panel --- packages/superdough/superdough.mjs | 11 ++++---- packages/superdough/synth.mjs | 12 +++++---- packages/superdough/worklets.mjs | 6 ++--- .../src/repl/components/panel/SettingsTab.jsx | 25 ++++++++++++++++++- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 3cb5349aa..e59930242 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -14,9 +14,10 @@ import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; -let maxPolyphony = 128; +export const DEFAULT_MAX_POLYPHONY = 128; +let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { - maxPolyphony = polyphony; + maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } export const soundMap = map(); @@ -166,8 +167,8 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, polyphony = maxPolyphony } = options; - setMaxPolyphony(polyphony); + const { disableWorklets = false, maxPolyphony } = options; + setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; } @@ -487,7 +488,7 @@ export const superdough = async (value, t, hapDuration) => { const ch = activeSoundSources.entries().next(); const source = ch.value[1]; const chainID = ch.value[0]; - const endTime = t + .25; + const endTime = t + 0.25; source?.node?.gain?.linearRampToValueAtTime(0, endTime); source?.stop?.(endTime); activeSoundSources.delete(chainID); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index f40b40c6d..8bb47bb7d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -25,6 +25,10 @@ const getFrequencyFromValue = (value) => { return Number(freq); }; +function destroyAudioWorkletNode(node) { + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +} const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const noises = ['pink', 'white', 'brown', 'crackle']; @@ -117,7 +121,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -173,13 +177,12 @@ export function registerSynthSounds() { let envGain = gainNode(1); envGain = o.connect(envGain); - getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); let timeoutNode = webAudioTimeout( ac, () => { - o.disconnect(); + destroyAudioWorkletNode(o); envGain.disconnect(); onended(); fm?.stop(); @@ -237,7 +240,7 @@ export function registerSynthSounds() { return { node, stop: (endTime) => { - stop(endTime) + stop(endTime); }, }; }, @@ -308,7 +311,6 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, stop: (time) => { - // console.info(time) fmModulator.stop(time); vibratoOscillator?.stop(time); noiseMix?.stop(time); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 286d8c039..03d3a966f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -663,9 +663,6 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor); class PulseOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); - // this.port.onmessage = (event) => { - // console.info(event) - // }; this.pi = _PI; this.phi = -this.pi; // phase this.Y0 = 0; // feedback memories @@ -713,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { } process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } if (currentTime <= params.begin[0]) { return true; } diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 4ad3cc9ac..4c174fa6c 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,10 +1,12 @@ import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; +import { Textbox } from '../textbox/Textbox.jsx'; import { isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; +import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -53,7 +55,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) { ); } -function FormItem({ label, children }) { +function FormItem({ label, children, sublabel }) { return (
@@ -105,6 +107,7 @@ export function SettingsTab({ started }) { audioDeviceName, audioEngineTarget, togglePanelTrigger, + maxPolyphony, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -139,6 +142,26 @@ export function SettingsTab({ started }) { }} /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? DEFAULT_MAX_POLYPHONY : v; + setMaxPolyphony(v); + settingsMap.setKey('maxPolyphony', v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + settingsMap.setKey('maxPolyphony', isNaN(v) ? undefined : v); + }} + type="number" + placeholder="" + value={maxPolyphony ?? ''} + /> + settingsMap.setKey('theme', theme)} /> From 2f4cba4e9436c0962e4917d16d7564a12eadedb1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 20:26:44 -0400 Subject: [PATCH 58/88] format --- packages/superdough/helpers.mjs | 2 +- packages/superdough/sampler.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index dfa8660f4..6bde69373 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -212,7 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.onended = () => { onComplete(); }; - return constantNode + return constantNode; } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index f8eddffc2..18d1b7797 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -278,7 +278,6 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; - export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, @@ -346,7 +345,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); const stop = (endTime) => { - bufferSource.stop(endTime) + bufferSource.stop(endTime); }; const handle = { node: out, bufferSource, stop }; From bceb37e8eab5de964f5386ac70f2cbbb51b42ca7 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 30 Mar 2025 20:32:19 -0400 Subject: [PATCH 59/88] rm comment --- packages/superdough/superdough.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e59930242..9a3a0aa86 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -680,7 +680,6 @@ export const superdough = async (value, t, hapDuration) => { // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); audioNodes = audioNodes.concat(chain); - // activeSoundSources.set(chainID, [...chain, delaySend, reverbSend, analyserSend].filter(Boolean)); }; export const superdoughTrigger = (t, hap, ct, cps) => { From 5ed28d6168b8d323ed64efa049c0feb285d73678 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 31 Mar 2025 00:17:26 -0400 Subject: [PATCH 60/88] gain curve func --- packages/superdough/helpers.mjs | 4 ++-- packages/superdough/superdough.mjs | 24 ++++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index e51156fec..2ad1567c5 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,9 +1,9 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback } from './util.mjs'; export function gainNode(value) { const node = getAudioContext().createGain(); - node.gain.value = value; + node.gain.value = value return node; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..a9372ca1d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -20,6 +20,16 @@ export function registerSound(key, onTrigger, data = {}) { soundMap.setKey(key.toLowerCase(), { onTrigger, data }); } +let gainCurveFunc = (val) => Math.pow(val,2) + +export function applyGainCurve(val) { + return gainCurveFunc(val) +} + +export function setGainCurve(newGainCurveFunc) { + gainCurveFunc = newGainCurveFunc +} + function aliasBankMap(aliasMap) { // Make all bank keys lower case for case insensitivity for (const key in aliasMap) { @@ -87,7 +97,7 @@ export function getSound(s) { const defaultDefaultValues = { s: 'triangle', - gain: 0.8, + gain: 1, postgain: 1, density: '.03', ftype: '12db', @@ -471,12 +481,17 @@ export const superdough = async (value, t, hapDuration) => { compressorRelease, } = value; - gain = nanFallback(gain, 1); + gain = applyGainCurve(nanFallback(gain, 1)); + postgain = applyGainCurve(postgain) + shapevol = applyGainCurve(shapevol) + distortvol = applyGainCurve(distortvol) + delay = applyGainCurve(delay) + velocity = applyGainCurve(velocity) + //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); - gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future let toDisconnect = []; // audio nodes that will be disconnected when the source has ended const onended = () => { toDisconnect.forEach((n) => n?.disconnect()); @@ -516,6 +531,7 @@ export const superdough = async (value, t, hapDuration) => { // gain stage chain.push(gainNode(gain)); + chain.push(gainNode(.8 * velocity)) //filter const ftype = getFilterType(value.ftype); @@ -616,7 +632,7 @@ export const superdough = async (value, t, hapDuration) => { } // last gain - const post = new GainNode(ac, { gain: postgain }); + const post = new GainNode(ac, { gain:postgain }); chain.push(post); connectToDestination(post, channels); From 999c0b23495583a96c89cea6d787390a8d40ad0b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 31 Mar 2025 00:21:01 -0400 Subject: [PATCH 61/88] lint --- packages/superdough/helpers.mjs | 4 ++-- packages/superdough/superdough.mjs | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 2ad1567c5..e51156fec 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,9 +1,9 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback } from './util.mjs'; export function gainNode(value) { const node = getAudioContext().createGain(); - node.gain.value = value + node.gain.value = value; return node; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a9372ca1d..d2c825ab7 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -20,14 +20,14 @@ export function registerSound(key, onTrigger, data = {}) { soundMap.setKey(key.toLowerCase(), { onTrigger, data }); } -let gainCurveFunc = (val) => Math.pow(val,2) +let gainCurveFunc = (val) => Math.pow(val, 2); export function applyGainCurve(val) { - return gainCurveFunc(val) + return gainCurveFunc(val); } export function setGainCurve(newGainCurveFunc) { - gainCurveFunc = newGainCurveFunc + gainCurveFunc = newGainCurveFunc; } function aliasBankMap(aliasMap) { @@ -482,12 +482,11 @@ export const superdough = async (value, t, hapDuration) => { } = value; gain = applyGainCurve(nanFallback(gain, 1)); - postgain = applyGainCurve(postgain) - shapevol = applyGainCurve(shapevol) - distortvol = applyGainCurve(distortvol) - delay = applyGainCurve(delay) - velocity = applyGainCurve(velocity) - + postgain = applyGainCurve(postgain); + shapevol = applyGainCurve(shapevol); + distortvol = applyGainCurve(distortvol); + delay = applyGainCurve(delay); + velocity = applyGainCurve(velocity); //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); @@ -531,7 +530,7 @@ export const superdough = async (value, t, hapDuration) => { // gain stage chain.push(gainNode(gain)); - chain.push(gainNode(.8 * velocity)) + chain.push(gainNode(0.8 * velocity)); //filter const ftype = getFilterType(value.ftype); @@ -632,7 +631,7 @@ export const superdough = async (value, t, hapDuration) => { } // last gain - const post = new GainNode(ac, { gain:postgain }); + const post = new GainNode(ac, { gain: postgain }); chain.push(post); connectToDestination(post, channels); From c2d4b529f6a49c9714b65812d4991b1ed56bb98d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 3 Apr 2025 00:59:46 -0400 Subject: [PATCH 62/88] working --- packages/superdough/superdough.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fc81f565e..cd9148228 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -17,7 +17,9 @@ import { loadBuffer } from './sampler.mjs'; export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { - soundMap.setKey(key.toLowerCase(), { onTrigger, data }); + key = key.toLowerCase().replace(/\s+/g, '_') + console.info(key, data) + soundMap.setKey(key, { onTrigger, data }); } function aliasBankMap(aliasMap) { From 6b8ee8ad11c92cd883d0fcf9554149a62feb59c8 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 3 Apr 2025 01:05:26 -0400 Subject: [PATCH 63/88] lint --- packages/superdough/superdough.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index cd9148228..1126af0fc 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -17,8 +17,7 @@ import { loadBuffer } from './sampler.mjs'; export const soundMap = map(); export function registerSound(key, onTrigger, data = {}) { - key = key.toLowerCase().replace(/\s+/g, '_') - console.info(key, data) + key = key.toLowerCase().replace(/\s+/g, '_'); soundMap.setKey(key, { onTrigger, data }); } From 05ea9c86fbe37ddc4beb4ea43db5c894a13fd33b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Apr 2025 16:08:42 -0400 Subject: [PATCH 64/88] working --- packages/core/controls.mjs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e58bc8caf..85b4c2398 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1780,3 +1780,32 @@ export const as = register('as', (mapping, pat) => { return v; }); }); + +/** + * Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file + * in the optional array syntax ex: "0.5:2", the second value controls the speed of playback + * @name scrub + * @memberof Pattern + * @returns Pattern + * @example + * samples('github:switchangel/pad') + * s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8") + * @example + * samples('github:yaxu/clean-breaks/main'); + * s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16)) + */ + +register( + 'scrub', + (beginPat, pat) => { + return beginPat.outerBind((v) => { + if (!Array.isArray(v)) { + v = [v]; + } + const [beginVal, speedMultiplier = 1] = v; + + return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1); + }); + }, + false, +); From a4682018224d3ed72b540ea3dfa35d0c7bffbaad Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Apr 2025 16:32:37 -0400 Subject: [PATCH 65/88] add tests --- test/__snapshots__/examples.test.mjs.snap | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index b9da0f538..2d88d8355 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7871,6 +7871,52 @@ but parts might be played more than once, or not at all, per cycle." example ind ] `; +exports[`runs examples > example "scrub" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/8 → 1/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 1/4 → 5/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 5/8 → 3/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 3/4 → 7/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/8 → 1/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 1/1 → 9/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/8 → 5/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 5/4 → 13/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 13/8 → 7/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 7/4 → 15/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/8 → 2/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 2/1 → 17/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 17/8 → 9/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 9/4 → 21/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 21/8 → 11/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 11/4 → 23/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 23/8 → 3/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", + "[ 3/1 → 25/8 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 25/8 → 13/4 | s:swpad n:0 begin:0.1 speed:1 clip:1 ]", + "[ 13/4 → 29/8 | s:swpad n:0 begin:0.25 speed:1 clip:1 ]", + "[ 29/8 → 15/4 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 15/4 → 31/8 | s:swpad n:0 begin:0.7 speed:1 clip:1 ]", + "[ 31/8 → 4/1 | s:swpad n:0 begin:0.8 speed:1.5 clip:1 ]", +] +`; + +exports[`runs examples > example "scrub" example index 1 1`] = ` +[ + "[ 0/1 → 3/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 3/8 → 5/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 5/8 → 1/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 1/1 → 11/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 11/8 → 13/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 13/8 → 2/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 2/1 → 19/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 19/8 → 21/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 21/8 → 3/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", + "[ 3/1 → 27/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 27/8 → 29/8 | s:amen speed:0.25 unit:c begin:0 clip:1 ]", + "[ 29/8 → 4/1 | s:amen speed:0.25 unit:c begin:0.25 clip:1 ]", +] +`; + exports[`runs examples > example "segment" example index 0 1`] = ` [ "[ 0/1 → 1/24 | note:40 ]", From 695ac6a5d3891d5c45fe570e33628c1b055ba32a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 6 Apr 2025 16:34:14 -0400 Subject: [PATCH 66/88] var --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 85b4c2398..a6c650e03 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1795,7 +1795,7 @@ export const as = register('as', (mapping, pat) => { * s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16)) */ -register( +export const scrub = register( 'scrub', (beginPat, pat) => { return beginPat.outerBind((v) => { From 344d9ca1a8692f4bef1fe63ba201edab9434c3e5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:43:29 -0400 Subject: [PATCH 67/88] working --- packages/superdough/superdough.mjs | 61 ++++++++++++++++++- .../components/panel/AudioDeviceSelector.jsx | 5 +- website/src/repl/useReplContext.jsx | 22 ++----- website/src/repl/util.mjs | 35 +---------- website/src/settings.mjs | 5 +- 5 files changed, 68 insertions(+), 60 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8c471eb3a..bb152dd98 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,6 +15,8 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; +export const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; + let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; @@ -91,6 +93,38 @@ export function getSound(s) { return soundMap.get()[s.toLowerCase()]; } +export const getAudioDevices = async () => { + await navigator.mediaDevices.getUserMedia({ audio: true }); + let mediaDevices = await navigator.mediaDevices.enumerateDevices(); + mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); + const devicesMap = new Map(); + devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, ''); + mediaDevices.forEach((device) => { + devicesMap.set(device.label, device.deviceId); + }); + return devicesMap; +}; + +export const setAudioDevice = async (id) => { + let audioCtx = getAudioContext(); + if (audioCtx.sinkId === id) { + return; + } + await audioCtx.suspend(); + await audioCtx.close(); + audioCtx = setDefaultAudioContext(); + await audioCtx.resume(); + const isValidID = (id ?? '').length > 0; + if (isValidID) { + try { + await audioCtx.setSinkId(id); + } catch { + logger('failed to set audio interface', 'warning'); + } + } + initializeAudioOutput(); +}; + const defaultDefaultValues = { s: 'triangle', gain: 0.8, @@ -161,19 +195,40 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); + const contextPromise = getAudioContext(); + workletsLoading = contextPromise.audioWorklet.addModule(workletsUrl); } + return workletsLoading; } // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName } = options; setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; } - await getAudioContext().resume(); + + const audioCtx = getAudioContext(); + + if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) { + try { + const devices = await getAudioDevices(); + const id = devices.get(audioDeviceName); + const isValidID = (id ?? '').length > 0; + if (audioCtx.sinkId !== id && isValidID) { + await audioCtx.setSinkId(id); + } + logger( + `[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`, + ); + } catch { + logger('[superdough] failed to set audio interface', 'warning'); + } + } + + await audioCtx.resume(); if (disableWorklets) { logger('[superdough]: AudioWorklets disabled with disableWorklets'); return; diff --git a/website/src/repl/components/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx index d1f13c22d..288ffc0a5 100644 --- a/website/src/repl/components/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; -import { getAudioDevices, setAudioDevice } from '../../util.mjs'; + import { SelectInput } from './SelectInput'; +import { getAudioDevices } from '@strudel/webaudio'; const initdevices = new Map(); @@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) { if (!devicesInitialized) { return; } - const deviceID = devices.get(deviceName); onChange(deviceName); - setAudioDevice(deviceID); }; const options = new Map(); Array.from(devices.keys()).forEach((deviceName) => { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 801a0886d..f0895aaa4 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -14,7 +14,7 @@ import { resetLoadedSounds, initAudioOnFirstClick, } from '@strudel/webaudio'; -import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; +import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -28,7 +28,7 @@ import { setViewingPatternData, } from '../user_pattern_utils.mjs'; import { superdirtOutput } from '@strudel/osc/superdirtoutput'; -import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs'; +import { audioEngineTargets } from '../settings.mjs'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; @@ -36,11 +36,11 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode, maxPolyphony } = settingsMap.get(); +const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick({ maxPolyphony }); + audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); @@ -159,20 +159,6 @@ export function useReplContext() { editorRef.current?.updateSettings(editorSettings); }, [_settings]); - // on first load, set stored audio device if possible - useEffect(() => { - const { audioDeviceName } = _settings; - if (audioDeviceName !== defaultAudioDeviceName) { - getAudioDevices().then((devices) => { - const deviceID = devices.get(audioDeviceName); - if (deviceID == null) { - return; - } - setAudioDevice(deviceID); - }); - } - }, []); - // // UI Actions // diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d184285..82f66cd4c 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; -import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio'; +import { settingPatterns } from '../settings.mjs'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,37 +159,6 @@ export const isUdels = () => { return window.top?.location?.pathname.includes('udels'); }; -export const getAudioDevices = async () => { - await navigator.mediaDevices.getUserMedia({ audio: true }); - let mediaDevices = await navigator.mediaDevices.enumerateDevices(); - mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); - const devicesMap = new Map(); - devicesMap.set(defaultAudioDeviceName, ''); - mediaDevices.forEach((device) => { - devicesMap.set(device.label, device.deviceId); - }); - return devicesMap; -}; - -export const setAudioDevice = async (id) => { - let audioCtx = getAudioContext(); - if (audioCtx.sinkId === id) { - return; - } - await audioCtx.suspend(); - await audioCtx.close(); - audioCtx = setDefaultAudioContext(); - await audioCtx.resume(); - const isValidID = (id ?? '').length > 0; - if (isValidID) { - try { - await audioCtx.setSinkId(id); - } catch { - logger('failed to set audio interface', 'warning'); - } - } - initializeAudioOutput(); -}; export function setVersionDefaultsFrom(code) { try { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 132d84bd5..c50c31f6e 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,8 +2,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; - -export const defaultAudioDeviceName = 'System Standard'; +import { DEFAULT_AUDIO_DEVICE_NAME } from '@strudel/webaudio'; export const audioEngineTargets = { webaudio: 'webaudio', @@ -36,7 +35,7 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: defaultAudioDeviceName, + audioDeviceName: DEFAULT_AUDIO_DEVICE_NAME, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, From b1090a1dd8a0ccb0a6c4c2e153a644afd5601eed Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:52:15 -0400 Subject: [PATCH 68/88] rm deadcode --- packages/superdough/superdough.mjs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index bb152dd98..b94d5d312 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -105,26 +105,6 @@ export const getAudioDevices = async () => { return devicesMap; }; -export const setAudioDevice = async (id) => { - let audioCtx = getAudioContext(); - if (audioCtx.sinkId === id) { - return; - } - await audioCtx.suspend(); - await audioCtx.close(); - audioCtx = setDefaultAudioContext(); - await audioCtx.resume(); - const isValidID = (id ?? '').length > 0; - if (isValidID) { - try { - await audioCtx.setSinkId(id); - } catch { - logger('failed to set audio interface', 'warning'); - } - } - initializeAudioOutput(); -}; - const defaultDefaultValues = { s: 'triangle', gain: 0.8, From 9e233fe587ed720d56de84a3d4ee4f0d3c7ca9f0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:55:34 -0400 Subject: [PATCH 69/88] var name --- packages/superdough/superdough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b94d5d312..55ff2eabf 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -175,8 +175,8 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - const contextPromise = getAudioContext(); - workletsLoading = contextPromise.audioWorklet.addModule(workletsUrl); + const audioCtx = getAudioContext(); + workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl); } return workletsLoading; From a32bb58fb324e1520deeca9eb1fdfe5539fd0ac9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Apr 2025 00:01:34 -0400 Subject: [PATCH 70/88] rm dependency --- packages/superdough/superdough.mjs | 4 ++-- website/src/settings.mjs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 55ff2eabf..e936d38e1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,7 +15,7 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; -export const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; +const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { @@ -184,7 +184,7 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony, audioDeviceName } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options; setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; diff --git a/website/src/settings.mjs b/website/src/settings.mjs index c50c31f6e..1b4ba33f9 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,7 +2,6 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; -import { DEFAULT_AUDIO_DEVICE_NAME } from '@strudel/webaudio'; export const audioEngineTargets = { webaudio: 'webaudio', @@ -35,7 +34,6 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: DEFAULT_AUDIO_DEVICE_NAME, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, From 2b69004eb442913072eacbcd22b0217c6571b4a0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Apr 2025 00:04:22 -0400 Subject: [PATCH 71/88] lint --- website/src/repl/util.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 82f66cd4c..f49dd568f 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; -import { setVersionDefaults } from '@strudel/webaudio'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,7 +159,6 @@ export const isUdels = () => { return window.top?.location?.pathname.includes('udels'); }; - export function setVersionDefaultsFrom(code) { try { const metadata = getMetadata(code); From ab76b4424a83967296987939df1299352a0356b5 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Fri, 11 Apr 2025 08:18:57 +0100 Subject: [PATCH 72/88] send delta in seconds, to match tidal/superdirt (#1323) --- packages/osc/osc.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 4b9ab14e7..3c7b92d4e 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -37,7 +37,7 @@ function connect() { export function parseControlsFromHap(hap, cps) { hap.ensureObjectValue(); const cycle = hap.wholeOrPart().begin.valueOf(); - const delta = hap.duration.valueOf(); + const delta = hap.duration.valueOf() / cps; const controls = Object.assign({}, { cps, cycle, delta }, hap.value); // make sure n and note are numbers controls.n && (controls.n = parseNumeral(controls.n)); From 2968017300c9cd6bd2b112191f2d799540ab2c70 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 17 Apr 2025 23:32:20 -0400 Subject: [PATCH 73/88] disabled --- website/astro.config.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 631584220..8b4fa4f86 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -58,6 +58,9 @@ const options = { // https://astro.build/config export default defineConfig({ + devToolbar: { + enabled: false + }, integrations: [ react(), mdx(options), From 6e3aa693d5898f9982f23bde05aeec14af98c0d5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 17 Apr 2025 23:35:10 -0400 Subject: [PATCH 74/88] format --- website/astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 8b4fa4f86..94c4e613a 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -59,7 +59,7 @@ const options = { // https://astro.build/config export default defineConfig({ devToolbar: { - enabled: false + enabled: false, }, integrations: [ react(), From ad25ce7b2199c200870615a4a82ced83570081cf Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 18 Apr 2025 01:01:10 -0400 Subject: [PATCH 75/88] working --- website/src/pages/udels/index.astro | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/website/src/pages/udels/index.astro b/website/src/pages/udels/index.astro index 4ab699e73..157e53151 100644 --- a/website/src/pages/udels/index.astro +++ b/website/src/pages/udels/index.astro @@ -1,12 +1,14 @@ --- +import HeadCommon from '@components/HeadCommon.astro'; import { Udels } from '../../components/Udels/Udels.jsx'; - - -const { BASE_URL } = import.meta.env; -const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; --- - - - - \ No newline at end of file + + + + Strudel UDELS + + + + + From 17e1ebeb93df8fa64d8b23f5abf29ddf9f122857 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 18 Apr 2025 01:02:13 -0400 Subject: [PATCH 76/88] udelsconfig --- website/astro.config.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 94c4e613a..631584220 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -58,9 +58,6 @@ const options = { // https://astro.build/config export default defineConfig({ - devToolbar: { - enabled: false, - }, integrations: [ react(), mdx(options), From bd08b74ebc5ffd7c23fe708d98dd4ac09765a5d9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 19 Apr 2025 16:10:38 -0400 Subject: [PATCH 77/88] fixed strudel bool --- packages/codemirror/flash.mjs | 4 +- packages/codemirror/themes.mjs | 58 +++++++++---------- packages/codemirror/themes/algoboy.mjs | 1 + packages/codemirror/themes/atomone.mjs | 2 + packages/codemirror/themes/green-text.mjs | 39 +++++++++++++ packages/codemirror/themes/red-text.mjs | 39 +++++++++++++ packages/codemirror/themes/sonic-pink.mjs | 39 +++++++++++++ packages/codemirror/themes/strudel-theme.mjs | 4 +- packages/codemirror/themes/theme-helper.mjs | 3 + .../src/repl/components/panel/SelectInput.jsx | 2 +- .../src/repl/components/panel/SettingsTab.jsx | 2 +- .../src/repl/components/textbox/Textbox.jsx | 2 +- 12 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 packages/codemirror/themes/green-text.mjs create mode 100644 packages/codemirror/themes/red-text.mjs create mode 100644 packages/codemirror/themes/sonic-pink.mjs diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 6b37038ff..243500bff 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -11,7 +11,9 @@ export const flashField = StateField.define({ for (let e of tr.effects) { if (e.is(setFlash)) { if (e.value && tr.newDoc.length > 0) { - const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } }); + const mark = Decoration.mark({ + attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, + }); flash = Decoration.set([mark.range(0, tr.newDoc.length)]); } else { flash = Decoration.set([]); diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index c2f49065a..771b6bdc1 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -5,16 +5,15 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre 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 sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs'; +import redText, { settings as redTextSettings } from './themes/red-text.mjs'; +import greenText, { settings as greenTextSettings } from './themes/green-text.mjs'; import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs'; import atomone, { settings as atomOneSettings } from './themes/atomone.mjs'; import aura, { settings as auraSettings } from './themes/aura.mjs'; -import bespin, { settings as bespinSettings } from './themes/bespin.mjs'; import darcula, { settings as darculaSettings } from './themes/darcula.mjs'; import dracula, { settings as draculaSettings } from './themes/dracula.mjs'; import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs'; -import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs'; import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs'; import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs'; import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs'; @@ -38,46 +37,45 @@ import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; import { setTheme } from '@strudel/draw'; - export const themes = { strudelTheme, - bluescreen, - blackscreen, - whitescreen, - teletext, algoboy, + androidstudio, atomone, aura, - darcula, - dracula, // todo: optimize // bespin, //abcdef, - androidstudio, - duotoneDark, - githubDark, + bbedit, + blackscreen, + bluescreen, CutiePi, + darcula, + dracula, + duotoneDark, + eclipse, + githubDark, + githubLight, + greenText, gruvboxDark, + gruvboxLight, + sonicPink, materialDark, - nord, + materialLight, monokai, + noctisLilac, + nord, + redText, solarizedDark, + solarizedLight, sublime, + teletext, tokyoNight, + tokyoNightDay, tokyoNightStorm, vscodeDark, - //xcodeDark, - // LIGHT - bbedit, - //duotoneLight, - eclipse, - githubLight, - gruvboxLight, - materialLight, vscodeLight, - noctisLilac, - solarizedLight, - tokyoNightDay, + whitescreen, xcodeLight, }; @@ -88,21 +86,19 @@ export const settings = { whitescreen: whitescreenSettings, teletext: teletextSettings, algoboy: algoboySettings, - terminal: terminalSettings, - abcdef: abcdefSettings, androidstudio: androidstudioSettings, atomone: atomOneSettings, aura: auraSettings, bbedit: bbeditSettings, - bespin: bespinSettings, darcula: darculaSettings, dracula: draculaSettings, - duotoneLight: duotoneLightSettings, duotoneDark: duotoneDarkSettings, eclipse: eclipseSettings, CutiePi: CutiePiSettings, + sonicPink: sonicPinkSettings, githubLight: githubLightSettings, githubDark: githubDarkSettings, + greenText: greenTextSettings, gruvboxDark: gruvboxDarkSettings, gruvboxLight: gruvboxLightSettings, materialDark: materialDarkSettings, @@ -110,6 +106,7 @@ export const settings = { noctisLilac: noctisLilacSettings, nord: nordSettings, monokai: monokaiSettings, + redText: redTextSettings, solarizedLight: solarizedLightSettings, solarizedDark: solarizedDarkSettings, sublime: sublimeSettings, @@ -118,7 +115,6 @@ export const settings = { vscodeDark: vscodeDarkSettings, vscodeLight: vscodeLightSettings, xcodeLight: xcodeLightSettings, - //xcodeDark: xcodeDarkSettings, tokyoNightDay: tokyoNightDaySettings, }; diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index 30812d126..d3c7e069d 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -54,6 +54,7 @@ export default createTheme({ tag: [t.keyword, t.tagName, t.arithmeticOperator], color: palette[1], }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0]}, { tag: [t.function(t.variableName), t.propertyName], color: palette[0] }, { tag: t.atom, color: palette[1] }, ], diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index 85951d157..eda87d4e3 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -38,12 +38,14 @@ export default createTheme({ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: 'hsl(207, 82%, 66%)', }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)'}, { tag: [t.tagName, t.heading], color: '#e06c75' }, { tag: t.comment, color: '#54636D' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, ], }); diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs new file mode 100644 index 000000000..bd21f6d3a --- /dev/null +++ b/packages/codemirror/themes/green-text.mjs @@ -0,0 +1,39 @@ +/** + * @name Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs new file mode 100644 index 000000000..9fe7f516a --- /dev/null +++ b/packages/codemirror/themes/red-text.mjs @@ -0,0 +1,39 @@ +/** + * @name Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717']; + +export const settings = { + background: hex[0], + lineBackground: 'transparent', + foreground: hex[2], + selection: hex[4], + selectionMatch: hex[0], + gutterBackground: hex[0], + gutterForeground: hex[3], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[2], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] }, + { tag: t.comment, color: hex[3] }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] }, + { tag: [t.attributeName, t.number], color: hex[1] }, + { tag: t.keyword, color: hex[2] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] }, + ], +}); diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs new file mode 100644 index 000000000..976597ecc --- /dev/null +++ b/packages/codemirror/themes/sonic-pink.mjs @@ -0,0 +1,39 @@ +/** + * @name Atom One + * Atom One dark syntax theme + * + * https://github.com/atom/one-dark-syntax + */ +import { tags as t } from '@lezer/highlight'; +import { createTheme } from './theme-helper.mjs'; + +const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c']; + +export const settings = { + background: '#000000', + lineBackground: 'transparent', + foreground: hex[4], + selection: hex[6], + gutterBackground: hex[0], + gutterForeground: hex[5], + gutterBorder: 'transparent', + lineHighlight: hex[0], +}; + +export default createTheme({ + theme: 'dark', + settings, + styles: [ + { + tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], + color: hex[4], + }, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] }, + + { tag: t.comment, color: '#54636D' }, + { tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] }, + { tag: [t.attributeName, t.number], color: hex[3] }, + { tag: t.keyword, color: hex[1] }, + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] }, + ], +}); diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 7fd6bb651..49e4fad13 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -5,14 +5,11 @@ export const settings = { background: '#222', lineBackground: '#22222299', foreground: '#fff', - // foreground: '#75baff', caret: '#ffcc00', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', - // lineHighlight: '#8a91991a', // original lineHighlight: '#00000050', gutterBackground: 'transparent', - // gutterForeground: '#8a919966', gutterForeground: '#8a919966', }; @@ -20,6 +17,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ + { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff'}, { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, diff --git a/packages/codemirror/themes/theme-helper.mjs b/packages/codemirror/themes/theme-helper.mjs index ee9bad8ef..952212990 100644 --- a/packages/codemirror/themes/theme-helper.mjs +++ b/packages/codemirror/themes/theme-helper.mjs @@ -1,7 +1,10 @@ import { EditorView } from '@codemirror/view'; +import { tags as t } from '@lezer/highlight'; import { syntaxHighlighting } from '@codemirror/language'; import { HighlightStyle } from '@codemirror/language'; + + export const createTheme = ({ theme, settings, styles }) => { const _theme = EditorView.theme( { diff --git a/website/src/repl/components/panel/SelectInput.jsx b/website/src/repl/components/panel/SelectInput.jsx index a28c39362..b47809671 100644 --- a/website/src/repl/components/panel/SelectInput.jsx +++ b/website/src/repl/components/panel/SelectInput.jsx @@ -5,7 +5,7 @@ export function SelectInput({ value, options, onChange, onClick, isDisabled }) { onChange(e.target.value)} > diff --git a/website/src/repl/components/textbox/Textbox.jsx b/website/src/repl/components/textbox/Textbox.jsx index f391dd6d9..1984007d9 100644 --- a/website/src/repl/components/textbox/Textbox.jsx +++ b/website/src/repl/components/textbox/Textbox.jsx @@ -3,7 +3,7 @@ import cx from '@src/cx.mjs'; export function Textbox({ onChange, className, ...inputProps }) { return ( onChange(e.target.value)} {...inputProps} /> From c852b5fab01be439d8f004f87c4635da136aa41f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 19 Apr 2025 16:21:11 -0400 Subject: [PATCH 78/88] fix lint --- packages/codemirror/themes.mjs | 4 -- packages/codemirror/themes/abcdef.mjs | 55 ------------------- packages/codemirror/themes/algoboy.mjs | 2 +- packages/codemirror/themes/atomone.mjs | 4 +- packages/codemirror/themes/bespin.mjs | 39 ------------- packages/codemirror/themes/strudel-theme.mjs | 2 +- packages/codemirror/themes/theme-helper.mjs | 3 - packages/codemirror/themes/xcodeDark.mjs | 34 ------------ .../src/repl/components/textbox/Textbox.jsx | 5 +- 9 files changed, 8 insertions(+), 140 deletions(-) delete mode 100644 packages/codemirror/themes/abcdef.mjs delete mode 100644 packages/codemirror/themes/bespin.mjs delete mode 100644 packages/codemirror/themes/xcodeDark.mjs diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 771b6bdc1..6aa70471d 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -31,7 +31,6 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs'; import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs'; import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs'; -// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs'; import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs'; import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs'; import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs'; @@ -43,9 +42,6 @@ export const themes = { androidstudio, atomone, aura, - // todo: optimize - // bespin, - //abcdef, bbedit, blackscreen, bluescreen, diff --git a/packages/codemirror/themes/abcdef.mjs b/packages/codemirror/themes/abcdef.mjs deleted file mode 100644 index 917823b9f..000000000 --- a/packages/codemirror/themes/abcdef.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @name abcdef - * @author codemirror.net - * https://codemirror.net/5/theme/abcdef.css - */ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#0f0f0f', - lineBackground: '#0f0f0f99', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - gutterBackground: '#555', - gutterForeground: '#FFFFFF', - lineHighlight: '#314151', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#0f0f0f', - foreground: '#defdef', - caret: '#00FF00', - selection: '#515151', - selectionMatch: '#515151', - // gutterBackground: '#555', - gutterBackground: 'transparent', - /* gutterForeground: '#FFFFFF', */ - gutterForeground: '#7a7b7c', - lineHighlight: '#0a6bcb3d', - }, - styles: [ - { tag: t.labelName, color: 'inherit' }, - { tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' }, - { tag: t.atom, color: '#77F' }, - { tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' }, - { tag: t.number, color: 'violet' }, - { tag: t.definition(t.variableName), color: '#fffabc' }, - { tag: t.variableName, color: '#abcdef' }, - { tag: t.function(t.variableName), color: '#fffabc' }, - { tag: t.typeName, color: '#FFDD44' }, - { tag: t.tagName, color: '#def' }, - { tag: t.string, color: '#2b4' }, - { tag: t.meta, color: '#C9F' }, - // { tag: t.qualifier, color: '#FFF700' }, - // { tag: t.builtin, color: '#30aabc' }, - { tag: t.bracket, color: '#8a8a8a' }, - { tag: t.attributeName, color: '#DDFF00' }, - { tag: t.heading, color: 'aquamarine', fontWeight: 'bold' }, - { tag: t.link, color: 'blueviolet', fontWeight: 'bold' }, - ], -}); diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index d3c7e069d..049ed596d 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -54,7 +54,7 @@ export default createTheme({ tag: [t.keyword, t.tagName, t.arithmeticOperator], color: palette[1], }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0]}, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] }, { tag: [t.function(t.variableName), t.propertyName], color: palette[0] }, { tag: t.atom, color: palette[1] }, ], diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index eda87d4e3..4c9fdec6e 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -38,14 +38,14 @@ export default createTheme({ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction], color: 'hsl(207, 82%, 66%)', }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)'}, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' }, { tag: [t.tagName, t.heading], color: '#e06c75' }, { tag: t.comment, color: '#54636D' }, { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' }, { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' }, { tag: t.className, color: 'hsl( 39, 67%, 69%)' }, { tag: t.keyword, color: 'hsl(286, 60%, 67%)' }, - + { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' }, ], }); diff --git a/packages/codemirror/themes/bespin.mjs b/packages/codemirror/themes/bespin.mjs deleted file mode 100644 index 41af821b0..000000000 --- a/packages/codemirror/themes/bespin.mjs +++ /dev/null @@ -1,39 +0,0 @@ -// this is different from https://thememirror.net/bespin -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#28211c', - lineBackground: '#28211c99', - foreground: '#9d9b97', - caret: '#797977', - selection: '#36312e', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: 'rgba(255, 255, 255, 0.1)', -}; -export default createTheme({ - theme: 'dark', - settings: { - background: '#28211c', - foreground: '#9d9b97', - caret: '#797977', - selection: '#4f382b', - selectionMatch: '#4f382b', - gutterBackground: '#28211c', - gutterForeground: '#666666', - lineHighlight: '#ffffff1a', - }, - styles: [ - { tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' }, - { tag: t.comment, color: '#937121' }, - { tag: [t.keyword, t.tagName], color: '#cf6a4c' }, - { tag: t.string, color: '#f9ee98' }, - { tag: t.bracket, color: '#9d9b97' }, - { tag: [t.variableName], color: '#5ea6ea' }, - { tag: t.definition(t.variableName), color: '#cf7d34' }, - { tag: [t.function(t.variableName), t.className], color: '#cf7d34' }, - { tag: [t.propertyName, t.attributeName], color: '#54be0d' }, - ], -}); diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 49e4fad13..7c4a3a919 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -17,7 +17,7 @@ export default createTheme({ theme: 'dark', settings, styles: [ - { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff'}, + { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' }, { tag: t.labelName, color: '#89ddff' }, { tag: t.keyword, color: '#c792ea' }, { tag: t.operator, color: '#89ddff' }, diff --git a/packages/codemirror/themes/theme-helper.mjs b/packages/codemirror/themes/theme-helper.mjs index 952212990..ee9bad8ef 100644 --- a/packages/codemirror/themes/theme-helper.mjs +++ b/packages/codemirror/themes/theme-helper.mjs @@ -1,10 +1,7 @@ import { EditorView } from '@codemirror/view'; -import { tags as t } from '@lezer/highlight'; import { syntaxHighlighting } from '@codemirror/language'; import { HighlightStyle } from '@codemirror/language'; - - export const createTheme = ({ theme, settings, styles }) => { const _theme = EditorView.theme( { diff --git a/packages/codemirror/themes/xcodeDark.mjs b/packages/codemirror/themes/xcodeDark.mjs deleted file mode 100644 index 41440cfd1..000000000 --- a/packages/codemirror/themes/xcodeDark.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { tags as t } from '@lezer/highlight'; -import { createTheme } from './theme-helper.mjs'; - -export const settings = { - background: '#292A30', - lineBackground: '#292A3099', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#2F3239', -}; - -export default createTheme({ - theme: 'dark', - settings: { - background: '#292A30', - foreground: '#CECFD0', - caret: '#fff', - selection: '#727377', - selectionMatch: '#727377', - lineHighlight: '#ffffff0f', - }, - styles: [ - { tag: [t.comment, t.quote], color: '#7F8C98' }, - { tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' }, - { tag: [t.string, t.meta], color: '#FF8170' }, - { tag: [t.typeName], color: '#DABAFF' }, - { tag: [t.definition(t.variableName)], color: '#6BDFFF' }, - { tag: [t.name], color: '#6BAA9F' }, - { tag: [t.variableName], color: '#ACF2E4' }, - { tag: [t.regexp, t.link], color: '#FF8170' }, - ], -}); diff --git a/website/src/repl/components/textbox/Textbox.jsx b/website/src/repl/components/textbox/Textbox.jsx index 1984007d9..6c3bc3283 100644 --- a/website/src/repl/components/textbox/Textbox.jsx +++ b/website/src/repl/components/textbox/Textbox.jsx @@ -3,7 +3,10 @@ import cx from '@src/cx.mjs'; export function Textbox({ onChange, className, ...inputProps }) { return ( onChange(e.target.value)} {...inputProps} /> From 3dae2a80ab113b484664abdced52beeef2e55937 Mon Sep 17 00:00:00 2001 From: hpunq <128979576+hpunq@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:51:44 +0100 Subject: [PATCH 79/88] change import sounds label to import sounds folder --- website/src/repl/components/panel/ImportSoundsButton.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx index 7862b7669..b668cba61 100644 --- a/website/src/repl/components/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -21,8 +21,9 @@ export default function ImportSoundsButton({ onComplete }) { return ( ); } From 759cdb627673f6883150b108fc3733e677bf624b Mon Sep 17 00:00:00 2001 From: hpunq <128979576+hpunq@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:53:42 +0100 Subject: [PATCH 80/88] add new import sounds tab with explanation and hyperlinks --- .../src/repl/components/panel/SoundsTab.jsx | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index c91721e00..4c88e8ae0 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -14,6 +14,8 @@ export function SoundsTab() { const sounds = useStore(soundMap); const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); + const { BASE_URL } = import.meta.env; + const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; const soundEntries = useMemo(() => { if (!sounds) { @@ -37,6 +39,9 @@ export function SoundsTab() { if (soundsFilter === 'synths') { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === 'importSounds') { + return []; + } return filtered; }, [sounds, soundsFilter, search]); @@ -51,7 +56,6 @@ export function SoundsTab() { ref?.stop(getAudioContext().currentTime + 0.01); }); }); - return (
setSearch(v)} /> @@ -65,9 +69,9 @@ export function SoundsTab() { drums: 'drum-machines', synths: 'Synths', user: 'User', + importSounds: 'import-sounds', }} > - settingsMap.setKey('soundsFilter', 'user')} />
@@ -101,7 +105,55 @@ export function SoundsTab() { ); })} - {!soundEntries.length ? 'No custom sounds loaded in this pattern (yet).' : ''} + {!soundEntries.length && soundsFilter === 'importSounds' ? ( +
+ settingsMap.setKey('soundsFilter', 'user')} /> +

+ To import sounds into strudel, they must be contained{' '} + + within a folder or subfolder + + . The best way to do this is to upload a “samples” folder containing subfolders of individual sounds or + soundbanks (see diagram below).{' '} +

+
+              {`└─ samples <-- import this folder
+   ├─ swoop
+   │  ├─ swoopshort.wav
+   │  ├─ swooplong.wav
+   │  └─ swooptight.wav
+   └─ smash
+      ├─ smashhigh.wav
+      ├─ smashlow.wav
+      └─ smashmiddle.wav`}
+            
+

+ The name of a subfolder corresponds to the sound name under the “user” tab. Multiple samples within a + subfolder are all labelled with the same name, but can be accessed using “.n( )” - remember sounds are + zero-indexed and in alphabetical order! +

+

+ For more information, and other ways to use your own sounds in strudel,{' '} + + check out the docs + + ! +

+

Preview Sounds

+
+              n("0 1 2 3 4 5").s("sample-name")
+            
+

+ Paste the line above into the main editor to hear the uploaded folder. Remember to use the name of your + sample as it appears under the "user" tab. +

+
+ ) : ( + '' + )} + {!soundEntries.length && soundsFilter !== 'importSounds' + ? 'No custom sounds loaded in this pattern (yet).' + : ''}
); From f539930509ec7c108f346c66534b54c0f2bc611b Mon Sep 17 00:00:00 2001 From: hpunq <128979576+hpunq@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:04:11 +0100 Subject: [PATCH 81/88] formatted code with prettier --- website/src/repl/components/panel/ImportSoundsButton.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx index b668cba61..999bbf69c 100644 --- a/website/src/repl/components/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -21,8 +21,7 @@ export default function ImportSoundsButton({ onComplete }) { return (