diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1f2d3e70..82330bb8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1,5 +1,5 @@ /* -controls.mjs - +controls.mjs - Registers audio controls for pattern manipulation and effects. Copyright (C) 2022 Strudel contributors - see 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 . */ @@ -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,/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); diff --git a/packages/midi/README.md b/packages/midi/README.md index 6bb649f2..aa90992d 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -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("").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'}) +``` + +
+Available Options + +| 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 | + +
+ + + + +### 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,/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') +) +``` \ No newline at end of file diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 2c1d1f20..ce7cdb0e 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -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( diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 96a11d49..ebd1c209 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -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 ]", diff --git a/test/runtime.mjs b/test/runtime.mjs index c35810da..6b75fb3b 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -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, diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index cec06a34..92379aa6 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -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?) + + + +## 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. -").voicing().midi()`} /> +").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: + + + +
+Available Options + +| 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 | + +
+ +### 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. -## 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. + +/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. + + 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 ` +## 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. + +").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. + + + +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) + + + + + # 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. + +``` + +```