commit ddb654cc669536f6d4693589d0ece2e05ce051fd Author: Alice Gaudon Date: Fri Jul 17 21:29:39 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe92ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +dist +node_modules +yarn.lock +yarn-error.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bb289a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# OBS midi + +Control OBS studio (and more!) with any midi device. diff --git a/config/default.ts b/config/default.ts new file mode 100644 index 0000000..d231234 --- /dev/null +++ b/config/default.ts @@ -0,0 +1,9 @@ +export default { + obs: { + address: '127.0.0.1:4444', + password: 'secret', + }, + midi: { + controller: 'Launchkey Mini MIDI 1', + } +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8db08b3 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "obs-midi", + "version": "0.1.0", + "description": "Control OBS with any midi controller.", + "main": "dist/main.js", + "author": "Alice Gaudon ", + "license": "MIT", + "scripts": { + "dev": "yarn tsc && node ." + }, + "devDependencies": { + "@types/node": "^14.0.23", + "typescript": "^3.9.7" + }, + "dependencies": { + "@types/config": "^0.0.36", + "config": "^3.3.1", + "jzz": "^1.0.8", + "obs-websocket-js": "^4.0.1", + "ts-node": "^8.10.2" + } +} diff --git a/src/Action.ts b/src/Action.ts new file mode 100644 index 0000000..33aed47 --- /dev/null +++ b/src/Action.ts @@ -0,0 +1,3 @@ +export default abstract class Action { + public abstract async execute(velocity: number): Promise; +} diff --git a/src/App.ts b/src/App.ts new file mode 100644 index 0000000..68d28e8 --- /dev/null +++ b/src/App.ts @@ -0,0 +1,88 @@ +import config from "config"; +import console from "console"; +import MidiControl from "./MidiControl"; +import jzz from "jzz"; +import ObsWebSocket from "obs-websocket-js"; + +export default class App { + private readonly obs: ObsWebSocket = new ObsWebSocket(); + private readonly controls: MidiControl[] = []; + private jzz: any; + + public constructor() { + + } + + public registerControl(control: MidiControl) { + this.controls.push(control); + } + + public async start(): Promise { + await this.initObs(); + await this.initMidi(); + } + + public async stop(): Promise { + await this.jzz.stop(); + } + + private async initObs(): Promise { + const connectionRetryListener = async () => { + try { + console.error('Connection closed or authentication failure. Retrying in 2s...'); + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 2000); + }); + await this.connectObs(); + } catch (e) { + console.error(e); + } + }; + this.obs.on('ConnectionClosed', connectionRetryListener); + this.obs.on('AuthenticationFailure', connectionRetryListener); + + await this.connectObs(); + } + + private async connectObs(): Promise { + await this.obs.connect({ + address: config.get('obs.address'), + password: config.get('obs.password'), + }); + } + + private async initMidi(): Promise { + this.jzz = await 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); + } + }); + } + + 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(eventType, velocity)) { + return; + } + } + } + + public getObs(): ObsWebSocket { + return this.obs; + } +} \ No newline at end of file diff --git a/src/ButtonAdvancedControl.ts b/src/ButtonAdvancedControl.ts new file mode 100644 index 0000000..4c0c8f1 --- /dev/null +++ b/src/ButtonAdvancedControl.ts @@ -0,0 +1,22 @@ +import MidiControl from "./MidiControl"; +import Action from "./Action"; + +export default class ButtonAdvancedControl extends MidiControl { + private readonly triggerOnUp: boolean; + + public constructor(id: number, action: Action, triggerOnUp: boolean = false) { + super(id, action); + this.triggerOnUp = triggerOnUp; + } + + public async handleEvent(eventType: number, velocity: number): Promise { + if (this.triggerOnUp && velocity === 0 || + !this.triggerOnUp && velocity !== 0) { + await this.performAction(velocity === 0 ? 0 : 1); + return true; + } + + return false; + } + +} \ No newline at end of file diff --git a/src/ButtonControl.ts b/src/ButtonControl.ts new file mode 100644 index 0000000..2953433 --- /dev/null +++ b/src/ButtonControl.ts @@ -0,0 +1,22 @@ +import MidiControl, {EventType} from "./MidiControl"; +import Action from "./Action"; + +export default class ButtonControl extends MidiControl { + private readonly triggerOnUp: boolean; + + public constructor(id: number, action: Action, triggerOnUp: boolean = false) { + super(id, action); + this.triggerOnUp = triggerOnUp; + } + + public async handleEvent(eventType: number, velocity: number): Promise { + if (this.triggerOnUp && eventType === EventType.BUTTON_UP || + !this.triggerOnUp && eventType === EventType.BUTTON_DOWN) { + await this.performAction(velocity); + return true; + } + + return false; + } + +} \ No newline at end of file diff --git a/src/KnobAdvancedControl.ts b/src/KnobAdvancedControl.ts new file mode 100644 index 0000000..97e6bc7 --- /dev/null +++ b/src/KnobAdvancedControl.ts @@ -0,0 +1,53 @@ +import MidiControl, {EventType} from "./MidiControl"; +import Action from "./Action"; + +export default class KnobAdvancedControl extends MidiControl { + public constructor(id: number, action: Action) { + super(id, action); + } + + public async handleEvent(eventType: number, velocity: number): Promise { + if (eventType === EventType.ADVANCED_CONTROL) { + await this.performAction(velocity); + return true; + } + + return false; + } +} + +export function toVolume(velocity: number) { + return dbToLinear(linearToDef(velocity / 127)); +} + +function dbToLinear(x: number) { + return Math.pow(10, x / 20); +} + +/** + * @author Arkhist (thanks!) + */ +function linearToDef(y: number) { + if (y >= 1) return 0; + + if (y >= 0.75) + return reverseDef(y, 9, 9, 0.25, 0.75); + else if (y >= 0.5) + return reverseDef(y, 20, 11, 0.25, 0.5); + else if (y >= 0.3) + return reverseDef(y, 30, 10, 0.2, 0.3); + else if (y >= 0.15) + return reverseDef(y, 40, 10, 0.15, 0.15); + else if (y >= 0.075) + return reverseDef(y, 50, 10, 0.075, 0.075); + else if (y >= 0.025) + return reverseDef(y, 60, 10, 0.05, 0.025); + else if (y > 0) + return reverseDef(y, 150, 90, 0.025, 0); + else + return -15000; +} + +function reverseDef(y: number, a1: number, d: number, m: number, a2: number) { + return ((y - a2) / m) * d - a1; +} \ No newline at end of file diff --git a/src/MidiControl.ts b/src/MidiControl.ts new file mode 100644 index 0000000..e2b956d --- /dev/null +++ b/src/MidiControl.ts @@ -0,0 +1,23 @@ +import Action from "./Action"; + +export default abstract class MidiControl { + public readonly id: number; + private readonly action: Action; + + protected constructor(id: number, action: Action) { + this.id = id; + this.action = action; + } + + public abstract async handleEvent(eventType: number, velocity: number): Promise; + + protected async performAction(velocity: number): Promise { + await this.action.execute(velocity); + } +} + +export enum EventType { + BUTTON_DOWN = 153, + BUTTON_UP = 137, + ADVANCED_CONTROL = 176, +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3b23f47 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,44 @@ +import * as console from "console"; +import App from "./App"; +import ButtonControl from "./ButtonControl"; +import Action from "./Action"; +import KnobAdvancedControl, {toVolume} from "./KnobAdvancedControl"; + +(async () => { + const app = new App(); + + app.registerControl(new ButtonControl(50, new class extends Action { + async execute(velocity: number): Promise { + await app.getObs().send('SetCurrentScene', {'scene-name': 'Scene 2'}); + } + })); + app.registerControl(new ButtonControl(51, new class extends Action { + async execute(velocity: number): Promise { + await app.getObs().send('SetCurrentScene', {'scene-name': 'BareMainScreen'}); + } + })); + + app.registerControl(new ButtonControl(40, new class extends Action { + async execute(velocity: number): Promise { + await app.getObs().send('ToggleMute', {source: 'Desktop Audio'}); + } + })); + + app.registerControl(new ButtonControl(41, new class extends Action { + async execute(velocity: number): Promise { + await app.getObs().send('ToggleMute', {source: 'Mic/Aux'}); + } + })); + app.registerControl(new KnobAdvancedControl(21, new class extends Action { + async execute(velocity: number): Promise { + await app.getObs().send('SetVolume', {source: 'Desktop Audio', volume: toVolume(velocity)}); + } + })); + app.registerControl(new KnobAdvancedControl(22, new class extends Action { + async execute(velocity: number): Promise { + await app.getObs().send('SetVolume', {source: 'Mic/Aux', volume: toVolume(velocity)}); + } + })); + + await app.start(); +})().catch(console.error); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff47c6c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "esModuleInterop": true, + "outDir": "dist", + "target": "ES6", + "strict": true, + "lib": [ + "es2020", + "DOM" + ], + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ] + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file