diff --git a/packages/core-rewrite/src/definition.ts b/packages/core-rewrite/src/definition.ts index 11f9539..e762d21 100644 --- a/packages/core-rewrite/src/definition.ts +++ b/packages/core-rewrite/src/definition.ts @@ -10,7 +10,7 @@ export function defineInputType< } abstract class SourceType { - constructor(public kind: TKind) {} + constructor(public id: TKind) {} abstract settings(): SourceType; } @@ -46,7 +46,7 @@ export class InputType< async getDefaultSettings(obs: OBS) { const res = await obs.ws.call("GetInputDefaultSettings", { - inputKind: this.kind, + inputKind: this.id, }); return res.defaultInputSettings as TSettings; @@ -56,13 +56,14 @@ export class InputType< type DefineFilterArgs = { name: string; enabled?: boolean; + index?: number; settings?: Partial; }; export type FilterTypeSettings> = TType extends FilterType ? TSettings : never; -class FilterType< +export class FilterType< TKind extends string = string, TSettings extends Record = any > extends SourceType { @@ -202,6 +203,20 @@ export class Input< value: i.itemValue as InputTypeSettings[K], })); } + + async getFilters(obs: OBS) { + const { filters } = await obs.ws.call("GetSourceFilterList", { + sourceName: this.args.name, + }); + + return filters.map((f) => ({ + enabled: f.filterEnabled as boolean, + index: f.filterIndex as number, + kind: f.filterKind as string, + name: f.filterName as string, + settings: f.filterSettings as any, + })); + } } export type InputFilters> = T extends Input< @@ -219,7 +234,7 @@ export type FilterSettings> = TInput extends Filter< export class Filter> { constructor( - public type: TType, + public kind: TType, public args: DefineFilterArgs> ) {} diff --git a/packages/core-rewrite/src/filters.ts b/packages/core-rewrite/src/filters.ts index c1ad14b..0e34008 100644 --- a/packages/core-rewrite/src/filters.ts +++ b/packages/core-rewrite/src/filters.ts @@ -114,10 +114,8 @@ export const noiseGateFilter = defineFilterType("noise_gate_filter").settings<{ }>(); export const noiseSuppressFilter = defineFilterType( - "noise_suppress_filter" -).settings<{ - method: "speex" | "rnnoise" | "nvafx"; -}>(); + "noise_suppress_filter_v2" +).settings<{ method: "speex" | "rnnoise" | "nvafx" }>(); export const renderDelayFilter = defineFilterType("gpu_delay").settings<{ delay_ms: number; diff --git a/packages/core-rewrite/src/global.d.ts b/packages/core-rewrite/src/global.d.ts index 7cb0bbd..58648c0 100644 --- a/packages/core-rewrite/src/global.d.ts +++ b/packages/core-rewrite/src/global.d.ts @@ -10,7 +10,9 @@ declare module "obs-websocket-js" { SetSourcePrivateSettings: { sourceName: string; sourceSettings: { - SCENEIFY?: SceneifyPrivateSettings; + SCENEIFY?: SceneifyPrivateSettings & { + filters?: Array<{ name: string }>; + }; }; }; GetSourcePrivateSettings: { @@ -33,7 +35,9 @@ declare module "obs-websocket-js" { SetSourcePrivateSettings: void; GetSourcePrivateSettings: { sourceSettings: { - SCENEIFY?: SceneifyPrivateSettings; + SCENEIFY?: SceneifyPrivateSettings & { + filters?: Array<{ name: string }>; + }; }; }; SetSceneItemPrivateSettings: {}; diff --git a/packages/core-rewrite/src/index.test.ts b/packages/core-rewrite/src/index.test.ts index 58bccb1..c0df4d1 100644 --- a/packages/core-rewrite/src/index.test.ts +++ b/packages/core-rewrite/src/index.test.ts @@ -1,5 +1,6 @@ import { defineScene } from "./definition.ts"; import { + gainFilter, noiseGateFilter, noiseSuppressFilter, sharpenFilter, @@ -7,14 +8,12 @@ import { } from "./filters.ts"; import { browserSource, - colorSource, coreAudioInputCapture, macOSScreenCapture, videoCaptureSource, } from "./inputs.ts"; import { OBS } from "./obs.ts"; -import { syncScene as syncScene } from "./runtime.ts"; -import { alignmentToOBS } from "./sceneItem.ts"; +import { FilterDefsOfInputDef, syncScene as syncScene } from "./runtime.ts"; export const GAP = 20; @@ -39,6 +38,20 @@ const micInput = coreAudioInputCapture.defineInput({ "AppleUSBAudioEngine:Burr-Brown from TI :USB Audio CODEC :130000:2", enable_downmix: true, }, + filters: { + gain: gainFilter.defineFilter({ + index: 0, + enabled: true, + name: "Gain", + settings: { db: 2.5 }, + }), + noiseSuppression: noiseSuppressFilter.defineFilter({ + index: 1, + enabled: true, + name: "Noise Suppression", + settings: { method: "speex" }, + }), + }, }); const OUTPUT_WIDTH = 1920; @@ -90,11 +103,6 @@ export const mainScene = defineScene({ index: 4, input: micInput, }, - - // mic: { - // index: 4, - // input: aud - // } // guest: { // input: browserSource.defineInput({ // name: "Guest", @@ -125,27 +133,6 @@ export const mainScene = defineScene({ // }, // }), // }, - // display: { - // index: 0, - // input: display, - // positionX: 0, - // positionY: 0, - // }, - // micAudio: { - // input: micInput, - // }, - // systemAudio: { - // input: coreAudioInputCapture.defineInput({ - // name: "System Audio", - // settings: { - // device_id: "BlackHole16ch_UID", - // }, - // // TODO - // // volume: { - // // db: -8 - // // } - // }), - // }, }, }); @@ -178,11 +165,7 @@ async function main() { const main = await syncScene(obs, mainScene); const camera = await syncScene(obs, cameraScene); - obs.setCurrentScene(camera); - - setTimeout(() => { - obs.setCurrentScene(main); - }, 1000); + await obs.setCurrentScene(main); } main(); diff --git a/packages/core-rewrite/src/obs.ts b/packages/core-rewrite/src/obs.ts index dd74db4..d484773 100644 --- a/packages/core-rewrite/src/obs.ts +++ b/packages/core-rewrite/src/obs.ts @@ -1,5 +1,5 @@ import OBSWebsocket from "obs-websocket-js"; -import { Scene } from "./runtime.ts"; +import { Scene, Input } from "./runtime.ts"; export type LogLevel = "none" | "info" | "error"; @@ -7,6 +7,9 @@ export class OBS { ws: OBSWebsocket; logging: LogLevel; + /** @internal */ + syncedInputs = new Map>(); + constructor() { this.ws = new OBSWebsocket(); this.logging = "info"; diff --git a/packages/core-rewrite/src/runtime.ts b/packages/core-rewrite/src/runtime.ts index a79e133..e349358 100644 --- a/packages/core-rewrite/src/runtime.ts +++ b/packages/core-rewrite/src/runtime.ts @@ -14,7 +14,7 @@ import { type SIOfSceneAsSI = { [K in keyof definition.SIOfScene]: SceneItem< - Input[K]> + definition.SIOfScene[K] >; }; export class Scene { @@ -34,7 +34,7 @@ export class Scene { item>( key: K - ): SceneItem[K]>> { + ): SceneItem[K]> { return this.items[key]; } @@ -42,70 +42,6 @@ export class Scene { return await this.def.getItems(this.obs); } - /* @internal */ - async syncItem< - TInput extends definition.Input, any> - >(def: definition.DefineSceneItemArgs) { - const { input } = def; - - let res: - | undefined - | { error: "same-name-different-kind"; kind: string } - | { error: "not-owned"; id: number } - | { success: SceneItem> } = undefined; - - for (const item of await this.getItems()) { - if (item.sourceName !== def.input.args.name) continue; - if (item.inputKind !== def.input.type.kind) { - if (!res) - res = { error: "same-name-different-kind", kind: item.inputKind }; - continue; - } - - const { sceneItemSettings } = await this.obs.ws.call( - "GetSceneItemPrivateSettings", - { sceneName: this.def.name, sceneItemId: item.sceneItemId } - ); - - if (sceneItemSettings.SCENEIFY?.init !== "created") { - res = { error: "not-owned", id: item.sceneItemId }; - continue; - } - - res = { - success: new SceneItem( - new Input(input, this.obs), - this, - item.sceneItemId - ), - }; - } - - let sceneItem: SceneItem>; - if (!res) sceneItem = await this.createItem(def); - else if ("success" in res) { - const item = res.success; - await item.updateFromDefinition(def); - sceneItem = item; - } else { - if (res.error === "same-name-different-kind") - throw new Error( - `Input '${input.args.name}' already exists with kind '${res.kind}' instead of expected kind '${def.input.type.kind}'` - ); - else - throw new Error( - `Scene items of input '${input.args.name}' cannot be synced as none are owned by Sceneify` - ); - } - - if (input.args.settings) - await input.setSettings(this.obs, input.args.settings); - - await sceneItem.setTransform(def); - - return sceneItem; - } - async createItem>( def: definition.DefineSceneItemArgs ) { @@ -132,7 +68,8 @@ export class Scene { return sceneItemId; }); - const item = new SceneItem(new Input(def.input, this.obs), this, id); + const [input] = await syncInput(this.obs, def.input); + const item = new SceneItem(input, this, id); await item.updateFromDefinition(def); @@ -140,8 +77,195 @@ export class Scene { } } -class Filter> { - constructor(public def: TDef, public obs: OBS, public input: Input) {} +export async function syncSceneItem< + TScene extends Scene, + TInput extends definition.Input, any> +>(scene: TScene, def: definition.DefineSceneItemArgs) { + const { input } = def; + let res: + | undefined + | { error: "same-name-different-kind"; kind: string } + | { error: "not-owned"; id: number } + | { success: SceneItem } = undefined; + + for (const item of await scene.getItems()) { + if (item.sourceName !== def.input.args.name) continue; + if (item.inputKind !== def.input.type.id) { + if (!res) + res = { error: "same-name-different-kind", kind: item.inputKind }; + continue; + } + + const { sceneItemSettings } = await scene.obs.ws.call( + "GetSceneItemPrivateSettings", + { sceneName: scene.def.name, sceneItemId: item.sceneItemId } + ); + + if (sceneItemSettings.SCENEIFY?.init !== "created") { + res = { error: "not-owned", id: item.sceneItemId }; + continue; + } + + const [input] = await syncInput(scene.obs, def.input); + + res = { success: new SceneItem(input, scene, item.sceneItemId) }; + } + + let sceneItem: SceneItem; + if (!res) sceneItem = await scene.createItem(def); + else if ("success" in res) { + const item = res.success; + sceneItem = item; + await sceneItem.updateFromDefinition(def); + } else { + if (res.error === "same-name-different-kind") + throw new Error( + `Input '${input.args.name}' already exists with kind '${res.kind}' instead of expected kind '${def.input.type.id}'` + ); + else + throw new Error( + `Scene items of input '${input.args.name}' cannot be synced as none are owned by Sceneify` + ); + } + + return sceneItem; +} + +async function syncInput>( + obs: OBS, + def: TInput +): Promise<[Input]>; +async function syncInput>( + obs: OBS, + def: TInput, + forceCreate: { + scene: Scene; + sceneItemArgs: definition.DefineSceneItemArgs; + } +): Promise<[Input, SceneItem]>; +async function syncInput< + TInput extends definition.Input< + any, + Record> + > +>( + obs: OBS, + def: TInput, + forceCreate?: { + scene: Scene; + sceneItemArgs: definition.DefineSceneItemArgs; + } +): Promise<[Input, ...([SceneItem] | [])]> { + let input: Input; + let sceneItem: SceneItem | undefined; + + const prev = obs.syncedInputs.get(def.name); + if (prev) return [prev] as any; + + const inputFilters: any = {}; + + if (forceCreate) + sceneItem = await forceCreate.scene.createItem(forceCreate.sceneItemArgs); + + input = new Input({ def, obs, filters: inputFilters }); + + await Promise.all([ + (async () => { + if (def.args.settings) await input.setSettings(def.args.settings as any); + })(), + (async () => { + if (def.args.filters) { + const filters = await input.getFilters(); + + const { + sourceSettings: { SCENEIFY }, + } = await obs.ws.call("GetSourcePrivateSettings", { + sourceName: input.def.name, + }); + + const entries = Object.entries(def.args.filters); + entries.sort(([_, f1], [__, f2]) => { + if (f1.args.index === undefined) return -1; + if (f2.args.index === undefined) return 1; + return f1.args.index - f2.args.index; + }); + + for (const [key, filterDef] of entries) { + let filter: Filter | undefined; + + for (const existingFilter of filters) { + if (existingFilter.name !== filterDef.args.name) continue; + if (existingFilter.kind !== filterDef.kind.id) + throw new Error( + `Filter '${existingFilter.name}' on source ${input.name} already exists with kind '${existingFilter.kind}' instead of expected kind '${filterDef.kind.id}'` + ); + + if (!SCENEIFY?.filters?.some((f) => f.name === filterDef.name)) + throw new Error( + `Filter '${filterDef.name}' of input '${input.name}' cannot be synced as it is not owned by Sceneify` + ); + + filter = new Filter(filterDef, obs, input); + } + + if (!filter) { + await obs.ws.call("CreateSourceFilter", { + sourceName: input.name, + filterName: filterDef.name, + filterKind: filterDef.kind.id, + }); + + filter = new Filter(filterDef, obs, input); + } + + await Promise.all([ + (async () => { + if (filterDef.args.index !== undefined) + await filter.setIndex(filterDef.args.index); + })(), + (async () => { + if (filterDef.args.enabled !== undefined) + await filter.setEnabled(filterDef.args.enabled); + })(), + (async () => { + if (filterDef.args.settings) + filter.setSettings(filterDef.args.settings); + })(), + ]); + + inputFilters[key] = filter; + } + } + })(), + ]); + + await obs.ws.call("SetSourcePrivateSettings", { + sourceName: def.args.name, + sourceSettings: { + SCENEIFY: { + init: "created", + ...(def.args.filters + ? { + filters: Object.entries(def.args.filters).map(([_, f]) => ({ + name: f.args.name, + })), + } + : undefined), + }, + }, + }); + + obs.syncedInputs.set(def.name, input); + + if (sceneItem) return [input, sceneItem]; + return [input]; +} + +class Filter< + TDef extends definition.Filter, + TInput extends definition.Input +> { + constructor(public def: TDef, public obs: OBS, public input: Input) {} get name() { return this.def.name; @@ -168,8 +292,32 @@ class Filter> { } } -class Input> { - constructor(public def: TDef, public obs: OBS) {} +export type FilterDefsOfInputDef> = + TDef extends definition.Input ? TFilters : never; +type InputFiltersFromDef< + TDef extends definition.Input, + TInput extends TDef +> = { + [K in keyof FilterDefsOfInputDef]: Filter< + FilterDefsOfInputDef[K], + TInput + >; +}; + +export class Input> { + obs: OBS; + def: TDef; + private filters: InputFiltersFromDef = {} as any; + + constructor(args: { + def: TDef; + obs: OBS; + filters: InputFiltersFromDef; + }) { + this.def = args.def; + this.filters = args.filters; + this.obs = args.obs; + } get name() { return this.def.name; @@ -177,8 +325,8 @@ class Input> { filter>( key: TKey - ): Filter[TKey]> { - return null; + ): Filter[TKey], TDef> { + return this.filters[key]; } async setSettings( @@ -225,14 +373,18 @@ class Input> { >(setting: K) { return await this.def.getSettingListItems(this.obs, setting); } + + async getFilters() { + return await this.def.getFilters(this.obs); + } } -class SceneItem>> { +class SceneItem> { obs: OBS; declared: boolean; constructor( - public input: TInput, + public input: Input, public scene: Scene, public id: number ) { @@ -242,9 +394,15 @@ class SceneItem>> { /* @internal */ async updateFromDefinition(def: definition.DefineSceneItemArgs) { - if (def.index !== undefined) this.setIndex(def.index); - - if (def.enabled !== undefined) this.setEnabled(def.enabled); + await Promise.all([ + this.setTransform(def), + (async () => { + if (def.index !== undefined) await this.setIndex(def.index); + })(), + (async () => { + if (def.enabled !== undefined) await this.setEnabled(def.enabled); + })(), + ]); } async getTransform() { @@ -354,7 +512,7 @@ export async function syncScene( }); for (const [key, args] of Object.entries(sceneDef.args.items ?? {})) { - const item = await scene.syncItem(args); + const item = await syncSceneItem(scene, args); items[key] = item; }