JavaScript · 5988 bytes Raw Blame History
1 // Web Audio API sound synthesis for dougk
2
3 let audioContext = null
4 let unlocked = false
5
6 function getContext() {
7 if (!audioContext) {
8 audioContext = new (window.AudioContext || window.webkitAudioContext)()
9 }
10 return audioContext
11 }
12
13 // Must be called on first user interaction to enable audio on mobile
14 export function unlockAudio() {
15 if (unlocked) return
16 unlocked = true // Set immediately to prevent multiple attempts
17
18 // Create context fresh during user gesture (mobile requirement)
19 if (!audioContext) {
20 audioContext = new (window.AudioContext || window.webkitAudioContext)()
21 }
22
23 const ctx = audioContext
24
25 // Resume synchronously - don't wait for promise
26 if (ctx.state === 'suspended') {
27 ctx.resume()
28 }
29
30 // Immediately play a sound to force audio pipeline open
31 // Must happen synchronously in the user gesture
32 try {
33 const osc = ctx.createOscillator()
34 const gain = ctx.createGain()
35
36 osc.type = 'triangle'
37 osc.frequency.value = 200
38 gain.gain.value = 0.001 // Nearly silent
39
40 osc.connect(gain)
41 gain.connect(ctx.destination)
42
43 osc.start(0)
44 osc.stop(ctx.currentTime + 0.1)
45 } catch (e) {
46 console.warn('Unlock sound failed:', e)
47 }
48 }
49
50 // Damp crunch sound - wet bread being chomped
51 export function playMonch() {
52 const ctx = getContext()
53
54 // Try to resume if needed, but don't block
55 if (ctx.state === 'suspended') {
56 ctx.resume()
57 }
58
59 const now = ctx.currentTime
60
61 // Master output - keep it gentle
62 const master = ctx.createGain()
63 master.gain.value = 0.25
64 master.connect(ctx.destination)
65
66 // High-pass to remove speaker-popping low frequencies
67 const highPass = ctx.createBiquadFilter()
68 highPass.type = 'highpass'
69 highPass.frequency.value = 150
70 highPass.connect(master)
71
72 // Create multiple small crunch "grains" for texture
73 const grainCount = 5
74 for (let i = 0; i < grainCount; i++) {
75 const delay = i * 0.018 + Math.random() * 0.01
76 const grainTime = now + delay
77
78 // Each grain is a short filtered noise burst
79 const grainLength = 0.04 + Math.random() * 0.03
80 const bufferSize = Math.floor(ctx.sampleRate * grainLength)
81 const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate)
82 const noiseData = noiseBuffer.getChannelData(0)
83
84 // Softer noise - not full amplitude
85 for (let j = 0; j < bufferSize; j++) {
86 noiseData[j] = (Math.random() * 2 - 1) * 0.7
87 }
88
89 const grain = ctx.createBufferSource()
90 grain.buffer = noiseBuffer
91
92 // Bandpass for crunch character - varied per grain
93 const filter = ctx.createBiquadFilter()
94 filter.type = 'bandpass'
95 filter.frequency.value = 300 + Math.random() * 400
96 filter.Q.value = 2 + Math.random() * 2
97
98 // Gentle envelope - no sharp attacks
99 const env = ctx.createGain()
100 const peakGain = 0.3 + Math.random() * 0.2
101 env.gain.setValueAtTime(0, grainTime)
102 env.gain.linearRampToValueAtTime(peakGain, grainTime + 0.008) // Soft attack
103 env.gain.linearRampToValueAtTime(peakGain * 0.6, grainTime + 0.02)
104 env.gain.linearRampToValueAtTime(0, grainTime + grainLength) // Soft release
105
106 grain.connect(filter)
107 filter.connect(env)
108 env.connect(highPass)
109
110 grain.start(grainTime)
111 grain.stop(grainTime + grainLength)
112 }
113
114 // Soft muffled "body" of the bite - no harsh transients
115 const bodyLength = 0.12
116 const bodyBuffer = ctx.createBuffer(1, Math.floor(ctx.sampleRate * bodyLength), ctx.sampleRate)
117 const bodyData = bodyBuffer.getChannelData(0)
118 for (let i = 0; i < bodyData.length; i++) {
119 bodyData[i] = (Math.random() * 2 - 1) * 0.5
120 }
121
122 const body = ctx.createBufferSource()
123 body.buffer = bodyBuffer
124
125 // Heavy lowpass for muffled wet sound
126 const wetFilter = ctx.createBiquadFilter()
127 wetFilter.type = 'lowpass'
128 wetFilter.frequency.setValueAtTime(600, now)
129 wetFilter.frequency.linearRampToValueAtTime(200, now + 0.1)
130 wetFilter.Q.value = 1
131
132 const bodyEnv = ctx.createGain()
133 bodyEnv.gain.setValueAtTime(0, now)
134 bodyEnv.gain.linearRampToValueAtTime(0.25, now + 0.02) // Gentle attack
135 bodyEnv.gain.linearRampToValueAtTime(0.15, now + 0.05)
136 bodyEnv.gain.linearRampToValueAtTime(0, now + bodyLength)
137
138 body.connect(wetFilter)
139 wetFilter.connect(bodyEnv)
140 bodyEnv.connect(highPass)
141
142 body.start(now)
143 body.stop(now + bodyLength)
144
145 // Tonal body - soft pitched "chomp" character
146 // Primary tone - warm mid frequency
147 const tone1 = ctx.createOscillator()
148 tone1.type = 'triangle'
149 tone1.frequency.setValueAtTime(280, now)
150 tone1.frequency.linearRampToValueAtTime(180, now + 0.08)
151
152 const tone1Env = ctx.createGain()
153 tone1Env.gain.setValueAtTime(0, now)
154 tone1Env.gain.linearRampToValueAtTime(0.12, now + 0.015) // Soft attack
155 tone1Env.gain.linearRampToValueAtTime(0.06, now + 0.05)
156 tone1Env.gain.linearRampToValueAtTime(0, now + 0.1)
157
158 tone1.connect(tone1Env)
159 tone1Env.connect(highPass)
160 tone1.start(now)
161 tone1.stop(now + 0.12)
162
163 // Secondary harmonic - adds richness
164 const tone2 = ctx.createOscillator()
165 tone2.type = 'sine'
166 tone2.frequency.setValueAtTime(420, now)
167 tone2.frequency.linearRampToValueAtTime(300, now + 0.06)
168
169 const tone2Env = ctx.createGain()
170 tone2Env.gain.setValueAtTime(0, now)
171 tone2Env.gain.linearRampToValueAtTime(0.06, now + 0.01)
172 tone2Env.gain.linearRampToValueAtTime(0, now + 0.07)
173
174 tone2.connect(tone2Env)
175 tone2Env.connect(highPass)
176 tone2.start(now)
177 tone2.stop(now + 0.1)
178
179 // Soft low "gulp" undertone - filtered to be safe
180 const gulp = ctx.createOscillator()
181 gulp.type = 'sine'
182 gulp.frequency.setValueAtTime(200, now + 0.02)
183 gulp.frequency.linearRampToValueAtTime(160, now + 0.1)
184
185 const gulpEnv = ctx.createGain()
186 gulpEnv.gain.setValueAtTime(0, now)
187 gulpEnv.gain.linearRampToValueAtTime(0, now + 0.02) // Delayed start
188 gulpEnv.gain.linearRampToValueAtTime(0.08, now + 0.04)
189 gulpEnv.gain.linearRampToValueAtTime(0, now + 0.12)
190
191 gulp.connect(gulpEnv)
192 gulpEnv.connect(highPass)
193 gulp.start(now)
194 gulp.stop(now + 0.15)
195 }