fix(stage-tamagotchi,stage-shared): correctly route beat-sync events
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Generated
+3
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user