diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 00d6e7a2..4d071a7a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1509,22 +1509,10 @@ 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 { control } = registerControl('control'); -export const { ccn } = registerControl('ccn'); -export const { ccv } = registerControl('ccv'); -export const { sysexid } = registerControl('sysexid'); -export const { sysexdata } = registerControl('sysexdata'); -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'); @@ -1616,6 +1604,73 @@ export const ar = register('ar', (t, pat) => { const [attack, release = attack] = t; 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 { 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 program number: Sends a MIDI program change message. + * + * @name progNum + * @param {number | Pattern} program MIDI program number (0-127) + */ +export const { progNum } = registerControl('progNum'); + +export const { polyTouch } = registerControl('polyTouch'); +export const { midibend } = registerControl('midibend'); +export const { miditouch } = registerControl('miditouch'); + +/** + * 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]'); @@ -1623,3 +1678,19 @@ export const sysex = register('sysex', (args, pat) => { 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'); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 058e7b68..fa7b2b6d 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -93,6 +93,12 @@ if (typeof window !== 'undefined') { }); } +/** + * MIDI output: Opens a MIDI output port. + * @param {string | number} output MIDI device name or index defaulting to 0 + * @example + * note("c4").midichan(1).midi("IAC Driver Bus 1") + */ Pattern.prototype.midi = function (output) { if (isPattern(output)) { throw new Error( @@ -105,6 +111,7 @@ Pattern.prototype.midi = function (output) { let isController = false; let mapping = {}; + //TODO: MIDI mapping related if (typeof output === 'object') { const { port, controller = false, ...remainingProps } = output; portName = port; @@ -187,26 +194,16 @@ Pattern.prototype.midi = function (output) { const ccvLsb = Math.round(value * 16383) & 0b1111111; device.sendControlChange(ccnLsb, ccvLsb, paramSpec.channel || midichan, { time: timeOffsetString }); } - } else if (paramSpec.pc !== undefined) { + } else if (paramSpec.progNum !== undefined) { if (typeof value !== 'number' || value < 0 || value > 127) { throw new Error(`Expected ${name} to be a number between 0 and 127 for program change`); } device.sendProgramChange(value, paramSpec.channel || midichan, { time: timeOffsetString }); } - // ToDo: support sysex for mapped parameters - // } else if (paramSpec.sysex) { - // if (!Array.isArray(value)) { - // throw new Error(`Expected ${name} to be an array of numbers (0-255) for sysex`); - // } - // if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { - // throw new Error(`All sysex bytes in ${name} must be integers between 0 and 255`); - // } - // device.sendSysex(0x43, value, { time: timeOffsetString }); - // //device.sendSysex(0x43, [0x79, 0x09, 0x11, 0x0A, 0x00,0x02], { time: timeOffsetString }); - // } } }); } + // Handle program change if (progNum !== undefined) { if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { @@ -283,6 +280,16 @@ Pattern.prototype.midi = function (output) { } else { device.sendProgramChange(midicmd[1], midichan, { time: timeOffsetString }); } + } else if (midicmd[0] === 'cc') { + if (midicmd.length === 2) { + if (typeof midicmd[0] !== 'number' || midicmd[0] < 0 || midicmd[0] > 127) { + throw new Error('expected ccn (control change number) to be a number between 0 and 127'); + } + if (typeof midicmd[1] !== 'number' || midicmd[1] < 0 || midicmd[1] > 127) { + throw new Error('expected ccv (control change value) to be a number between 0 and 127'); + } + device.sendControlChange(midicmd[0], midicmd[1], midichan, { time: timeOffsetString }); + } } } }); @@ -291,6 +298,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(