Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit f652c2c

Browse files
authored
Merge pull request #1244 from nkymut/add-program-change
Add MIDI Program Change, SysEx, NRPN, PitchBend and AfterTouch Output
2 parents 4f6c4bb + 5fcb96f commit f652c2c

File tree

6 files changed

+1024
-53
lines changed

6 files changed

+1024
-53
lines changed

packages/core/controls.mjs

Lines changed: 147 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
controls.mjs - <short description TODO>
2+
controls.mjs - Registers audio controls for pattern manipulation and effects.
33
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs>
44
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/>.
55
*/
@@ -1513,22 +1513,11 @@ export const { scram } = registerControl('scram');
15131513
export const { binshift } = registerControl('binshift');
15141514
export const { hbrick } = registerControl('hbrick');
15151515
export const { lbrick } = registerControl('lbrick');
1516-
export const { midichan } = registerControl('midichan');
1517-
export const { midimap } = registerControl('midimap');
1518-
export const { midiport } = registerControl('midiport');
1519-
export const { control } = registerControl('control');
1520-
export const { ccn } = registerControl('ccn');
1521-
export const { ccv } = registerControl('ccv');
1522-
export const { polyTouch } = registerControl('polyTouch');
1523-
export const { midibend } = registerControl('midibend');
1524-
export const { miditouch } = registerControl('miditouch');
1525-
export const { ctlNum } = registerControl('ctlNum');
1516+
15261517
export const { frameRate } = registerControl('frameRate');
15271518
export const { frames } = registerControl('frames');
15281519
export const { hours } = registerControl('hours');
1529-
export const { midicmd } = registerControl('midicmd');
15301520
export const { minutes } = registerControl('minutes');
1531-
export const { progNum } = registerControl('progNum');
15321521
export const { seconds } = registerControl('seconds');
15331522
export const { songPtr } = registerControl('songPtr');
15341523
export const { uid } = registerControl('uid');
@@ -1621,6 +1610,151 @@ export const ar = register('ar', (t, pat) => {
16211610
return pat.set({ attack, release });
16221611
});
16231612

1613+
//MIDI
1614+
1615+
/**
1616+
* MIDI channel: Sets the MIDI channel for the event.
1617+
*
1618+
* @name midichan
1619+
* @param {number | Pattern} channel MIDI channel number (0-15)
1620+
* @example
1621+
* note("c4").midichan(1).midi()
1622+
*/
1623+
export const { midichan } = registerControl('midichan');
1624+
1625+
export const { midimap } = registerControl('midimap');
1626+
1627+
/**
1628+
* MIDI port: Sets the MIDI port for the event.
1629+
*
1630+
* @name midiport
1631+
* @param {number | Pattern} port MIDI port
1632+
* @example
1633+
* note("c a f e").midiport("<0 1 2 3>").midi()
1634+
*/
1635+
export const { midiport } = registerControl('midiport');
1636+
1637+
/**
1638+
* MIDI command: Sends a MIDI command message.
1639+
*
1640+
* @name midicmd
1641+
* @param {number | Pattern} command MIDI command
1642+
* @example
1643+
* midicmd("clock*48,<start stop>/2").midi()
1644+
*/
1645+
export const { midicmd } = registerControl('midicmd');
1646+
1647+
/**
1648+
* MIDI control: Sends a MIDI control change message.
1649+
*
1650+
* @name control
1651+
* @param {number | Pattern} MIDI control number (0-127)
1652+
* @param {number | Pattern} MIDI controller value (0-127)
1653+
*/
1654+
export const control = register('control', (args, pat) => {
1655+
if (!Array.isArray(args)) {
1656+
throw new Error('control expects an array of [ccn, ccv]');
1657+
}
1658+
const [_ccn, _ccv] = args;
1659+
return pat.ccn(_ccn).ccv(_ccv);
1660+
});
1661+
1662+
/**
1663+
* MIDI control number: Sends a MIDI control change message.
1664+
*
1665+
* @name ccn
1666+
* @param {number | Pattern} MIDI control number (0-127)
1667+
*/
1668+
export const { ccn } = registerControl('ccn');
1669+
/**
1670+
* MIDI control value: Sends a MIDI control change message.
1671+
*
1672+
* @name ccv
1673+
* @param {number | Pattern} MIDI control value (0-127)
1674+
*/
1675+
export const { ccv } = registerControl('ccv');
1676+
export const { ctlNum } = registerControl('ctlNum');
1677+
// TODO: ctlVal?
1678+
1679+
/**
1680+
* MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message.
1681+
* @name nrpnn
1682+
* @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127)
1683+
* @example
1684+
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
1685+
*/
1686+
export const { nrpnn } = registerControl('nrpnn');
1687+
/**
1688+
* MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message.
1689+
* @name nrpv
1690+
* @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127)
1691+
* @example
1692+
* note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi()
1693+
*/
1694+
export const { nrpv } = registerControl('nrpv');
1695+
1696+
/**
1697+
* MIDI program number: Sends a MIDI program change message.
1698+
*
1699+
* @name progNum
1700+
* @param {number | Pattern} program MIDI program number (0-127)
1701+
* @example
1702+
* note("c4").progNum(10).midichan(1).midi()
1703+
*/
1704+
export const { progNum } = registerControl('progNum');
1705+
1706+
/**
1707+
* MIDI sysex: Sends a MIDI sysex message.
1708+
* @name sysex
1709+
* @param {number | Pattern} id Sysex ID
1710+
* @param {number | Pattern} data Sysex data
1711+
* @example
1712+
* note("c4").sysex(["0x77", "0x01:0x02:0x03:0x04"]).midichan(1).midi()
1713+
*/
1714+
export const sysex = register('sysex', (args, pat) => {
1715+
if (!Array.isArray(args)) {
1716+
throw new Error('sysex expects an array of [id, data]');
1717+
}
1718+
const [id, data] = args;
1719+
return pat.sysexid(id).sysexdata(data);
1720+
});
1721+
/**
1722+
* MIDI sysex ID: Sends a MIDI sysex identifier message.
1723+
* @name sysexid
1724+
* @param {number | Pattern} id Sysex ID
1725+
* @example
1726+
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
1727+
*/
1728+
export const { sysexid } = registerControl('sysexid');
1729+
/**
1730+
* MIDI sysex data: Sends a MIDI sysex message.
1731+
* @name sysexdata
1732+
* @param {number | Pattern} data Sysex data
1733+
* @example
1734+
* note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi()
1735+
*/
1736+
export const { sysexdata } = registerControl('sysexdata');
1737+
1738+
/**
1739+
* MIDI pitch bend: Sends a MIDI pitch bend message.
1740+
* @name midibend
1741+
* @param {number | Pattern} midibend MIDI pitch bend (-1 - 1)
1742+
* @example
1743+
* note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi()
1744+
*/
1745+
export const { midibend } = registerControl('midibend');
1746+
/**
1747+
* MIDI key after touch: Sends a MIDI key after touch message.
1748+
* @name miditouch
1749+
* @param {number | Pattern} miditouch MIDI key after touch (0-1)
1750+
* @example
1751+
* note("c4").miditouch(sine.slow(4).range(0,1)).midi()
1752+
*/
1753+
export const { miditouch } = registerControl('miditouch');
1754+
1755+
// TODO: what is this?
1756+
export const { polyTouch } = registerControl('polyTouch');
1757+
16241758
export const getControlName = (alias) => {
16251759
if (controlAlias.has(alias)) {
16261760
return controlAlias.get(alias);

packages/midi/README.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,187 @@ This package adds midi functionality to strudel Patterns.
77
```sh
88
npm i @strudel/midi --save
99
```
10+
11+
## Available Controls
12+
13+
The following MIDI controls are available:
14+
15+
OUTPUT:
16+
17+
- `midi` - opens a midi output device.
18+
- `note` - Sends MIDI note messages. Can accept note names (e.g. "c4") or MIDI note numbers (0-127)
19+
- `midichan` - Sets the MIDI channel (1-16, defaults to 1)
20+
- `velocity` - Sets note velocity (0-1, defaults to 0.9)
21+
- `gain` - Modifies velocity by multiplying with it (0-1, defaults to 1)
22+
- `control` - Sets MIDI control change messages
23+
- `ccn` - Sets MIDI CC controller number (0-127)
24+
- `ccv` - Sets MIDI CC value (0-1)
25+
- `progNum` - Sends MIDI program change messages (0-127)
26+
- `sysex` - Sends MIDI System Exclusive messages (id: number 0-127 or array of bytes 0-127, data: array of bytes 0-127)
27+
- `sysexid` - Sets MIDI System Exclusive ID (number 0-127 or array of bytes 0-127)
28+
- `sysexdata` - Sets MIDI System Exclusive data (array of bytes 0-127)
29+
- `midibend` - Sets MIDI pitch bend (-1 - 1)
30+
- `miditouch` - Sets MIDI key after touch (0-1)
31+
- `midicmd` - Sends MIDI system real-time messages to control timing and transport on MIDI devices.
32+
- `nrpnn` - Sets MIDI NRPN non-registered parameter number (array of bytes 0-127)
33+
- `nrpv` - Sets MIDI NRPN non-registered parameter value (0-127)
34+
35+
36+
INPUT:
37+
38+
- `midin` - Opens a MIDI input port to receive MIDI control change messages.
39+
40+
Additional controls can be mapped using the mapping object passed to `.midi()`:
41+
42+
## Examples
43+
44+
### midi(outputName?, options?)
45+
46+
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
47+
If no outputName is given, it uses the first midi output it finds.
48+
49+
```javascript
50+
$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')
51+
```
52+
53+
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".`
54+
55+
### Options
56+
57+
The `.midi()` function accepts an options object with the following properties:
58+
59+
```javascript
60+
$: note("c a f e").midi('IAC Driver', { isController: true, midimap: 'default'})
61+
```
62+
63+
<details>
64+
<summary>Available Options</summary>
65+
66+
| Option | Type | Default | Description |
67+
|--------|------|---------|-------------|
68+
| isController | boolean | false | When true, disables sending note messages. Useful for MIDI controllers |
69+
| latencyMs | number | 34 | Latency in milliseconds to align MIDI with audio engine |
70+
| noteOffsetMs | number | 10 | Offset in milliseconds for note-off messages to prevent glitching |
71+
| midichannel | number | 1 | Default MIDI channel (1-16) |
72+
| velocity | number | 0.9 | Default note velocity (0-1) |
73+
| gain | number | 1 | Default gain multiplier for velocity (0-1) |
74+
| midimap | string | 'default' | Name of MIDI mapping to use for control changes |
75+
| midiport | string/number | - | MIDI device name or index |
76+
77+
</details>
78+
79+
80+
81+
82+
### midiport(outputName)
83+
84+
Selects the MIDI output device to use, pattern can be used to switch between devices.
85+
86+
```javascript
87+
$: midiport('IAC Driver')
88+
$: note("c a f e").midiport("<0 1 2 3>").midi()
89+
```
90+
91+
### midichan(number)
92+
93+
Selects the MIDI channel to use. If not used, `.midi` will use channel 1 by default.
94+
95+
### control, ccn && ccv
96+
97+
`control` sends MIDI control change messages to your MIDI device.
98+
99+
- `ccn` sets the cc number. Depends on your synths midi mapping
100+
- `ccv` sets the cc value. normalized from 0 to 1.
101+
102+
```javascript
103+
$: note("c a f e").control([74, sine.slow(4)]).midi()
104+
$: note("c a f e").ccn(74).ccv(sine.slow(4)).midi()
105+
```
106+
107+
In the above snippet, `ccn` is set to 74, which is the filter cutoff for many synths. `ccv` is controlled by a saw pattern.
108+
Having everything in one pattern, the `ccv` pattern will be aligned to the note pattern, because the structure comes from the left by default.
109+
But you can also control cc messages separately like this:
110+
111+
```javascript
112+
$: note("c a f e").midi()
113+
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()
114+
```
115+
116+
### progNum (Program Change)
117+
118+
`progNum` control sends MIDI program change messages to switch between different presets/patches on your MIDI device.
119+
Program change values should be numbers between 0 and 127.
120+
121+
```javascript
122+
// Play notes while changing programs
123+
note("c3 e3 g3").progNum("<0 1 2>").midi()
124+
```
125+
126+
Program change messages are useful for switching between different instrument sounds or presets during a performance.
127+
The exact sound that each program number maps to depends on your MIDI device's configuration.
128+
129+
## sysex, sysexid && sysexdata (System Exclusive Message)
130+
131+
`sysex`, `sysexid` and `sysexdata` control sends MIDI System Exclusive (SysEx) messages to your MIDI device.
132+
sysEx messages are device-specific commands that allow deeper control over synthesizer parameters.
133+
The value should be an array of numbers between 0-255 representing the SysEx data bytes.
134+
135+
```javascript
136+
// Send a simple SysEx message
137+
let id = 0x43; //Yamaha
138+
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
139+
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
140+
$: note("c d e f e d c").sysex(id, data).midi();
141+
$: note("c d e f e d c").sysexid(id).sysexdata(data).midi();
142+
```
143+
144+
The exact format of SysEx messages depends on your MIDI device's specification.
145+
Consult your device's MIDI implementation guide for details on supported SysEx messages.
146+
147+
### midibend && miditouch
148+
149+
`midibend` sets MIDI pitch bend (-1 - 1)
150+
`miditouch` sets MIDI key after touch (0-1)
151+
152+
```javascript
153+
154+
$: note("c d e f e d c").midibend(sine.slow(4).range(-0.4,0.4)).midi();
155+
$: note("c d e f e d c").miditouch(sine.slow(4).range(0,1)).midi();
156+
157+
```
158+
159+
### midicmd
160+
161+
`midicmd` sends MIDI system real-time messages to control timing and transport on MIDI devices.
162+
163+
It supports the following commands:
164+
165+
- `clock`/`midiClock` - Sends MIDI timing clock messages
166+
- `start` - Sends MIDI start message
167+
- `stop` - Sends MIDI stop message
168+
- `continue` - Sends MIDI continue message
169+
170+
```javascript
171+
// You can control the clock with a pattern and ensure it starts in sync when the repl begins.
172+
// Note: It might act unexpectedly if MIDI isn't set up initially.
173+
stack(
174+
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
175+
)
176+
```
177+
178+
`midicmd` also supports sending control change, program change and sysex messages.
179+
180+
- `cc` - sends MIDI control change messages.
181+
- `progNum` - sends MIDI program change messages.
182+
- `sysex` - sends MIDI system exclusive messages.
183+
184+
```javascript
185+
stack(
186+
// "cc:ccn:ccv"
187+
midicmd("cc:74:1").midi('IAC Driver'),
188+
// "progNum:progNum"
189+
midicmd("progNum:1").midi('IAC Driver'),
190+
// "sysex:[sysexid]:[sysexdata]"
191+
midicmd("sysex:[0x43]:[0x79:0x09:0x11:0x0A:0x00:0x00]").midi('IAC Driver')
192+
)
193+
```

0 commit comments

Comments
 (0)