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>
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 { hbrick } = registerControl('hbrick');
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 { 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');
@ -1621,6 +1610,151 @@ export const ar = register('ar', (t, pat) => {
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) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);

View File

@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns.
```sh
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 { noteToMidi, getControlName } 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 },
);
});
}
@ -174,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)])
@ -196,18 +201,127 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
Pattern.prototype.midi = function (output) {
if (isPattern(output)) {
// 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');
}
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(
`.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'
}')`,
);
}
// 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(output, outputs);
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
@ -221,26 +335,35 @@ 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();
//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,
gain = 1,
velocity = 0.9,
midimap = 'default',
midiport = output,
midibend,
miditouch,
polyTouch,
gain = midiConfig.gain,
velocity = midiConfig.velocity,
progNum,
sysexid,
sysexdata,
midimap = midiConfig.midimap,
midiport = midiConfig.midiport,
} = hap.value;
const device = getDevice(midiport, WebMidi.outputs);
@ -252,24 +375,62 @@ 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) {
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
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) {
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) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
@ -282,6 +443,19 @@ 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') {
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 = {};
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(

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`] = `
[
"[ 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`] = `
[
"[ 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`] = `
[
"[ 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`] = `
[
"[ 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 * 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';
@ -126,6 +126,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,
@ -142,6 +148,8 @@ evalScope(
uiHelpers,
*/
{
midin,
sysex,
// gist,
// euclid,
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:
## 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.
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)
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
- `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()`} />
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} />
## 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
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"
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)
@ -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:
```
> 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:
@ -155,11 +272,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.
```
```