Initial commit

This commit is contained in:
Alice Gaudon 2020-07-17 21:29:39 +02:00
commit ddb654cc66
12 changed files with 314 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea
dist
node_modules
yarn.lock
yarn-error.log

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# OBS midi
Control OBS studio (and more!) with any midi device.

9
config/default.ts Normal file
View File

@ -0,0 +1,9 @@
export default {
obs: {
address: '127.0.0.1:4444',
password: 'secret',
},
midi: {
controller: 'Launchkey Mini MIDI 1',
}
};

22
package.json Normal file
View File

@ -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 <alice@gaudon.pro>",
"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"
}
}

3
src/Action.ts Normal file
View File

@ -0,0 +1,3 @@
export default abstract class Action {
public abstract async execute(velocity: number): Promise<void>;
}

88
src/App.ts Normal file
View File

@ -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<void> {
await this.initObs();
await this.initMidi();
}
public async stop(): Promise<void> {
await this.jzz.stop();
}
private async initObs(): Promise<void> {
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<void> {
await this.obs.connect({
address: config.get<string>('obs.address'),
password: config.get<string>('obs.password'),
});
}
private async initMidi(): Promise<void> {
this.jzz = await jzz()
.openMidiIn(config.get<string>('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;
}
}

View File

@ -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<boolean> {
if (this.triggerOnUp && velocity === 0 ||
!this.triggerOnUp && velocity !== 0) {
await this.performAction(velocity === 0 ? 0 : 1);
return true;
}
return false;
}
}

22
src/ButtonControl.ts Normal file
View File

@ -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<boolean> {
if (this.triggerOnUp && eventType === EventType.BUTTON_UP ||
!this.triggerOnUp && eventType === EventType.BUTTON_DOWN) {
await this.performAction(velocity);
return true;
}
return false;
}
}

View File

@ -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<boolean> {
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;
}

23
src/MidiControl.ts Normal file
View File

@ -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<boolean>;
protected async performAction(velocity: number): Promise<void> {
await this.action.execute(velocity);
}
}
export enum EventType {
BUTTON_DOWN = 153,
BUTTON_UP = 137,
ADVANCED_CONTROL = 176,
}

44
src/main.ts Normal file
View File

@ -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<void> {
await app.getObs().send('SetCurrentScene', {'scene-name': 'Scene 2'});
}
}));
app.registerControl(new ButtonControl(51, new class extends Action {
async execute(velocity: number): Promise<void> {
await app.getObs().send('SetCurrentScene', {'scene-name': 'BareMainScreen'});
}
}));
app.registerControl(new ButtonControl(40, new class extends Action {
async execute(velocity: number): Promise<void> {
await app.getObs().send('ToggleMute', {source: 'Desktop Audio'});
}
}));
app.registerControl(new ButtonControl(41, new class extends Action {
async execute(velocity: number): Promise<void> {
await app.getObs().send('ToggleMute', {source: 'Mic/Aux'});
}
}));
app.registerControl(new KnobAdvancedControl(21, new class extends Action {
async execute(velocity: number): Promise<void> {
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<void> {
await app.getObs().send('SetVolume', {source: 'Mic/Aux', volume: toVolume(velocity)});
}
}));
await app.start();
})().catch(console.error);

20
tsconfig.json Normal file
View File

@ -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/**/*"
]
}