mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-25 20:48:27 +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>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
@ -1513,22 +1513,11 @@ export const { scram } = registerControl('scram');
|
|||||||
export const { binshift } = registerControl('binshift');
|
export const { binshift } = registerControl('binshift');
|
||||||
export const { hbrick } = registerControl('hbrick');
|
export const { hbrick } = registerControl('hbrick');
|
||||||
export const { lbrick } = registerControl('lbrick');
|
export const { lbrick } = registerControl('lbrick');
|
||||||
export const { midichan } = registerControl('midichan');
|
|
||||||
export const { midimap } = registerControl('midimap');
|
|
||||||
export const { midiport } = registerControl('midiport');
|
|
||||||
export const { control } = registerControl('control');
|
|
||||||
export const { ccn } = registerControl('ccn');
|
|
||||||
export const { ccv } = registerControl('ccv');
|
|
||||||
export const { polyTouch } = registerControl('polyTouch');
|
|
||||||
export const { midibend } = registerControl('midibend');
|
|
||||||
export const { miditouch } = registerControl('miditouch');
|
|
||||||
export const { ctlNum } = registerControl('ctlNum');
|
|
||||||
export const { frameRate } = registerControl('frameRate');
|
export const { frameRate } = registerControl('frameRate');
|
||||||
export const { frames } = registerControl('frames');
|
export const { frames } = registerControl('frames');
|
||||||
export const { hours } = registerControl('hours');
|
export const { hours } = registerControl('hours');
|
||||||
export const { midicmd } = registerControl('midicmd');
|
|
||||||
export const { minutes } = registerControl('minutes');
|
export const { minutes } = registerControl('minutes');
|
||||||
export const { progNum } = registerControl('progNum');
|
|
||||||
export const { seconds } = registerControl('seconds');
|
export const { seconds } = registerControl('seconds');
|
||||||
export const { songPtr } = registerControl('songPtr');
|
export const { songPtr } = registerControl('songPtr');
|
||||||
export const { uid } = registerControl('uid');
|
export const { uid } = registerControl('uid');
|
||||||
@ -1621,6 +1610,151 @@ export const ar = register('ar', (t, pat) => {
|
|||||||
return pat.set({ attack, release });
|
return pat.set({ attack, release });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//MIDI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI channel: Sets the MIDI channel for the event.
|
||||||
|
*
|
||||||
|
* @name midichan
|
||||||
|
* @param {number | Pattern} channel MIDI channel number (0-15)
|
||||||
|
* @example
|
||||||
|
* note("c4").midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const { midichan } = registerControl('midichan');
|
||||||
|
|
||||||
|
export const { midimap } = registerControl('midimap');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI port: Sets the MIDI port for the event.
|
||||||
|
*
|
||||||
|
* @name midiport
|
||||||
|
* @param {number | Pattern} port MIDI port
|
||||||
|
* @example
|
||||||
|
* note("c a f e").midiport("<0 1 2 3>").midi()
|
||||||
|
*/
|
||||||
|
export const { midiport } = registerControl('midiport');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI command: Sends a MIDI command message.
|
||||||
|
*
|
||||||
|
* @name midicmd
|
||||||
|
* @param {number | Pattern} command MIDI command
|
||||||
|
* @example
|
||||||
|
* midicmd("clock*48,<start stop>/2").midi()
|
||||||
|
*/
|
||||||
|
export const { midicmd } = registerControl('midicmd');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI control: Sends a MIDI control change message.
|
||||||
|
*
|
||||||
|
* @name control
|
||||||
|
* @param {number | Pattern} MIDI control number (0-127)
|
||||||
|
* @param {number | Pattern} MIDI controller value (0-127)
|
||||||
|
*/
|
||||||
|
export const control = register('control', (args, pat) => {
|
||||||
|
if (!Array.isArray(args)) {
|
||||||
|
throw new Error('control expects an array of [ccn, ccv]');
|
||||||
|
}
|
||||||
|
const [_ccn, _ccv] = args;
|
||||||
|
return pat.ccn(_ccn).ccv(_ccv);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI control number: Sends a MIDI control change message.
|
||||||
|
*
|
||||||
|
* @name ccn
|
||||||
|
* @param {number | Pattern} MIDI control number (0-127)
|
||||||
|
*/
|
||||||
|
export const { ccn } = registerControl('ccn');
|
||||||
|
/**
|
||||||
|
* MIDI control value: Sends a MIDI control change message.
|
||||||
|
*
|
||||||
|
* @name ccv
|
||||||
|
* @param {number | Pattern} MIDI control value (0-127)
|
||||||
|
*/
|
||||||
|
export const { ccv } = registerControl('ccv');
|
||||||
|
export const { ctlNum } = registerControl('ctlNum');
|
||||||
|
// TODO: ctlVal?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message.
|
||||||
|
* @name nrpnn
|
||||||
|
* @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127)
|
||||||
|
* @example
|
||||||
|
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const { nrpnn } = registerControl('nrpnn');
|
||||||
|
/**
|
||||||
|
* MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message.
|
||||||
|
* @name nrpv
|
||||||
|
* @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127)
|
||||||
|
* @example
|
||||||
|
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const { nrpv } = registerControl('nrpv');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI program number: Sends a MIDI program change message.
|
||||||
|
*
|
||||||
|
* @name progNum
|
||||||
|
* @param {number | Pattern} program MIDI program number (0-127)
|
||||||
|
* @example
|
||||||
|
* note("c4").progNum(10).midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const { progNum } = registerControl('progNum');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI sysex: Sends a MIDI sysex message.
|
||||||
|
* @name sysex
|
||||||
|
* @param {number | Pattern} id Sysex ID
|
||||||
|
* @param {number | Pattern} data Sysex data
|
||||||
|
* @example
|
||||||
|
* note("c4").sysex(["0x77", "0x01:0x02:0x03:0x04"]).midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const sysex = register('sysex', (args, pat) => {
|
||||||
|
if (!Array.isArray(args)) {
|
||||||
|
throw new Error('sysex expects an array of [id, data]');
|
||||||
|
}
|
||||||
|
const [id, data] = args;
|
||||||
|
return pat.sysexid(id).sysexdata(data);
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* MIDI sysex ID: Sends a MIDI sysex identifier message.
|
||||||
|
* @name sysexid
|
||||||
|
* @param {number | Pattern} id Sysex ID
|
||||||
|
* @example
|
||||||
|
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const { sysexid } = registerControl('sysexid');
|
||||||
|
/**
|
||||||
|
* MIDI sysex data: Sends a MIDI sysex message.
|
||||||
|
* @name sysexdata
|
||||||
|
* @param {number | Pattern} data Sysex data
|
||||||
|
* @example
|
||||||
|
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
|
||||||
|
*/
|
||||||
|
export const { sysexdata } = registerControl('sysexdata');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI pitch bend: Sends a MIDI pitch bend message.
|
||||||
|
* @name midibend
|
||||||
|
* @param {number | Pattern} midibend MIDI pitch bend (-1 - 1)
|
||||||
|
* @example
|
||||||
|
* note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi()
|
||||||
|
*/
|
||||||
|
export const { midibend } = registerControl('midibend');
|
||||||
|
/**
|
||||||
|
* MIDI key after touch: Sends a MIDI key after touch message.
|
||||||
|
* @name miditouch
|
||||||
|
* @param {number | Pattern} miditouch MIDI key after touch (0-1)
|
||||||
|
* @example
|
||||||
|
* note("c4").miditouch(sine.slow(4).range(0,1)).midi()
|
||||||
|
*/
|
||||||
|
export const { miditouch } = registerControl('miditouch');
|
||||||
|
|
||||||
|
// TODO: what is this?
|
||||||
|
export const { polyTouch } = registerControl('polyTouch');
|
||||||
|
|
||||||
export const getControlName = (alias) => {
|
export const getControlName = (alias) => {
|
||||||
if (controlAlias.has(alias)) {
|
if (controlAlias.has(alias)) {
|
||||||
return controlAlias.get(alias);
|
return controlAlias.get(alias);
|
||||||
|
|||||||
@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns.
|
|||||||
```sh
|
```sh
|
||||||
npm i @strudel/midi --save
|
npm i @strudel/midi --save
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Available Controls
|
||||||
|
|
||||||
|
The following MIDI controls are available:
|
||||||
|
|
||||||
|
OUTPUT:
|
||||||
|
|
||||||
|
- `midi` - opens a midi output device.
|
||||||
|
- `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127)
|
||||||
|
- `midichan` - Sets the MIDI channel (1-16, defaults to 1)
|
||||||
|
- `velocity` - Sets note velocity (0-1, defaults to 0.9)
|
||||||
|
- `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1)
|
||||||
|
- `control` - Sets MIDI control change messages
|
||||||
|
- `ccn` - Sets MIDI CC controller number (0-127)
|
||||||
|
- `ccv` - Sets MIDI CC value (0-1)
|
||||||
|
- `progNum` - Sends MIDI program change messages (0-127)
|
||||||
|
- `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127)
|
||||||
|
- `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127)
|
||||||
|
- `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127)
|
||||||
|
- `midibend` - Sets MIDI pitch bend (-1 - 1)
|
||||||
|
- `miditouch` - Sets MIDI key after touch (0-1)
|
||||||
|
- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices.
|
||||||
|
- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127)
|
||||||
|
- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127)
|
||||||
|
|
||||||
|
|
||||||
|
INPUT:
|
||||||
|
|
||||||
|
- `midin` - Opens a MIDI input port to receive MIDI control change messages.
|
||||||
|
|
||||||
|
Additional controls can be mapped using the mapping object passed to `.midi()`:
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### midi(outputName?, options?)
|
||||||
|
|
||||||
|
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||||
|
If no outputName is given, it uses the first midi output it finds.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
|
||||||
|
```
|
||||||
|
|
||||||
|
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
The `.midi()` function accepts an options object with the following properties:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$: note("c a f e").midi('IAC Driver', { isController: true, midimap: 'default'})
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Available Options</summary>
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
|
||||||
|
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
|
||||||
|
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
|
||||||
|
| midichannel | number | 1 | Default MIDI channel (1-16) |
|
||||||
|
| velocity | number | 0.9 | Default note velocity (0-1) |
|
||||||
|
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
|
||||||
|
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
|
||||||
|
| midiport | string/number | - | MIDI device name or index |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### midiport(outputName)
|
||||||
|
|
||||||
|
Selects the MIDI output device to use, pattern can be used to switch between devices.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$: midiport('IAC Driver')
|
||||||
|
$: note("c a f e").midiport("<0 1 2 3>").midi()
|
||||||
|
```
|
||||||
|
|
||||||
|
### midichan(number)
|
||||||
|
|
||||||
|
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
|
||||||
|
|
||||||
|
### control, ccn && ccv
|
||||||
|
|
||||||
|
`control` sends MIDI control change messages to your MIDI device.
|
||||||
|
|
||||||
|
- `ccn` sets the cc number. Depends on your synths midi mapping
|
||||||
|
- `ccv` sets the cc value. normalized from 0 to 1.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$: note("c a f e").control([74, sine.slow(4)]).midi()
|
||||||
|
$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi()
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
|
||||||
|
Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default.
|
||||||
|
But you can also control cc messages separately like this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$: note("c a f e").midi()
|
||||||
|
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()
|
||||||
|
```
|
||||||
|
|
||||||
|
### progNum (Program Change)
|
||||||
|
|
||||||
|
`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device.
|
||||||
|
Program change values should be numbers between 0 and 127.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Play notes while changing programs
|
||||||
|
note("c3 e3 g3").progNum("<0 1 2>").midi()
|
||||||
|
```
|
||||||
|
|
||||||
|
Program change messages are useful for switching between different instrument sounds or presets during a performance.
|
||||||
|
The exact sound that each program number maps to depends on your MIDI device's configuration.
|
||||||
|
|
||||||
|
## sysex, sysexid && sysexdata (System Exclusive Message)
|
||||||
|
|
||||||
|
`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device.
|
||||||
|
sysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
|
||||||
|
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Send a simple SysEx message
|
||||||
|
let id = 0x43; //Yamaha
|
||||||
|
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
|
||||||
|
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
|
||||||
|
$: note("c d e f e d c").sysex(id, data).midi();
|
||||||
|
$: note("c d e f e d c").sysexid(id).sysexdata(data).midi();
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact format of SysEx messages depends on your MIDI device's specification.
|
||||||
|
Consult your device's MIDI implementation guide for details on supported SysEx messages.
|
||||||
|
|
||||||
|
### midibend && miditouch
|
||||||
|
|
||||||
|
`midibend` sets MIDI pitch bend (-1 - 1)
|
||||||
|
`miditouch` sets MIDI key after touch (0-1)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi();
|
||||||
|
$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi();
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### midicmd
|
||||||
|
|
||||||
|
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
|
||||||
|
|
||||||
|
It supports the following commands:
|
||||||
|
|
||||||
|
- `clock`/`midiClock` - Sends MIDI timing clock messages
|
||||||
|
- `start` - Sends MIDI start message
|
||||||
|
- `stop` - Sends MIDI stop message
|
||||||
|
- `continue` - Sends MIDI continue message
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
|
||||||
|
// Note: It might act unexpectedly if MIDI isn't set up initially.
|
||||||
|
stack(
|
||||||
|
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`midicmd` also supports sending control change, program change and sysex messages.
|
||||||
|
|
||||||
|
- `cc` - sends MIDI control change messages.
|
||||||
|
- `progNum` - sends MIDI program change messages.
|
||||||
|
- `sysex` - sends MIDI system exclusive messages.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
stack(
|
||||||
|
// "cc:ccn:ccv"
|
||||||
|
midicmd("cc:74:1").midi('IAC Driver'),
|
||||||
|
// "progNum:progNum"
|
||||||
|
midicmd("progNum:1").midi('IAC Driver'),
|
||||||
|
// "sysex:[sysexid]:[sysexdata]"
|
||||||
|
midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver')
|
||||||
|
)
|
||||||
|
```
|
||||||
@ -8,6 +8,7 @@ import * as _WebMidi from 'webmidi';
|
|||||||
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
||||||
import { noteToMidi, getControlName } from '@strudel/core';
|
import { noteToMidi, getControlName } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
|
|
||||||
@ -43,13 +44,16 @@ export function enableWebMidi(options = {}) {
|
|||||||
resolve(WebMidi);
|
resolve(WebMidi);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WebMidi.enable((err) => {
|
WebMidi.enable(
|
||||||
if (err) {
|
(err) => {
|
||||||
reject(err);
|
if (err) {
|
||||||
}
|
reject(err);
|
||||||
onReady?.(WebMidi);
|
}
|
||||||
resolve(WebMidi);
|
onReady?.(WebMidi);
|
||||||
});
|
resolve(WebMidi);
|
||||||
|
},
|
||||||
|
{ sysex: true },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +178,7 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) {
|
|||||||
normalized = Math.min(1, Math.max(0, normalized));
|
normalized = Math.min(1, Math.max(0, normalized));
|
||||||
return Math.pow(normalized, exp);
|
return Math.pow(normalized, exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCC(mapping, value) {
|
function mapCC(mapping, value) {
|
||||||
return Object.keys(value)
|
return Object.keys(value)
|
||||||
.filter((key) => !!mapping[getControlName(key)])
|
.filter((key) => !!mapping[getControlName(key)])
|
||||||
@ -196,18 +201,127 @@ function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
|
|||||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
// sends a program change message to the given device on the given channel
|
||||||
if (isPattern(output)) {
|
function sendProgramChange(progNum, device, midichan, timeOffsetString) {
|
||||||
|
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
|
||||||
|
throw new Error('expected progNum (program change) to be a number between 0 and 127');
|
||||||
|
}
|
||||||
|
device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a sysex message to the given device on the given channel
|
||||||
|
function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
|
||||||
|
if (Array.isArray(sysexid)) {
|
||||||
|
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
|
throw new Error('all sysexid bytes must be integers between 0 and 255');
|
||||||
|
}
|
||||||
|
} else if (!Number.isInteger(sysexid) || sysexid < 0 || sysexid > 255) {
|
||||||
|
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(sysexdata)) {
|
||||||
|
throw new Error('expected sysex to be an array of numbers (0-255)');
|
||||||
|
}
|
||||||
|
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
|
throw new Error('all sysex bytes must be integers between 0 and 255');
|
||||||
|
}
|
||||||
|
device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a NRPN message to the given device on the given channel
|
||||||
|
function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
|
||||||
|
if (Array.isArray(nrpnn)) {
|
||||||
|
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
|
throw new Error('all nrpnn bytes must be integers between 0 and 255');
|
||||||
|
}
|
||||||
|
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
|
||||||
|
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
||||||
|
}
|
||||||
|
|
||||||
|
device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a pitch bend message to the given device on the given channel
|
||||||
|
function sendPitchBend(midibend, device, midichan, timeOffsetString) {
|
||||||
|
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
|
||||||
|
throw new Error('expected midibend to be a number between -1 and 1');
|
||||||
|
}
|
||||||
|
device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a channel aftertouch message to the given device on the given channel
|
||||||
|
function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
|
||||||
|
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
|
||||||
|
throw new Error('expected miditouch to be a number between 0 and 1');
|
||||||
|
}
|
||||||
|
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a note message to the given device on the given channel
|
||||||
|
function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
|
||||||
|
if (note == null || note === '') {
|
||||||
|
throw new Error('note cannot be null or empty');
|
||||||
|
}
|
||||||
|
if (velocity != null && (typeof velocity !== 'number' || velocity < 0 || velocity > 1)) {
|
||||||
|
throw new Error('velocity must be a number between 0 and 1');
|
||||||
|
}
|
||||||
|
if (duration != null && (typeof duration !== 'number' || duration < 0)) {
|
||||||
|
throw new Error('duration must be a positive number');
|
||||||
|
}
|
||||||
|
|
||||||
|
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||||
|
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
||||||
|
device.playNote(midiNote, midichan, {
|
||||||
|
time: timeOffsetString,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI output: Opens a MIDI output port.
|
||||||
|
* @param {string | number} midiport MIDI device name or index defaulting to 0
|
||||||
|
* @param {object} options Additional MIDI configuration options
|
||||||
|
* @example
|
||||||
|
* note("c4").midichan(1).midi('IAC Driver Bus 1')
|
||||||
|
* @example
|
||||||
|
* note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 })
|
||||||
|
*/
|
||||||
|
|
||||||
|
Pattern.prototype.midi = function (midiport, options = {}) {
|
||||||
|
if (isPattern(midiport)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
`.midi does not accept Pattern input for midiport. Make sure to pass device name with single quotes. Example: .midi('${
|
||||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For backward compatibility
|
||||||
|
if (typeof midiport === 'object') {
|
||||||
|
const { port, isController = false, ...configOptions } = midiport;
|
||||||
|
options = {
|
||||||
|
isController,
|
||||||
|
...configOptions,
|
||||||
|
...options, // Keep any options passed separately
|
||||||
|
};
|
||||||
|
midiport = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
let midiConfig = {
|
||||||
|
// Default configuration values
|
||||||
|
isController: false, // Disable sending notes for midi controllers
|
||||||
|
latencyMs: 34, // Default latency to get audio engine to line up in ms
|
||||||
|
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
|
||||||
|
midichannel: 1, // Default MIDI channel
|
||||||
|
velocity: 0.9, // Default velocity
|
||||||
|
gain: 1, // Default gain
|
||||||
|
midimap: 'default', // Default MIDI map
|
||||||
|
midiport: midiport, // Store the port in the config
|
||||||
|
...options, // Override defaults with provided options
|
||||||
|
};
|
||||||
|
|
||||||
enableWebMidi({
|
enableWebMidi({
|
||||||
onEnabled: ({ outputs }) => {
|
onEnabled: ({ outputs }) => {
|
||||||
const device = getDevice(output, outputs);
|
const device = getDevice(midiConfig.midiport, outputs);
|
||||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
||||||
logger(
|
logger(
|
||||||
`Midi enabled! Using "${device.name}". ${
|
`Midi enabled! Using "${device.name}". ${
|
||||||
@ -221,26 +335,35 @@ Pattern.prototype.midi = function (output) {
|
|||||||
|
|
||||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
console.log('not enabled');
|
logger('Midi not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latencyMs = 34;
|
const latencyMs = midiConfig.latencyMs;
|
||||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||||
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
||||||
|
|
||||||
// destructure value
|
// midi event values from hap with configurable defaults
|
||||||
let {
|
let {
|
||||||
note,
|
note,
|
||||||
|
nrpnn,
|
||||||
|
nrpv,
|
||||||
ccn,
|
ccn,
|
||||||
ccv,
|
ccv,
|
||||||
midichan = 1,
|
midichan = midiConfig.midichannel,
|
||||||
midicmd,
|
midicmd,
|
||||||
gain = 1,
|
midibend,
|
||||||
velocity = 0.9,
|
miditouch,
|
||||||
midimap = 'default',
|
polyTouch,
|
||||||
midiport = output,
|
gain = midiConfig.gain,
|
||||||
|
velocity = midiConfig.velocity,
|
||||||
|
progNum,
|
||||||
|
sysexid,
|
||||||
|
sysexdata,
|
||||||
|
midimap = midiConfig.midimap,
|
||||||
|
midiport = midiConfig.midiport,
|
||||||
} = hap.value;
|
} = hap.value;
|
||||||
|
|
||||||
const device = getDevice(midiport, WebMidi.outputs);
|
const device = getDevice(midiport, WebMidi.outputs);
|
||||||
@ -252,24 +375,62 @@ Pattern.prototype.midi = function (output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
velocity = gain * velocity;
|
velocity = gain * velocity;
|
||||||
|
|
||||||
|
// Handle midimap
|
||||||
// if midimap is set, send a cc messages from defined controls
|
// if midimap is set, send a cc messages from defined controls
|
||||||
if (midicontrolMap.has(midimap)) {
|
if (midicontrolMap.has(midimap)) {
|
||||||
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
||||||
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
||||||
|
} else if (midimap !== 'default') {
|
||||||
|
// Add warning when a non-existent midimap is specified
|
||||||
|
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
// Handle note
|
||||||
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
|
if (note !== undefined && !midiConfig.isController) {
|
||||||
if (note != null) {
|
// note off messages will often a few ms arrive late,
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
// try to prevent glitching by subtracting noteOffsetMs from the duration length
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
|
||||||
device.playNote(midiNote, midichan, {
|
|
||||||
time: timeOffsetString,
|
sendNote(note, velocity, duration, device, midichan, timeOffsetString);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle program change
|
||||||
|
if (progNum !== undefined) {
|
||||||
|
sendProgramChange(progNum, device, midichan, timeOffsetString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sysex
|
||||||
|
// sysex data is consist of 2 arrays, first is sysexid, second is sysexdata
|
||||||
|
// sysexid is a manufacturer id it is either a number or an array of 3 numbers.
|
||||||
|
// list of manufacturer ids can be found here : https://midi.org/sysexidtable
|
||||||
|
// if sysexid is an array the first byte is 0x00
|
||||||
|
|
||||||
|
if (sysexid !== undefined && sysexdata !== undefined) {
|
||||||
|
sendSysex(sysexid, sysexdata, device, timeOffsetString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle control change
|
||||||
if (ccv !== undefined && ccn !== undefined) {
|
if (ccv !== undefined && ccn !== undefined) {
|
||||||
sendCC(ccn, ccv, device, midichan, timeOffsetString);
|
sendCC(ccn, ccv, device, midichan, timeOffsetString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle NRPN non-registered parameter number
|
||||||
|
if (nrpnn !== undefined && nrpv !== undefined) {
|
||||||
|
sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle midibend
|
||||||
|
if (midibend !== undefined) {
|
||||||
|
sendPitchBend(midibend, device, midichan, timeOffsetString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle miditouch
|
||||||
|
if (miditouch !== undefined) {
|
||||||
|
sendAftertouch(miditouch, device, midichan, timeOffsetString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle midicmd
|
||||||
if (hap.whole.begin + 0 === 0) {
|
if (hap.whole.begin + 0 === 0) {
|
||||||
// we need to start here because we have the timing info
|
// we need to start here because we have the timing info
|
||||||
device.sendStart({ time: timeOffsetString });
|
device.sendStart({ time: timeOffsetString });
|
||||||
@ -282,6 +443,19 @@ Pattern.prototype.midi = function (output) {
|
|||||||
device.sendStop({ time: timeOffsetString });
|
device.sendStop({ time: timeOffsetString });
|
||||||
} else if (['continue'].includes(midicmd)) {
|
} else if (['continue'].includes(midicmd)) {
|
||||||
device.sendContinue({ time: timeOffsetString });
|
device.sendContinue({ time: timeOffsetString });
|
||||||
|
} else if (Array.isArray(midicmd)) {
|
||||||
|
if (midicmd[0] === 'progNum') {
|
||||||
|
sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
|
||||||
|
} else if (midicmd[0] === 'cc') {
|
||||||
|
if (midicmd.length === 2) {
|
||||||
|
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
|
||||||
|
}
|
||||||
|
} else if (midicmd[0] === 'sysex') {
|
||||||
|
if (midicmd.length === 3) {
|
||||||
|
const [_, id, data] = midicmd;
|
||||||
|
sendSysex(id, data, device, timeOffsetString);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -289,6 +463,14 @@ Pattern.prototype.midi = function (output) {
|
|||||||
let listeners = {};
|
let listeners = {};
|
||||||
const refs = {};
|
const refs = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
||||||
|
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||||
|
* @returns {Function}
|
||||||
|
* @example
|
||||||
|
* let cc = await midin('IAC Driver Bus 1')
|
||||||
|
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
|
||||||
|
*/
|
||||||
export async function midin(input) {
|
export async function midin(input) {
|
||||||
if (isPattern(input)) {
|
if (isPattern(input)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -5206,6 +5206,292 @@ exports[`runs examples > example "mask" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midi" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midi" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midibend" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 midibend:0 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 midibend:0.4 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 midibend:1.1102230246251565e-16 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 midibend:-0.4 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midichan" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midicmd" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/48 | midicmd:clock ]",
|
||||||
|
"[ 0/1 → 2/1 | midicmd:start ]",
|
||||||
|
"[ 1/48 → 1/24 | midicmd:clock ]",
|
||||||
|
"[ 1/24 → 1/16 | midicmd:clock ]",
|
||||||
|
"[ 1/16 → 1/12 | midicmd:clock ]",
|
||||||
|
"[ 1/12 → 5/48 | midicmd:clock ]",
|
||||||
|
"[ 5/48 → 1/8 | midicmd:clock ]",
|
||||||
|
"[ 1/8 → 7/48 | midicmd:clock ]",
|
||||||
|
"[ 7/48 → 1/6 | midicmd:clock ]",
|
||||||
|
"[ 1/6 → 3/16 | midicmd:clock ]",
|
||||||
|
"[ 3/16 → 5/24 | midicmd:clock ]",
|
||||||
|
"[ 5/24 → 11/48 | midicmd:clock ]",
|
||||||
|
"[ 11/48 → 1/4 | midicmd:clock ]",
|
||||||
|
"[ 1/4 → 13/48 | midicmd:clock ]",
|
||||||
|
"[ 13/48 → 7/24 | midicmd:clock ]",
|
||||||
|
"[ 7/24 → 5/16 | midicmd:clock ]",
|
||||||
|
"[ 5/16 → 1/3 | midicmd:clock ]",
|
||||||
|
"[ 1/3 → 17/48 | midicmd:clock ]",
|
||||||
|
"[ 17/48 → 3/8 | midicmd:clock ]",
|
||||||
|
"[ 3/8 → 19/48 | midicmd:clock ]",
|
||||||
|
"[ 19/48 → 5/12 | midicmd:clock ]",
|
||||||
|
"[ 5/12 → 7/16 | midicmd:clock ]",
|
||||||
|
"[ 7/16 → 11/24 | midicmd:clock ]",
|
||||||
|
"[ 11/24 → 23/48 | midicmd:clock ]",
|
||||||
|
"[ 23/48 → 1/2 | midicmd:clock ]",
|
||||||
|
"[ 1/2 → 25/48 | midicmd:clock ]",
|
||||||
|
"[ 25/48 → 13/24 | midicmd:clock ]",
|
||||||
|
"[ 13/24 → 9/16 | midicmd:clock ]",
|
||||||
|
"[ 9/16 → 7/12 | midicmd:clock ]",
|
||||||
|
"[ 7/12 → 29/48 | midicmd:clock ]",
|
||||||
|
"[ 29/48 → 5/8 | midicmd:clock ]",
|
||||||
|
"[ 5/8 → 31/48 | midicmd:clock ]",
|
||||||
|
"[ 31/48 → 2/3 | midicmd:clock ]",
|
||||||
|
"[ 2/3 → 11/16 | midicmd:clock ]",
|
||||||
|
"[ 11/16 → 17/24 | midicmd:clock ]",
|
||||||
|
"[ 17/24 → 35/48 | midicmd:clock ]",
|
||||||
|
"[ 35/48 → 3/4 | midicmd:clock ]",
|
||||||
|
"[ 3/4 → 37/48 | midicmd:clock ]",
|
||||||
|
"[ 37/48 → 19/24 | midicmd:clock ]",
|
||||||
|
"[ 19/24 → 13/16 | midicmd:clock ]",
|
||||||
|
"[ 13/16 → 5/6 | midicmd:clock ]",
|
||||||
|
"[ 5/6 → 41/48 | midicmd:clock ]",
|
||||||
|
"[ 41/48 → 7/8 | midicmd:clock ]",
|
||||||
|
"[ 7/8 → 43/48 | midicmd:clock ]",
|
||||||
|
"[ 43/48 → 11/12 | midicmd:clock ]",
|
||||||
|
"[ 11/12 → 15/16 | midicmd:clock ]",
|
||||||
|
"[ 15/16 → 23/24 | midicmd:clock ]",
|
||||||
|
"[ 23/24 → 47/48 | midicmd:clock ]",
|
||||||
|
"[ 47/48 → 1/1 | midicmd:clock ]",
|
||||||
|
"[ 1/1 → 49/48 | midicmd:clock ]",
|
||||||
|
"[ 49/48 → 25/24 | midicmd:clock ]",
|
||||||
|
"[ 25/24 → 17/16 | midicmd:clock ]",
|
||||||
|
"[ 17/16 → 13/12 | midicmd:clock ]",
|
||||||
|
"[ 13/12 → 53/48 | midicmd:clock ]",
|
||||||
|
"[ 53/48 → 9/8 | midicmd:clock ]",
|
||||||
|
"[ 9/8 → 55/48 | midicmd:clock ]",
|
||||||
|
"[ 55/48 → 7/6 | midicmd:clock ]",
|
||||||
|
"[ 7/6 → 19/16 | midicmd:clock ]",
|
||||||
|
"[ 19/16 → 29/24 | midicmd:clock ]",
|
||||||
|
"[ 29/24 → 59/48 | midicmd:clock ]",
|
||||||
|
"[ 59/48 → 5/4 | midicmd:clock ]",
|
||||||
|
"[ 5/4 → 61/48 | midicmd:clock ]",
|
||||||
|
"[ 61/48 → 31/24 | midicmd:clock ]",
|
||||||
|
"[ 31/24 → 21/16 | midicmd:clock ]",
|
||||||
|
"[ 21/16 → 4/3 | midicmd:clock ]",
|
||||||
|
"[ 4/3 → 65/48 | midicmd:clock ]",
|
||||||
|
"[ 65/48 → 11/8 | midicmd:clock ]",
|
||||||
|
"[ 11/8 → 67/48 | midicmd:clock ]",
|
||||||
|
"[ 67/48 → 17/12 | midicmd:clock ]",
|
||||||
|
"[ 17/12 → 23/16 | midicmd:clock ]",
|
||||||
|
"[ 23/16 → 35/24 | midicmd:clock ]",
|
||||||
|
"[ 35/24 → 71/48 | midicmd:clock ]",
|
||||||
|
"[ 71/48 → 3/2 | midicmd:clock ]",
|
||||||
|
"[ 3/2 → 73/48 | midicmd:clock ]",
|
||||||
|
"[ 73/48 → 37/24 | midicmd:clock ]",
|
||||||
|
"[ 37/24 → 25/16 | midicmd:clock ]",
|
||||||
|
"[ 25/16 → 19/12 | midicmd:clock ]",
|
||||||
|
"[ 19/12 → 77/48 | midicmd:clock ]",
|
||||||
|
"[ 77/48 → 13/8 | midicmd:clock ]",
|
||||||
|
"[ 13/8 → 79/48 | midicmd:clock ]",
|
||||||
|
"[ 79/48 → 5/3 | midicmd:clock ]",
|
||||||
|
"[ 5/3 → 27/16 | midicmd:clock ]",
|
||||||
|
"[ 27/16 → 41/24 | midicmd:clock ]",
|
||||||
|
"[ 41/24 → 83/48 | midicmd:clock ]",
|
||||||
|
"[ 83/48 → 7/4 | midicmd:clock ]",
|
||||||
|
"[ 7/4 → 85/48 | midicmd:clock ]",
|
||||||
|
"[ 85/48 → 43/24 | midicmd:clock ]",
|
||||||
|
"[ 43/24 → 29/16 | midicmd:clock ]",
|
||||||
|
"[ 29/16 → 11/6 | midicmd:clock ]",
|
||||||
|
"[ 11/6 → 89/48 | midicmd:clock ]",
|
||||||
|
"[ 89/48 → 15/8 | midicmd:clock ]",
|
||||||
|
"[ 15/8 → 91/48 | midicmd:clock ]",
|
||||||
|
"[ 91/48 → 23/12 | midicmd:clock ]",
|
||||||
|
"[ 23/12 → 31/16 | midicmd:clock ]",
|
||||||
|
"[ 31/16 → 47/24 | midicmd:clock ]",
|
||||||
|
"[ 47/24 → 95/48 | midicmd:clock ]",
|
||||||
|
"[ 95/48 → 2/1 | midicmd:clock ]",
|
||||||
|
"[ 2/1 → 97/48 | midicmd:clock ]",
|
||||||
|
"[ 2/1 → 4/1 | midicmd:stop ]",
|
||||||
|
"[ 97/48 → 49/24 | midicmd:clock ]",
|
||||||
|
"[ 49/24 → 33/16 | midicmd:clock ]",
|
||||||
|
"[ 33/16 → 25/12 | midicmd:clock ]",
|
||||||
|
"[ 25/12 → 101/48 | midicmd:clock ]",
|
||||||
|
"[ 101/48 → 17/8 | midicmd:clock ]",
|
||||||
|
"[ 17/8 → 103/48 | midicmd:clock ]",
|
||||||
|
"[ 103/48 → 13/6 | midicmd:clock ]",
|
||||||
|
"[ 13/6 → 35/16 | midicmd:clock ]",
|
||||||
|
"[ 35/16 → 53/24 | midicmd:clock ]",
|
||||||
|
"[ 53/24 → 107/48 | midicmd:clock ]",
|
||||||
|
"[ 107/48 → 9/4 | midicmd:clock ]",
|
||||||
|
"[ 9/4 → 109/48 | midicmd:clock ]",
|
||||||
|
"[ 109/48 → 55/24 | midicmd:clock ]",
|
||||||
|
"[ 55/24 → 37/16 | midicmd:clock ]",
|
||||||
|
"[ 37/16 → 7/3 | midicmd:clock ]",
|
||||||
|
"[ 7/3 → 113/48 | midicmd:clock ]",
|
||||||
|
"[ 113/48 → 19/8 | midicmd:clock ]",
|
||||||
|
"[ 19/8 → 115/48 | midicmd:clock ]",
|
||||||
|
"[ 115/48 → 29/12 | midicmd:clock ]",
|
||||||
|
"[ 29/12 → 39/16 | midicmd:clock ]",
|
||||||
|
"[ 39/16 → 59/24 | midicmd:clock ]",
|
||||||
|
"[ 59/24 → 119/48 | midicmd:clock ]",
|
||||||
|
"[ 119/48 → 5/2 | midicmd:clock ]",
|
||||||
|
"[ 5/2 → 121/48 | midicmd:clock ]",
|
||||||
|
"[ 121/48 → 61/24 | midicmd:clock ]",
|
||||||
|
"[ 61/24 → 41/16 | midicmd:clock ]",
|
||||||
|
"[ 41/16 → 31/12 | midicmd:clock ]",
|
||||||
|
"[ 31/12 → 125/48 | midicmd:clock ]",
|
||||||
|
"[ 125/48 → 21/8 | midicmd:clock ]",
|
||||||
|
"[ 21/8 → 127/48 | midicmd:clock ]",
|
||||||
|
"[ 127/48 → 8/3 | midicmd:clock ]",
|
||||||
|
"[ 8/3 → 43/16 | midicmd:clock ]",
|
||||||
|
"[ 43/16 → 65/24 | midicmd:clock ]",
|
||||||
|
"[ 65/24 → 131/48 | midicmd:clock ]",
|
||||||
|
"[ 131/48 → 11/4 | midicmd:clock ]",
|
||||||
|
"[ 11/4 → 133/48 | midicmd:clock ]",
|
||||||
|
"[ 133/48 → 67/24 | midicmd:clock ]",
|
||||||
|
"[ 67/24 → 45/16 | midicmd:clock ]",
|
||||||
|
"[ 45/16 → 17/6 | midicmd:clock ]",
|
||||||
|
"[ 17/6 → 137/48 | midicmd:clock ]",
|
||||||
|
"[ 137/48 → 23/8 | midicmd:clock ]",
|
||||||
|
"[ 23/8 → 139/48 | midicmd:clock ]",
|
||||||
|
"[ 139/48 → 35/12 | midicmd:clock ]",
|
||||||
|
"[ 35/12 → 47/16 | midicmd:clock ]",
|
||||||
|
"[ 47/16 → 71/24 | midicmd:clock ]",
|
||||||
|
"[ 71/24 → 143/48 | midicmd:clock ]",
|
||||||
|
"[ 143/48 → 3/1 | midicmd:clock ]",
|
||||||
|
"[ 3/1 → 145/48 | midicmd:clock ]",
|
||||||
|
"[ 145/48 → 73/24 | midicmd:clock ]",
|
||||||
|
"[ 73/24 → 49/16 | midicmd:clock ]",
|
||||||
|
"[ 49/16 → 37/12 | midicmd:clock ]",
|
||||||
|
"[ 37/12 → 149/48 | midicmd:clock ]",
|
||||||
|
"[ 149/48 → 25/8 | midicmd:clock ]",
|
||||||
|
"[ 25/8 → 151/48 | midicmd:clock ]",
|
||||||
|
"[ 151/48 → 19/6 | midicmd:clock ]",
|
||||||
|
"[ 19/6 → 51/16 | midicmd:clock ]",
|
||||||
|
"[ 51/16 → 77/24 | midicmd:clock ]",
|
||||||
|
"[ 77/24 → 155/48 | midicmd:clock ]",
|
||||||
|
"[ 155/48 → 13/4 | midicmd:clock ]",
|
||||||
|
"[ 13/4 → 157/48 | midicmd:clock ]",
|
||||||
|
"[ 157/48 → 79/24 | midicmd:clock ]",
|
||||||
|
"[ 79/24 → 53/16 | midicmd:clock ]",
|
||||||
|
"[ 53/16 → 10/3 | midicmd:clock ]",
|
||||||
|
"[ 10/3 → 161/48 | midicmd:clock ]",
|
||||||
|
"[ 161/48 → 27/8 | midicmd:clock ]",
|
||||||
|
"[ 27/8 → 163/48 | midicmd:clock ]",
|
||||||
|
"[ 163/48 → 41/12 | midicmd:clock ]",
|
||||||
|
"[ 41/12 → 55/16 | midicmd:clock ]",
|
||||||
|
"[ 55/16 → 83/24 | midicmd:clock ]",
|
||||||
|
"[ 83/24 → 167/48 | midicmd:clock ]",
|
||||||
|
"[ 167/48 → 7/2 | midicmd:clock ]",
|
||||||
|
"[ 7/2 → 169/48 | midicmd:clock ]",
|
||||||
|
"[ 169/48 → 85/24 | midicmd:clock ]",
|
||||||
|
"[ 85/24 → 57/16 | midicmd:clock ]",
|
||||||
|
"[ 57/16 → 43/12 | midicmd:clock ]",
|
||||||
|
"[ 43/12 → 173/48 | midicmd:clock ]",
|
||||||
|
"[ 173/48 → 29/8 | midicmd:clock ]",
|
||||||
|
"[ 29/8 → 175/48 | midicmd:clock ]",
|
||||||
|
"[ 175/48 → 11/3 | midicmd:clock ]",
|
||||||
|
"[ 11/3 → 59/16 | midicmd:clock ]",
|
||||||
|
"[ 59/16 → 89/24 | midicmd:clock ]",
|
||||||
|
"[ 89/24 → 179/48 | midicmd:clock ]",
|
||||||
|
"[ 179/48 → 15/4 | midicmd:clock ]",
|
||||||
|
"[ 15/4 → 181/48 | midicmd:clock ]",
|
||||||
|
"[ 181/48 → 91/24 | midicmd:clock ]",
|
||||||
|
"[ 91/24 → 61/16 | midicmd:clock ]",
|
||||||
|
"[ 61/16 → 23/6 | midicmd:clock ]",
|
||||||
|
"[ 23/6 → 185/48 | midicmd:clock ]",
|
||||||
|
"[ 185/48 → 31/8 | midicmd:clock ]",
|
||||||
|
"[ 31/8 → 187/48 | midicmd:clock ]",
|
||||||
|
"[ 187/48 → 47/12 | midicmd:clock ]",
|
||||||
|
"[ 47/12 → 63/16 | midicmd:clock ]",
|
||||||
|
"[ 63/16 → 95/24 | midicmd:clock ]",
|
||||||
|
"[ 95/24 → 191/48 | midicmd:clock ]",
|
||||||
|
"[ 191/48 → 4/1 | midicmd:clock ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 1/4 → 1/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 1/2 → 3/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 3/4 → 1/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 1/1 → 5/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 5/4 → 3/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 3/2 → 7/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 7/4 → 2/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 2/1 → 9/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 9/4 → 5/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 5/2 → 11/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 11/4 → 3/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 3/1 → 13/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 13/4 → 7/2 | note:a cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 7/2 → 15/4 | note:f cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
"[ 15/4 → 4/1 | note:e cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midiport" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:c midiport:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | note:a midiport:0 ]",
|
||||||
|
"[ 1/2 → 3/4 | note:f midiport:0 ]",
|
||||||
|
"[ 3/4 → 1/1 | note:e midiport:0 ]",
|
||||||
|
"[ 1/1 → 5/4 | note:c midiport:1 ]",
|
||||||
|
"[ 5/4 → 3/2 | note:a midiport:1 ]",
|
||||||
|
"[ 3/2 → 7/4 | note:f midiport:1 ]",
|
||||||
|
"[ 7/4 → 2/1 | note:e midiport:1 ]",
|
||||||
|
"[ 2/1 → 9/4 | note:c midiport:2 ]",
|
||||||
|
"[ 9/4 → 5/2 | note:a midiport:2 ]",
|
||||||
|
"[ 5/2 → 11/4 | note:f midiport:2 ]",
|
||||||
|
"[ 11/4 → 3/1 | note:e midiport:2 ]",
|
||||||
|
"[ 3/1 → 13/4 | note:c midiport:3 ]",
|
||||||
|
"[ 13/4 → 7/2 | note:a midiport:3 ]",
|
||||||
|
"[ 7/2 → 15/4 | note:f midiport:3 ]",
|
||||||
|
"[ 15/4 → 4/1 | note:e midiport:3 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "miditouch" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 miditouch:0.5 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 miditouch:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 miditouch:0.5000000000000001 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 miditouch:0 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "mousex" example index 0 1`] = `
|
exports[`runs examples > example "mousex" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:C3 ]",
|
"[ 0/1 → 1/4 | note:C3 ]",
|
||||||
@ -5426,6 +5712,24 @@ exports[`runs examples > example "note" example index 2 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "nrpv" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "octave" example index 0 1`] = `
|
exports[`runs examples > example "octave" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
|
"[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]",
|
||||||
@ -6333,6 +6637,15 @@ exports[`runs examples > example "pressBy" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "progNum" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 progNum:10 midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 progNum:10 midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 progNum:10 midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 progNum:10 midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "pure" example index 0 1`] = `
|
exports[`runs examples > example "pure" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | e4 ]",
|
"[ 0/1 → 1/1 | e4 ]",
|
||||||
@ -8957,6 +9270,33 @@ exports[`runs examples > example "swingBy" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "sysex" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "sysexdata" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "sysexid" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:c4 sysexid:119 sysexdata:[1 2 3 4] midichan:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "take" example index 0 1`] = `
|
exports[`runs examples > example "take" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | s:bd ]",
|
"[ 0/1 → 1/2 | s:bd ]",
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import * as webaudio from '@strudel/webaudio';
|
|||||||
import { mini, m } from '@strudel/mini/mini.mjs';
|
import { mini, m } from '@strudel/mini/mini.mjs';
|
||||||
// import * as voicingHelpers from '@strudel/tonal/voicings.mjs';
|
// import * as voicingHelpers from '@strudel/tonal/voicings.mjs';
|
||||||
// import euclid from '@strudel/core/euclid.mjs';
|
// import euclid from '@strudel/core/euclid.mjs';
|
||||||
// import '@strudel/midi/midi.mjs';
|
//import '@strudel/midi/midi.mjs';
|
||||||
import * as tonalHelpers from '@strudel/tonal';
|
import * as tonalHelpers from '@strudel/tonal';
|
||||||
import '@strudel/xen/xen.mjs';
|
import '@strudel/xen/xen.mjs';
|
||||||
// import '@strudel/xen/tune.mjs';
|
// import '@strudel/xen/tune.mjs';
|
||||||
@ -126,6 +126,12 @@ const loadCsound = () => {};
|
|||||||
const loadCSound = () => {};
|
const loadCSound = () => {};
|
||||||
const loadcsound = () => {};
|
const loadcsound = () => {};
|
||||||
|
|
||||||
|
const midin = () => {
|
||||||
|
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const sysex = ([id, data]) => {};
|
||||||
|
|
||||||
// TODO: refactor to evalScope
|
// TODO: refactor to evalScope
|
||||||
evalScope(
|
evalScope(
|
||||||
// Tone,
|
// Tone,
|
||||||
@ -142,6 +148,8 @@ evalScope(
|
|||||||
uiHelpers,
|
uiHelpers,
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
midin,
|
||||||
|
sysex,
|
||||||
// gist,
|
// gist,
|
||||||
// euclid,
|
// euclid,
|
||||||
csound: id,
|
csound: id,
|
||||||
|
|||||||
@ -16,24 +16,97 @@ It is also possible to pattern other things with Strudel, such as software and h
|
|||||||
|
|
||||||
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
|
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
|
||||||
|
|
||||||
## midi(outputName?)
|
## midiin(inputName?)
|
||||||
|
|
||||||
|
<JsDoc client:idle name="midin" h={0} />
|
||||||
|
|
||||||
|
## midi(outputName?,options?)
|
||||||
|
|
||||||
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||||
If no outputName is given, it uses the first midi output it finds.
|
If no outputName is given, it uses the first midi output it finds.
|
||||||
|
|
||||||
<MiniRepl client:idle tune={`chord("<C^7 A7 Dm7 G7>").voicing().midi()`} />
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`
|
||||||
|
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
|
In the console, you will see a log of the available MIDI devices as soon as you run the code,
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
`Midi connected! Using "Midi Through Port-0".`
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.midi()` function accepts an options object with the following properties:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`$: note("d e c a f").midi('IAC Driver', { isController: true, midimap: 'default'})
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Available Options</summary>
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
| ------------ | ------------- | --------- | ---------------------------------------------------------------------- |
|
||||||
|
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
|
||||||
|
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
|
||||||
|
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
|
||||||
|
| midichannel | number | 1 | Default MIDI channel (1-16) |
|
||||||
|
| velocity | number | 0.9 | Default note velocity (0-1) |
|
||||||
|
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
|
||||||
|
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
|
||||||
|
| midiport | string/number | - | MIDI device name or index |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### midiport(outputName)
|
||||||
|
|
||||||
|
Selects the MIDI output device to use, pattern can be used to switch between devices.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$: midiport('IAC Driver');
|
||||||
|
$: note('c a f e').midiport('<0 1 2 3>').midi();
|
||||||
|
```
|
||||||
|
|
||||||
|
<JsDoc client:idle name="midiport" h={0} />
|
||||||
|
|
||||||
## midichan(number)
|
## midichan(number)
|
||||||
|
|
||||||
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
|
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
|
||||||
|
|
||||||
## ccn && ccv
|
## midicmd(command)
|
||||||
|
|
||||||
|
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
|
||||||
|
|
||||||
|
It supports the following commands:
|
||||||
|
|
||||||
|
- `clock`/`midiClock` - Sends MIDI timing clock messages
|
||||||
|
- `start` - Sends MIDI start message
|
||||||
|
- `stop` - Sends MIDI stop message
|
||||||
|
- `continue` - Sends MIDI continue message
|
||||||
|
|
||||||
|
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
|
||||||
|
// Note: It might act unexpectedly if MIDI isn't set up initially.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`$:stack(
|
||||||
|
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
|
||||||
|
)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## control, ccn && ccv
|
||||||
|
|
||||||
|
- `control` sends MIDI control change messages to your MIDI device.
|
||||||
- `ccn` sets the cc number. Depends on your synths midi mapping
|
- `ccn` sets the cc number. Depends on your synths midi mapping
|
||||||
- `ccv` sets the cc value. normalized from 0 to 1.
|
- `ccv` sets the cc value. normalized from 0 to 1.
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("c a f e").control([74, sine.slow(4)]).midi()`} />
|
||||||
|
|
||||||
<MiniRepl client:idle tune={`note("c a f e").ccn(74).ccv(sine.slow(4)).midi()`} />
|
<MiniRepl client:idle tune={`note("c a f e").ccn(74).ccv(sine.slow(4)).midi()`} />
|
||||||
|
|
||||||
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
|
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
|
||||||
@ -56,6 +129,48 @@ Instead of setting `ccn` and `ccv` directly, you can also create mappings with `
|
|||||||
|
|
||||||
<JsDoc client:idle name="defaultmidimap" h={0} />
|
<JsDoc client:idle name="defaultmidimap" h={0} />
|
||||||
|
|
||||||
|
## progNum (Program Change)
|
||||||
|
|
||||||
|
`progNum` sends MIDI program change messages to switch between different presets/patches on your MIDI device.
|
||||||
|
Program change values should be numbers between 0 and 127.
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`// Switch between programs 0 and 1 every cycle
|
||||||
|
progNum("<0 1>").midi()
|
||||||
|
|
||||||
|
// Play notes while changing programs
|
||||||
|
note("c3 e3 g3").progNum("<0 1 2>").midi()`} />
|
||||||
|
|
||||||
|
Program change messages are useful for switching between different instrument sounds or presets during a performance.
|
||||||
|
The exact sound that each program number maps to depends on your MIDI device's configuration.
|
||||||
|
|
||||||
|
## sysex, sysexid && sysexdata (System Exclusive Message)
|
||||||
|
|
||||||
|
`sysex` sends MIDI System Exclusive (SysEx) messages to your MIDI device.
|
||||||
|
ysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
|
||||||
|
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`// Send a simple SysEx message
|
||||||
|
let id = 0x43; //Yamaha
|
||||||
|
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
|
||||||
|
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
|
||||||
|
$: note("c a f e").sysex(id, data).midi();
|
||||||
|
$: note("c a f e").sysexid(id).sysexdata(data).midi();`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
The exact format of SysEx messages depends on your MIDI device's specification.
|
||||||
|
Consult your device's MIDI implementation guide for details on supported SysEx messages.
|
||||||
|
|
||||||
|
## midibend && miditouch
|
||||||
|
|
||||||
|
`midibend` sets MIDI pitch bend (-1 - 1)
|
||||||
|
`miditouch` sets MIDI key after touch (0-1)
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("c a f e").midibend(sine.slow(4).range(-0.4,0.4)).midi()`} />
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("c a f e").miditouch(sine.slow(4).range(0,1)).midi()`} />
|
||||||
|
|
||||||
# OSC/SuperDirt/StrudelDirt
|
# OSC/SuperDirt/StrudelDirt
|
||||||
|
|
||||||
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
||||||
@ -118,8 +233,8 @@ The following example shows how to send a pattern to an MQTT broker:
|
|||||||
client:only="react"
|
client:only="react"
|
||||||
tune={`"hello world"
|
tune={`"hello world"
|
||||||
.mqtt(undefined, // username (undefined for open/public servers)
|
.mqtt(undefined, // username (undefined for open/public servers)
|
||||||
undefined, // password
|
undefined, // password
|
||||||
'/strudel-pattern', // mqtt 'topic'
|
'/strudel-pattern', // mqtt 'topic'
|
||||||
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
|
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
|
||||||
'mystrudel', // MQTT client id - randomly generated if not supplied
|
'mystrudel', // MQTT client id - randomly generated if not supplied
|
||||||
0 // latency / delay before sending messages (0 = no delay)
|
0 // latency / delay before sending messages (0 = no delay)
|
||||||
@ -130,12 +245,14 @@ The following example shows how to send a pattern to an MQTT broker:
|
|||||||
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
|
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
|
||||||
|
|
||||||
```
|
```
|
||||||
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
|
|
||||||
hello
|
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
|
||||||
world
|
> hello
|
||||||
hello
|
> world
|
||||||
world
|
> hello
|
||||||
...
|
> world
|
||||||
|
> ...
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Control patterns will be encoded as JSON, for example:
|
Control patterns will be encoded as JSON, for example:
|
||||||
@ -155,11 +272,17 @@ Control patterns will be encoded as JSON, for example:
|
|||||||
Will send messages like the following:
|
Will send messages like the following:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
{"s":"sax","speed":2}
|
{"s":"sax","speed":2}
|
||||||
{"s":"sax","speed":2}
|
{"s":"sax","speed":2}
|
||||||
{"s":"sax","speed":3}
|
{"s":"sax","speed":3}
|
||||||
{"s":"sax","speed":2}
|
{"s":"sax","speed":2}
|
||||||
...
|
...
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Libraries for receiving MQTT are available for many programming languages.
|
Libraries for receiving MQTT are available for many programming languages.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user