diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1bec79a0..b681db35 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1514,7 +1514,8 @@ export const { control } = registerControl('control'); export const { ccn } = registerControl('ccn'); export const { ccv } = registerControl('ccv'); export const { pc } = registerControl('pc'); -export const { sysex } = registerControl('sysex'); +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'); diff --git a/packages/midi/README.md b/packages/midi/README.md index 0efdd351..d8494728 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -76,19 +76,17 @@ The exact sound that each program number maps to depends on your MIDI device's c ## sysex (System Exclusive Message) The `sysex` control sends MIDI System Exclusive (SysEx) messages to your MIDI device. -ysEx messages are device-specific commands that allow deeper control over synthesizer parameters. +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 -sysex([0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]).midi() -``` +let id = 0x43; //Yamaha +//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers -```javascript -//Send SysEx while playing notes -note("c3 e3 g3") - .sysex("<[0xF0,0x7E,0x7F,0x09,0x01,0xF7] [0xF0,0x7E,0x7F,0x09,0x02,0xF7]>") - .midi() +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(); ``` The exact format of SysEx messages depends on your MIDI device's specification. diff --git a/packages/midi/gm.mjs b/packages/midi/gm.mjs new file mode 100644 index 00000000..6ba23737 --- /dev/null +++ b/packages/midi/gm.mjs @@ -0,0 +1,130 @@ +export const gm = { + piano: 0, + bright_acoustic_piano: 1, + electric_grand_piano: 2, + honky_tonk_piano: 3, + epiano1: 4, + epiano2: 5, + harpsichord: 6, + clavinet: 7, + celesta: 8, + glockenspiel: 9, + music_box: 10, + vibraphone: 11, + marimba: 12, + xylophone: 13, + tubular_bells: 14, + dulcimer: 15, + drawbar_organ: 16, + percussive_organ: 17, + rock_organ: 18, + church_organ: 19, + reed_organ: 20, + accordion: 21, + harmonica: 22, + bandoneon: 23, + acoustic_guitar_nylon: 24, + acoustic_guitar_steel: 25, + electric_guitar_jazz: 26, + electric_guitar_clean: 27, + electric_guitar_muted: 28, + overdriven_guitar: 29, + distortion_guitar: 30, + guitar_harmonics: 31, + acoustic_bass: 32, + electric_bass_finger: 33, + electric_bass_pick: 34, + fretless_bass: 35, + slap_bass_1: 36, + slap_bass_2: 37, + synth_bass_1: 38, + synth_bass_2: 39, + violin: 40, + viola: 41, + cello: 42, + contrabass: 43, + tremolo_strings: 44, + pizzicato_strings: 45, + orchestral_harp: 46, + timpani: 47, + string_ensemble_1: 48, + string_ensemble_2: 49, + synth_strings_1: 50, + synth_strings_2: 51, + choir_aahs: 52, + voice_oohs: 53, + synth_choir: 54, + orchestra_hit: 55, + trumpet: 56, + trombone: 57, + tuba: 58, + muted_trumpet: 59, + french_horn: 60, + brass_section: 61, + synth_brass_1: 62, + synth_brass_2: 63, + soprano_sax: 64, + alto_sax: 65, + tenor_sax: 66, + baritone_sax: 67, + oboe: 68, + english_horn: 69, + bassoon: 70, + clarinet: 71, + piccolo: 72, + flute: 73, + recorder: 74, + pan_flute: 75, + blown_bottle: 76, + shakuhachi: 77, + whistle: 78, + ocarina: 79, + lead_1_square: 80, + lead_2_sawtooth: 81, + lead_3_calliope: 82, + lead_4_chiff: 83, + lead_5_charang: 84, + lead_6_voice: 85, + lead_7_fifths: 86, + lead_8_bass_lead: 87, + pad_1_new_age: 88, + pad_2_warm: 89, + pad_3_polysynth: 90, + pad_4_choir: 91, + pad_5_bowed: 92, + pad_6_metallic: 93, + pad_7_halo: 94, + pad_8_sweep: 95, + fx_1_rain: 96, + fx_2_soundtrack: 97, + fx_3_crystal: 98, + fx_4_atmosphere: 99, + fx_5_brightness: 100, + fx_6_goblins: 101, + fx_7_echoes: 102, + fx_8_sci_fi: 103, + sitar: 104, + banjo: 105, + shamisen: 106, + koto: 107, + kalimba: 108, + bagpipe: 109, + fiddle: 110, + shanai: 111, + tinkle_bell: 112, + agogo: 113, + steel_drums: 114, + woodblock: 115, + taiko_drum: 116, + melodic_tom: 117, + synth_drum: 118, + reverse_cymbal: 119, + guitar_fret_noise: 120, + breath_noise: 121, + seashore: 122, + bird_tweet: 123, + telephone: 124, + helicopter: 125, + applause: 126, + gunshot: 127, +}; diff --git a/packages/midi/index.mjs b/packages/midi/index.mjs index 399227f7..2a226a53 100644 --- a/packages/midi/index.mjs +++ b/packages/midi/index.mjs @@ -1,3 +1,4 @@ import './midi.mjs'; export * from './midi.mjs'; +export * from './gm.mjs'; diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index a7745620..2ef266a1 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 } 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 }, + ); }); } @@ -134,7 +138,20 @@ Pattern.prototype.midi = function (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}`; // destructure value - let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9, pc, sysex } = hap.value; + let { + note, + nrpnn, + nrpv, + ccn, + ccv, + midichan = 1, + midicmd, + gain = 1, + velocity = 0.9, + pc, + sysexid, + sysexdata, + } = hap.value; velocity = gain * velocity; @@ -173,15 +190,18 @@ Pattern.prototype.midi = function (output) { throw new Error(`Expected ${name} to be a number between 0 and 127 for program change`); } device.sendProgramChange(value, paramSpec.channel || midichan, { time: timeOffsetString }); - } 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(undefined, value, { 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 }); + // } } }); } @@ -193,14 +213,25 @@ Pattern.prototype.midi = function (output) { device.sendProgramChange(pc, midichan, { time: timeOffsetString }); } // Handle sysex - if (sysex !== undefined) { - if (!Array.isArray(sysex)) { + if (sysexid !== undefined && sysexdata !== undefined) { + //console.log('sysex', sysexid, sysexdata); + 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 (!sysex.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 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(undefined, sysex, { time: timeOffsetString }); + + device.sendSysex(sysexid, sysexdata, { time: timeOffsetString }); + //device.sendSysex(0x43, [0x79, 0x09, 0x11, 0x0A, 0x00,0x1e], { time: timeOffsetString }); } // Handle control change