Refactor: Consolidate configuration variables into midiConfig object

- add options argument to .midi
- add midiConfig object with properties
This commit is contained in:
nkymut 2025-03-11 23:29:37 +08:00
parent 3c2692bdda
commit 2ccb95aec0

View File

@ -257,35 +257,71 @@ function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString }); 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. * MIDI output: Opens a MIDI output port.
* @param {string | number} output MIDI device name or index defaulting to 0 * @param {string | number} midiport MIDI device name or index defaulting to 0
* @param {object} options Additional MIDI configuration options
* @example * @example
* note("c4").midichan(1).midi("IAC Driver Bus 1") * 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 (output) {
if (isPattern(output)) { 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'
}')`, }')`,
); );
} }
let portName = output;
let isController = false;
let mapping = {};
//TODO: MIDI mapping related // For backward compatibility
if (typeof output === 'object') { if (typeof midiport === 'object') {
const { port, controller = false, ...remainingProps } = output; const { port, isController = false, ...configOptions } = midiport;
portName = port; options = {
isController = controller; isController,
mapping = remainingProps; ...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(portName, 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}". ${
@ -303,30 +339,31 @@ Pattern.prototype.midi = function (output) {
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, nrpnn,
nrpv, nrpv,
ccn, ccn,
ccv, ccv,
midichan = 1, midichan = midiConfig.midichannel,
midicmd, midicmd,
midibend, midibend,
miditouch, miditouch,
polyTouch, //?? polyTouch,
gain = 1, gain = midiConfig.gain,
velocity = 0.9, velocity = midiConfig.velocity,
progNum, progNum,
sysexid, sysexid,
sysexdata, sysexdata,
midimap = 'default', midimap = midiConfig.midimap,
midiport = output, midiport = midiConfig.midiport,
} = hap.value; } = hap.value;
const device = getDevice(midiport, WebMidi.outputs); const device = getDevice(midiport, WebMidi.outputs);
@ -338,20 +375,24 @@ 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 && !isController) { // 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 // Handle program change
@ -427,7 +468,7 @@ const refs = {};
* @param {string | number} input MIDI device name or index defaulting to 0 * @param {string | number} input MIDI device name or index defaulting to 0
* @returns {Function} * @returns {Function}
* @example * @example
* let cc = await midin("IAC Driver Bus 1") * 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") * 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) {