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
No known key found for this signature in database
GPG key ID: FF813827E1D9D2CD
7 changed files with 64 additions and 35 deletions

View file

@ -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<BrowserWindow>
captionWindow: ReturnType<typeof setupCaptionWindowManager>
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 },

View file

@ -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
},
}
}

View file

@ -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) {

View file

@ -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<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'

View file

@ -23,6 +23,7 @@
},
"devDependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@moeru/eventa": "^0.3.0",
"@nekopaw/tempora": "0.3.1-alpha.1"
}
}

View file

@ -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<AudioNode>) => Promise<void>
updateParameters: (params: Partial<AnalyserWorkletParameters>) => void
startScreenCapture: () => Promise<void>
stop: () => void
on: <E extends keyof DetectorEventMap>(event: E, listener: DetectorEventMap[E]) => void
off: <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 BeatSyncDetectorEventMap>(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<any>
@ -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 = <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))
}
@ -161,7 +162,7 @@ export function createDetector(options: CreateDetectorOptions): Detector {
updateParameters,
startScreenCapture,
stop,
on: <E extends keyof DetectorEventMap>(event: E, listener: DetectorEventMap[E]) => {
on: <E extends keyof BeatSyncDetectorEventMap>(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: <E extends keyof BeatSyncDetectorEventMap>(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<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
pnpm-lock.yaml generated
View file

@ -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