Improve heart rate device reconnect flow
This commit is contained in:
@@ -13,12 +13,14 @@ export interface AudioCueConfig {
|
||||
volume: number
|
||||
loop: boolean
|
||||
loopGapMs: number
|
||||
backgroundMode: 'disabled' | 'guidance'
|
||||
}
|
||||
|
||||
export interface GameAudioConfig {
|
||||
enabled: boolean
|
||||
masterVolume: number
|
||||
obeyMuteSwitch: boolean
|
||||
backgroundAudioEnabled: boolean
|
||||
approachDistanceMeters: number
|
||||
cues: Record<AudioCueKey, AudioCueConfig>
|
||||
}
|
||||
@@ -28,12 +30,14 @@ export interface PartialAudioCueConfig {
|
||||
volume?: number
|
||||
loop?: boolean
|
||||
loopGapMs?: number
|
||||
backgroundMode?: 'disabled' | 'guidance'
|
||||
}
|
||||
|
||||
export interface GameAudioConfigOverrides {
|
||||
enabled?: boolean
|
||||
masterVolume?: number
|
||||
obeyMuteSwitch?: boolean
|
||||
backgroundAudioEnabled?: boolean
|
||||
approachDistanceMeters?: number
|
||||
cues?: Partial<Record<AudioCueKey, PartialAudioCueConfig>>
|
||||
}
|
||||
@@ -42,6 +46,7 @@ export const DEFAULT_GAME_AUDIO_CONFIG: GameAudioConfig = {
|
||||
enabled: true,
|
||||
masterVolume: 1,
|
||||
obeyMuteSwitch: true,
|
||||
backgroundAudioEnabled: true,
|
||||
approachDistanceMeters: 20,
|
||||
cues: {
|
||||
session_started: {
|
||||
@@ -49,48 +54,56 @@ export const DEFAULT_GAME_AUDIO_CONFIG: GameAudioConfig = {
|
||||
volume: 0.78,
|
||||
loop: false,
|
||||
loopGapMs: 0,
|
||||
backgroundMode: 'disabled',
|
||||
},
|
||||
'control_completed:start': {
|
||||
src: '/assets/sounds/start-complete.wav',
|
||||
volume: 0.84,
|
||||
loop: false,
|
||||
loopGapMs: 0,
|
||||
backgroundMode: 'disabled',
|
||||
},
|
||||
'control_completed:control': {
|
||||
src: '/assets/sounds/control-complete.wav',
|
||||
volume: 0.8,
|
||||
loop: false,
|
||||
loopGapMs: 0,
|
||||
backgroundMode: 'disabled',
|
||||
},
|
||||
'control_completed:finish': {
|
||||
src: '/assets/sounds/finish-complete.wav',
|
||||
volume: 0.92,
|
||||
loop: false,
|
||||
loopGapMs: 0,
|
||||
backgroundMode: 'disabled',
|
||||
},
|
||||
'punch_feedback:warning': {
|
||||
src: '/assets/sounds/warning.wav',
|
||||
volume: 0.72,
|
||||
loop: false,
|
||||
loopGapMs: 0,
|
||||
backgroundMode: 'disabled',
|
||||
},
|
||||
'guidance:searching': {
|
||||
src: '/assets/sounds/guidance-searching.wav',
|
||||
volume: 0.48,
|
||||
loop: true,
|
||||
loopGapMs: 1800,
|
||||
backgroundMode: 'guidance',
|
||||
},
|
||||
'guidance:approaching': {
|
||||
src: '/assets/sounds/guidance-approaching.wav',
|
||||
volume: 0.58,
|
||||
loop: true,
|
||||
loopGapMs: 950,
|
||||
backgroundMode: 'guidance',
|
||||
},
|
||||
'guidance:ready': {
|
||||
src: '/assets/sounds/guidance-ready.wav',
|
||||
volume: 0.68,
|
||||
loop: true,
|
||||
loopGapMs: 650,
|
||||
backgroundMode: 'guidance',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -143,6 +156,10 @@ export function mergeGameAudioConfig(overrides?: GameAudioConfigOverrides | null
|
||||
if (cue.loopGapMs !== undefined) {
|
||||
cues[key].loopGapMs = clampGap(Number(cue.loopGapMs), cues[key].loopGapMs)
|
||||
}
|
||||
|
||||
if (cue.backgroundMode === 'disabled' || cue.backgroundMode === 'guidance') {
|
||||
cues[key].backgroundMode = cue.backgroundMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +167,9 @@ export function mergeGameAudioConfig(overrides?: GameAudioConfigOverrides | null
|
||||
enabled: overrides && overrides.enabled !== undefined ? !!overrides.enabled : DEFAULT_GAME_AUDIO_CONFIG.enabled,
|
||||
masterVolume: clampVolume(Number(overrides && overrides.masterVolume), DEFAULT_GAME_AUDIO_CONFIG.masterVolume),
|
||||
obeyMuteSwitch: overrides && overrides.obeyMuteSwitch !== undefined ? !!overrides.obeyMuteSwitch : DEFAULT_GAME_AUDIO_CONFIG.obeyMuteSwitch,
|
||||
backgroundAudioEnabled: overrides && overrides.backgroundAudioEnabled !== undefined
|
||||
? !!overrides.backgroundAudioEnabled
|
||||
: true,
|
||||
approachDistanceMeters: clampDistance(Number(overrides && overrides.approachDistanceMeters), DEFAULT_GAME_AUDIO_CONFIG.approachDistanceMeters),
|
||||
cues,
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@ export class SoundDirector {
|
||||
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 {
|
||||
@@ -34,6 +40,7 @@ export class SoundDirector {
|
||||
}
|
||||
}
|
||||
this.loopTimers = {}
|
||||
this.clearBackgroundLoopTimer()
|
||||
|
||||
const keys = Object.keys(this.contexts) as AudioCueKey[]
|
||||
for (const key of keys) {
|
||||
@@ -46,6 +53,7 @@ export class SoundDirector {
|
||||
}
|
||||
this.contexts = {}
|
||||
this.activeGuidanceCue = null
|
||||
this.stopBackgroundGuidance()
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
@@ -108,7 +116,43 @@ export class SoundDirector {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -132,11 +176,17 @@ export class SoundDirector {
|
||||
|
||||
this.stopGuidanceLoop()
|
||||
this.activeGuidanceCue = key
|
||||
this.play(key)
|
||||
if (this.appAudioMode === 'background') {
|
||||
this.startBackgroundGuidance(key)
|
||||
return
|
||||
}
|
||||
|
||||
this.playForeground(key)
|
||||
}
|
||||
|
||||
stopGuidanceLoop(): void {
|
||||
if (!this.activeGuidanceCue) {
|
||||
this.stopBackgroundGuidance()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,6 +198,7 @@ export class SoundDirector {
|
||||
context.seek(0)
|
||||
}
|
||||
}
|
||||
this.stopBackgroundGuidance()
|
||||
this.activeGuidanceCue = null
|
||||
}
|
||||
|
||||
@@ -175,6 +226,105 @@ export class SoundDirector {
|
||||
}, 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) {
|
||||
|
||||
Reference in New Issue
Block a user