WIP: reorganizing and beunsize mapping

This commit is contained in:
Rik Berkelder 2026-05-10 04:04:30 +02:00
parent 569a03245c
commit cad9433266
6 changed files with 502 additions and 401 deletions

187
src/devices/Midi.ts Normal file
View 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
View 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;
}
}