Add synthesized bark sound and vibration feedback
This commit is contained in:
parent
57aeb989d4
commit
b43a9ea35a
98
js/barkSound.js
Normal file
98
js/barkSound.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { SCREEN_WIDTH, SCREEN_HEIGHT } from './render';
|
|||||||
import AudioRecorder from './audioRecorder';
|
import AudioRecorder from './audioRecorder';
|
||||||
import AIOpponent from './ai';
|
import AIOpponent from './ai';
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
|
import playBark, { playWinSound } from './barkSound';
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
@ -279,8 +280,9 @@ export default class Main {
|
|||||||
this.combo = status.combo;
|
this.combo = status.combo;
|
||||||
if (status.isBark && this.state === 'BATTLE') {
|
if (status.isBark && this.state === 'BATTLE') {
|
||||||
this.energy = Math.min(100, this.energy + status.pushPower * 100);
|
this.energy = Math.min(100, this.energy + status.pushPower * 100);
|
||||||
|
playBark();
|
||||||
try {
|
try {
|
||||||
if (wx.vibrateShort) wx.vibrateShort({ type: 'light' });
|
if (wx.vibrateShort) wx.vibrateShort();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -434,7 +436,10 @@ export default class Main {
|
|||||||
|
|
||||||
this.rewardWP = isWin ? (this.combo >= 3 ? 50 : 30) : 5;
|
this.rewardWP = isWin ? (this.combo >= 3 ? 50 : 30) : 5;
|
||||||
this.wp = storage.addWP(this.rewardWP);
|
this.wp = storage.addWP(this.rewardWP);
|
||||||
if (isWin) storage.addWin();
|
if (isWin) {
|
||||||
|
storage.addWin();
|
||||||
|
playWinSound();
|
||||||
|
}
|
||||||
|
|
||||||
this.drawResult();
|
this.drawResult();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user