Add support for basic led feedback
Current configuration is only tested with Novation Launchkey Mini mk1. Wider support will come through extended configuration files.
This commit is contained in:
parent
ddb654cc66
commit
29bd7822a3
@ -2,8 +2,13 @@ export default {
|
||||
obs: {
|
||||
address: '127.0.0.1:4444',
|
||||
password: 'secret',
|
||||
audio_sources: [
|
||||
'Desktop Audio',
|
||||
'Mic/Aux',
|
||||
],
|
||||
},
|
||||
midi: {
|
||||
controller: 'Launchkey Mini MIDI 1',
|
||||
controller: 'Launchkey Mini MIDI 2',
|
||||
output: 'Launchkey Mini MIDI 2',
|
||||
}
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
export default abstract class Action {
|
||||
public abstract async execute(velocity: number): Promise<void>;
|
||||
import App from "./App";
|
||||
|
||||
export default interface Action {
|
||||
performAction(app: App, velocity: number): Promise<void>;
|
||||
}
|
||||
|
108
src/App.ts
108
src/App.ts
@ -3,11 +3,15 @@ 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";
|
||||
|
||||
export default class App {
|
||||
private readonly obs: ObsWebSocket = new ObsWebSocket();
|
||||
private readonly controls: MidiControl[] = [];
|
||||
private jzz: any;
|
||||
private obs: ObsWebSocket = new ObsWebSocket();
|
||||
private obsStateTracker: ObsStateTracker = new ObsStateTracker(this.obs);
|
||||
private controls: MidiControl[] = [];
|
||||
private midiIn: any;
|
||||
private pendingStates: LedState[] = [];
|
||||
|
||||
public constructor() {
|
||||
|
||||
@ -19,11 +23,23 @@ export default class App {
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await this.initObs();
|
||||
await this.obsStateTracker.init(this);
|
||||
await this.initMidi();
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
await this.jzz.stop();
|
||||
await this.midiIn.close();
|
||||
await this.obs.removeAllListeners();
|
||||
await this.obs.disconnect();
|
||||
}
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
await this.obs.removeAllListeners();
|
||||
await this.midiIn.close();
|
||||
|
||||
this.obsStateTracker = new ObsStateTracker(this.obs);
|
||||
await this.obsStateTracker.init(this);
|
||||
await this.initMidi();
|
||||
}
|
||||
|
||||
private async initObs(): Promise<void> {
|
||||
@ -54,7 +70,7 @@ export default class App {
|
||||
}
|
||||
|
||||
private async initMidi(): Promise<void> {
|
||||
this.jzz = await jzz()
|
||||
this.midiIn = jzz()
|
||||
.openMidiIn(config.get<string>('midi.controller'))
|
||||
.or('Cannot open MIDI In port!')
|
||||
.and(function (this: any) {
|
||||
@ -67,6 +83,13 @@ export default class App {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
await this.midiIn;
|
||||
|
||||
await this.enableInControl();
|
||||
|
||||
for (const control of this.controls) {
|
||||
await control.init(this);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMidiMessage(msg: any) {
|
||||
@ -76,7 +99,7 @@ export default class App {
|
||||
console.log('Midi:', eventType, id, velocity);
|
||||
|
||||
for (const control of this.controls) {
|
||||
if (control.id === id && await control.handleEvent(eventType, velocity)) {
|
||||
if (control.id === id && await control.handleEvent(this, eventType, velocity)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -85,4 +108,75 @@ export default class App {
|
||||
public getObs(): ObsWebSocket {
|
||||
return this.obs;
|
||||
}
|
||||
}
|
||||
|
||||
public getObsStateTracker(): ObsStateTracker {
|
||||
return this.obsStateTracker;
|
||||
}
|
||||
|
||||
private async enableInControl(): Promise<void> {
|
||||
// Enable "in control"
|
||||
this.led(0, 10, 0, 1);
|
||||
this.led(0, 12, 0, 1);
|
||||
|
||||
await this.tick();
|
||||
|
||||
// Led rainbow
|
||||
for (let n = 96; n < 105; n++) {
|
||||
this.led(0, n, n % 4, (n + 2) % 4, 2000);
|
||||
this.led(0, n + 16, n % 4, (n + 2) % 4, 2000);
|
||||
await this.tick();
|
||||
}
|
||||
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
|
||||
// Init led controls
|
||||
await this.updateControls();
|
||||
}
|
||||
|
||||
public async updateControls(): Promise<void> {
|
||||
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<void> {
|
||||
let action = jzz().openMidiOut(config.get<string>('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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
import MidiControl from "./MidiControl";
|
||||
import Action from "./Action";
|
||||
import App from "./App";
|
||||
|
||||
export default class ButtonAdvancedControl extends MidiControl {
|
||||
export default abstract class ButtonAdvancedControl extends MidiControl {
|
||||
private readonly triggerOnUp: boolean;
|
||||
|
||||
public constructor(id: number, action: Action, triggerOnUp: boolean = false) {
|
||||
super(id, action);
|
||||
protected constructor(id: number, triggerOnUp: boolean = false) {
|
||||
super(id);
|
||||
this.triggerOnUp = triggerOnUp;
|
||||
}
|
||||
|
||||
public async handleEvent(eventType: number, velocity: number): Promise<boolean> {
|
||||
public async handleEvent(app: App, eventType: number, velocity: number): Promise<boolean> {
|
||||
if (this.triggerOnUp && velocity === 0 ||
|
||||
!this.triggerOnUp && velocity !== 0) {
|
||||
await this.performAction(velocity === 0 ? 0 : 1);
|
||||
await this.executeAction(app, velocity === 0 ? 0 : 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
import MidiControl, {EventType} from "./MidiControl";
|
||||
import Action from "./Action";
|
||||
import App from "./App";
|
||||
|
||||
export default class ButtonControl extends MidiControl {
|
||||
export default abstract class ButtonControl extends MidiControl {
|
||||
private readonly triggerOnUp: boolean;
|
||||
|
||||
public constructor(id: number, action: Action, triggerOnUp: boolean = false) {
|
||||
super(id, action);
|
||||
protected constructor(id: number, triggerOnUp: boolean = false) {
|
||||
super(id);
|
||||
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);
|
||||
public async handleEvent(app: App, eventType: number, velocity: number): Promise<boolean> {
|
||||
if (this.triggerOnUp && [EventType.BUTTON_UP, EventType.IN_BUTTON_UP].indexOf(eventType) >= 0 ||
|
||||
!this.triggerOnUp && [EventType.BUTTON_DOWN, EventType.IN_BUTTON_DOWN].indexOf(eventType) >= 0) {
|
||||
await this.executeAction(app, velocity);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import MidiControl, {EventType} from "./MidiControl";
|
||||
import Action from "./Action";
|
||||
import App from "./App";
|
||||
|
||||
export default class KnobAdvancedControl extends MidiControl {
|
||||
public constructor(id: number, action: Action) {
|
||||
super(id, action);
|
||||
export default abstract class KnobAdvancedControl extends MidiControl {
|
||||
protected constructor(id: number) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public async handleEvent(eventType: number, velocity: number): Promise<boolean> {
|
||||
public async handleEvent(app: App, eventType: number, velocity: number): Promise<boolean> {
|
||||
if (eventType === EventType.ADVANCED_CONTROL) {
|
||||
await this.performAction(velocity);
|
||||
await this.executeAction(app, velocity);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -16,8 +16,12 @@ export default class KnobAdvancedControl extends MidiControl {
|
||||
}
|
||||
}
|
||||
|
||||
export function toRawVolume(velocity: number) {
|
||||
return velocity / 127;
|
||||
}
|
||||
|
||||
export function toVolume(velocity: number) {
|
||||
return dbToLinear(linearToDef(velocity / 127));
|
||||
return dbToLinear(linearToDef(toRawVolume(velocity)));
|
||||
}
|
||||
|
||||
function dbToLinear(x: number) {
|
||||
|
21
src/LedState.ts
Normal file
21
src/LedState.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default class LedState {
|
||||
public readonly channel: number;
|
||||
public readonly note: number;
|
||||
public readonly green: number;
|
||||
public readonly red: number;
|
||||
public readonly duration: number;
|
||||
|
||||
constructor(
|
||||
channel: number,
|
||||
note: number,
|
||||
green: number,
|
||||
red: number,
|
||||
duration: number = 0
|
||||
) {
|
||||
this.channel = channel;
|
||||
this.note = note;
|
||||
this.green = green;
|
||||
this.red = red;
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
5
src/LedStateUpdater.ts
Normal file
5
src/LedStateUpdater.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import App from "./App";
|
||||
|
||||
export default interface LedStateUpdater {
|
||||
updateLedState(app: App, ledId: number): Promise<void>;
|
||||
}
|
@ -1,18 +1,31 @@
|
||||
import Action from "./Action";
|
||||
import App from "./App";
|
||||
import LedStateUpdater from "./LedStateUpdater";
|
||||
|
||||
export default abstract class MidiControl {
|
||||
public readonly id: number;
|
||||
private readonly action: Action;
|
||||
protected action?: Action;
|
||||
protected ledId?: number;
|
||||
protected ledStateUpdater?: LedStateUpdater;
|
||||
|
||||
protected constructor(id: number, action: Action) {
|
||||
protected constructor(id: number) {
|
||||
this.id = id;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public abstract async handleEvent(eventType: number, velocity: number): Promise<boolean>;
|
||||
public abstract async init(app: App): Promise<void>;
|
||||
|
||||
protected async performAction(velocity: number): Promise<void> {
|
||||
await this.action.execute(velocity);
|
||||
public abstract async handleEvent(app: App, eventType: number, velocity: number): Promise<boolean>;
|
||||
|
||||
protected async executeAction(app: App, velocity: number): Promise<void> {
|
||||
if (this.action) {
|
||||
await this.action.performAction(app, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
public async update(app: App): Promise<void> {
|
||||
if (typeof this.ledId === 'number' && this.ledStateUpdater) {
|
||||
await this.ledStateUpdater.updateLedState(app, this.ledId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,4 +33,6 @@ export enum EventType {
|
||||
BUTTON_DOWN = 153,
|
||||
BUTTON_UP = 137,
|
||||
ADVANCED_CONTROL = 176,
|
||||
IN_BUTTON_DOWN = 144,
|
||||
IN_BUTTON_UP = 128,
|
||||
}
|
||||
|
62
src/main.ts
62
src/main.ts
@ -1,44 +1,32 @@
|
||||
import * as console from "console";
|
||||
import App from "./App";
|
||||
import ButtonControl from "./ButtonControl";
|
||||
import Action from "./Action";
|
||||
import KnobAdvancedControl, {toVolume} from "./KnobAdvancedControl";
|
||||
import ObsSceneButton from "./obs/ObsSceneButton";
|
||||
import ObsSourceToggleMuteButton from "./obs/ObsSourceToggleMuteButton";
|
||||
import ObsSourceKnob from "./obs/ObsSourceKnob";
|
||||
import config from "config";
|
||||
|
||||
(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();
|
||||
|
||||
await configureApp(app);
|
||||
|
||||
await app.reload();
|
||||
})().catch(console.error);
|
||||
|
||||
async function configureApp(app: App): Promise<void> {
|
||||
let obsStateTracker = app.getObsStateTracker();
|
||||
|
||||
// Scenes
|
||||
const scenes = obsStateTracker.getSceneList();
|
||||
for (let i = 0; i < scenes.length && i < 8; i++) {
|
||||
app.registerControl(new ObsSceneButton(112 + i, scenes[i]));
|
||||
}
|
||||
|
||||
// Sources
|
||||
const sources = config.get<string[]>('obs.audio_sources');
|
||||
for (let i = 0; i < sources.length && i < 8; i++) {
|
||||
app.registerControl(new ObsSourceToggleMuteButton(96 + i, sources[i]));
|
||||
app.registerControl(new ObsSourceKnob(21 + i, sources[i]))
|
||||
}
|
||||
}
|
44
src/obs/ObsSceneButton.ts
Normal file
44
src/obs/ObsSceneButton.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import ButtonControl from "../ButtonControl";
|
||||
import App from "../App";
|
||||
import LedStateUpdater from "../LedStateUpdater";
|
||||
import Action from "../Action";
|
||||
|
||||
export default class ObsSceneButton extends ButtonControl implements Action, LedStateUpdater {
|
||||
private readonly sceneName: string;
|
||||
|
||||
public constructor(id: number, sceneName: string, triggerOnUp: boolean = false) {
|
||||
super(id, triggerOnUp);
|
||||
this.sceneName = sceneName;
|
||||
this.action = this;
|
||||
this.ledId = id;
|
||||
this.ledStateUpdater = this;
|
||||
}
|
||||
|
||||
public async init(app: App): Promise<void> {
|
||||
app.getObs().on('SwitchScenes', async data => {
|
||||
try {
|
||||
await this.update(app);
|
||||
await app.tick();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async updateLedState(app: App, ledId: number): Promise<void> {
|
||||
let isCurrentScene = this.isCurrentScene(app);
|
||||
app.led(0, ledId, isCurrentScene ? 3 : 0, isCurrentScene ? 0 : 3);
|
||||
}
|
||||
|
||||
private isCurrentScene(app: App) {
|
||||
return app.getObsStateTracker().getCurrentScene() === this.sceneName;
|
||||
}
|
||||
|
||||
public async performAction(app: App, velocity: number): Promise<void> {
|
||||
if (!this.isCurrentScene(app)) {
|
||||
app.led(0, this.ledId!, 1, 3);
|
||||
await app.tick();
|
||||
}
|
||||
await app.getObs().send('SetCurrentScene', {'scene-name': this.sceneName});
|
||||
}
|
||||
}
|
21
src/obs/ObsSourceKnob.ts
Normal file
21
src/obs/ObsSourceKnob.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import KnobAdvancedControl, {toVolume} from "../KnobAdvancedControl";
|
||||
import Action from "../Action";
|
||||
import App from "../App";
|
||||
|
||||
export default class ObsSourceKnob extends KnobAdvancedControl implements Action {
|
||||
private readonly sourceName: string;
|
||||
|
||||
constructor(id: number, sourceName: string) {
|
||||
super(id);
|
||||
this.sourceName = sourceName;
|
||||
this.action = this;
|
||||
}
|
||||
|
||||
public async init(app: App): Promise<void> {
|
||||
}
|
||||
|
||||
public async performAction(app: App, velocity: number): Promise<void> {
|
||||
await app.getObs().send('SetVolume', {source: this.sourceName, volume: toVolume(velocity)});
|
||||
}
|
||||
|
||||
}
|
40
src/obs/ObsSourceToggleMuteButton.ts
Normal file
40
src/obs/ObsSourceToggleMuteButton.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import ButtonControl from "../ButtonControl";
|
||||
import Action from "../Action";
|
||||
import LedStateUpdater from "../LedStateUpdater";
|
||||
import App from "../App";
|
||||
|
||||
export default class ObsSourceToggleMuteButton extends ButtonControl implements Action, LedStateUpdater {
|
||||
private readonly sourceName: string;
|
||||
|
||||
|
||||
public constructor(id: number, sourceName: string, triggerOnUp: boolean = false) {
|
||||
super(id, triggerOnUp);
|
||||
this.sourceName = sourceName;
|
||||
this.action = this;
|
||||
this.ledId = id;
|
||||
this.ledStateUpdater = this;
|
||||
}
|
||||
|
||||
public async init(app: App): Promise<void> {
|
||||
app.getObs().on('SourceMuteStateChanged', async data => {
|
||||
if (data.sourceName === this.sourceName) {
|
||||
try {
|
||||
await this.update(app);
|
||||
await app.tick();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async performAction(app: App, velocity: number): Promise<void> {
|
||||
await app.getObs().send('ToggleMute', {source: this.sourceName});
|
||||
}
|
||||
|
||||
public async updateLedState(app: App, ledId: number): Promise<void> {
|
||||
const muted = app.getObsStateTracker().isSourceMuted(this.sourceName);
|
||||
app.led(0, ledId, muted ? 0 : 3, muted ? 3 : 0);
|
||||
}
|
||||
|
||||
}
|
98
src/obs/ObsStateTracker.ts
Normal file
98
src/obs/ObsStateTracker.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import ObsWebSocket from "obs-websocket-js";
|
||||
import App from "../App";
|
||||
|
||||
export default class ObsStateTracker {
|
||||
private readonly obs: ObsWebSocket;
|
||||
private currentScene: string = '';
|
||||
private scenes: ObsWebSocket.Scene[] = [];
|
||||
private sources: { name: string, type: string, typeId: string }[] = [];
|
||||
private sourceMuteStates: { [p: string]: boolean } = {};
|
||||
|
||||
public constructor(obs: ObsWebSocket) {
|
||||
this.obs = obs;
|
||||
}
|
||||
|
||||
public async init(app: App): Promise<void> {
|
||||
// Current scene
|
||||
this.obs.on('SwitchScenes', async data => {
|
||||
try {
|
||||
await this.setScene(data['scene-name']);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
this.currentScene = (await this.obs.send('GetCurrentScene')).name;
|
||||
|
||||
// Scene list
|
||||
this.obs.on('ScenesChanged', async data => {
|
||||
console.log('Scenes changed.');
|
||||
try {
|
||||
await this.updateScenes();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
await this.updateScenes();
|
||||
|
||||
// Source list
|
||||
const sourceUpdateListener = async () => {
|
||||
try {
|
||||
await this.updateSources();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
this.obs.on('SourceCreated', sourceUpdateListener);
|
||||
this.obs.on('SourceDestroyed', sourceUpdateListener);
|
||||
this.obs.on('SourceRenamed', sourceUpdateListener);
|
||||
|
||||
// Source mute states
|
||||
this.obs.on('SourceMuteStateChanged', data => {
|
||||
this.sourceMuteStates[data.sourceName] = data.muted;
|
||||
});
|
||||
|
||||
await this.updateSources();
|
||||
}
|
||||
|
||||
private async updateScenes(): Promise<void> {
|
||||
let data = await this.obs.send('GetSceneList');
|
||||
this.scenes = data.scenes;
|
||||
await this.setScene(data["current-scene"]);
|
||||
console.log('Scene switched to', this.currentScene);
|
||||
}
|
||||
|
||||
private async updateSources(): Promise<void> {
|
||||
this.sources = (<any>(await this.obs.send('GetSourcesList')).sources);
|
||||
|
||||
// Source mute states
|
||||
this.sourceMuteStates = {};
|
||||
console.log('Loading source mute states...');
|
||||
for (const source of this.sources) {
|
||||
console.log('>', source.name);
|
||||
this.sourceMuteStates[source.name] = (await this.obs.send('GetMute', {source: source.name})).muted;
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentScene(): string {
|
||||
return this.currentScene;
|
||||
}
|
||||
|
||||
private async setScene(scene: string): Promise<void> {
|
||||
if (this.currentScene !== scene) {
|
||||
this.currentScene = scene;
|
||||
console.log('Scene switched to', this.currentScene);
|
||||
}
|
||||
}
|
||||
|
||||
public getSceneList(): string[] {
|
||||
return this.scenes.map(s => s.name);
|
||||
}
|
||||
|
||||
public getSourcesList(): string[] {
|
||||
return this.sources.map(s => s.name);
|
||||
}
|
||||
|
||||
public isSourceMuted(sceneName: string): boolean {
|
||||
return this.sourceMuteStates[sceneName];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user