tarinai/js/audio.js

88 lines
3.4 KiB
JavaScript
Raw Normal View History

2026-06-18 18:59:56 +09:00
"use strict";
const audio = {
enabled: false,
ctx: null,
2026-06-19 02:27:19 +09:00
samples: {},
sampleLast: {},
samplePaths: {
hau: "assets/sounds/voice_001_hau.wav",
flee: "assets/sounds/voice_002_flee.wav",
poop: "assets/sounds/voice_003_po.wav",
eat: "assets/sounds/voice_004_eat.wav",
stress: "assets/sounds/voice_005_stress.wav",
sleep: "assets/sounds/voice_006_sleep.wav",
explosion: "assets/sounds/firecracker_explosion.mp3",
},
2026-06-18 18:59:56 +09:00
ensure() {
const AudioClass = window.AudioContext || window.webkitAudioContext;
if (!AudioClass) return null;
if (!this.ctx) this.ctx = new AudioClass();
if (this.ctx.state === "suspended") this.ctx.resume();
2026-06-19 02:27:19 +09:00
this.initSamples();
2026-06-18 18:59:56 +09:00
return this.ctx;
},
2026-06-19 02:27:19 +09:00
initSamples() {
if (this._samplesReady) return;
this._samplesReady = true;
for (const [key, path] of Object.entries(this.samplePaths)) {
const a = new Audio(path);
a.preload = "auto";
a.volume = 0.72;
this.samples[key] = a;
}
},
2026-06-18 18:59:56 +09:00
toggle() {
this.enabled = !this.enabled;
if (this.enabled) this.ensure();
return this.enabled;
},
2026-06-19 02:27:19 +09:00
playSample(key, minGap = 0.16) {
if (!this.enabled) return false;
this.ensure();
const base = this.samples[key];
if (!base) return false;
const t = performance.now() / 1000;
if (t - (this.sampleLast[key] || 0) < minGap) return true;
this.sampleLast[key] = t;
const node = base.cloneNode(true);
node.volume = base.volume;
node.play().catch(() => {});
return true;
},
2026-06-18 18:59:56 +09:00
tone(freq, dur = 0.08, type = "sine", gain = 0.035, slide = 0) {
if (!this.enabled) return;
const ctx = this.ensure();
if (!ctx) return;
const t0 = ctx.currentTime;
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t0);
if (slide) osc.frequency.linearRampToValueAtTime(Math.max(40, freq + slide), t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(gain, t0 + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(g).connect(ctx.destination);
osc.start(t0);
osc.stop(t0 + dur + 0.02);
},
2026-06-19 02:27:19 +09:00
bubble(text) {
const s = String(text || "");
if (s.includes("\u3082\u3050")) return this.playSample("eat", 0.20);
if (s.includes("\u3076\u308a")) return this.playSample("poop", 0.28);
if (s.includes("\u306f\u3063")) return this.playSample("stress", 0.80);
if (s.includes("Zzz")) return this.playSample("sleep", 1.25);
if (s.includes("\u306f\u3041") || s.includes("\u306f\u3042")) return this.playSample("flee", 0.70);
if (s.includes("\u306f\u3046") || s.includes("\u306f\u3045")) return this.playSample("hau", 0.45);
return false;
},
eat() { if (!this.playSample("eat", 0.20)) this.tone(660 + Math.random() * 140, 0.055, "triangle", 0.026, 120); },
2026-06-18 18:59:56 +09:00
poke() { this.tone(180, 0.09, "sawtooth", 0.022, -70); },
birth() { this.tone(520, 0.08, "sine", 0.028, 100); setTimeout(() => this.tone(760, 0.10, "triangle", 0.024, 80), 70); },
death() { this.tone(150, 0.22, "sine", 0.028, -70); },
fight() { this.tone(110, 0.055, "square", 0.018, 70); setTimeout(() => this.tone(90, 0.045, "sawtooth", 0.014, -40), 35); },
2026-06-19 02:27:19 +09:00
explode() { if (!this.playSample("explosion", 0.30)) { this.tone(90, 0.12, "sawtooth", 0.06, -30); setTimeout(() => this.tone(55, 0.20, "square", 0.04, -20), 40); } },
2026-06-18 18:59:56 +09:00
phase() { this.tone(440, 0.06, "sine", 0.018, 60); },
};
window.TarinaiAudio = audio;