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 01/37] 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 32e66f6c..92cf8290 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 02/37] 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 92cf8290..83806f91 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 03/37] 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 83806f91..af8ca297 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 04/37] 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 af8ca297..a13ba584 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 05/37] 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 a13ba584..a7745620 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 06/37] 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 5a275294..1bec79a0 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 07/37] 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 6bb649f2..0efdd351 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 93cbfdc5..17435a81 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 08/37] 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 1bec79a0..b681db35 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 0efdd351..d8494728 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 00000000..6ba23737 --- /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 399227f7..2a226a53 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 a7745620..2ef266a1 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 09/37] 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 00000000..6e929991 --- /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 10/37] 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 6e929991..b7c1b321 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 11/37] 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 d8494728..2a1cc020 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 3871216c..5c4f363d 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 12/37] 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 2a1cc020..2678ab71 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 5c4f363d..7bd8dd09 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 13/37] 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 7bd8dd09..ad42c56d 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 14/37] 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 b681db35..7bfeb1e3 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 2ef266a1..5fabb3ea 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 15/37] 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 7bfeb1e3..00d6e7a2 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 5fabb3ea..058e7b68 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 16/37] 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 00d6e7a2..4d071a7a 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 058e7b68..fa7b2b6d 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 17/37] 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 4d071a7a..ca0bdacf 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 c071b536..0f644ce7 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 18d4a29e..80a06303 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 18/37] 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 ca0bdacf..1e1a1afb 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 fa7b2b6d..01b66395 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 19/37] 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 2678ab71..524baa17 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 ad42c56d..aab4be49 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 20/37] 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 1e1a1afb..1b4feb60 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 01b66395..b1d7e715 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 21/37] 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 b1d7e715..aee54202 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 22/37] 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 0f644ce7..9382bce7 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 23/37] 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 b7c1b321..00000000 --- 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 6ba23737..00000000 --- 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 2a226a53..63efd4cf 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 24/37] codeformat --- packages/midi/index.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/midi/index.mjs b/packages/midi/index.mjs index 63efd4cf..399227f7 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 25/37] 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 1b4feb60..37f37c98 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 9382bce7..4cdafb78 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 26/37] 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 aee54202..2c46d61c 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 27/37] 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 524baa17..faf9993f 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 28/37] 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 faf9993f..cefe40ab 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 29/37] '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 2c46d61c..b2b96e30 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 30/37] 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 b2b96e30..24b7e382 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 31/37] 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 ae3a71ca..b4f65fbf 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 a39e8e1d..4606dbb5 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 9eade34e..b395ed27 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 e0118f61..15d31f2a 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 32/37] 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 b4f65fbf..0c89ff6c 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 4606dbb5..9ce93396 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 33/37] 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 0c89ff6c..967c0904 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 9ce93396..8de6c447 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 3c2692bdda372915fd804f7332ec4ef272a63f78 Mon Sep 17 00:00:00 2001 From: nkymut Date: Sun, 9 Mar 2025 06:06:59 +0800 Subject: [PATCH 34/37] 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 8de6c447..4f518e0d 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 35/37] 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 4f518e0d..45f93d1d 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 36/37] 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 967c0904..82330bb8 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 cefe40ab..aa90992d 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 2920a7a4..ebd1c209 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 15d31f2a..92379aa6 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 37/37] 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 45f93d1d..ce7cdb0e 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