import * as midi from 'easymidi'; import osc from 'osc'; console.log('inputs', midi.getInputs()) console.log('outputs', midi.getOutputs()) function mapNumber(value: number, fromMin: number, fromMax: number, toMin: number, toMax: number): number { if (value <= fromMin) return toMin; if (value >= fromMax) return toMax; const absFromMax = fromMax - fromMin; const absToMax = toMax - toMin; const mapped = (value / absFromMax) * absToMax; return mapped + toMin; } enum MessageType { NoteOn = 'noteon', NoteOff = 'noteoff', Pitch = 'pitch' } type DataLookup = { [MessageType.NoteOn]: midi.Note, [MessageType.NoteOff]: midi.Note, [MessageType.Pitch]: midi.Pitch } const defaultData = { [MessageType.NoteOn]: { channel: 0, note: 0, velocity: 0 }, [MessageType.NoteOff]: { channel: 0, note: 0, velocity: 0 }, [MessageType.Pitch]: { channel: 0, value: 0 } } interface IMidiControlOptions { feedbackInput: boolean, noPageFeedback: boolean, noStoreInput: boolean } class MidiControl { public values: { [page: number]: DataLookup[T] } = []; private outputs: { [page: number]: (data: any) => void } = []; private page: number = 0; constructor( private device: MidiDevice, public type: MessageType, private filter: Partial, private options: Partial = {} ) { this.device.input.addListener(type, (data: DataLookup[T]) => { // Check incoming data against filter for (const [k, v] of Object.entries(this.filter)) { const rawData = data as unknown as { [key: string]: string | number }; if (typeof rawData !== 'object' || rawData === null) return; if (!(k in rawData)) return; if (rawData[k] !== v) return; } //console.debug('matched data', data); // store new value if (!this.options.noStoreInput) this.values[this.page] = data; if (this.options.feedbackInput) this.feedback(data); const output = this.outputs[this.page]; if (!output || output === undefined) return; output(data); }) } private defaultData(): D { return defaultData[this.type] as D; } private feedback(data: DataLookup[T]) { // ugly typing here, but library overloads are a little annoying to work around. worst case scenario nothing happens this.device.output.send(this.type as any, { ...data, ...this.filter } as any); } public getPage(): number { return this.page }; public getValue(page: number): DataLookup[T] | undefined { return this.values[page]; } public setPage(page: number) { this.page = page; const values = this.values[page] ?? this.defaultData(); if (!this.options.noPageFeedback) this.feedback(values); } public addOutput(pages: number | number[], cb: (D: D) => void): void { if (typeof pages === 'number') pages = [pages]; for (const page of pages) { this.outputs[page] = cb; } } public handleFeedback(pages: number | number[], data: Partial, noStore?: boolean): void { if (typeof pages === 'number') pages = [pages]; const newValue = { ...this.defaultData(), ...this.filter, ...data }; for (const page of pages) { if (!noStore) this.values[page] = newValue; if (page === this.page) { this.feedback(newValue) } } } } class MidiDevice { public input: midi.Input; public output: midi.Output; private _page: number = 0; public controls: Array> = []; constructor(device: string) { this.input = new midi.Input(device); this.output = new midi.Output(device); } public addControl(type: T, filter: Partial, options: Partial = {}): MidiControl { const control = new MidiControl(this, type, filter, options); this.controls.push(control); return control; } public setPage(page: number) { this._page = page; for (const control of this.controls) { control.setPage(page); } } public get page(): number { return this._page; } } interface IOSCEvent { address: string; handler: (message: osc.ResponseMessage) => void; } class OSCDevice { private port: osc.UDPPort; private listeners: Array = []; constructor(localHost: string, remoteHost: string, port: number) { this.port = new osc.UDPPort({ localAddress: localHost, remoteAddress: remoteHost, localPort: port, remotePort: port }); this.port.on('message', msg => { // console.log('msg', msg) for (const listener of this.listeners) { if (msg.address !== listener.address) continue; listener.handler(msg); } }) this.port.open(); this.pollFeedback() setInterval(() => { this.pollFeedback() }, 5000); } private pollFeedback() { this.port.send({ address: '/xremote', args: [{ type: 'i', value: 1 }] }) } public sendInt(address: string, value: number) { this.port.send({ address, args: [{ type: 'i', value }] }); } public sendFloat(address: string, value: number) { this.port.send({ address, args: [{ type: 'f', value }] }); } public sendString(address: string, value: string) { this.port.send({ address, args: [{ type: 's', value }] }); } public sendBytes(address: string, value: osc.Uint8Array) { this.port.send({ address, args: [{ type: 's', value }] }); } public sendNull(address: string) { this.port.send({ address }); } public addListener(address: string, handler: IOSCEvent['handler']): number { const newLength = this.listeners.push({ address, handler }); return newLength - 1; } } const deviceName = midi.getInputs().find(i => i.includes('Platform X')); if (!deviceName) { console.log('midi device not found') process.exit(1); } const mdev = new MidiDevice(deviceName); const odev = new OSCDevice('0.0.0.0', '192.168.0.47', 10024); // const client = new osc.OSCClient('192.168.0.26', 9889); // DEBUG OUT //['noteon', 'pitch'].map((v) => mdev.input.addListener(v, (data) => { console.log('data', data) })) // MM MM MMMM MMMMMM MMMMMM MMMMMM MM MM MMMM MMMM // MMMM MMMM MM MM MM MM MM MM MM MMMM MM MM MM MM MM // MM MM MM MMMMMMMM MM MM MM MM MM MM MMMM MM MM // MM MM MM MM MMMMMM MMMMMM MM MM MM MM MMMM MM // MM MM MM MM MM MM MM MM MM MM MM MM MM // MM MM MM MM MM MM MMMMMM MM MM MMMM MMMM const PAGE_MAIN = 0; const PAGE_HP = 1; const PAGE_PC = 2; const ALL_PAGES = [PAGE_MAIN, PAGE_HP, PAGE_PC]; const OSC_LEVEL_0 = 0.75; // TURN OFF ALL LEDS const feedbackOff = () => { for (const control of mdev.controls) { if (control.type === MessageType.NoteOn) { control.handleFeedback(ALL_PAGES, { velocity: 0 }, true); } } } const offButton = mdev.addControl(MessageType.NoteOn, { channel: 0, note: 39 }, { noPageFeedback: true }) offButton.addOutput(ALL_PAGES, (d) => feedbackOff()); // PAGE SWITCHING const pageFeedback = () => { const page = mdev.page; buttonSel1.handleFeedback(ALL_PAGES, { velocity: page === PAGE_MAIN ? 127 : 0 }) buttonSel2.handleFeedback(ALL_PAGES, { velocity: page === PAGE_HP ? 127 : 0 }) buttonSel3.handleFeedback(ALL_PAGES, { velocity: page === PAGE_PC ? 127 : 0 }) } const buttonSel1 = mdev.addControl(MessageType.NoteOn, { channel: 0, note: 8 }, { noPageFeedback: true }) buttonSel1.addOutput(ALL_PAGES, (d) => { if (d.velocity === 127) mdev.setPage(PAGE_MAIN) pageFeedback(); }); const buttonSel2 = mdev.addControl(MessageType.NoteOn, { channel: 0, note: 9 }, { noPageFeedback: true }) buttonSel2.addOutput(ALL_PAGES, (d) => { if (d.velocity === 127) mdev.setPage(PAGE_HP) pageFeedback(); }); const buttonSel3 = mdev.addControl(MessageType.NoteOn, { channel: 0, note: 10 }, { noPageFeedback: true }) buttonSel3.addOutput(ALL_PAGES, (d) => { if (d.velocity === 127) mdev.setPage(PAGE_PC) pageFeedback(); }); // FADERS const faders = [0, 1, 2, 3, 4, 5, 6, 7].map((f) => mdev.addControl(MessageType.Pitch, { channel: f as midi.Channel }, { feedbackInput: true })); const setLevel = (addr: string, value: number, max: number = 1) => odev.sendFloat(addr, mapNumber(value, 0, 16383, 0, max)); const levelFeedback = (fader: number, page: number, value: number, max: number = 1) => { faders[fader]?.handleFeedback(page, { value: mapNumber(value, 0, max, 0, 16383) }); } 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.sendNull(addr); } // PAGE 1: Main fader2way(0, PAGE_MAIN, '/dca/1/fader', OSC_LEVEL_0) fader2way(1, PAGE_MAIN, '/rtn/1/mix/fader') fader2way(2, PAGE_MAIN, '/bus/1/mix/fader') fader2way(3, PAGE_MAIN, '/ch/01/mix/fader') fader2way(4, PAGE_MAIN, '/ch/03/mix/fader') fader2way(5, PAGE_MAIN, '/ch/05/mix/fader') fader2way(6, PAGE_MAIN, '/ch/07/mix/fader') fader2way(7, PAGE_MAIN, '/ch/09/mix/fader') // PAGE 2: Headphones fader2way(0, PAGE_HP, '/bus/1/mix/fader') fader2way(1, PAGE_HP, '/ch/15/mix/01/level') fader2way(2, PAGE_HP, '/ch/16/mix/01/level') fader2way(3, PAGE_HP, '/ch/01/mix/01/level') fader2way(4, PAGE_HP, '/ch/03/mix/01/level') fader2way(5, PAGE_HP, '/ch/05/mix/01/level') fader2way(6, PAGE_HP, '/ch/07/mix/01/level') fader2way(7, PAGE_HP, '/ch/09/mix/01/level') // PAGE 3: PC fader2way(0, PAGE_PC, '/bus/1/mix/fader') fader2way(1, PAGE_PC, '/ch/15/mix/03/level') fader2way(2, PAGE_PC, '/ch/16/mix/03/level') fader2way(3, PAGE_PC, '/ch/01/mix/03/level') fader2way(4, PAGE_PC, '/ch/03/mix/03/level') fader2way(5, PAGE_PC, '/ch/05/mix/03/level') fader2way(6, PAGE_PC, '/ch/07/mix/03/level') fader2way(7, PAGE_PC, '/ch/09/mix/03/level') const button = (note: number) => mdev.addControl(MessageType.NoteOn, { channel: 0, note }, { feedbackInput: false, noStoreInput: true }); const mutes = [16, 17, 18, 19, 20, 21, 22, 23].map(n => button(n)); // MUTE BUTTONS const buttonToggle = (control: MidiControl | undefined, page: number, addr: string) => { if (!control) return; control.addOutput(page, (d) => { if (d.velocity !== 127) return; const value = control.getValue(page); if (!value || value.velocity > 1) { control.handleFeedback(page, { velocity: 0 }); odev.sendInt(addr, 1); } else { control.handleFeedback(page, { velocity: 127 }); odev.sendInt(addr, 0); } }) odev.addListener(addr, d => { control.handleFeedback(page, { velocity: (d.args as any)[0] === 0 ? 127 : 0 }); }) odev.sendNull(addr); } // PAGE 1: Main buttonToggle(mutes[0], PAGE_MAIN, '/dca/1/on') buttonToggle(mutes[1], PAGE_MAIN, '/rtn/1/mix/on') buttonToggle(mutes[2], PAGE_MAIN, '/bus/1/mix/on') buttonToggle(mutes[3], PAGE_MAIN, '/ch/01/mix/on') buttonToggle(mutes[4], PAGE_MAIN, '/ch/03/mix/on') buttonToggle(mutes[5], PAGE_MAIN, '/ch/05/mix/on') buttonToggle(mutes[6], PAGE_MAIN, '/ch/07/mix/on') buttonToggle(mutes[7], PAGE_MAIN, '/ch/09/mix/on') // PAGE 2: Headphones buttonToggle(mutes[0], PAGE_HP, '/bus/1/mix/on') buttonToggle(mutes[1], PAGE_HP, '/ch/15/mix/on') buttonToggle(mutes[2], PAGE_HP, '/ch/16/mix/on') buttonToggle(mutes[3], PAGE_HP, '/ch/01/mix/on') buttonToggle(mutes[4], PAGE_HP, '/ch/03/mix/on') buttonToggle(mutes[5], PAGE_HP, '/ch/05/mix/on') buttonToggle(mutes[6], PAGE_HP, '/ch/07/mix/on') buttonToggle(mutes[7], PAGE_HP, '/ch/09/mix/on') // PAGE 3: PC buttonToggle(mutes[0], PAGE_PC, '/bus/1/mix/on') buttonToggle(mutes[1], PAGE_PC, '/ch/15/mix/on') buttonToggle(mutes[2], PAGE_PC, '/ch/16/mix/on') buttonToggle(mutes[3], PAGE_PC, '/ch/01/mix/on') buttonToggle(mutes[4], PAGE_PC, '/ch/03/mix/on') buttonToggle(mutes[5], PAGE_PC, '/ch/05/mix/on') buttonToggle(mutes[6], PAGE_PC, '/ch/07/mix/on') buttonToggle(mutes[7], PAGE_PC, '/ch/09/mix/on')