Whitespace fixes

This commit is contained in:
Nick Matantsev 2026-01-14 23:37:26 -05:00
parent 2fe1712dae
commit 44ad05fb20
2 changed files with 224 additions and 224 deletions

View file

@ -1,180 +1,180 @@
/* /*
input.mjs - MIDI input wrapper input.mjs - MIDI input wrapper
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs> Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs>
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/>. 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/>.
*/ */
import { WebMidi } from 'webmidi'; import { WebMidi } from 'webmidi';
import { logger, ref } from '@strudel/core'; import { logger, ref } from '@strudel/core';
import { getDevice } from './util.mjs'; import { getDevice } from './util.mjs';
export class MidiInput { export class MidiInput {
/** /**
* *
* @param {string | number} input MIDI device name or index defaulting to 0 * @param {string | number} input MIDI device name or index defaulting to 0
*/ */
constructor(input) { constructor(input) {
this.input = input; this.input = input;
this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs
this._refs = {}; this._refs = {};
this._refsByChan = {}; this._refsByChan = {};
this._loadAllStates(); this._loadAllStates();
this.initialDevice = this._startDeviceListener(); this.initialDevice = this._startDeviceListener();
} }
createCC(cc, chan) { createCC(cc, chan) {
const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan]; const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan];
if (!(cc in lookupMap)) { if (!(cc in lookupMap)) {
const initialState = this._loadState(chan); const initialState = this._loadState(chan);
lookupMap[cc] = initialState[cc] || 0; lookupMap[cc] = initialState[cc] || 0;
} }
return ref(() => lookupMap[cc]); return ref(() => lookupMap[cc]);
} }
_startDeviceListener() { _startDeviceListener() {
const initialDevice = getDevice(this.input, WebMidi.inputs); const initialDevice = getDevice(this.input, WebMidi.inputs);
// Background connection loop // Background connection loop
(async () => { (async () => {
const midiListener = this._onMidiMessage.bind(this); const midiListener = this._onMidiMessage.bind(this);
let device = initialDevice; let device = initialDevice;
while (true) { while (true) {
if (!device) { if (!device) {
device = await this._waitForDevice(); device = await this._waitForDevice();
} }
// Wait a bit for device to be ready to receive last state // Wait a bit for device to be ready to receive last state
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
try { try {
// Still continue if sending did not work // Still continue if sending did not work
this._sendAllStates(device); this._sendAllStates(device);
} catch (err) { } catch (err) {
console.error('midiin: failed to send last state on connect:', device.name, err); console.error('midiin: failed to send last state on connect:', device.name, err);
} }
// Listen for incoming MIDI messages and for disconnection // Listen for incoming MIDI messages and for disconnection
device.addListener('midimessage', midiListener); device.addListener('midimessage', midiListener);
await this._waitForDeviceDisconnect(device); await this._waitForDeviceDisconnect(device);
device.removeListener('midimessage', midiListener); device.removeListener('midimessage', midiListener);
device = null; // Clear var to trigger wait for connection device = null; // Clear var to trigger wait for connection
} }
})(); })();
return initialDevice; return initialDevice;
} }
_waitForDevice() { _waitForDevice() {
return new Promise((resolve) => { return new Promise((resolve) => {
const connListener = () => { const connListener = () => {
const device = getDevice(this.input, WebMidi.inputs); const device = getDevice(this.input, WebMidi.inputs);
if (device) { if (device) {
logger(`[midi] device reconnected: ${device.name}`); logger(`[midi] device reconnected: ${device.name}`);
WebMidi.removeListener('connected', connListener); WebMidi.removeListener('connected', connListener);
resolve(device); resolve(device);
} }
}; };
WebMidi.addListener('connected', connListener); WebMidi.addListener('connected', connListener);
}); });
} }
_waitForDeviceDisconnect(device) { _waitForDeviceDisconnect(device) {
return new Promise((resolve) => { return new Promise((resolve) => {
const disconnListener = (e) => { const disconnListener = (e) => {
if (e.port.name === device.name) { if (e.port.name === device.name) {
logger(`[midi] device disconnected: ${device.name}`); logger(`[midi] device disconnected: ${device.name}`);
WebMidi.removeListener('disconnected', disconnListener); WebMidi.removeListener('disconnected', disconnListener);
resolve(); resolve();
} }
}; };
WebMidi.addListener('disconnected', disconnListener); WebMidi.addListener('disconnected', disconnListener);
}); });
} }
_onMidiMessage(e) { _onMidiMessage(e) {
const ccNum = e.dataBytes[0]; const ccNum = e.dataBytes[0];
const v = e.dataBytes[1]; const v = e.dataBytes[1];
const chan = e.message.channel; const chan = e.message.channel;
const scaled = v / 127; const scaled = v / 127;
this._refs[ccNum] = scaled; this._refs[ccNum] = scaled;
this._refsByChan[ccNum] ??= {}; this._refsByChan[ccNum] ??= {};
this._refsByChan[ccNum][chan] = scaled; this._refsByChan[ccNum][chan] = scaled;
this._saveState(undefined, ccNum, scaled); this._saveState(undefined, ccNum, scaled);
this._saveState(chan, ccNum, scaled); this._saveState(chan, ccNum, scaled);
} }
_loadAllStates() { _loadAllStates() {
Object.assign(this._refs, this._loadState(undefined)); Object.assign(this._refs, this._loadState(undefined));
for (let chan = 1; chan <= 16; chan++) { for (let chan = 1; chan <= 16; chan++) {
this._refsByChan[chan] ??= {}; this._refsByChan[chan] ??= {};
Object.assign(this._refsByChan[chan], this._loadState(chan)); Object.assign(this._refsByChan[chan], this._loadState(chan));
} }
} }
_loadState(chan) { _loadState(chan) {
if (!this.stateKey) { if (!this.stateKey) {
return {}; return {};
} }
const initialDataRaw = localStorage.getItem( const initialDataRaw = localStorage.getItem(
`strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`, `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
); );
if (!initialDataRaw) { if (!initialDataRaw) {
return {}; return {};
} }
try { try {
return JSON.parse(initialDataRaw); return JSON.parse(initialDataRaw);
} catch (err) { } catch (err) {
console.warn( console.warn(
`Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`, `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`,
initialDataRaw, initialDataRaw,
err, err,
); );
return {}; return {};
} }
} }
_saveState(chan, cc, value) { _saveState(chan, cc, value) {
if (!this.stateKey) { if (!this.stateKey) {
return; return;
} }
const state = this._loadState(chan); const state = this._loadState(chan);
state[cc] = value; state[cc] = value;
localStorage.setItem( localStorage.setItem(
`strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`, `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`,
JSON.stringify(state), JSON.stringify(state),
); );
} }
_sendAllStates(device) { _sendAllStates(device) {
const output = WebMidi.outputs.find((o) => o.name === device.name); const output = WebMidi.outputs.find((o) => o.name === device.name);
if (!output) { if (!output) {
return; return;
} }
for (const [chan, refs] of Object.entries(this._refsByChan)) { for (const [chan, refs] of Object.entries(this._refsByChan)) {
const channel = Number(chan); const channel = Number(chan);
for (const [cc, value] of Object.entries(refs)) { for (const [cc, value] of Object.entries(refs)) {
const ccn = Number(cc); const ccn = Number(cc);
const scaled = Math.round(value * 127); const scaled = Math.round(value * 127);
output.sendControlChange(ccn, scaled, channel); output.sendControlChange(ccn, scaled, channel);
} }
} }
} }
} }

View file

@ -1,44 +1,44 @@
/* /*
util.mjs - MIDI utility functions util.mjs - MIDI utility functions
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs> Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs>
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/>. 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/>.
*/ */
import { Input, Output } from 'webmidi'; import { Input, Output } from 'webmidi';
/** /**
* Get a string listing device names for error messages. * Get a string listing device names for error messages.
* @param {Input[] | Output[]} devices * @param {Input[] | Output[]} devices
* @returns {string} * @returns {string}
*/ */
export function getMidiDeviceNamesString(devices) { export function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | '); return devices.map((o) => `'${o.name}'`).join(' | ');
} }
/** /**
* Look up a device by index or name. Otherwise return a default device, or fail if none are connected. * Look up a device by index or name. Otherwise return a default device, or fail if none are connected.
* *
* @param {string | number} indexOrName * @param {string | number} indexOrName
* @param {Input[] | Output[]} devices * @param {Input[] | Output[]} devices
* @returns {Input | Output | undefined} * @returns {Input | Output | undefined}
*/ */
export function getDevice(indexOrName, devices) { export function getDevice(indexOrName, devices) {
if (typeof indexOrName === 'number') { if (typeof indexOrName === 'number') {
return devices[indexOrName]; return devices[indexOrName];
} }
const byName = (name) => devices.find((output) => output.name.includes(name)); const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') { if (typeof indexOrName === 'string') {
return byName(indexOrName); return byName(indexOrName);
} }
// attempt to default to first IAC device if none is specified // attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC'); const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0]; const device = IACOutput ?? devices[0];
if (!device) { if (!device) {
if (!devices.length) { if (!devices.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`); throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
} }
throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`); throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`);
} }
return device; return device;
} }