fix(stage-tamagotchi,stage-shared): correctly route beat-sync events

This commit is contained in:
Makito
2025-11-30 03:31:58 +09:00
parent 078e604795
commit c9d148f992
7 changed files with 64 additions and 35 deletions
+25 -13
View File
@@ -19,7 +19,7 @@ import { openDebugger, setupDebugger } from './app/debugger'
import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed, onAppBeforeQuit } from './libs/bootkit/lifecycle' import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed, onAppBeforeQuit } from './libs/bootkit/lifecycle'
import { setElectronMainDirname } from './libs/electron/location' import { setElectronMainDirname } from './libs/electron/location'
import { setupChannelServer } from './services/airi/channel-server' 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 { setupCaptionWindowManager } from './windows/caption'
import { setupChatWindowReusableFunc } from './windows/chat' import { setupChatWindowReusableFunc } from './windows/chat'
import { setupInlayWindow } from './windows/inlay' import { setupInlayWindow } from './windows/inlay'
@@ -77,7 +77,7 @@ function setupTray(params: {
settingsWindow: () => Promise<BrowserWindow> settingsWindow: () => Promise<BrowserWindow>
captionWindow: ReturnType<typeof setupCaptionWindowManager> captionWindow: ReturnType<typeof setupCaptionWindowManager>
widgetsWindow: WidgetsWindowManager widgetsWindow: WidgetsWindowManager
beatSyncBackgroundWindow: BrowserWindow beatSyncBgWindow: BrowserWindow
}): void { }): void {
once(() => { once(() => {
const trayImage = nativeImage.createFromPath(isMacOS ? macOSTrayIcon : icon).resize({ width: 16 }) 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 ...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG
? [ ? [
{ type: 'header', label: 'DevTools' }, { type: 'header', label: 'DevTools' },
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBackgroundWindow.webContents.openDevTools() }, { label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() },
{ type: 'separator' }, { type: 'separator' },
] as const // :( ] as const // :(
: [], : [],
@@ -130,26 +130,38 @@ app.whenReady().then(async () => {
const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() }) const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() }) const noticeWindow = injeca.provide('windows:notice', { build: () => setupNoticeWindowManager() })
// This is a background window // BeatSync will create a background window to capture and process audio.
const beatSyncBackgroundWindow = injeca.provide('windows:beat-sync', { const beatSync = injeca.provide('windows:beat-sync', {
build: () => setupBeatSyncBackgroundWindow(), build: () => setupBeatSync(),
}) })
const settingsWindow = injeca.provide('windows:settings', { const settingsWindow = injeca.provide('windows:settings', {
dependsOn: { widgetsManager, beatSyncBackgroundWindow }, dependsOn: { widgetsManager, beatSync },
build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn), 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', { const mainWindow = injeca.provide('windows:main', {
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSyncBackgroundWindow }, dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync },
build: async ({ dependsOn }) => setupMainWindow(dependsOn), build: async ({ dependsOn }) => {
const { beatSync: _, ...setupArgs } = dependsOn
const window = await setupMainWindow(setupArgs)
dependsOn.beatSync.dispatchTo(window)
return window
},
}) })
const captionWindow = injeca.provide('windows:caption', { const captionWindow = injeca.provide('windows:caption', {
dependsOn: { mainWindow }, dependsOn: { mainWindow },
build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn), build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn),
}) })
const tray = injeca.provide('app:tray', { const tray = injeca.provide('app:tray', {
dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSyncBackgroundWindow }, dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSync },
build: async ({ dependsOn }) => setupTray(dependsOn), build: async ({ dependsOn }) => {
const { beatSync, ...setupArgs } = dependsOn
return setupTray({ ...setupArgs, beatSyncBgWindow: beatSync.window })
},
}) })
injeca.invoke({ injeca.invoke({
dependsOn: { mainWindow, tray, channelServerModule }, dependsOn: { mainWindow, tray, channelServerModule },
@@ -1,11 +1,14 @@
import { dirname, join, resolve } from 'node:path' import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' 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' import { baseUrl, getElectronMainDirname, load } from '../../libs/electron/location'
export async function setupBeatSyncBackgroundWindow() { export async function setupBeatSync() {
const window = new BrowserWindow({ const window = new BrowserWindow({
show: false, show: false,
webPreferences: { webPreferences: {
@@ -13,6 +16,16 @@ export async function setupBeatSyncBackgroundWindow() {
sandbox: false, sandbox: false,
}, },
}) })
await load(window, baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'), 'beat-sync.html')) 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
},
}
} }
@@ -1,13 +1,11 @@
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa' import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
import { createContext } from '@moeru/eventa/adapters/electron/renderer' import { createContext } from '@moeru/eventa/adapters/electron/renderer'
import { createDetector, StageEnvironment } from '@proj-airi/stage-shared' import { beatSyncRequestSignalBeat, beatSyncToggle, createBeatSyncDetector, StageEnvironment } from '@proj-airi/stage-shared'
import { beatSyncSignalBeat, beatSyncToggle } from '../shared/eventa'
const { ipcRenderer } = window.electron const { ipcRenderer } = window.electron
const context = createContext(ipcRenderer).context const context = createContext(ipcRenderer).context
const signalBeat = defineInvoke(context, beatSyncSignalBeat) const requestSignalBeat = defineInvoke(context, beatSyncRequestSignalBeat)
function enableLoopbackAudio() { function enableLoopbackAudio() {
// electron-audio-loopback currently registers this handler internally // electron-audio-loopback currently registers this handler internally
@@ -19,13 +17,13 @@ function disableLoopbackAudio() {
return ipcRenderer.invoke('disable-loopback-audio') return ipcRenderer.invoke('disable-loopback-audio')
} }
const detector = createDetector({ const detector = createBeatSyncDetector({
env: StageEnvironment.Tamagotchi, env: StageEnvironment.Tamagotchi,
enableLoopbackAudio, enableLoopbackAudio,
disableLoopbackAudio, disableLoopbackAudio,
}) })
detector.on('beat', () => signalBeat()) detector.on('beat', e => requestSignalBeat(e))
defineInvokeHandler(context, beatSyncToggle, async (enabled) => { defineInvokeHandler(context, beatSyncToggle, async (enabled) => {
if (enabled) { if (enabled) {
@@ -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 widgetsClearEvent = defineEventa('eventa:event:electron:windows:widgets:clear')
export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Record<string, any> }>('eventa:event:electron:windows:widgets:update') export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Record<string, any> }>('eventa:event:electron:windows:widgets:update')
export const beatSyncToggle = defineInvokeEventa<void, boolean>('eventa:event:electron:beat-sync:toggle')
export const beatSyncSignalBeat = defineInvokeEventa<void, void>('eventa:event:electron:beat-sync:signal-beat')
export { electron } from './electron' export { electron } from './electron'
+1
View File
@@ -23,6 +23,7 @@
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@moeru/eventa": "^0.3.0",
"@nekopaw/tempora": "0.3.1-alpha.1" "@nekopaw/tempora": "0.3.1-alpha.1"
} }
} }
+15 -10
View File
@@ -2,29 +2,30 @@ import type { Analyser, AnalyserBeatEvent, AnalyserWorkletParameters } from '@ne
import analyserWorklet from '@nekopaw/tempora/worklet?url' import analyserWorklet from '@nekopaw/tempora/worklet?url'
import { defineInvokeEventa } from '@moeru/eventa'
import { startAnalyser as startTemporaAnalyser } from '@nekopaw/tempora' import { startAnalyser as startTemporaAnalyser } from '@nekopaw/tempora'
import { StageEnvironment } from './environment' import { StageEnvironment } from './environment'
export interface DetectorEventMap { export interface BeatSyncDetectorEventMap {
stateChange: (isActive: boolean) => void stateChange: (isActive: boolean) => void
beat: (e: AnalyserBeatEvent) => void beat: (e: AnalyserBeatEvent) => void
} }
export interface Detector { export interface BeatSyncDetector {
start: (createSource: (context: AudioContext) => Promise<AudioNode>) => Promise<void> start: (createSource: (context: AudioContext) => Promise<AudioNode>) => Promise<void>
updateParameters: (params: Partial<AnalyserWorkletParameters>) => void updateParameters: (params: Partial<AnalyserWorkletParameters>) => void
startScreenCapture: () => Promise<void> startScreenCapture: () => Promise<void>
stop: () => void stop: () => void
on: <E extends keyof DetectorEventMap>(event: E, listener: DetectorEventMap[E]) => void on: <E extends keyof BeatSyncDetectorEventMap>(event: E, listener: BeatSyncDetectorEventMap[E]) => void
off: <E extends keyof DetectorEventMap>(event: E, listener: DetectorEventMap[E]) => void off: <E extends keyof BeatSyncDetectorEventMap>(event: E, listener: BeatSyncDetectorEventMap[E]) => void
readonly isActive: boolean readonly isActive: boolean
readonly context: AudioContext | undefined readonly context: AudioContext | undefined
readonly analyser: Analyser | undefined readonly analyser: Analyser | undefined
readonly source: AudioNode | undefined readonly source: AudioNode | undefined
} }
export type CreateDetectorOptions = | export type CreateBeatSyncDetectorOptions = |
{ {
env: StageEnvironment.Tamagotchi env: StageEnvironment.Tamagotchi
enableLoopbackAudio: () => Promise<any> enableLoopbackAudio: () => Promise<any>
@@ -34,7 +35,7 @@ export type CreateDetectorOptions = |
env: StageEnvironment.Web env: StageEnvironment.Web
} }
export function createDetector(options: CreateDetectorOptions): Detector { export function createBeatSyncDetector(options: CreateBeatSyncDetectorOptions): BeatSyncDetector {
let context: AudioContext | undefined let context: AudioContext | undefined
let analyser: Analyser | undefined let analyser: Analyser | undefined
let source: AudioNode | undefined let source: AudioNode | undefined
@@ -42,12 +43,12 @@ export function createDetector(options: CreateDetectorOptions): Detector {
let stopSource: (() => void) | undefined 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: [], stateChange: [],
beat: [], beat: [],
} }
const emit = <E extends keyof DetectorEventMap>(event: E, ...args: Parameters<DetectorEventMap[E]>) => { const emit = <E extends keyof BeatSyncDetectorEventMap>(event: E, ...args: Parameters<BeatSyncDetectorEventMap[E]>) => {
listeners[event].forEach(listener => listener(...args)) listeners[event].forEach(listener => listener(...args))
} }
@@ -161,7 +162,7 @@ export function createDetector(options: CreateDetectorOptions): Detector {
updateParameters, updateParameters,
startScreenCapture, startScreenCapture,
stop, stop,
on: <E extends keyof DetectorEventMap>(event: E, listener: DetectorEventMap[E]) => { on: <E extends keyof BeatSyncDetectorEventMap>(event: E, listener: BeatSyncDetectorEventMap[E]) => {
switch (event) { switch (event) {
case 'beat': case 'beat':
listeners.beat.push(listener) listeners.beat.push(listener)
@@ -170,7 +171,7 @@ export function createDetector(options: CreateDetectorOptions): Detector {
throw new Error(`Unknown event: ${event}`) throw new Error(`Unknown event: ${event}`)
} }
}, },
off: (event, listener) => { off: <E extends keyof BeatSyncDetectorEventMap>(event: E, listener: BeatSyncDetectorEventMap[E]) => {
switch (event) { switch (event) {
case 'beat': { case 'beat': {
const index = listeners.beat.indexOf(listener) const index = listeners.beat.indexOf(listener)
@@ -189,3 +190,7 @@ export function createDetector(options: CreateDetectorOptions): Detector {
get source() { return source }, get source() { return source },
} }
} }
export const beatSyncToggle = defineInvokeEventa<void, boolean>('eventa:event:electron:beat-sync:toggle')
export const beatSyncRequestSignalBeat = defineInvokeEventa<void, AnalyserBeatEvent>('eventa:event:electron:beat-sync:request-signal-beat')
export const beatSyncSignalBeat = defineInvokeEventa<void, AnalyserBeatEvent>('eventa:event:electron:beat-sync:signal-beat')
+3
View File
@@ -1337,6 +1337,9 @@ importers:
'@electron-toolkit/preload': '@electron-toolkit/preload':
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2(electron@39.1.0) 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': '@nekopaw/tempora':
specifier: 0.3.1-alpha.1 specifier: 0.3.1-alpha.1
version: 0.3.1-alpha.1 version: 0.3.1-alpha.1