| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 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>>
- activeGuidanceCue: AudioCueKey | null
- constructor(config: GameAudioConfig = DEFAULT_GAME_AUDIO_CONFIG) {
- this.enabled = true
- this.config = config
- this.contexts = {}
- this.loopTimers = {}
- this.activeGuidanceCue = null
- }
- 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 = {}
- 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
- }
- 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 === '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')
- }
- }
- }
- }
- play(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
- this.play(key)
- }
- stopGuidanceLoop(): void {
- if (!this.activeGuidanceCue) {
- return
- }
- this.clearLoopTimer(this.activeGuidanceCue)
- const context = this.contexts[this.activeGuidanceCue]
- if (context) {
- context.stop()
- if (typeof context.seek === 'function') {
- context.seek(0)
- }
- }
- 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
- }
- 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
- }
- }
|