Merge pull request #1244 from nkymut/add-program-change

Add MIDI Program Change, SysEx, NRPN, PitchBend and AfterTouch Output
This commit is contained in:
Felix Roos 2025-03-20 23:35:17 +01:00 committed by GitHub
commit f652c2ca86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1024 additions and 53 deletions

View File

@ -1,5 +1,5 @@
/* /*
controls.mjs - <short description TODO> controls.mjs - Registers audio controls for pattern manipulation and effects.
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs>
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 <https://www.gnu.org/licenses/>. 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 <https://www.gnu.org/licenses/>.
*/ */
@ -1513,22 +1513,11 @@ export const { scram } = registerControl('scram');
export const { binshift } = registerControl('binshift'); export const { binshift } = registerControl('binshift');
export const { hbrick } = registerControl('hbrick'); export const { hbrick } = registerControl('hbrick');
export const { lbrick } = registerControl('lbrick'); export const { lbrick } = registerControl('lbrick');
export const { midichan } = registerControl('midichan');
export const { midimap } = registerControl('midimap');
export const { midiport } = registerControl('midiport');
export const { control } = registerControl('control');
export const { ccn } = registerControl('ccn');
export const { ccv } = registerControl('ccv');
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 { frameRate } = registerControl('frameRate');
export const { frames } = registerControl('frames'); export const { frames } = registerControl('frames');
export const { hours } = registerControl('hours'); export const { hours } = registerControl('hours');
export const { midicmd } = registerControl('midicmd');
export const { minutes } = registerControl('minutes'); export const { minutes } = registerControl('minutes');
export const { progNum } = registerControl('progNum');
export const { seconds } = registerControl('seconds'); export const { seconds } = registerControl('seconds');
export const { songPtr } = registerControl('songPtr'); export const { songPtr } = registerControl('songPtr');
export const { uid } = registerControl('uid'); export const { uid } = registerControl('uid');
@ -1621,6 +1610,151 @@ export const ar = register('ar', (t, pat) => {
return pat.set({ attack, release }); 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 { midimap } = registerControl('midimap');
/**
* MIDI port: Sets the MIDI port for the event.
*
* @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');
/**
* MIDI command: Sends a MIDI command message.
*
* @name midicmd
* @param {number | Pattern} command MIDI command
* @example
* midicmd("clock*48,<start stop>/2").midi()
*/
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 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');
/**
* 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]');
}
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');
/**
* 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');
export const getControlName = (alias) => { export const getControlName = (alias) => {
if (controlAlias.has(alias)) { if (controlAlias.has(alias)) {
return controlAlias.get(alias); return controlAlias.get(alias);

View File

@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns.
```sh ```sh
npm i @strudel/midi --save 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)
- `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)
- `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:
- `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?, 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.
```javascript
$: chord("<C^7 A7 Dm7 G7>").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'})
```
<details>
<summary>Available Options</summary>
| 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 |
</details>
### 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.
### 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.
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*48,<start stop>/2").midi('IAC Driver')
)
```
`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')
)
```

View File

@ -8,6 +8,7 @@ import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core'; import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core'; import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi'; import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi; export const { WebMidi } = _WebMidi;
@ -43,13 +44,16 @@ export function enableWebMidi(options = {}) {
resolve(WebMidi); resolve(WebMidi);
return; return;
} }
WebMidi.enable((err) => { WebMidi.enable(
if (err) { (err) => {
reject(err); if (err) {
} reject(err);
onReady?.(WebMidi); }
resolve(WebMidi); onReady?.(WebMidi);
}); resolve(WebMidi);
},
{ sysex: true },
);
}); });
} }
@ -174,6 +178,7 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) {
normalized = Math.min(1, Math.max(0, normalized)); normalized = Math.min(1, Math.max(0, normalized));
return Math.pow(normalized, exp); return Math.pow(normalized, exp);
} }
function mapCC(mapping, value) { function mapCC(mapping, value) {
return Object.keys(value) return Object.keys(value)
.filter((key) => !!mapping[getControlName(key)]) .filter((key) => !!mapping[getControlName(key)])
@ -196,18 +201,127 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
} }
Pattern.prototype.midi = function (output) { // sends a program change message to the given device on the given channel
if (isPattern(output)) { 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');
}
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
}
// 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');
}
} 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');
}
} 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 });
}
// 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');
}
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
}
// 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');
}
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} 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')
* @example
* note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 })
*/
Pattern.prototype.midi = function (midiport, options = {}) {
if (isPattern(midiport)) {
throw new Error( 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' WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`, }')`,
); );
} }
// 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({ enableWebMidi({
onEnabled: ({ outputs }) => { onEnabled: ({ outputs }) => {
const device = getDevice(output, outputs); const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name); const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger( logger(
`Midi enabled! Using "${device.name}". ${ `Midi enabled! Using "${device.name}". ${
@ -221,26 +335,35 @@ Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
console.log('not enabled'); logger('Midi not enabled');
return; return;
} }
hap.ensureObjectValue(); hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow //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 // 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}`; const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// destructure value // midi event values from hap with configurable defaults
let { let {
note, note,
nrpnn,
nrpv,
ccn, ccn,
ccv, ccv,
midichan = 1, midichan = midiConfig.midichannel,
midicmd, midicmd,
gain = 1, midibend,
velocity = 0.9, miditouch,
midimap = 'default', polyTouch,
midiport = output, gain = midiConfig.gain,
velocity = midiConfig.velocity,
progNum,
sysexid,
sysexdata,
midimap = midiConfig.midimap,
midiport = midiConfig.midiport,
} = hap.value; } = hap.value;
const device = getDevice(midiport, WebMidi.outputs); const device = getDevice(midiport, WebMidi.outputs);
@ -252,24 +375,62 @@ Pattern.prototype.midi = function (output) {
} }
velocity = gain * velocity; velocity = gain * velocity;
// Handle midimap
// if midimap is set, send a cc messages from defined controls // if midimap is set, send a cc messages from defined controls
if (midicontrolMap.has(midimap)) { if (midicontrolMap.has(midimap)) {
const ccs = mapCC(midicontrolMap.get(midimap), hap.value); const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString)); ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, 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 // Handle note
const duration = (hap.duration.valueOf() / cps) * 1000 - 10; if (note !== undefined && !midiConfig.isController) {
if (note != null) { // note off messages will often a few ms arrive late,
const midiNumber = typeof note === 'number' ? note : noteToMidi(note); // try to prevent glitching by subtracting noteOffsetMs from the duration length
const midiNote = new Note(midiNumber, { attack: velocity, duration }); const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
device.playNote(midiNote, midichan, {
time: timeOffsetString, sendNote(note, velocity, duration, device, midichan, timeOffsetString);
});
} }
// Handle program change
if (progNum !== undefined) {
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.
// list of manufacturer ids can be found here : https://midi.org/sysexidtable
// if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, timeOffsetString);
}
// Handle control change
if (ccv !== undefined && ccn !== undefined) { if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, timeOffsetString); sendCC(ccn, ccv, device, midichan, timeOffsetString);
} }
// Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
}
// Handle midibend
if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, timeOffsetString);
}
// Handle miditouch
if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, timeOffsetString);
}
// Handle midicmd
if (hap.whole.begin + 0 === 0) { if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info // we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString }); device.sendStart({ time: timeOffsetString });
@ -282,6 +443,19 @@ Pattern.prototype.midi = function (output) {
device.sendStop({ time: timeOffsetString }); device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) { } else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString }); device.sendContinue({ time: timeOffsetString });
} else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
} else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
}
} else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) {
const [_, id, data] = midicmd;
sendSysex(id, data, device, timeOffsetString);
}
}
} }
}); });
}; };
@ -289,6 +463,14 @@ Pattern.prototype.midi = function (output) {
let listeners = {}; let listeners = {};
const refs = {}; 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) { export async function midin(input) {
if (isPattern(input)) { if (isPattern(input)) {
throw new Error( throw new Error(

View File

@ -5206,6 +5206,292 @@ 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 "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 ]",
"[ 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 ]",
]
`;
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 "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 ]",
"[ 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 "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 ]",
"[ 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 ]",
]
`;
exports[`runs examples > example "mousex" example index 0 1`] = ` exports[`runs examples > example "mousex" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:C3 ]", "[ 0/1 → 1/4 | note:C3 ]",
@ -5426,6 +5712,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`] = ` exports[`runs examples > example "octave" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]", "[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
@ -6333,6 +6637,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`] = ` exports[`runs examples > example "pure" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | e4 ]", "[ 0/1 → 1/1 | e4 ]",
@ -8957,6 +9270,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 "take" example index 0 1`] = ` exports[`runs examples > example "take" example index 0 1`] = `
[ [
"[ 0/1 → 1/2 | s:bd ]", "[ 0/1 → 1/2 | s:bd ]",

View File

@ -11,7 +11,7 @@ import * as webaudio from '@strudel/webaudio';
import { mini, m } from '@strudel/mini/mini.mjs'; import { mini, m } from '@strudel/mini/mini.mjs';
// import * as voicingHelpers from '@strudel/tonal/voicings.mjs'; // import * as voicingHelpers from '@strudel/tonal/voicings.mjs';
// import euclid from '@strudel/core/euclid.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 * as tonalHelpers from '@strudel/tonal';
import '@strudel/xen/xen.mjs'; import '@strudel/xen/xen.mjs';
// import '@strudel/xen/tune.mjs'; // import '@strudel/xen/tune.mjs';
@ -126,6 +126,12 @@ const loadCsound = () => {};
const loadCSound = () => {}; 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 // TODO: refactor to evalScope
evalScope( evalScope(
// Tone, // Tone,
@ -142,6 +148,8 @@ evalScope(
uiHelpers, uiHelpers,
*/ */
{ {
midin,
sysex,
// gist, // gist,
// euclid, // euclid,
csound: id, csound: id,

View File

@ -16,24 +16,97 @@ 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: 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?)
<JsDoc client:idle name="midin" h={0} />
## midi(outputName?,options?)
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. 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. If no outputName is given, it uses the first midi output it finds.
<MiniRepl client:idle tune={`chord("<C^7 A7 Dm7 G7>").voicing().midi()`} /> <MiniRepl
client:idle
tune={`
$: chord("<C^7 A7 Dm7 G7>").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:
<MiniRepl
client:idle
tune={`$: note("d e c a f").midi('IAC Driver', { isController: true, midimap: 'default'})
`}
/>
<details>
<summary>Available Options</summary>
| 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 |
</details>
### 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();
```
<JsDoc client:idle name="midiport" h={0} />
## midichan(number) ## midichan(number)
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default. Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
## ccn && ccv ## midicmd(command)
`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
// 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.
<MiniRepl
client:idle
tune={`$:stack(
midicmd("clock*48,<start stop>/2").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 - `ccn` sets the cc number. Depends on your synths midi mapping
- `ccv` sets the cc value. normalized from 0 to 1. - `ccv` sets the cc value. normalized from 0 to 1.
<MiniRepl client:idle tune={`note("c a f e").control([74, sine.slow(4)]).midi()`} />
<MiniRepl client:idle tune={`note("c a f e").ccn(74).ccv(sine.slow(4)).midi()`} /> <MiniRepl client:idle tune={`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. In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
@ -56,6 +129,48 @@ Instead of setting `ccn` and `ccv` directly, you can also create mappings with `
<JsDoc client:idle name="defaultmidimap" h={0} /> <JsDoc client:idle name="defaultmidimap" h={0} />
## progNum (Program Change)
`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.
<MiniRepl client:idle tune={`// Switch between programs 0 and 1 every cycle
progNum("<0 1>").midi()
// 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` 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.
<MiniRepl
client:idle
tune={`// 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 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.
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)
<MiniRepl client:idle tune={`note("c a f e").midibend(sine.slow(4).range(-0.4,0.4)).midi()`} />
<MiniRepl client:idle tune={`note("c a f e").miditouch(sine.slow(4).range(0,1)).midi()`} />
# OSC/SuperDirt/StrudelDirt # 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. 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.
@ -118,8 +233,8 @@ The following example shows how to send a pattern to an MQTT broker:
client:only="react" client:only="react"
tune={`"hello world" tune={`"hello world"
.mqtt(undefined, // username (undefined for open/public servers) .mqtt(undefined, // username (undefined for open/public servers)
undefined, // password undefined, // password
'/strudel-pattern', // mqtt 'topic' '/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address 'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied 'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay) 0 // latency / delay before sending messages (0 = no delay)
@ -130,12 +245,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: 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 > mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
world > hello
hello > world
world > hello
... > world
> ...
``` ```
Control patterns will be encoded as JSON, for example: Control patterns will be encoded as JSON, for example:
@ -155,11 +272,17 @@ Control patterns will be encoded as JSON, for example:
Will send messages like the following: Will send messages like the following:
``` ```
{"s":"sax","speed":2} {"s":"sax","speed":2}
{"s":"sax","speed":2} {"s":"sax","speed":2}
{"s":"sax","speed":3} {"s":"sax","speed":3}
{"s":"sax","speed":2} {"s":"sax","speed":2}
... ...
``` ```
Libraries for receiving MQTT are available for many programming languages. Libraries for receiving MQTT are available for many programming languages.
```
```