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