import config from "config"; import console from "console"; import MidiControl from "./MidiControl"; import jzz from "jzz"; import ObsWebSocket from "obs-websocket-js"; import LedState from "./LedState"; import ObsStateTracker from "./obs/ObsStateTracker"; import WMSMusicWidget from "./wms/WMSMusicWidget"; import {sleep} from "./Utils"; export default class App { private obs: ObsWebSocket = new ObsWebSocket(); private obsStateTracker: ObsStateTracker = new ObsStateTracker(this.obs); private controls: MidiControl[] = []; private midiIn: any; private pendingStates: LedState[] = []; private readonly wmsMusicWidget: WMSMusicWidget = new WMSMusicWidget(); private readonly configCallback: () => Promise; private animationHandler?: NodeJS.Timeout; private animating: boolean = false; private animationStopCallback?: () => void; private animationCounter: number = 0; public constructor(configCallback: () => Promise) { this.configCallback = configCallback; } public registerControl(control: MidiControl) { this.controls.push(control); } public async init(): Promise { await this.initialRainbowAnimation(); } public async start(): Promise { await this.enableInControl(); this.obs = new ObsWebSocket(); await this.initObs(); this.obsStateTracker = new ObsStateTracker(this.obs); await this.obsStateTracker.init(this); await this.configCallback(); await this.initMidiIn(); await this.wmsMusicWidget.start(); } public async reload(): Promise { await this.stop(); await this.start(); } public async stop(): Promise { if (this.midiIn) { await this.midiIn.close(); this.midiIn = undefined; } await this.obs.removeAllListeners(); await this.obs.disconnect(); } private async initObs(): Promise { let retried = false; this.obs.once('ConnectionClosed', async () => { if (retried) return; retried = true; try { console.error('Connection closed or authentication failure. Retrying in 2s...'); await new Promise(resolve => { setTimeout(() => { resolve(); }, 2000); }); await this.reload(); } catch (e) { console.error(e); } }); await this.connectObs(); } private async connectObs(): Promise { await this.obs.connect({ address: config.get('obs.address'), password: config.get('obs.password'), }); } private async initMidiIn(): Promise { this.midiIn = jzz() .openMidiIn(config.get('midi.controller')) .or('Cannot open MIDI In port!') .and(function (this: any) { console.log('MIDI-In:', this.name()); }) .connect(async (msg: any) => { try { await this.handleMidiMessage(msg); } catch (e) { console.error(e); } }); await this.midiIn; await this.stopAnimation(); // Init led controls for (const control of this.controls) { await control.init(this); } await this.updateControls(); } private async handleMidiMessage(msg: any) { const eventType = msg['0']; const id = msg['1']; const velocity = msg['2']; console.log('Midi:', eventType, id, velocity); for (const control of this.controls) { if (control.id === id && await control.handleEvent(this, eventType, velocity)) { return; } } } public getObs(): ObsWebSocket { return this.obs; } public getObsStateTracker(): ObsStateTracker { return this.obsStateTracker; } private async enableInControl(): Promise { // Enable "in control" this.led(0, 10, 0, 1); this.led(0, 12, 0, 1); await this.tick(); await this.loadingMidiAnimation(); } private async initialRainbowAnimation(): Promise { // Led rainbow for (let n = 96; n < 105; n++) { this.led(0, n, n % 4, (n + 2) % 4, 750); this.led(0, n + 16, n % 4, (n + 2) % 4, 750); await this.tick(); } await sleep(1000); } private async loadingMidiAnimation(): Promise { await this.animate(async () => { let i = this.animationCounter % 18; let n = 96 + i + (i >= 9 ? 7 : 0); this.led(0, n, 1, 3, 250); await this.tick(); await sleep(250); }, 0); } private async animate(animation: () => Promise, ms: number): Promise { await this.stopAnimation(); const run = (first: boolean) => { this.animating = true; this.animationHandler = setTimeout(async () => { try { await animation(); this.animationCounter++; if (typeof this.animationHandler !== 'undefined') { run(false); } else { this.signalAnimationEnd(); } } catch (e) { console.error(e); this.stopAnimation() .catch(console.error); this.signalAnimationEnd(); } }, first ? 0 : ms); }; run(true); } private signalAnimationEnd() { this.animating = false; if (this.animationStopCallback) { this.animationStopCallback(); this.animationStopCallback = undefined; } } private async stopAnimation(): Promise { if (typeof this.animationHandler !== 'undefined') { clearTimeout(this.animationHandler); this.animationHandler = undefined; } await new Promise(resolve => { if (this.animating) { this.animationStopCallback = resolve; } else { resolve(); } }); } public async updateControls(): Promise { for (const control of this.controls) { await control.update(this); } await this.tick(); } public led( channel: number, note: number, green: number, red: number, duration: number = 0 ): void { this.pendingStates.push(new LedState( channel, note, green, red, duration )); } public async tick(): Promise { let action = jzz().openMidiOut(config.get('midi.output')) .or('Cannot open MIDI Out port!'); for (const state of this.pendingStates) { let color = (state.green << 4) | state.red; // console.log('Out', state.channel, state.note, color, state.duration); // console.log('>', state.green, state.red); if (state.duration > 0) { action.note(state.channel, state.note, color, state.duration); } else { action.noteOn(state.channel, state.note, color); } } this.pendingStates = []; await action; } }