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