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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue