Files
reader-star/apps/admin/src/utils/audio.ts

122 lines
3.7 KiB
TypeScript
Raw Normal View History

export class AudioController {
private audioContext: AudioContext | null = null
private soundFiles: Record<string, string> = {}
private audioCache: Record<string, HTMLAudioElement> = {}
constructor(soundFiles: Record<string, string> = {}) {
this.soundFiles = soundFiles
}
private initAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
}
}
/**
*
* 使使 Web Audio API
*/
async play(type: 'start' | 'tick' | 'flip') {
// 尝试播放音频文件
if (this.soundFiles[type]) {
try {
await this.playFile(type)
return
}
catch (error) {
console.warn(`Failed to play audio file for ${type}, falling back to synth.`, error)
}
}
// 回退到合成音效
this.playSynth(type)
}
private playFile(type: string): Promise<void> {
return new Promise((resolve, reject) => {
const url = this.soundFiles[type]
if (!url) {
reject(new Error('No file url'))
return
}
// 使用缓存的 Audio 对象
if (!this.audioCache[type]) {
this.audioCache[type] = new Audio(url)
}
const audio = this.audioCache[type]
audio.currentTime = 0
audio.play()
.then(() => resolve())
.catch(e => reject(e))
})
}
private playSynth(type: 'start' | 'tick' | 'flip') {
this.initAudioContext()
if (!this.audioContext)
return
const ctxTime = this.audioContext.currentTime
const gain = this.audioContext.createGain()
gain.connect(this.audioContext.destination)
if (type === 'start') {
const osc = this.audioContext.createOscillator()
osc.connect(gain)
// 开始音效:高音短促提示
osc.type = 'sine'
osc.frequency.setValueAtTime(880, ctxTime)
osc.frequency.exponentialRampToValueAtTime(440, ctxTime + 0.3)
gain.gain.setValueAtTime(0.5, ctxTime)
gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.3)
osc.start(ctxTime)
osc.stop(ctxTime + 0.3)
}
else if (type === 'tick') {
const osc = this.audioContext.createOscillator()
osc.connect(gain)
// 倒计时音效:急促的滴答声
osc.type = 'triangle'
osc.frequency.setValueAtTime(600, ctxTime)
gain.gain.setValueAtTime(0.3, ctxTime)
gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.1)
osc.start(ctxTime)
osc.stop(ctxTime + 0.1)
}
else if (type === 'flip') {
// 翻书音效:使用白噪声模拟纸张摩擦
const bufferSize = this.audioContext.sampleRate * 0.5 // 0.5秒缓冲
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate)
const data = buffer.getChannelData(0)
// 生成白噪声
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1
}
const noise = this.audioContext.createBufferSource()
noise.buffer = buffer
// 滤波器:低通滤波器,模拟纸张的闷声
const filter = this.audioContext.createBiquadFilter()
filter.type = 'lowpass'
filter.frequency.setValueAtTime(400, ctxTime)
filter.frequency.exponentialRampToValueAtTime(3000, ctxTime + 0.1) // 频率快速扫过,模拟快速翻动
noise.connect(filter)
filter.connect(gain)
// 音量包络:快速淡入淡出
gain.gain.setValueAtTime(0, ctxTime)
gain.gain.linearRampToValueAtTime(0.8, ctxTime + 0.05)
gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.3)
noise.start(ctxTime)
noise.stop(ctxTime + 0.3)
}
}
}