fix(stage-pocket): guard for beatsync
This commit is contained in:
@@ -140,6 +140,7 @@ export default defineConfig({
|
||||
exclude: base => [
|
||||
...base,
|
||||
'**/settings/connection/index.vue',
|
||||
'**/settings/modules/beat-sync.vue',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
getBeatSyncInputByteFrequencyData,
|
||||
getBeatSyncState,
|
||||
inputAnalyserFFTSize,
|
||||
isBeatSyncSupported,
|
||||
listenBeatSyncBeatSignal,
|
||||
listenBeatSyncStateChange,
|
||||
toggleBeatSync,
|
||||
updateBeatSyncParameters,
|
||||
} from './detector'
|
||||
|
||||
describe('capacitor Beat Sync runtime', () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv('RUNTIME_ENVIRONMENT', 'capacitor')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('registers and removes inactive listeners without throwing', () => {
|
||||
const removeStateListener = listenBeatSyncStateChange(vi.fn())
|
||||
const removeBeatListener = listenBeatSyncBeatSignal(vi.fn())
|
||||
|
||||
expect(removeStateListener).toBeTypeOf('function')
|
||||
expect(removeBeatListener).toBeTypeOf('function')
|
||||
expect(() => removeStateListener()).not.toThrow()
|
||||
expect(() => removeBeatListener()).not.toThrow()
|
||||
})
|
||||
|
||||
it('returns a deterministic inactive state', async () => {
|
||||
expect(isBeatSyncSupported()).toBe(false)
|
||||
await expect(getBeatSyncState()).resolves.toEqual({ isActive: false })
|
||||
})
|
||||
|
||||
it('allows inactive configuration calls to settle', async () => {
|
||||
await expect(toggleBeatSync(false)).resolves.toBeUndefined()
|
||||
await expect(updateBeatSyncParameters({})).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects attempts to enable unsupported screen capture', async () => {
|
||||
await expect(toggleBeatSync(true)).rejects.toThrow('Beat Sync is not available in Stage Pocket')
|
||||
})
|
||||
|
||||
it('returns silent frequency data', async () => {
|
||||
const data = await getBeatSyncInputByteFrequencyData()
|
||||
|
||||
expect(data).toBeInstanceOf(Uint8Array)
|
||||
expect(data).toHaveLength(inputAnalyserFFTSize / 2)
|
||||
expect(data.every(value => value === 0)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { EventContext } from '@moeru/eventa'
|
||||
import type { Analyser, AnalyserBeatEvent, AnalyserWorkletParameters } from '@nekopaw/tempora'
|
||||
import type { SerializableDesktopCapturerSource } from '@proj-airi/electron-screen-capture'
|
||||
|
||||
@@ -10,7 +9,7 @@ import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
|
||||
import { startAnalyser as startTemporaAnalyser } from '@nekopaw/tempora'
|
||||
import { setupElectronScreenCapture } from '@proj-airi/electron-screen-capture/renderer'
|
||||
|
||||
import { isStageTamagotchi, isStageWeb, StageEnvironment } from '../environment'
|
||||
import { isStageCapacitor, isStageTamagotchi, isStageWeb, StageEnvironment } from '../environment'
|
||||
import { isElectronWindow } from '../window'
|
||||
import {
|
||||
beatSyncBeatSignaledInvokeEventa,
|
||||
@@ -249,14 +248,38 @@ function getDetector() {
|
||||
return detector
|
||||
}
|
||||
|
||||
let context: EventContext<any, any> | undefined
|
||||
let context: ReturnType<typeof createContext> | undefined
|
||||
|
||||
function installCapacitorHandlers(context: ReturnType<typeof createContext>) {
|
||||
defineInvokeHandler(context, beatSyncToggleInvokeEventa, async (enabled) => {
|
||||
if (enabled)
|
||||
throw new Error('Beat Sync is not available in Stage Pocket')
|
||||
})
|
||||
defineInvokeHandler(context, beatSyncGetStateInvokeEventa, async () => ({ isActive: false }))
|
||||
defineInvokeHandler(context, beatSyncUpdateParametersInvokeEventa, async () => {})
|
||||
defineInvokeHandler(context, beatSyncGetInputByteFrequencyDataInvokeEventa, async () => {
|
||||
return new Uint8Array(inputAnalyserFFTSize / 2)
|
||||
})
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
if (!context)
|
||||
if (!context) {
|
||||
context = createContext()
|
||||
|
||||
// Capacitor cannot capture system audio. Register every request handler so
|
||||
// callers receive deterministic inactive results instead of pending forever.
|
||||
if (isStageCapacitor())
|
||||
installCapacitorHandlers(context)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/** Returns whether the current Stage runtime can capture audio for Beat Sync. */
|
||||
export function isBeatSyncSupported() {
|
||||
return isStageWeb() || isStageTamagotchi()
|
||||
}
|
||||
|
||||
export function toggleBeatSync(enabled: boolean) {
|
||||
if (isStageWeb()) {
|
||||
if (enabled) {
|
||||
@@ -267,7 +290,7 @@ export function toggleBeatSync(enabled: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
if (isStageTamagotchi()) {
|
||||
if (isStageTamagotchi() || isStageCapacitor()) {
|
||||
const toggleFn = defineInvoke(getContext(), beatSyncToggleInvokeEventa)
|
||||
return toggleFn(enabled)
|
||||
}
|
||||
@@ -280,7 +303,7 @@ export async function getBeatSyncState() {
|
||||
return getDetector().state
|
||||
}
|
||||
|
||||
if (isStageTamagotchi()) {
|
||||
if (isStageTamagotchi() || isStageCapacitor()) {
|
||||
return defineInvoke(getContext(), beatSyncGetStateInvokeEventa)()
|
||||
}
|
||||
|
||||
@@ -292,7 +315,7 @@ export function updateBeatSyncParameters(params: Partial<AnalyserWorkletParamete
|
||||
return getDetector().updateParameters(params)
|
||||
}
|
||||
|
||||
if (isStageTamagotchi()) {
|
||||
if (isStageTamagotchi() || isStageCapacitor()) {
|
||||
return defineInvoke(getContext(), beatSyncUpdateParametersInvokeEventa)(params)
|
||||
}
|
||||
|
||||
@@ -304,7 +327,7 @@ export function listenBeatSyncStateChange(listener: (state: BeatSyncDetectorStat
|
||||
return getDetector().on('stateChange', listener)
|
||||
}
|
||||
|
||||
if (isStageTamagotchi()) {
|
||||
if (isStageTamagotchi() || isStageCapacitor()) {
|
||||
return defineInvokeHandler(getContext(), beatSyncStateChangedInvokeEventa, listener)
|
||||
}
|
||||
|
||||
@@ -316,7 +339,7 @@ export function listenBeatSyncBeatSignal(listener: (e: AnalyserBeatEvent) => voi
|
||||
return getDetector().on('beat', listener)
|
||||
}
|
||||
|
||||
if (isStageTamagotchi()) {
|
||||
if (isStageTamagotchi() || isStageCapacitor()) {
|
||||
return defineInvokeHandler(getContext(), beatSyncBeatSignaledInvokeEventa, listener)
|
||||
}
|
||||
|
||||
@@ -328,7 +351,7 @@ export async function getBeatSyncInputByteFrequencyData() {
|
||||
return getDetector().getInputByteFrequencyData()
|
||||
}
|
||||
|
||||
if (isStageTamagotchi()) {
|
||||
if (isStageTamagotchi() || isStageCapacitor()) {
|
||||
return defineInvoke(getContext(), beatSyncGetInputByteFrequencyDataInvokeEventa)()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BeatSyncDetectorState } from '@proj-airi/stage-shared/beat-sync'
|
||||
|
||||
import { getBeatSyncState, listenBeatSyncStateChange } from '@proj-airi/stage-shared/beat-sync'
|
||||
import { getBeatSyncState, isBeatSyncSupported, listenBeatSyncStateChange } from '@proj-airi/stage-shared/beat-sync'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -44,6 +44,7 @@ export function useModulesList() {
|
||||
const factorioStore = useFactorioStore()
|
||||
const artistryStore = useArtistryStore()
|
||||
const beatSyncState = ref<BeatSyncDetectorState>()
|
||||
const beatSyncSupported = isBeatSyncSupported()
|
||||
|
||||
minecraftStore.initialize()
|
||||
|
||||
@@ -165,15 +166,17 @@ export function useModulesList() {
|
||||
configured: false,
|
||||
category: 'essential',
|
||||
},
|
||||
{
|
||||
id: 'beat-sync',
|
||||
name: t('settings.pages.modules.beat_sync.title'),
|
||||
description: t('settings.pages.modules.beat_sync.description'),
|
||||
icon: 'i-solar:music-notes-bold-duotone',
|
||||
to: '/settings/modules/beat-sync',
|
||||
configured: beatSyncState.value?.isActive ?? false,
|
||||
category: 'essential',
|
||||
},
|
||||
...(beatSyncSupported
|
||||
? [{
|
||||
id: 'beat-sync',
|
||||
name: t('settings.pages.modules.beat_sync.title'),
|
||||
description: t('settings.pages.modules.beat_sync.description'),
|
||||
icon: 'i-solar:music-notes-bold-duotone',
|
||||
to: '/settings/modules/beat-sync',
|
||||
configured: beatSyncState.value?.isActive ?? false,
|
||||
category: 'essential',
|
||||
}]
|
||||
: []),
|
||||
])
|
||||
|
||||
const categorizedModules = computed(() => {
|
||||
@@ -196,6 +199,9 @@ export function useModulesList() {
|
||||
|
||||
// TODO(Makito): We can make this a reactive value from a synthetic store.
|
||||
onMounted(() => {
|
||||
if (!beatSyncSupported)
|
||||
return
|
||||
|
||||
getBeatSyncState().then(initialState => beatSyncState.value = initialState)
|
||||
const removeListener = listenBeatSyncStateChange(newState => beatSyncState.value = { ...newState })
|
||||
onUnmounted(() => removeListener())
|
||||
|
||||
Reference in New Issue
Block a user