init
This commit is contained in:
265
src/engine/audio.js
Normal file
265
src/engine/audio.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Web Audio API synthesizer for tactile, Apple-grade acoustic feedback
|
||||
* Requires 0 external audio files, operates instantly with zero latency
|
||||
*/
|
||||
class SoundEngine {
|
||||
constructor() {
|
||||
this.ctx = null;
|
||||
this.muted = localStorage.getItem('quartiles_sound_muted') === 'true';
|
||||
}
|
||||
|
||||
initContext() {
|
||||
if (!this.ctx) {
|
||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioCtx) {
|
||||
this.ctx = new AudioCtx();
|
||||
}
|
||||
}
|
||||
if (this.ctx && this.ctx.state === 'suspended') {
|
||||
this.ctx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
isMuted() {
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
setMuted(muted) {
|
||||
this.muted = !!muted;
|
||||
localStorage.setItem('quartiles_sound_muted', this.muted ? 'true' : 'false');
|
||||
}
|
||||
|
||||
toggleMute() {
|
||||
this.setMuted(!this.muted);
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
// Soft tactile click for tile selection
|
||||
playTileClick() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const now = this.ctx.currentTime;
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(620, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(380, now + 0.04);
|
||||
|
||||
gain.gain.setValueAtTime(0.12, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.045);
|
||||
}
|
||||
|
||||
// Gentle acoustic pop when tile is removed
|
||||
playTileRemove() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const now = this.ctx.currentTime;
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(320, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(540, now + 0.05);
|
||||
|
||||
gain.gain.setValueAtTime(0.1, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.055);
|
||||
}
|
||||
|
||||
// Shuffle swoosh sound
|
||||
playShuffle() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const delay = i * 0.025;
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(280 + i * 70, now + delay);
|
||||
osc.frequency.exponentialRampToValueAtTime(160, now + delay + 0.06);
|
||||
|
||||
gain.gain.setValueAtTime(0.06, now + delay);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.06);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now + delay);
|
||||
osc.stop(now + delay + 0.065);
|
||||
}
|
||||
}
|
||||
|
||||
// Gentle double boop on invalid submission
|
||||
playError() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
[0, 0.09].forEach(delay => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(140, now + delay);
|
||||
osc.frequency.exponentialRampToValueAtTime(110, now + delay + 0.08);
|
||||
|
||||
gain.gain.setValueAtTime(0.08, now + delay);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.08);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now + delay);
|
||||
osc.stop(now + delay + 0.085);
|
||||
});
|
||||
}
|
||||
|
||||
// Cheerful chime on valid word
|
||||
playWordValid(tileCount) {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
const baseFreqs = {
|
||||
1: [523.25, 659.25], // C5, E5
|
||||
2: [523.25, 659.25, 783.99], // C5, E5, G5
|
||||
3: [587.33, 739.99, 880.00, 1046.50] // D5, F#5, A5, C6
|
||||
};
|
||||
|
||||
const freqs = baseFreqs[tileCount] || baseFreqs[2];
|
||||
|
||||
freqs.forEach((freq, idx) => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const delay = idx * 0.045;
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now + delay);
|
||||
|
||||
gain.gain.setValueAtTime(0.12, now + delay);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.22);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now + delay);
|
||||
osc.stop(now + delay + 0.23);
|
||||
});
|
||||
}
|
||||
|
||||
// Sparkling golden fanfare for 4-tile Quartile
|
||||
playQuartileFound() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
// Major 7th arpeggio: C5, E5, G5, B5, C6
|
||||
const notes = [523.25, 659.25, 783.99, 987.77, 1046.50];
|
||||
|
||||
notes.forEach((freq, idx) => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const delay = idx * 0.055;
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(freq, now + delay);
|
||||
|
||||
gain.gain.setValueAtTime(0.15, now + delay);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.45);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now + delay);
|
||||
osc.stop(now + delay + 0.46);
|
||||
});
|
||||
}
|
||||
|
||||
// Grand celebration fanfare when all 5 Quartiles are discovered (+40 pts bonus!)
|
||||
playBonusCelebration() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
// Fanfare chord progression
|
||||
const chords = [
|
||||
{ delay: 0.0, freqs: [523.25, 659.25, 783.99], dur: 0.2 },
|
||||
{ delay: 0.2, freqs: [587.33, 739.99, 880.00], dur: 0.2 },
|
||||
{ delay: 0.4, freqs: [659.25, 830.61, 987.77], dur: 0.25 },
|
||||
{ delay: 0.7, freqs: [783.99, 987.77, 1174.66, 1567.98], dur: 0.8 }
|
||||
];
|
||||
|
||||
chords.forEach(chord => {
|
||||
chord.freqs.forEach(freq => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const startTime = now + chord.delay;
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(freq, startTime);
|
||||
|
||||
gain.gain.setValueAtTime(0.14, startTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, startTime + chord.dur);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(startTime);
|
||||
osc.stop(startTime + chord.dur + 0.05);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Uplifting chord for rank progression (e.g. hitting Expert)
|
||||
playRankUp() {
|
||||
if (this.muted) return;
|
||||
this.initContext();
|
||||
if (!this.ctx) return;
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
const freqs = [440, 554.37, 659.25, 880];
|
||||
freqs.forEach((freq, idx) => {
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const delay = idx * 0.08;
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, now + delay);
|
||||
|
||||
gain.gain.setValueAtTime(0.13, now + delay);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.35);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.ctx.destination);
|
||||
|
||||
osc.start(now + delay);
|
||||
osc.stop(now + delay + 0.36);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const sound = new SoundEngine();
|
||||
Reference in New Issue
Block a user