Skip to content

Commit bbb437a

Browse files
committed
feat(stage-tamagotchi,stage-pages,stage-shared,stage-ui): complete beat-sync integration
1 parent 4d3c97c commit bbb437a

File tree

17 files changed

+456
-240
lines changed

17 files changed

+456
-240
lines changed

apps/stage-tamagotchi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"@iconify-json/vscode-icons": "^1.2.34",
135135
"@iconify/utils": "^3.0.2",
136136
"@intlify/unplugin-vue-i18n": "^11.0.1",
137+
"@nekopaw/tempora": "0.3.1-alpha.1",
137138
"@proj-airi/lobe-icons": "^1.0.14",
138139
"@proj-airi/stage-shared": "workspace:^",
139140
"@proj-airi/ui-transitions": "workspace:^",

apps/stage-tamagotchi/src/main/windows/beat-sync/index.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,22 @@ import { fileURLToPath } from 'node:url'
33

44
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
55
import { createContext } from '@moeru/eventa/adapters/electron/main'
6-
import { beatSyncRequestSignalBeat, beatSyncSignalBeat } from '@proj-airi/stage-shared/beat-sync/eventa'
6+
import {
7+
beatSyncBeatSignaledInvokeEventa,
8+
beatSyncGetStateInvokeEventa,
9+
beatSyncStateChangedInvokeEventa,
10+
beatSyncToggleInvokeEventa,
11+
beatSyncUpdateParametersInvokeEventa,
12+
} from '@proj-airi/stage-shared/beat-sync'
713
import { BrowserWindow, ipcMain } from 'electron'
814

15+
import {
16+
beatSyncElectronChangeState,
17+
beatSyncElectronGetState,
18+
beatSyncElectronSignalBeat,
19+
beatSyncElectronToggle,
20+
beatSyncElectronUpdateParameters,
21+
} from '../../../shared/eventa'
922
import { baseUrl, getElectronMainDirname, load } from '../../libs/electron/location'
1023

1124
export async function setupBeatSync() {
@@ -16,16 +29,35 @@ export async function setupBeatSync() {
1629
sandbox: false,
1730
},
1831
})
32+
const context = createContext(ipcMain, window).context
33+
34+
// TODO(Makito): Revisit and improve
35+
// [main] -> [renderer] beat-sync
36+
const toggle = defineInvoke(context, beatSyncElectronToggle) as (enabled: boolean) => Promise<void> // TODO: Better type
37+
const getState = defineInvoke(context, beatSyncElectronGetState)
38+
const updateParameters = defineInvoke(context, beatSyncElectronUpdateParameters)
1939

2040
await load(window, baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'), 'beat-sync.html'))
2141
return {
2242
window,
2343
dispatchTo: (window: BrowserWindow) => {
2444
const context = createContext(ipcMain, window).context
25-
const signalBeat = defineInvoke(context, beatSyncSignalBeat)
26-
const removeHandler = defineInvokeHandler(context, beatSyncRequestSignalBeat, async e => signalBeat(e))
27-
window.on('closed', () => removeHandler())
28-
return removeHandler
45+
46+
const stateChanged = defineInvoke(context, beatSyncStateChangedInvokeEventa)
47+
const beatSignaled = defineInvoke(context, beatSyncBeatSignaledInvokeEventa)
48+
const removeHandlerFns = [
49+
// [renderer] beat-sync -> [main] -> [renderer] index (Events)
50+
defineInvokeHandler(context, beatSyncElectronChangeState, async e => stateChanged(e)),
51+
defineInvokeHandler(context, beatSyncElectronSignalBeat, async e => beatSignaled(e)),
52+
53+
// [renderer] index -> [main] -> [renderer] beat-sync (Functions)
54+
defineInvokeHandler(context, beatSyncToggleInvokeEventa, async enabled => toggle(enabled)),
55+
defineInvokeHandler(context, beatSyncGetStateInvokeEventa, async () => getState()),
56+
defineInvokeHandler(context, beatSyncUpdateParametersInvokeEventa, async params => updateParameters(params)),
57+
]
58+
const removeHandlers = () => removeHandlerFns.forEach(fn => fn())
59+
window.on('closed', () => removeHandlers())
60+
return removeHandlers
2961
},
3062
}
3163
}

apps/stage-tamagotchi/src/renderer/beat-sync.main.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
22
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
3-
import { createBeatSyncDetector, StageEnvironment } from '@proj-airi/stage-shared'
4-
import { beatSyncRequestSignalBeat, beatSyncToggle } from '@proj-airi/stage-shared/beat-sync/eventa'
3+
import { StageEnvironment } from '@proj-airi/stage-shared'
4+
import { createBeatSyncDetector } from '@proj-airi/stage-shared/beat-sync/browser'
5+
6+
import {
7+
beatSyncElectronChangeState,
8+
beatSyncElectronGetState,
9+
beatSyncElectronSignalBeat,
10+
beatSyncElectronToggle,
11+
beatSyncElectronUpdateParameters,
12+
} from '../shared/eventa'
513

614
const { ipcRenderer } = window.electron
715

816
const context = createContext(ipcRenderer).context
9-
const requestSignalBeat = defineInvoke(context, beatSyncRequestSignalBeat)
17+
18+
// [renderer] beat-sync -> [main] -> [renderer] index
19+
const changeState = defineInvoke(context, beatSyncElectronChangeState)
20+
const signalBeat = defineInvoke(context, beatSyncElectronSignalBeat)
1021

1122
function enableLoopbackAudio() {
1223
// electron-audio-loopback currently registers this handler internally
@@ -24,13 +35,26 @@ const detector = createBeatSyncDetector({
2435
disableLoopbackAudio,
2536
})
2637

27-
detector.on('beat', e => requestSignalBeat(e))
38+
detector.on('stateChange', state => changeState(state))
39+
detector.on('beat', (e) => {
40+
// eslint-disable-next-line no-console
41+
console.debug('[beat]', e) // This could be noisy.
42+
signalBeat(e)
43+
})
2844

29-
defineInvokeHandler(context, beatSyncToggle, async (enabled) => {
45+
defineInvokeHandler(context, beatSyncElectronToggle, async (enabled) => {
46+
// eslint-disable-next-line no-console
47+
console.log('[toggle]', enabled)
3048
if (enabled) {
3149
detector.startScreenCapture()
3250
}
3351
else {
3452
detector.stop()
3553
}
3654
})
55+
defineInvokeHandler(context, beatSyncElectronGetState, async () => detector.state)
56+
defineInvokeHandler(context, beatSyncElectronUpdateParameters, async (params) => {
57+
// eslint-disable-next-line no-console
58+
console.log('[update-params]', params)
59+
detector.updateParameters(params)
60+
})

apps/stage-tamagotchi/src/shared/eventa.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { AnalyserBeatEvent, AnalyserWorkletParameters } from '@nekopaw/tempora'
2+
import type { BeatSyncDetectorState } from '@proj-airi/stage-shared/beat-sync'
3+
14
import { defineEventa, defineInvokeEventa } from '@moeru/eventa'
25

36
export const electronStartTrackMousePosition = defineInvokeEventa('eventa:invoke:electron:start-tracking-mouse-position')
@@ -71,4 +74,10 @@ export const widgetsRemoveEvent = defineEventa<{ id: string }>('eventa:event:ele
7174
export const widgetsClearEvent = defineEventa('eventa:event:electron:windows:widgets:clear')
7275
export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Record<string, any> }>('eventa:event:electron:windows:widgets:update')
7376

77+
export const beatSyncElectronSignalBeat = defineInvokeEventa<void, AnalyserBeatEvent>('eventa:event:electron:beat-sync:electron:signal-beat')
78+
export const beatSyncElectronChangeState = defineInvokeEventa<void, BeatSyncDetectorState>('eventa:event:electron:beat-sync:electron:change-state')
79+
export const beatSyncElectronGetState = defineInvokeEventa<BeatSyncDetectorState>('eventa:event:electron:beat-sync:electron:get-state')
80+
export const beatSyncElectronToggle = defineInvokeEventa<void, boolean>('eventa:event:electron:beat-sync:electron:toggle')
81+
export const beatSyncElectronUpdateParameters = defineInvokeEventa<void, Partial<AnalyserWorkletParameters>>('eventa:event:electron:beat-sync:electron:update-parameters')
82+
7483
export { electron } from './electron'

packages/stage-pages/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"vue-router": "^4.6.3"
4646
},
4747
"devDependencies": {
48+
"@proj-airi/stage-shared": "workspace:^",
4849
"@types/three": "^0.181.0",
4950
"vue-tsc": "^3.1.4"
5051
}

packages/stage-pages/src/pages/settings/modules/beat-sync.vue

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
<script setup lang="ts">
2-
import type { AnalyserBeatEvent, AnalyserWorkletParameters } from '@nekopaw/tempora'
2+
import type { AnalyserWorkletParameters } from '@nekopaw/tempora'
3+
import type { BeatSyncDetectorState } from '@proj-airi/stage-shared/beat-sync'
34
45
import { DEFAULT_ANALYSER_WORKLET_PARAMS } from '@nekopaw/tempora'
6+
import {
7+
getBeatSyncState,
8+
listenBeatSyncBeatSignal,
9+
listenBeatSyncStateChange,
10+
toggleBeatSync,
11+
updateBeatSyncParameters,
12+
} from '@proj-airi/stage-shared/beat-sync/browser'
513
import { Button } from '@proj-airi/stage-ui/components'
6-
import { useBeatSyncStore } from '@proj-airi/stage-ui/stores/beat-sync'
714
import { FieldCheckbox, FieldRange } from '@proj-airi/ui'
815
import { createTimeline } from 'animejs'
916
import { nanoid } from 'nanoid'
10-
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
17+
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
1118
import { useI18n } from 'vue-i18n'
1219
13-
const beatSyncStore = useBeatSyncStore()
20+
const state = ref<BeatSyncDetectorState>()
21+
1422
const { t } = useI18n()
1523
1624
const beatsHistory = ref<Array<{
@@ -21,9 +29,7 @@ const beatsHistory = ref<Array<{
2129
2230
const parameters = ref<AnalyserWorkletParameters>({ ...DEFAULT_ANALYSER_WORKLET_PARAMS })
2331
24-
watchEffect(() => {
25-
beatSyncStore.updateParameters(parameters.value)
26-
})
32+
watch<AnalyserWorkletParameters>(parameters, newParameters => updateBeatSyncParameters(toRaw(newParameters)), { deep: true })
2733
2834
function normalizeEnergy(energy: number) {
2935
const base = 2
@@ -32,19 +38,23 @@ function normalizeEnergy(energy: number) {
3238
}
3339
3440
onMounted(() => {
35-
const onBeat = ({ energy }: AnalyserBeatEvent) => {
36-
beatsHistory.value.unshift({
37-
id: nanoid(),
38-
energy,
39-
normalizedEnergy: normalizeEnergy(energy),
40-
})
41-
}
41+
getBeatSyncState().then(initialState => state.value = initialState)
4242
43-
beatSyncStore.on('beat', onBeat)
43+
const removeHandlerFns = [
44+
listenBeatSyncStateChange((newState) => {
45+
state.value = { ...newState }
46+
}),
47+
listenBeatSyncBeatSignal(({ energy }) => {
48+
beatsHistory.value.unshift({
49+
id: nanoid(),
50+
energy,
51+
normalizedEnergy: normalizeEnergy(energy),
52+
})
53+
}),
54+
]
4455
45-
onUnmounted(() => {
46-
beatSyncStore.off('beat', onBeat)
47-
})
56+
const removeHandlers = () => removeHandlerFns.forEach(fn => fn())
57+
onUnmounted(() => removeHandlers())
4858
})
4959
5060
function onRippleEnter(el: Element, done: () => void) {
@@ -93,14 +103,14 @@ function resetDefaultParameters() {
93103
</div>
94104

95105
<div max-w-full flex="~ row gap-4 wrap">
96-
<template v-if="beatSyncStore.isActive">
97-
<Button @click="beatSyncStore.stop">
106+
<template v-if="state?.isActive">
107+
<Button @click="toggleBeatSync(false)">
98108
{{ t('settings.pages.modules.beat_sync.sections.audio_source.actions.stop') }}
99109
</Button>
100110
</template>
101111

102112
<template v-else>
103-
<Button @click="beatSyncStore.startFromScreenCapture">
113+
<Button @click="toggleBeatSync(true)">
104114
{{ t('settings.pages.modules.beat_sync.sections.audio_source.actions.start_screen_capture') }}
105115
</Button>
106116
</template>

packages/stage-shared/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"exports": {
1818
".": "./src/index.ts",
1919
"./beat-sync": "./src/beat-sync/index.ts",
20-
"./beat-sync/eventa": "./src/beat-sync/eventa.ts",
20+
"./beat-sync/browser": "./src/beat-sync/browser/index.ts",
2121
"./electron-renderer": "./src/electron-renderer.d.ts"
2222
},
2323
"scripts": {

0 commit comments

Comments
 (0)