From d79c6421e5f1e3dac9876ebb3fbba455cf7c6e56 Mon Sep 17 00:00:00 2001 From: RikSolo Date: Tue, 12 May 2026 01:06:47 +0200 Subject: [PATCH] WIP: more beunsize map work --- package.json | 4 +- src/devices/Midi.ts | 3 +- src/devices/OSC.ts | 15 ++- src/index.ts | 3 +- src/mappings/beunsize-ma3.ts | 204 ++++++++++++++++++++++++++++------- src/mappings/xr18-home.ts | 4 +- src/types/osc/osc.d.ts | 2 + src/utilityFunctions.ts | 34 ++++++ 8 files changed, 222 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 358d024..d6217e4 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "main": "dist/index.js", "scripts": { - "start": "node dist/index.js", - "dev": "tsc-watch --onSuccess \"npm start\"" + "start": "node --expose-gc dist/index.js", + "dev": "tsc-watch --onSuccess \"npm start beunsize-ma3\"" }, "author": "Rik Berkelder (https://riksolo.com)", "license": "ISC", diff --git a/src/devices/Midi.ts b/src/devices/Midi.ts index b8ddd35..1d5811b 100644 --- a/src/devices/Midi.ts +++ b/src/devices/Midi.ts @@ -50,7 +50,6 @@ export class MidiControl { this.device.input.setMaxListeners(50); if (type === MessageType.NoteOnOff) { - this.addListener(MessageType.NoteOn) this.addListener(MessageType.NoteOff) } else { @@ -90,7 +89,7 @@ export class MidiControl { const output = this.outputs[this.page]; if (!output || output === undefined) return; - output(data); + output(outData); }) } diff --git a/src/devices/OSC.ts b/src/devices/OSC.ts index 7429707..0ecb4fd 100644 --- a/src/devices/OSC.ts +++ b/src/devices/OSC.ts @@ -2,7 +2,7 @@ import osc from "osc"; interface IOSCEvent { address: string; - handler: (message: osc.ResponseMessage) => void; + handler: (data: string | number) => void; } export class OSCDevice { @@ -20,10 +20,17 @@ export class OSCDevice { this.port.on('error', (e) => { console.error('osc error', e) }) this.port.on('message', msg => { - // console.log('msg', msg) for (const listener of this.listeners) { if (msg.address !== listener.address) continue; - listener.handler(msg); + if (msg.args[0] !== undefined) { + let value: string | number | undefined; + if (typeof msg.args[0] === 'object' && ['string', 'number'].includes(typeof msg.args[0].value)) value = msg.args[0].value as string | number; + if (typeof msg.args[0] === 'string' || typeof msg.args[0] === 'number') value = msg.args[0]; + + if (value !== undefined) { + listener.handler(value); + } + } } }) @@ -52,7 +59,7 @@ export class OSCDevice { this.port.send({ address }); } - public addListener(address: string, handler: IOSCEvent['handler']): number { + public addListener(address: string, handler: (data: string | number) => void): number { const newLength = this.listeners.push({ address, handler diff --git a/src/index.ts b/src/index.ts index cdfa242..fd8cb9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,9 @@ async function main() { console.log('inputs', midi.getInputs()) console.log('outputs', midi.getOutputs()) + const mapName = process.argv[process.argv.length - 1] - const mapping = await import('./mappings/xr18-home.js'); + const mapping = await import(`./mappings/${mapName}.js`); mapping.default() } catch (e) { console.error('Uncaught error', e) diff --git a/src/mappings/beunsize-ma3.ts b/src/mappings/beunsize-ma3.ts index 55b9ca9..0abf943 100644 --- a/src/mappings/beunsize-ma3.ts +++ b/src/mappings/beunsize-ma3.ts @@ -1,25 +1,63 @@ import * as midi from 'easymidi' import { MessageType, MidiControl, MidiDevice } from "../devices/Midi.js" import { OSCDevice } from "../devices/OSC.js" -import { mapNumber } from '../utilityFunctions.js' +import { delay, mapNumber, numberRange, retry } from '../utilityFunctions.js' + + +function ma3HardKey(ma3: OSCDevice, keycode: string, status: boolean) { + ma3.sendString('/cmd', `Plugin "RBOSCKeys" "${keycode.toUpperCase()} ${status ? "press" : "release"}"`); +} + +enum ApcColor { + Off = 0, + Green = 1, + Red = 3, + Yellow = 5 +} + + +const MA3_FADER_MIN = 0; +const MA3_FADER_MAX = 100; export default async function mapping() { - // DEVICES + const ma3 = new OSCDevice('0.0.0.0', '127.0.0.1', 8002, 8000) - const xtouch = new MidiDevice('X Touch Compact', true) - const xtouch_loop = new MidiDevice('XTouch-loop', true, true) - const keyboard = new MidiDevice('idobo') + const tryXTouch = async () => { + const [xtouch, xtouch_loop] = await retry(() => { + return [ + new MidiDevice('X Touch Compact', true), + new MidiDevice('XTouch-loop', true, true) + ] + }, 1000, 'xtouch') - const ma3 = new OSCDevice('0.0.0.0', '127.0.0.1', 3000, 3001) + xtouchMapping(xtouch, xtouch_loop, ma3); + } - // UTILITY FUNCTIONS + const tryKeyboard = async () => { + const keyboard = await retry(() => new MidiDevice('idobo'), 1000, 'keyboard'); + keyboardMapping(keyboard, ma3) + } + + + const tryApcMini = async () => { + const apcmini = await retry(() => new MidiDevice('APC MINI', true), 1000, 'apc mini'); + apcminiMapping(apcmini, ma3) + } + + //tryXTouch(); + //tryKeyboard(); + tryApcMini(); + +} + +async function xtouchMapping(xtouch: MidiDevice, xtouch_loop: MidiDevice, ma3: OSCDevice) { const noteButtonFeedback = (note: number, address: string) => { const button = xtouch.addControl(MessageType.NoteOnOff, { note }); - ma3.addListener(address, message => { - const value = message.args[0]?.value + ma3.addListener(address, data => { + const value = data if (typeof value !== 'number') return; button.handleFeedback(0, { value: value === 1 ? true : false }) @@ -30,22 +68,19 @@ export default async function mapping() { return button; } - const ma3HardKey = (keycode: string, status: boolean) => { - ma3.sendString('/cmd', `Call Plugin "RBOSCKeys" "${keycode} ${status ? "press" : "release"}"`); - } // MAPPINGS // passthroughs for encoders plugin - [21, 22, 23, 24, 25, 26].forEach(encoder => { + numberRange(21, 26).forEach(encoder => { xtouch.addControl(MessageType.CC, { controller: encoder }).addOutput(0, message => { xtouch_loop.output?.send(MessageType.CC, message) }) }) // Executor Faders - const faders = [0, 1, 2, 3, 4, 5, 6, 7, 8].map(f => xtouch.addControl(MessageType.CC, { controller: f })) + const faders = numberRange(0, 8).map(f => xtouch.addControl(MessageType.CC, { controller: f })) faders.forEach((fader, index) => { const firstExec = 201; @@ -53,12 +88,11 @@ export default async function mapping() { const address = `/Page/Fader${exec}`; fader.addOutput(0, (message) => { - ma3.sendFloat(address, mapNumber(message.value, 0, 127, 0, 1)); + ma3.sendFloat(address, mapNumber(message.value, 0, 127, MA3_FADER_MIN, MA3_FADER_MAX)); }) - ma3.addListener(address, d => { - const value = (d.args as any)[0]?.value; - if (value !== undefined) { + ma3.addListener(address, value => { + if (typeof value === 'number') { const mappedValue = mapNumber(value, 0, 1, 0, 127); fader.handleFeedback(0, { value: mappedValue }) } @@ -68,10 +102,10 @@ export default async function mapping() { // Executor Keys const executorButtons: MidiControl[][] = [ - [16, 17, 18, 19, 20, 21, 22, 23], - [24, 25, 26, 27, 28, 29, 30, 31], - [32, 33, 34, 35, 36, 37, 38, 39], - [40, 41, 42, 43, 44, 45, 46, 47, 48], + numberRange(16, 23), + numberRange(24, 31), + numberRange(32, 39), + numberRange(40, 48) ].map((row, rowIndex) => row.map((note, noteIndex) => { const rowLookup = [401, 301, 201, 101]; const address = `/Page/Key${rowLookup[rowIndex] ?? 101 + noteIndex}` @@ -86,13 +120,13 @@ export default async function mapping() { { note: 50, key: 'PAGE_DOWN' }, { note: 51, key: '' }, { note: 52, key: '' }, - { note: 53, key: '' }, - { note: 53, key: '' }, + { note: 53, key: 'DEF_GOBACK' }, + { note: 53, key: 'DEF_GO' }, ] .map(i => { const button = xtouch.addControl(MessageType.NoteOnOff, { note: i.note }) button.addOutput(0, (message) => { - ma3HardKey(i.key, message.value) + ma3HardKey(ma3, i.key, message.value) }) return button }) @@ -110,17 +144,15 @@ export default async function mapping() { ma3.sendInt(address, mapNumber(message.value, 0, 1, -1, 1)) }) - ma3.addListener(feedbackAddress, message => { - if (message.args[0] === undefined || message.args[0].type !== 'f') return; - const value = message.args[0].value; + ma3.addListener(feedbackAddress, data => { + if (typeof data !== 'number') return; - encoder.handleFeedback(0, { value: mapNumber(value, 0, 1, 0, 127) }) + encoder.handleFeedback(0, { value: mapNumber(data, 0, 1, 0, 127) }) }) }) +} - - // HARDKEYS - +async function keyboardMapping(keyboard: MidiDevice, ma3: OSCDevice) { const keymap: string[][] = [ ['pause', 'goback', 'go', 'fixture', 'channel', 'group', 'menu', 'oops', 'num7', 'num8', 'num9', 'plus', 'highlight', 'on', 'off'], ['learn', 'gobackfast', 'gofast', 'preset', 'sequence', 'cue', 'update', 'esc', 'num4', 'num5', 'num6', 'thru', 'solo', 'move', 'copy'], @@ -131,8 +163,8 @@ export default async function mapping() { const xkeymap: number[] = [ - 291, 292, 293, 294, 295, 296, 297, 298, - 191, 192, 193, 194, 195, 196, 197, 198 + ...numberRange(291, 298), + ...numberRange(191, 198) ]; keymap.forEach((row, rowIndex) => { @@ -156,7 +188,7 @@ export default async function mapping() { keyboard .addControl(MessageType.CC, { controller: totalIndex }) .addOutput(0, message => { - ma3HardKey(key.toUpperCase(), message.value ? true : false); + ma3HardKey(ma3, key.toUpperCase(), message.value ? true : false); }) } @@ -164,6 +196,106 @@ export default async function mapping() { totalIndex++ }) }) +} +async function apcminiMapping(apcmini: MidiDevice, ma3: OSCDevice) { + const APC_FIRST_EXEC = 201; -} \ No newline at end of file + apcmini.input.on('cc', m => console.dir(m)); + apcmini.input.on('noteon', m => console.dir(m)); + apcmini.input.on('noteoff', m => console.dir(m)); + + // playback faders + + numberRange(48, 56).forEach((controller, index) => { + const fader = apcmini.addControl(MessageType.CC, { controller }); + const exec = APC_FIRST_EXEC + index; + const address = `/Page/Fader${exec}`; + + fader.addOutput(0, (message) => { + ma3.sendFloat(address, mapNumber(message.value, 2, 125, MA3_FADER_MIN, MA3_FADER_MAX)); + }); + }); + + // playback fader buttons + [...numberRange(64, 71), 98].forEach((note, index) => { + const button = apcmini.addControl(MessageType.NoteOnOff, { note }); + const exec = APC_FIRST_EXEC + index; + const address = `/Page/Key${exec}` + + button.addOutput(0, message => { + ma3.sendInt(address, message.value ? 1 : 0); + }) + + ma3.addListener(address, value => { + button.handleFeedback(0, { velocity: value ? 3 : 0, value: true }) + }) + }); + + // main key mapping, upside down + + const rowColors = [ + ApcColor.Red, + ApcColor.Red, + ApcColor.Red, + ApcColor.Off, + ApcColor.Off, + ApcColor.Off, + ApcColor.Yellow, + ApcColor.Yellow, + ]; + + [ + numberRange(101, 108), + numberRange(301, 308), + numberRange(401, 408), + null, + null, + null, + numberRange(191, 198), + numberRange(291, 298), + ].forEach((rowContent, row) => { + const rowFirstCC = row * 8; + if (rowContent !== null) rowContent.forEach((exec, index) => { + const button = apcmini.addControl(MessageType.NoteOnOff, { note: rowFirstCC + index }) + const address = `/Page/Key${exec}` + + button.addOutput(0, value => { + ma3.sendInt(address, value.value ? 1 : 0) + }) + + ma3.addListener(address, value => { + let color: number = ApcColor.Off; + + if (rowColors[row]) { + if (value === 1) color = rowColors[row] + if (value === 2) color = ApcColor.Green + } + + button.handleFeedback(0, + { value: true, velocity: color } + ) + }) + + }) + }); + + const firstCommandNote = 82; + // Command Keys + [ + 'clear', + 'next', + 'blind', + 'highlight', + 'store', + 'update', + 'def_goback', + 'def_go' + ].forEach((key, index) => { + const note = firstCommandNote + index; + + apcmini.addControl(MessageType.NoteOnOff, { note }).addOutput(0, data => { + ma3HardKey(ma3, key, data.value); + }) + }) +} diff --git a/src/mappings/xr18-home.ts b/src/mappings/xr18-home.ts index 93409c7..c6754a1 100644 --- a/src/mappings/xr18-home.ts +++ b/src/mappings/xr18-home.ts @@ -91,7 +91,7 @@ export default async function mapping() { const fader2way = (fader: number, page: number, addr: string, max: number = 1) => { faders[fader]?.addOutput(page, d => setLevel(addr, d.value, max)); - odev.addListener(addr, d => { levelFeedback(fader, page, (d.args as any)[0], max) }); + odev.addListener(addr, d => { if (typeof d === 'number') levelFeedback(fader, page, d, max) }); odev.sendNull(addr); } @@ -150,7 +150,7 @@ export default async function mapping() { }) odev.addListener(addr, d => { - control.handleFeedback(page, { velocity: (d.args as any)[0] === 0 ? 127 : 0 }); + control.handleFeedback(page, { velocity: d === 0 ? 127 : 0 }); }) odev.sendNull(addr); diff --git a/src/types/osc/osc.d.ts b/src/types/osc/osc.d.ts index 8294f52..c795225 100644 --- a/src/types/osc/osc.d.ts +++ b/src/types/osc/osc.d.ts @@ -37,6 +37,8 @@ type MessageArg = | MessageArgString | MessageArgInt | MessageArgBytes + | string + | number type SendMessage = { address: string diff --git a/src/utilityFunctions.ts b/src/utilityFunctions.ts index 8dcb82c..e549e92 100644 --- a/src/utilityFunctions.ts +++ b/src/utilityFunctions.ts @@ -8,4 +8,38 @@ export function mapNumber(value: number, fromMin: number, fromMax: number, toMin const mapped = (value / absFromMax) * absToMax; return mapped + toMin; +} + +export function delay(t: number) { + return new Promise(resolve => setTimeout(resolve, t)); +} + + +export async function retry(create: () => T, time: number, desc?: string): Promise { + + let thing: T | undefined; + + while (!thing) { + try { + thing = create(); + } catch (e) { + console.error(`Error in retry ${desc ?? 'unnamed'}: ${e}`) + await delay(time) + } + } + + return thing; +} + +export function numberRange(from: number, to: number): number[] { + let array = []; + for (let i = from; i <= to; i++) { + array.push(i); + } + + return array; +} + +export function numbersFrom(from: number, amount: number): number[] { + return numberRange(from, from + amount - 1) } \ No newline at end of file