refactor(stage-tamagotchi,stage-pages,stage-shared,stage-ui): migrate beat-sync to use broadcast channel-based transport

This commit is contained in:
Makito
2025-12-18 01:20:25 +09:00
parent e80a82d921
commit 97f53a8182
14 changed files with 52 additions and 117 deletions
+4 -16
View File
@@ -1,5 +1,3 @@
import type { BrowserWindow } from 'electron'
import { env, platform } from 'node:process'
import { electronApp, optimizer } from '@electron-toolkit/utils'
@@ -79,12 +77,6 @@ app.whenReady().then(async () => {
// BeatSync will create a background window to capture and process audio.
const beatSync = injeca.provide('windows:beat-sync', () => setupBeatSync())
const beatSyncDispatchTo = injeca.provide('windows:beat-sync:dispatch-to', {
dependsOn: { beatSync },
build: async ({ dependsOn }) => (window: BrowserWindow) => {
return dependsOn.beatSync.dispatchTo(window)
},
})
const chatWindow = injeca.provide('windows:chat', {
dependsOn: { widgetsManager },
@@ -92,17 +84,13 @@ app.whenReady().then(async () => {
})
const settingsWindow = injeca.provide('windows:settings', {
dependsOn: { widgetsManager, beatSync, beatSyncDispatchTo },
build: async ({ dependsOn }) => {
return setupSettingsWindowReusableFunc({ ...dependsOn, onWindowCreated: window => dependsOn.beatSyncDispatchTo(window) })
},
dependsOn: { widgetsManager, beatSync },
build: async ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn),
})
const mainWindow = injeca.provide('windows:main', {
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, beatSyncDispatchTo },
build: async ({ dependsOn }) => {
return setupMainWindow({ ...dependsOn, onWindowCreated: window => dependsOn.beatSyncDispatchTo(window) })
},
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync },
build: async ({ dependsOn }) => setupMainWindow(dependsOn),
})
const captionWindow = injeca.provide('windows:caption', {
+1 -1
View File
@@ -54,7 +54,7 @@ export function setupTray(params: {
...is.dev || env.MAIN_APP_DEBUG || env.APP_DEBUG
? [
{ type: 'header', label: 'DevTools' },
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.window.webContents.openDevTools() },
{ label: 'Troubleshoot BeatSync...', click: () => params.beatSyncBgWindow.webContents.openDevTools() },
{ type: 'separator' },
] as const // :(
: [],
@@ -1,26 +1,8 @@
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
import { createContext } from '@moeru/eventa/adapters/electron/main'
import {
beatSyncBeatSignaledInvokeEventa,
beatSyncGetInputByteFrequencyDataInvokeEventa,
beatSyncGetStateInvokeEventa,
beatSyncStateChangedInvokeEventa,
beatSyncToggleInvokeEventa,
beatSyncUpdateParametersInvokeEventa,
} from '@proj-airi/stage-shared/beat-sync'
import { BrowserWindow, ipcMain } from 'electron'
import { BrowserWindow } from 'electron'
import {
beatSyncElectronChangeState,
beatSyncElectronGetInputByteFrequencyData,
beatSyncElectronGetState,
beatSyncElectronSignalBeat,
beatSyncElectronToggle,
beatSyncElectronUpdateParameters,
} from '../../../shared/eventa'
import { baseUrl, getElectronMainDirname, load } from '../../libs/electron/location'
export async function setupBeatSync() {
@@ -31,37 +13,7 @@ export async function setupBeatSync() {
sandbox: false,
},
})
const context = createContext(ipcMain, window).context
// TODO(Makito): Bypass here with the broadcast channel-based transport when it is released.
// [main] -> [renderer] beat-sync
const toggle = defineInvoke(context, beatSyncElectronToggle) as (enabled: boolean) => Promise<void> // TODO: Better type
const getState = defineInvoke(context, beatSyncElectronGetState)
const updateParameters = defineInvoke(context, beatSyncElectronUpdateParameters)
const getInputByteFrequencyData = defineInvoke(context, beatSyncElectronGetInputByteFrequencyData)
await load(window, baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'), 'beat-sync.html'))
return {
window,
dispatchTo: (window: BrowserWindow) => {
const context = createContext(ipcMain, window).context
const stateChanged = defineInvoke(context, beatSyncStateChangedInvokeEventa)
const beatSignaled = defineInvoke(context, beatSyncBeatSignaledInvokeEventa)
const removeHandlerFns = [
// [renderer] beat-sync -> [main] -> [renderer] index (Events)
defineInvokeHandler(context, beatSyncElectronChangeState, async e => stateChanged(e)),
defineInvokeHandler(context, beatSyncElectronSignalBeat, async e => beatSignaled(e)),
// [renderer] index -> [main] -> [renderer] beat-sync (Functions)
defineInvokeHandler(context, beatSyncToggleInvokeEventa, async enabled => toggle(enabled)),
defineInvokeHandler(context, beatSyncGetStateInvokeEventa, async () => getState()),
defineInvokeHandler(context, beatSyncUpdateParametersInvokeEventa, async params => updateParameters(params)),
defineInvokeHandler(context, beatSyncGetInputByteFrequencyDataInvokeEventa, async () => getInputByteFrequencyData()),
]
const removeHandlers = () => removeHandlerFns.forEach(fn => fn())
window.on('closed', () => removeHandlers())
return removeHandlers
},
}
return window
}
@@ -1,24 +1,22 @@
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
import { StageEnvironment } from '@proj-airi/stage-shared'
import { createBeatSyncDetector } from '@proj-airi/stage-shared/beat-sync/browser'
import {
beatSyncElectronChangeState,
beatSyncElectronGetInputByteFrequencyData,
beatSyncElectronGetState,
beatSyncElectronSignalBeat,
beatSyncElectronToggle,
beatSyncElectronUpdateParameters,
} from '../shared/eventa'
beatSyncBeatSignaledInvokeEventa,
beatSyncGetInputByteFrequencyDataInvokeEventa,
beatSyncGetStateInvokeEventa,
beatSyncStateChangedInvokeEventa,
beatSyncToggleInvokeEventa,
beatSyncUpdateParametersInvokeEventa,
createBeatSyncDetector,
createContext,
} from '@proj-airi/stage-shared/beat-sync'
const { ipcRenderer } = window.electron
const context = createContext(ipcRenderer).context
const context = createContext()
// [renderer] beat-sync -> [main] -> [renderer] index
const changeState = defineInvoke(context, beatSyncElectronChangeState)
const signalBeat = defineInvoke(context, beatSyncElectronSignalBeat)
const changeState = defineInvoke(context, beatSyncStateChangedInvokeEventa)
const signalBeat = defineInvoke(context, beatSyncBeatSignaledInvokeEventa)
function enableLoopbackAudio() {
// electron-audio-loopback currently registers this handler internally
@@ -43,7 +41,7 @@ detector.on('beat', (e) => {
signalBeat(e)
})
defineInvokeHandler(context, beatSyncElectronToggle, async (enabled) => {
defineInvokeHandler(context, beatSyncToggleInvokeEventa, async (enabled) => {
// eslint-disable-next-line no-console
console.log('[toggle]', enabled)
if (enabled) {
@@ -53,13 +51,13 @@ defineInvokeHandler(context, beatSyncElectronToggle, async (enabled) => {
detector.stop()
}
})
defineInvokeHandler(context, beatSyncElectronGetState, async () => detector.state)
defineInvokeHandler(context, beatSyncElectronUpdateParameters, async (params) => {
defineInvokeHandler(context, beatSyncGetStateInvokeEventa, async () => detector.state)
defineInvokeHandler(context, beatSyncUpdateParametersInvokeEventa, async (params) => {
// eslint-disable-next-line no-console
console.log('[update-params]', params)
detector.updateParameters(params)
})
defineInvokeHandler(context, beatSyncElectronGetInputByteFrequencyData, async () => {
defineInvokeHandler(context, beatSyncGetInputByteFrequencyDataInvokeEventa, async () => {
// eslint-disable-next-line no-console
console.debug('[get-input-byte-frequency-data]') // This could be noisy.
return detector.getInputByteFrequencyData()
@@ -74,11 +74,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 beatSyncElectronSignalBeat = defineInvokeEventa<void, AnalyserBeatEvent>('eventa:event:electron:beat-sync:electron:signal-beat')
export const beatSyncElectronChangeState = defineInvokeEventa<void, BeatSyncDetectorState>('eventa:event:electron:beat-sync:electron:change-state')
export const beatSyncElectronGetState = defineInvokeEventa<BeatSyncDetectorState>('eventa:event:electron:beat-sync:electron:get-state')
export const beatSyncElectronToggle = defineInvokeEventa<void, boolean>('eventa:event:electron:beat-sync:electron:toggle')
export const beatSyncElectronUpdateParameters = defineInvokeEventa<void, Partial<AnalyserWorkletParameters>>('eventa:event:electron:beat-sync:electron:update-parameters')
export const beatSyncElectronGetInputByteFrequencyData = defineInvokeEventa<Uint8Array<ArrayBuffer>>('eventa:event:electron:beat-sync:electron:get-input-byte-frequency-data')
export { electron } from './electron'
@@ -10,7 +10,7 @@ import {
listenBeatSyncStateChange,
toggleBeatSync,
updateBeatSyncParameters,
} from '@proj-airi/stage-shared/beat-sync/browser'
} from '@proj-airi/stage-shared/beat-sync'
import { Alert, AudioSpectrumVisualizer } from '@proj-airi/stage-ui/components'
import { Button, FieldCheckbox, FieldRange } from '@proj-airi/ui'
import { createTimeline } from 'animejs'
-1
View File
@@ -17,7 +17,6 @@
"exports": {
".": "./src/index.ts",
"./beat-sync": "./src/beat-sync/index.ts",
"./beat-sync/browser": "./src/beat-sync/browser/index.ts",
"./electron-renderer": "./src/electron-renderer.d.ts"
},
"scripts": {
@@ -1,13 +0,0 @@
import { createContext as createWebContext } from '@moeru/eventa'
import { createContext as createElectronRendererContext } from '@moeru/eventa/adapters/electron/renderer'
import { isElectronWindow } from '@proj-airi/stage-shared'
export function createContext() {
if (isElectronWindow(window)) {
// TODO(Makito): Replace this with the broadcast channel-based transport when it is released.
return createElectronRendererContext(window.electron.ipcRenderer).context
}
else {
return createWebContext()
}
}
@@ -1,2 +0,0 @@
export * from './detector'
export * from './eventa'
@@ -1,14 +1,14 @@
import type { EventContext } from '@moeru/eventa'
import type { Analyser, AnalyserBeatEvent, AnalyserWorkletParameters } from '@nekopaw/tempora'
import type { BeatSyncDetectorEventMap, BeatSyncDetectorState } from '../types'
import type { BeatSyncDetectorEventMap, BeatSyncDetectorState } from './types'
import analyserWorklet from '@nekopaw/tempora/worklet?url'
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
import { startAnalyser as startTemporaAnalyser } from '@nekopaw/tempora'
import { isStageTamagotchi, isStageWeb, StageEnvironment } from '../../environment'
import { isStageTamagotchi, isStageWeb, StageEnvironment } from '../environment'
import {
beatSyncBeatSignaledInvokeEventa,
beatSyncGetInputByteFrequencyDataInvokeEventa,
@@ -16,8 +16,6 @@ import {
beatSyncStateChangedInvokeEventa,
beatSyncToggleInvokeEventa,
beatSyncUpdateParametersInvokeEventa,
} from '../eventa'
import {
createContext,
} from './eventa'
@@ -260,7 +258,7 @@ export function toggleBeatSync(enabled: boolean) {
}
if (isStageTamagotchi()) {
const toggleFn = defineInvoke(getContext(), beatSyncToggleInvokeEventa) as (enabled: boolean) => Promise<void> // TODO: Better type
const toggleFn = defineInvoke(getContext(), beatSyncToggleInvokeEventa)
return toggleFn(enabled)
}
+22 -1
View File
@@ -2,7 +2,9 @@ import type { AnalyserBeatEvent, AnalyserWorkletParameters } from '@nekopaw/temp
import type { BeatSyncDetectorState } from './types'
import { defineInvokeEventa } from '@moeru/eventa'
import { createContext as createWebContext, defineInvokeEventa } from '@moeru/eventa'
import { createContext as createBroadcastChannelContext } from '@moeru/eventa/adapters/broadcast-channel'
import { isElectronWindow } from '@proj-airi/stage-shared'
// Functions
export const beatSyncToggleInvokeEventa = defineInvokeEventa<void, boolean>('eventa:invoke:electron:beat-sync:toggle')
@@ -13,3 +15,22 @@ export const beatSyncGetInputByteFrequencyDataInvokeEventa = defineInvokeEventa<
// Events
export const beatSyncStateChangedInvokeEventa = defineInvokeEventa<void, BeatSyncDetectorState>('eventa:event:electron:beat-sync:state-changed')
export const beatSyncBeatSignaledInvokeEventa = defineInvokeEventa<void, AnalyserBeatEvent>('eventa:event:electron:beat-sync:beat-signaled')
let _broadcastChannel: BroadcastChannel | undefined
function getBroadcastChannel() {
if (!_broadcastChannel) {
_broadcastChannel = new BroadcastChannel('airi::beat-sync')
_broadcastChannel.onmessage = (event) => {
}
}
return _broadcastChannel
}
export function createContext() {
if (isElectronWindow(window)) {
return createBroadcastChannelContext(getBroadcastChannel()).context
}
else {
return createWebContext()
}
}
@@ -1,2 +1,3 @@
export * from './detector'
export * from './eventa'
export * from './types'
@@ -3,7 +3,7 @@ import type { Application } from '@pixi/app'
import type { PixiLive2DInternalModel } from '../../../composables/live2d'
import { listenBeatSyncBeatSignal } from '@proj-airi/stage-shared/beat-sync/browser'
import { listenBeatSyncBeatSignal } from '@proj-airi/stage-shared/beat-sync'
import { useTheme } from '@proj-airi/ui'
import { breakpointsTailwind, until, useBreakpoints, useDebounceFn } from '@vueuse/core'
import { formatHex } from 'culori'
@@ -1,6 +1,6 @@
import type { BeatSyncDetectorState } from '@proj-airi/stage-shared/beat-sync'
import { getBeatSyncState, listenBeatSyncStateChange } from '@proj-airi/stage-shared/beat-sync/browser'
import { getBeatSyncState, listenBeatSyncStateChange } from '@proj-airi/stage-shared/beat-sync'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'