mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 05:38:34 +00:00
Merge pull request #1244 from nkymut/add-program-change
Add MIDI Program Change, SysEx, NRPN, PitchBend and AfterTouch Output
This commit is contained in:
commit
f652c2ca86
@ -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);
|
||||
|
||||
@ -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')
|
||||
)
|
||||
```
|
||||
@ -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(
|
||||
|
||||
@ -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 ]",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user