-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaudio.js
More file actions
119 lines (107 loc) · 3.74 KB
/
Copy pathaudio.js
File metadata and controls
119 lines (107 loc) · 3.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* AudioEngine
* Encapsulates the Web Audio API and SpeechSynthesis API.
* Manages audio context initialization, oscillator nodes, and TTS rates.
*/
export class AudioEngine {
constructor() {
this.AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = null;
}
/**
* Initializes the AudioContext upon first user gesture.
*/
init() {
if (!this.ctx) {
this.ctx = new this.AudioContext();
if ('speechSynthesis' in window) {
const unlockUtterance = new SpeechSynthesisUtterance('');
unlockUtterance.volume = 0;
window.speechSynthesis.speak(unlockUtterance);
}
}
}
/**
* Resumes the AudioContext if it was suspended by the browser.
*/
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
/**
* Synthesizes a soft, toddler-friendly "bloop" or a piano note.
* @param {number|null} specificFreq - The Hz frequency to play (e.g., C4 = 261.63).
* @param {boolean} isPluck - If true, extends the decay of the envelope.
*/
playSound(specificFreq = null, isPluck = false) {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gainNode = this.ctx.createGain();
if (specificFreq) {
osc.type = 'sine';
osc.frequency.setValueAtTime(specificFreq, this.ctx.currentTime);
} else {
const frequencies = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25, 783.99];
const freq = frequencies[Math.floor(Math.random() * frequencies.length)];
osc.type = Math.random() > 0.5 ? 'sine' : 'triangle';
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
}
gainNode.gain.setValueAtTime(0, this.ctx.currentTime);
gainNode.gain.linearRampToValueAtTime(0.4, this.ctx.currentTime + 0.02);
gainNode.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + (isPluck ? 0.8 : 0.6));
osc.connect(gainNode);
gainNode.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + (isPluck ? 0.8 : 0.6));
}
speakWord(text, isMobile = false) {
return new Promise((resolve) => {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.pitch = 1.2;
// Reduce the speed on mobile devices to make it clearer for toddlers
if (isMobile) {
utterance.rate = 0.85;
} else {
utterance.rate = 1.1;
}
// Guarantee resolve fires exactly when audio finishes (or fails)
let resolved = false;
const complete = () => {
if (!resolved) {
resolved = true;
resolve();
}
};
utterance.onend = complete;
utterance.onerror = complete;
// Fallback timeout in case the browser TTS engine glitches
setTimeout(complete, 2000);
window.speechSynthesis.speak(utterance);
} else {
// Automatically resolve if TTS is unsupported
resolve();
}
});
}
playHarpSweep() {
if (!this.ctx) return;
const notes = [261.63, 329.63, 392.00, 523.25, 659.25, 783.99, 1046.50];
notes.forEach((freq, index) => {
const osc = this.ctx.createOscillator();
const gainNode = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
const startTime = this.ctx.currentTime + (index * 0.1);
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(0.3, startTime + 0.05);
gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + 1.5);
osc.connect(gainNode);
gainNode.connect(this.ctx.destination);
osc.start(startTime);
osc.stop(startTime + 1.5);
});
}
}