diff --git a/src/devices/Midi.ts b/src/devices/Midi.ts new file mode 100644 index 0000000..3d0107e --- /dev/null +++ b/src/devices/Midi.ts @@ -0,0 +1,187 @@ +import * as midi from 'easymidi'; + +export enum MessageType { + NoteOn = 'noteon', + NoteOff = 'noteoff', + NoteOnOff = 'noteonoff', + Pitch = 'pitch', + CC = 'cc' +} + +interface NoteOnOffValue extends midi.Note { + value: boolean +} + +type DataLookup = { + [MessageType.NoteOn]: midi.Note, + [MessageType.NoteOff]: midi.Note, + [MessageType.NoteOnOff]: NoteOnOffValue + [MessageType.Pitch]: midi.Pitch, + [MessageType.CC]: midi.ControlChange, +} + +const defaultData = { + [MessageType.NoteOn]: { channel: 0, note: 0, velocity: 0 }, + [MessageType.NoteOnOff]: { channel: 0, note: 0, velocity: 0, value: false }, + [MessageType.NoteOff]: { channel: 0, note: 0, velocity: 0 }, + [MessageType.Pitch]: { channel: 0, value: 0 }, + [MessageType.CC]: { channel: 0, controller: 0, value: 0 }, +} + +interface IMidiControlOptions { + feedbackInput: boolean, + noPageFeedback: boolean, + noStoreInput: boolean +} + + +export 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.setMaxListeners(50); + + if (type === MessageType.NoteOnOff) { + + this.addListener(MessageType.NoteOn) + this.addListener(MessageType.NoteOff) + } else { + this.addListener(type) + } + } + + private addListener(rawType: MessageType) { + + this.device.input.addListener(rawType, (data: DataLookup[typeof rawType]) => { + const outData = this.defaultData(); + + // copy over data from source message to destination message as they may differ + for (const [k, v] of Object.entries(data)) { + if (k in outData) (outData as any)[k] = v; + } + + // add on/off value to data if noteonoff + if (this.type === MessageType.NoteOnOff) { + (outData as NoteOnOffValue).value = rawType === MessageType.NoteOn + } + + // 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] = outData; + + if (this.options.feedbackInput) this.feedback(outData); + + 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]) { + if (!this.device.output) console.log('midi device tried to send output without output device defined') + // 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) + } + } + + } +} + +export class MidiDevice { + public input: midi.Input; + public output?: midi.Output; + private _page: number = 0; + + public controls: Array> = []; + + constructor(inputDevice: string, outputDevice: string | boolean = false) { + if (outputDevice === true) outputDevice = inputDevice; + + const inputDeviceFull = midi.getInputs().find(i => i.includes(inputDevice)); + + if (!inputDeviceFull) { + throw `MIDI input device "${inputDevice}" not found` + } + this.input = new midi.Input(inputDeviceFull); + + if (outputDevice) { + const outputDeviceFull = midi.getOutputs().find(i => i.includes(outputDevice)) + if (!outputDeviceFull) { + throw `MIDI output device "${inputDevice}" not found` + } + + this.output = new midi.Output(outputDeviceFull); + } + + } + + 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; + } +} \ No newline at end of file diff --git a/src/devices/OSC.ts b/src/devices/OSC.ts new file mode 100644 index 0000000..968c63b --- /dev/null +++ b/src/devices/OSC.ts @@ -0,0 +1,62 @@ +import osc from "osc"; + +interface IOSCEvent { + address: string; + handler: (message: osc.ResponseMessage) => void; +} + +export class OSCDevice { + private port: osc.UDPPort; + private listeners: Array = []; + + constructor(inputHost: string, outputHost: string, inputPort: number, outputPort: number = inputPort) { + this.port = new osc.UDPPort({ + localAddress: inputHost, + remoteAddress: outputHost, + localPort: inputPort, + remotePort: outputPort + }); + + 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); + } + + }) + this.port.open(); + + } + + 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; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 8f19bd2..cdfa242 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,406 +1,16 @@ import * as midi from 'easymidi'; -import osc from 'osc'; + +async function main() { + try { + console.log('inputs', midi.getInputs()) + console.log('outputs', midi.getOutputs()) -try { - 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; + const mapping = await import('./mappings/xr18-home.js'); + mapping.default() + } catch (e) { + console.error('Uncaught error', e) } +} - 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.setMaxListeners(50); - 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('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); - } - - }) - 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()); - odev.addListener('/offleds', () => 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', OSC_LEVEL_0) - fader2way(2, PAGE_MAIN, '/bus/1/mix/fader', OSC_LEVEL_0) - 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', OSC_LEVEL_0) - 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', OSC_LEVEL_0) - 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') - -} catch (e) { - console.error('Uncaught error', e) -} \ No newline at end of file +main(); \ No newline at end of file diff --git a/src/mappings/beunsize-ma3.ts b/src/mappings/beunsize-ma3.ts new file mode 100644 index 0000000..c81e37b --- /dev/null +++ b/src/mappings/beunsize-ma3.ts @@ -0,0 +1,43 @@ +import * as midi from 'easymidi' +import { MessageType, MidiControl, MidiDevice } from "../devices/Midi.js" +import { OSCDevice } from "../devices/OSC.js" +import { mapNumber } from '../utilityFunctions.js' + +export default async function mapping() { + + const xtouch = new MidiDevice('X Touch Compact') + const keyboard = new MidiDevice('idobo') + + const ma3 = new OSCDevice('0.0.0.0', '127.0.0.1', 3000, 3001) + + // Executor Faders + const faders = [0, 1, 2, 3, 4, 5, 6, 7, 8].map(f => xtouch.addControl(MessageType.CC, { controller: f })) + + faders.forEach((fader, index) => { + const firstExec = 201; + const exec = firstExec - 1 + index; + const address = `/Page/Fader${exec}`; + + fader.addOutput(0, (message) => { + ma3.sendFloat(address, mapNumber(message.value, 0, 127, 0, 1)); + }) + + ma3.addListener(address, d => { + const value = (d.args as any)[0]?.value; + if (value !== undefined) { + const mappedValue = mapNumber(value, 0, 1, 0, 127); + fader.handleFeedback(0, { value: mappedValue }) + } + }) + }) + + 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], + ].map(row => row.map(note => { xtouch.addControl(MessageType.NoteOnOff))) + + + +} \ No newline at end of file diff --git a/src/mappings/xr18-home.ts b/src/mappings/xr18-home.ts new file mode 100644 index 0000000..93409c7 --- /dev/null +++ b/src/mappings/xr18-home.ts @@ -0,0 +1,188 @@ + +import * as midi from 'easymidi'; +import { MessageType, MidiControl, MidiDevice } from '../devices/Midi.js'; +import { OSCDevice } from '../devices/OSC.js'; +import { mapNumber } from '../utilityFunctions.js'; + +export default async function mapping() { + 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('Platform X', true); + + const odev = new OSCDevice('0.0.0.0', '192.168.0.47', 10024); + + // Polling for XR18 feedback + setInterval(() => { + odev.sendInt('/xremote', 1) + }, 5000) + + // 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()); + odev.addListener('/offleds', () => 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', OSC_LEVEL_0) + fader2way(2, PAGE_MAIN, '/bus/1/mix/fader', OSC_LEVEL_0) + 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', OSC_LEVEL_0) + 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', OSC_LEVEL_0) + 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') +} \ No newline at end of file diff --git a/src/utilityFunctions.ts b/src/utilityFunctions.ts new file mode 100644 index 0000000..8dcb82c --- /dev/null +++ b/src/utilityFunctions.ts @@ -0,0 +1,11 @@ + +export 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; +} \ No newline at end of file