Initial commit
This commit is contained in:
commit
ddb654cc66
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.idea
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# OBS midi
|
||||||
|
|
||||||
|
Control OBS studio (and more!) with any midi device.
|
9
config/default.ts
Normal file
9
config/default.ts
Normal 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
22
package.json
Normal 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
3
src/Action.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default abstract class Action {
|
||||||
|
public abstract async execute(velocity: number): Promise<void>;
|
||||||
|
}
|
88
src/App.ts
Normal file
88
src/App.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
22
src/ButtonAdvancedControl.ts
Normal file
22
src/ButtonAdvancedControl.ts
Normal 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
22
src/ButtonControl.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
53
src/KnobAdvancedControl.ts
Normal file
53
src/KnobAdvancedControl.ts
Normal 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
23
src/MidiControl.ts
Normal 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
44
src/main.ts
Normal 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
20
tsconfig.json
Normal 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/**/*"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user