diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 2e09daa47..2fab89e75 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -19,7 +19,7 @@ import { openDebugger, setupDebugger } from './app/debugger' import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed, onAppBeforeQuit } from './libs/bootkit/lifecycle' import { setElectronMainDirname } from './libs/electron/location' import { setupChannelServer } from './services/airi/channel-server' -import { setupBeatSyncBackgroundWindow } from './windows/beat-sync' +import { setupBeatSync } from './windows/beat-sync' import { setupCaptionWindowManager } from './windows/caption' import { setupChatWindowReusableFunc } from './windows/chat' import { setupInlayWindow } from './windows/inlay' @@ -77,7 +77,7 @@ function setupTray(params: { settingsWindow: () => Promise captionWindow: ReturnType widgetsWindow: WidgetsWindowManager - beatSyncBackgroundWindow: BrowserWindow + beatSyncBgWindow: BrowserWindow }): void { once(() => { const trayImage = nativeImage.createFromPath(isMacOS ? macOSTrayIcon : icon).resize({ width: 16 }) @@ -105,7 +105,7 @@ function setupTray(params: { ...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG ? [ { type: 'header', label: 'DevTools' }, - { label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBackgroundWindow.webContents.openDevTools() }, + { label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() }, { type: 'separator' }, ] as const // :( : [], @@ -130,26 +130,38 @@ app.whenReady().then(async () => { const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() }) const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() }) - // This is a background window - const beatSyncBackgroundWindow = injeca.provide('windows:beat-sync', { - build: () => setupBeatSyncBackgroundWindow(), + // BeatSync will create a background window to capture and process audio. + const beatSync = injeca.provide('windows:beat-sync', { + build: () => setupBeatSync(), }) - const settingsWindow = injeca.provide('windows:settings', { - dependsOn: { widgetsManager, beatSyncBackgroundWindow }, - build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn), + dependsOn: { widgetsManager, beatSync }, + build: async ({ dependsOn }) => async () => { + const { beatSync: _, ...setupArgs } = dependsOn + const window = await setupSettingsWindowReusableFunc(setupArgs)() + dependsOn.beatSync.dispatchTo(window) + return window + }, }) const mainWindow = injeca.provide('windows:main', { - dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSyncBackgroundWindow }, - build: async ({ dependsOn }) => setupMainWindow(dependsOn), + dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync }, + build: async ({ dependsOn }) => { + const { beatSync: _, ...setupArgs } = dependsOn + const window = await setupMainWindow(setupArgs) + dependsOn.beatSync.dispatchTo(window) + return window + }, }) const captionWindow = injeca.provide('windows:caption', { dependsOn: { mainWindow }, build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn), }) const tray = injeca.provide('app:tray', { - dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSyncBackgroundWindow }, - build: async ({ dependsOn }) => setupTray(dependsOn), + dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSync }, + build: async ({ dependsOn }) => { + const { beatSync, ...setupArgs } = dependsOn + return setupTray({ ...setupArgs, beatSyncBgWindow: beatSync.window }) + }, }) injeca.invoke({ dependsOn: { mainWindow, tray, channelServerModule }, diff --git a/apps/stage-tamagotchi/src/main/windows/beat-sync/index.ts b/apps/stage-tamagotchi/src/main/windows/beat-sync/index.ts index 923a833a9..b54fb3625 100644 --- a/apps/stage-tamagotchi/src/main/windows/beat-sync/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/beat-sync/index.ts @@ -1,11 +1,14 @@ import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { BrowserWindow } from 'electron' +import { defineInvoke, defineInvokeHandler } from '@moeru/eventa' +import { createContext } from '@moeru/eventa/adapters/electron/main' +import { beatSyncRequestSignalBeat, beatSyncSignalBeat } from '@proj-airi/stage-shared' +import { BrowserWindow, ipcMain } from 'electron' import { baseUrl, getElectronMainDirname, load } from '../../libs/electron/location' -export async function setupBeatSyncBackgroundWindow() { +export async function setupBeatSync() { const window = new BrowserWindow({ show: false, webPreferences: { @@ -13,6 +16,16 @@ export async function setupBeatSyncBackgroundWindow() { sandbox: false, }, }) + await load(window, baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'), 'beat-sync.html')) - return window + return { + window, + dispatchTo: (window: BrowserWindow) => { + const context = createContext(ipcMain, window).context + const signalBeat = defineInvoke(context, beatSyncSignalBeat) + const removeHandler = defineInvokeHandler(context, beatSyncRequestSignalBeat, async e => signalBeat(e)) + window.on('closed', () => removeHandler()) + return removeHandler + }, + } } diff --git a/apps/stage-tamagotchi/src/renderer/beat-sync.main.ts b/apps/stage-tamagotchi/src/renderer/beat-sync.main.ts index f4842bebc..5a59a80dd 100644 --- a/apps/stage-tamagotchi/src/renderer/beat-sync.main.ts +++ b/apps/stage-tamagotchi/src/renderer/beat-sync.main.ts @@ -1,13 +1,11 @@ import { defineInvoke, defineInvokeHandler } from '@moeru/eventa' import { createContext } from '@moeru/eventa/adapters/electron/renderer' -import { createDetector, StageEnvironment } from '@proj-airi/stage-shared' - -import { beatSyncSignalBeat, beatSyncToggle } from '../shared/eventa' +import { beatSyncRequestSignalBeat, beatSyncToggle, createBeatSyncDetector, StageEnvironment } from '@proj-airi/stage-shared' const { ipcRenderer } = window.electron const context = createContext(ipcRenderer).context -const signalBeat = defineInvoke(context, beatSyncSignalBeat) +const requestSignalBeat = defineInvoke(context, beatSyncRequestSignalBeat) function enableLoopbackAudio() { // electron-audio-loopback currently registers this handler internally @@ -19,13 +17,13 @@ function disableLoopbackAudio() { return ipcRenderer.invoke('disable-loopback-audio') } -const detector = createDetector({ +const detector = createBeatSyncDetector({ env: StageEnvironment.Tamagotchi, enableLoopbackAudio, disableLoopbackAudio, }) -detector.on('beat', () => signalBeat()) +detector.on('beat', e => requestSignalBeat(e)) defineInvokeHandler(context, beatSyncToggle, async (enabled) => { if (enabled) { diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 104a64ea7..ea5379384 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -71,7 +71,4 @@ export const widgetsRemoveEvent = defineEventa<{ id: string }>('eventa:event:ele export const widgetsClearEvent = defineEventa('eventa:event:electron:windows:widgets:clear') export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Record }>('eventa:event:electron:windows:widgets:update') -export const beatSyncToggle = defineInvokeEventa('eventa:event:electron:beat-sync:toggle') -export const beatSyncSignalBeat = defineInvokeEventa('eventa:event:electron:beat-sync:signal-beat') - export { electron } from './electron' diff --git a/packages/stage-shared/package.json b/packages/stage-shared/package.json index 4d21951d4..0727f7c45 100644 --- a/packages/stage-shared/package.json +++ b/packages/stage-shared/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@electron-toolkit/preload": "^3.0.2", + "@moeru/eventa": "^0.3.0", "@nekopaw/tempora": "0.3.1-alpha.1" } } diff --git a/packages/stage-shared/src/beat-sync.ts b/packages/stage-shared/src/beat-sync.ts index 0671dd1ce..122102902 100644 --- a/packages/stage-shared/src/beat-sync.ts +++ b/packages/stage-shared/src/beat-sync.ts @@ -2,29 +2,30 @@ import type { Analyser, AnalyserBeatEvent, AnalyserWorkletParameters } from '@ne import analyserWorklet from '@nekopaw/tempora/worklet?url' +import { defineInvokeEventa } from '@moeru/eventa' import { startAnalyser as startTemporaAnalyser } from '@nekopaw/tempora' import { StageEnvironment } from './environment' -export interface DetectorEventMap { +export interface BeatSyncDetectorEventMap { stateChange: (isActive: boolean) => void beat: (e: AnalyserBeatEvent) => void } -export interface Detector { +export interface BeatSyncDetector { start: (createSource: (context: AudioContext) => Promise) => Promise updateParameters: (params: Partial) => void startScreenCapture: () => Promise stop: () => void - on: (event: E, listener: DetectorEventMap[E]) => void - off: (event: E, listener: DetectorEventMap[E]) => void + on: (event: E, listener: BeatSyncDetectorEventMap[E]) => void + off: (event: E, listener: BeatSyncDetectorEventMap[E]) => void readonly isActive: boolean readonly context: AudioContext | undefined readonly analyser: Analyser | undefined readonly source: AudioNode | undefined } -export type CreateDetectorOptions = | +export type CreateBeatSyncDetectorOptions = | { env: StageEnvironment.Tamagotchi enableLoopbackAudio: () => Promise @@ -34,7 +35,7 @@ export type CreateDetectorOptions = | env: StageEnvironment.Web } -export function createDetector(options: CreateDetectorOptions): Detector { +export function createBeatSyncDetector(options: CreateBeatSyncDetectorOptions): BeatSyncDetector { let context: AudioContext | undefined let analyser: Analyser | undefined let source: AudioNode | undefined @@ -42,12 +43,12 @@ export function createDetector(options: CreateDetectorOptions): Detector { let stopSource: (() => void) | undefined - const listeners: { [K in keyof DetectorEventMap]: Array<(...args: any) => void> } = { + const listeners: { [K in keyof BeatSyncDetectorEventMap]: Array<(...args: any) => void> } = { stateChange: [], beat: [], } - const emit = (event: E, ...args: Parameters) => { + const emit = (event: E, ...args: Parameters) => { listeners[event].forEach(listener => listener(...args)) } @@ -161,7 +162,7 @@ export function createDetector(options: CreateDetectorOptions): Detector { updateParameters, startScreenCapture, stop, - on: (event: E, listener: DetectorEventMap[E]) => { + on: (event: E, listener: BeatSyncDetectorEventMap[E]) => { switch (event) { case 'beat': listeners.beat.push(listener) @@ -170,7 +171,7 @@ export function createDetector(options: CreateDetectorOptions): Detector { throw new Error(`Unknown event: ${event}`) } }, - off: (event, listener) => { + off: (event: E, listener: BeatSyncDetectorEventMap[E]) => { switch (event) { case 'beat': { const index = listeners.beat.indexOf(listener) @@ -189,3 +190,7 @@ export function createDetector(options: CreateDetectorOptions): Detector { get source() { return source }, } } + +export const beatSyncToggle = defineInvokeEventa('eventa:event:electron:beat-sync:toggle') +export const beatSyncRequestSignalBeat = defineInvokeEventa('eventa:event:electron:beat-sync:request-signal-beat') +export const beatSyncSignalBeat = defineInvokeEventa('eventa:event:electron:beat-sync:signal-beat') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd124fd1f..986351cb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1337,6 +1337,9 @@ importers: '@electron-toolkit/preload': specifier: ^3.0.2 version: 3.0.2(electron@39.1.0) + '@moeru/eventa': + specifier: ^0.3.0 + version: 0.3.0(electron@39.1.0)(h3@2.0.1-rc.5(crossws@0.4.1(srvx@0.9.4)))(web-worker@1.5.0) '@nekopaw/tempora': specifier: 0.3.1-alpha.1 version: 0.3.1-alpha.1