WIP: reorganizing and beunsize mapping
This commit is contained in:
parent
569a03245c
commit
cad9433266
6 changed files with 502 additions and 401 deletions
187
src/devices/Midi.ts
Normal file
187
src/devices/Midi.ts
Normal file
|
|
@ -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<T extends MessageType> {
|
||||||
|
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<DataLookup[T]>,
|
||||||
|
private options: Partial<IMidiControlOptions> = {}
|
||||||
|
|
||||||
|
) {
|
||||||
|
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 extends DataLookup[T]>(): 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<D extends DataLookup[T]>(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<D extends DataLookup[T]>(pages: number | number[], data: Partial<D>, 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<MidiControl<MessageType>> = [];
|
||||||
|
|
||||||
|
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<T extends MessageType>(type: T, filter: Partial<DataLookup[T]> = {}, options: Partial<IMidiControlOptions> = {}): MidiControl<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/devices/OSC.ts
Normal file
62
src/devices/OSC.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import osc from "osc";
|
||||||
|
|
||||||
|
interface IOSCEvent {
|
||||||
|
address: string;
|
||||||
|
handler: (message: osc.ResponseMessage<osc.MessageArg>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OSCDevice {
|
||||||
|
private port: osc.UDPPort;
|
||||||
|
private listeners: Array<IOSCEvent> = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
412
src/index.ts
412
src/index.ts
|
|
@ -1,406 +1,16 @@
|
||||||
import * as midi from 'easymidi';
|
import * as midi from 'easymidi';
|
||||||
import osc from 'osc';
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('inputs', midi.getInputs())
|
||||||
|
console.log('outputs', midi.getOutputs())
|
||||||
|
|
||||||
|
|
||||||
try {
|
const mapping = await import('./mappings/xr18-home.js');
|
||||||
console.log('inputs', midi.getInputs())
|
mapping.default()
|
||||||
console.log('outputs', midi.getOutputs())
|
} catch (e) {
|
||||||
|
console.error('Uncaught error', e)
|
||||||
|
|
||||||
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 {
|
main();
|
||||||
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<T extends MessageType> {
|
|
||||||
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<DataLookup[T]>,
|
|
||||||
private options: Partial<IMidiControlOptions> = {}
|
|
||||||
|
|
||||||
) {
|
|
||||||
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 extends DataLookup[T]>(): 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<D extends DataLookup[T]>(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<D extends DataLookup[T]>(pages: number | number[], data: Partial<D>, 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<MidiControl<MessageType>> = [];
|
|
||||||
|
|
||||||
constructor(device: string) {
|
|
||||||
this.input = new midi.Input(device);
|
|
||||||
this.output = new midi.Output(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addControl<T extends MessageType>(type: T, filter: Partial<DataLookup[T]>, options: Partial<IMidiControlOptions> = {}): MidiControl<T> {
|
|
||||||
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<osc.MessageArg>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OSCDevice {
|
|
||||||
private port: osc.UDPPort;
|
|
||||||
private listeners: Array<IOSCEvent> = [];
|
|
||||||
|
|
||||||
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<MessageType.NoteOn> | 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)
|
|
||||||
}
|
|
||||||
43
src/mappings/beunsize-ma3.ts
Normal file
43
src/mappings/beunsize-ma3.ts
Normal file
|
|
@ -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<MessageType.NoteOnOff>[][] = [
|
||||||
|
[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)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
188
src/mappings/xr18-home.ts
Normal file
188
src/mappings/xr18-home.ts
Normal file
|
|
@ -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<MessageType.NoteOn> | 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')
|
||||||
|
}
|
||||||
11
src/utilityFunctions.ts
Normal file
11
src/utilityFunctions.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue