| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- import { type GameEffect } from '../core/gameResult'
- import { DEFAULT_GAME_AUDIO_CONFIG, type AudioCueKey, type GameAudioConfig } from './audioConfig'
- export class SoundDirector {
- enabled: boolean
- config: GameAudioConfig
- contexts: Partial<Record<AudioCueKey, WechatMiniprogram.InnerAudioContext>>
- loopTimers: Partial<Record<AudioCueKey, number>>
- backgroundLoopTimer: number
- activeGuidanceCue: AudioCueKey | null
- backgroundManager: WechatMiniprogram.BackgroundAudioManager | null
- appAudioMode: 'foreground' | 'background'
- constructor(config: GameAudioConfig = DEFAULT_GAME_AUDIO_CONFIG) {
- this.enabled = true
- this.config = config
- this.contexts = {}
- this.loopTimers = {}
- this.backgroundLoopTimer = 0
- this.activeGuidanceCue = null
- this.backgroundManager = null
- this.appAudioMode = 'foreground'
- }
- configure(config: GameAudioConfig): void {
- this.config = config
- this.resetContexts()
- }
- setEnabled(enabled: boolean): void {
- this.enabled = enabled
- }
- resetContexts(): void {
- const timerKeys = Object.keys(this.loopTimers) as AudioCueKey[]
- for (const key of timerKeys) {
- const timer = this.loopTimers[key]
- if (timer) {
- clearTimeout(timer)
- }
- }
- this.loopTimers = {}
- this.clearBackgroundLoopTimer()
- const keys = Object.keys(this.contexts) as AudioCueKey[]
- for (const key of keys) {
- const context = this.contexts[key]
- if (!context) {
- continue
- }
- context.stop()
- context.destroy()
- }
- this.contexts = {}
- this.activeGuidanceCue = null
- this.stopBackgroundGuidance()
- }
- destroy(): void {
- this.resetContexts()
- }
- handleEffects(effects: GameEffect[]): void {
- if (!this.enabled || !this.config.enabled || !effects.length) {
- return
- }
- const hasFinishCompletion = effects.some((effect) => effect.type === 'control_completed' && effect.controlKind === 'finish')
- for (const effect of effects) {
- if (effect.type === 'session_started') {
- this.play('session_started')
- continue
- }
- if (effect.type === 'session_cancelled') {
- this.stopGuidanceLoop()
- this.play('control_completed:finish')
- continue
- }
- if (effect.type === 'punch_feedback' && effect.tone === 'warning') {
- this.play('punch_feedback:warning')
- continue
- }
- if (effect.type === 'guidance_state_changed') {
- if (effect.guidanceState === 'searching') {
- this.startGuidanceLoop('guidance:searching')
- continue
- }
- if (effect.guidanceState === 'approaching') {
- this.startGuidanceLoop('guidance:approaching')
- continue
- }
- this.startGuidanceLoop('guidance:ready')
- continue
- }
- if (effect.type === 'control_completed') {
- this.stopGuidanceLoop()
- if (effect.controlKind === 'start') {
- this.play('control_completed:start')
- continue
- }
- if (effect.controlKind === 'finish') {
- this.play('control_completed:finish')
- continue
- }
- this.play('control_completed:control')
- continue
- }
- if (effect.type === 'session_finished') {
- this.stopGuidanceLoop()
- if (!hasFinishCompletion) {
- this.play('control_completed:finish')
- }
- }
- }
- }
- setAppAudioMode(mode: 'foreground' | 'background'): void {
- if (this.appAudioMode === mode) {
- return
- }
- this.appAudioMode = mode
- const activeGuidanceCue = this.activeGuidanceCue
- if (!activeGuidanceCue) {
- this.stopBackgroundGuidance()
- return
- }
- if (mode === 'background') {
- this.stopForegroundCue(activeGuidanceCue)
- this.startBackgroundGuidance(activeGuidanceCue)
- return
- }
- this.stopBackgroundGuidance()
- this.playForeground(activeGuidanceCue)
- }
- play(key: AudioCueKey): void {
- if (this.appAudioMode === 'background') {
- const cue = this.config.cues[key]
- if (!cue || cue.backgroundMode !== 'guidance' || !this.isGuidanceCue(key)) {
- return
- }
- this.startBackgroundGuidance(key)
- return
- }
- this.playForeground(key)
- }
- playForeground(key: AudioCueKey): void {
- const cue = this.config.cues[key]
- if (!cue || !cue.src) {
- return
- }
- this.clearLoopTimer(key)
- const context = this.getContext(key)
- context.stop()
- if (typeof context.seek === 'function') {
- context.seek(0)
- }
- context.volume = Math.max(0, Math.min(1, this.config.masterVolume * cue.volume))
- context.play()
- }
- startGuidanceLoop(key: AudioCueKey): void {
- if (this.activeGuidanceCue === key) {
- return
- }
- this.stopGuidanceLoop()
- this.activeGuidanceCue = key
- if (this.appAudioMode === 'background') {
- this.startBackgroundGuidance(key)
- return
- }
- this.playForeground(key)
- }
- stopGuidanceLoop(): void {
- if (!this.activeGuidanceCue) {
- this.stopBackgroundGuidance()
- return
- }
- this.clearLoopTimer(this.activeGuidanceCue)
- const context = this.contexts[this.activeGuidanceCue]
- if (context) {
- context.stop()
- if (typeof context.seek === 'function') {
- context.seek(0)
- }
- }
- this.stopBackgroundGuidance()
- this.activeGuidanceCue = null
- }
- clearLoopTimer(key: AudioCueKey): void {
- const timer = this.loopTimers[key]
- if (timer) {
- clearTimeout(timer)
- delete this.loopTimers[key]
- }
- }
- handleCueEnded(key: AudioCueKey): void {
- const cue = this.config.cues[key]
- if (!cue.loop || this.activeGuidanceCue !== key || !this.enabled || !this.config.enabled) {
- return
- }
- this.clearLoopTimer(key)
- this.loopTimers[key] = setTimeout(() => {
- delete this.loopTimers[key]
- if (this.activeGuidanceCue === key && this.enabled && this.config.enabled) {
- this.play(key)
- }
- }, cue.loopGapMs) as unknown as number
- }
- handleBackgroundCueEnded(): void {
- const key = this.activeGuidanceCue
- if (!key || !this.enabled || !this.config.enabled || this.appAudioMode !== 'background') {
- return
- }
- const cue = this.config.cues[key]
- if (!cue || !cue.loop) {
- return
- }
- this.clearBackgroundLoopTimer()
- this.backgroundLoopTimer = setTimeout(() => {
- this.backgroundLoopTimer = 0
- if (this.activeGuidanceCue === key && this.appAudioMode === 'background' && this.enabled && this.config.enabled) {
- this.playBackgroundCue(key)
- }
- }, cue.loopGapMs) as unknown as number
- }
- clearBackgroundLoopTimer(): void {
- if (this.backgroundLoopTimer) {
- clearTimeout(this.backgroundLoopTimer)
- this.backgroundLoopTimer = 0
- }
- }
- stopForegroundCue(key: AudioCueKey): void {
- this.clearLoopTimer(key)
- const context = this.contexts[key]
- if (!context) {
- return
- }
- context.stop()
- if (typeof context.seek === 'function') {
- context.seek(0)
- }
- }
- isGuidanceCue(key: AudioCueKey): boolean {
- return key === 'guidance:searching'
- || key === 'guidance:approaching'
- || key === 'guidance:ready'
- }
- startBackgroundGuidance(key: AudioCueKey): void {
- if (!this.enabled || !this.config.enabled || !this.config.backgroundAudioEnabled) {
- return
- }
- const cue = this.config.cues[key]
- if (!cue || cue.backgroundMode !== 'guidance' || !cue.src) {
- return
- }
- this.playBackgroundCue(key)
- }
- playBackgroundCue(key: AudioCueKey): void {
- const cue = this.config.cues[key]
- if (!cue || !cue.src) {
- return
- }
- const manager = this.getBackgroundManager()
- this.clearBackgroundLoopTimer()
- manager.stop()
- manager.title = 'ColorMapRun 引导音'
- manager.epname = 'ColorMapRun'
- manager.singer = 'ColorMapRun'
- manager.coverImgUrl = ''
- manager.src = cue.src
- manager.play()
- }
- stopBackgroundGuidance(): void {
- this.clearBackgroundLoopTimer()
- if (!this.backgroundManager) {
- return
- }
- this.backgroundManager.stop()
- }
- getBackgroundManager(): WechatMiniprogram.BackgroundAudioManager {
- if (this.backgroundManager) {
- return this.backgroundManager
- }
- const manager = wx.getBackgroundAudioManager()
- if (typeof manager.onEnded === 'function') {
- manager.onEnded(() => {
- this.handleBackgroundCueEnded()
- })
- }
- this.backgroundManager = manager
- return manager
- }
- getContext(key: AudioCueKey): WechatMiniprogram.InnerAudioContext {
- const existing = this.contexts[key]
- if (existing) {
- return existing
- }
- const cue = this.config.cues[key]
- const context = wx.createInnerAudioContext()
- context.src = cue.src
- context.autoplay = false
- context.loop = false
- context.obeyMuteSwitch = this.config.obeyMuteSwitch
- if (typeof context.onEnded === 'function') {
- context.onEnded(() => {
- this.handleCueEnded(key)
- })
- }
- context.volume = Math.max(0, Math.min(1, this.config.masterVolume * cue.volume))
- this.contexts[key] = context
- return context
- }
- }
|