diff --git a/js/barkSound.js b/js/barkSound.js new file mode 100644 index 0000000..75f9b20 --- /dev/null +++ b/js/barkSound.js @@ -0,0 +1,98 @@ +/** + * 简单的合成狗叫声 + * 使用 Web Audio API,无需外部音频文件 + */ +let audioCtx = null; + +function getAudioContext() { + if (!audioCtx) { + const AC = window.AudioContext || window.webkitAudioContext; + if (AC) { + audioCtx = new AC(); + } + } + return audioCtx; +} + +export default function playBark() { + const ctx = getAudioContext(); + if (!ctx) return; + + // 如果 AudioContext 被暂停(某些浏览器/微信环境),先 resume + if (ctx.state === 'suspended') { + ctx.resume(); + } + + const now = ctx.currentTime; + const duration = 0.18; // 吠叫持续约 180ms + + // 1. 噪声源(吠叫的嘶哑感) + const sampleRate = ctx.sampleRate; + const bufferSize = sampleRate * duration; + const noiseBuffer = ctx.createBuffer(1, bufferSize, sampleRate); + const output = noiseBuffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + output[i] = Math.random() * 2 - 1; + } + + const noise = ctx.createBufferSource(); + noise.buffer = noiseBuffer; + + // 2. 带通滤波器,集中在 600-1200Hz 类似狗叫的频段 + const bandpass = ctx.createBiquadFilter(); + bandpass.type = 'bandpass'; + bandpass.frequency.value = 900; + bandpass.Q.value = 0.8; + + // 3. 低频振荡器(吠叫的胸腔共鸣感) + const osc = ctx.createOscillator(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(180, now); + osc.frequency.exponentialRampToValueAtTime(120, now + duration); + + const oscGain = ctx.createGain(); + oscGain.gain.value = 0.25; + + // 4. 主音量包络:快速Attack,快速Decay + const masterGain = ctx.createGain(); + masterGain.gain.setValueAtTime(0, now); + masterGain.gain.linearRampToValueAtTime(0.6, now + 0.02); + masterGain.gain.exponentialRampToValueAtTime(0.01, now + duration); + + // 连接 + noise.connect(bandpass); + bandpass.connect(masterGain); + osc.connect(oscGain); + oscGain.connect(masterGain); + masterGain.connect(ctx.destination); + + // 播放 + noise.start(now); + noise.stop(now + duration); + osc.start(now); + osc.stop(now + duration); +} + +export function playWinSound() { + const ctx = getAudioContext(); + if (!ctx) return; + if (ctx.state === 'suspended') ctx.resume(); + + const now = ctx.currentTime; + const notes = [523.25, 659.25, 783.99, 1046.50]; // C E G C' + notes.forEach((freq, i) => { + const osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.value = freq; + + const gain = ctx.createGain(); + gain.gain.setValueAtTime(0, now + i * 0.1); + gain.gain.linearRampToValueAtTime(0.2, now + i * 0.1 + 0.02); + gain.gain.exponentialRampToValueAtTime(0.01, now + i * 0.1 + 0.25); + + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(now + i * 0.1); + osc.stop(now + i * 0.1 + 0.3); + }); +} diff --git a/js/main.js b/js/main.js index 9e80487..e3304a2 100644 --- a/js/main.js +++ b/js/main.js @@ -2,6 +2,7 @@ import { SCREEN_WIDTH, SCREEN_HEIGHT } from './render'; import AudioRecorder from './audioRecorder'; import AIOpponent from './ai'; import storage from './storage'; +import playBark, { playWinSound } from './barkSound'; const ctx = canvas.getContext('2d'); @@ -279,8 +280,9 @@ export default class Main { this.combo = status.combo; if (status.isBark && this.state === 'BATTLE') { this.energy = Math.min(100, this.energy + status.pushPower * 100); + playBark(); try { - if (wx.vibrateShort) wx.vibrateShort({ type: 'light' }); + if (wx.vibrateShort) wx.vibrateShort(); } catch (e) {} } }, @@ -434,7 +436,10 @@ export default class Main { this.rewardWP = isWin ? (this.combo >= 3 ? 50 : 30) : 5; this.wp = storage.addWP(this.rewardWP); - if (isWin) storage.addWin(); + if (isWin) { + storage.addWin(); + playWinSound(); + } this.drawResult(); }