@@ -19,89 +19,87 @@ export function playMonch() { |
| 19 | ctx.resume() | 19 | ctx.resume() |
| 20 | } | 20 | } |
| 21 | | 21 | |
| 22 | - // Create noise buffer for the crunch texture | 22 | + // Master output - keep it gentle |
| 23 | - const noiseLength = 0.15 | 23 | + const master = ctx.createGain() |
| 24 | - const bufferSize = ctx.sampleRate * noiseLength | 24 | + master.gain.value = 0.25 |
| 25 | - const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate) | 25 | + master.connect(ctx.destination) |
| 26 | - const noiseData = noiseBuffer.getChannelData(0) | 26 | + |
| 27 | - | 27 | + // High-pass to remove speaker-popping low frequencies |
| 28 | - // Fill with noise | 28 | + const highPass = ctx.createBiquadFilter() |
| 29 | - for (let i = 0; i < bufferSize; i++) { | 29 | + highPass.type = 'highpass' |
| 30 | - noiseData[i] = Math.random() * 2 - 1 | 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) |
| 31 | } | 73 | } |
| 32 | | 74 | |
| 33 | - // Noise source | 75 | + // Soft muffled "body" of the bite - no harsh transients |
| 34 | - const noise = ctx.createBufferSource() | 76 | + const bodyLength = 0.12 |
| 35 | - noise.buffer = noiseBuffer | 77 | + const bodyBuffer = ctx.createBuffer(1, Math.floor(ctx.sampleRate * bodyLength), ctx.sampleRate) |
| 36 | - | 78 | + const bodyData = bodyBuffer.getChannelData(0) |
| 37 | - // Low-pass filter for the "damp" wet quality | 79 | + for (let i = 0; i < bodyData.length; i++) { |
| 38 | - const dampFilter = ctx.createBiquadFilter() | 80 | + bodyData[i] = (Math.random() * 2 - 1) * 0.5 |
| 39 | - dampFilter.type = 'lowpass' | 81 | + } |
| 40 | - dampFilter.frequency.setValueAtTime(800, now) | | |
| 41 | - dampFilter.frequency.exponentialRampToValueAtTime(300, now + 0.08) | | |
| 42 | - dampFilter.Q.value = 2 | | |
| 43 | - | | |
| 44 | - // Bandpass for crunch character | | |
| 45 | - const crunchFilter = ctx.createBiquadFilter() | | |
| 46 | - crunchFilter.type = 'bandpass' | | |
| 47 | - crunchFilter.frequency.value = 400 | | |
| 48 | - crunchFilter.Q.value = 1.5 | | |
| 49 | - | | |
| 50 | - // Envelope for the noise burst | | |
| 51 | - const noiseGain = ctx.createGain() | | |
| 52 | - noiseGain.gain.setValueAtTime(0, now) | | |
| 53 | - noiseGain.gain.linearRampToValueAtTime(0.4, now + 0.01) // Quick attack | | |
| 54 | - noiseGain.gain.exponentialRampToValueAtTime(0.15, now + 0.04) // Initial drop | | |
| 55 | - noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.12) // Tail off | | |
| 56 | - | | |
| 57 | - // Low thump for the bite impact | | |
| 58 | - const thump = ctx.createOscillator() | | |
| 59 | - thump.type = 'sine' | | |
| 60 | - thump.frequency.setValueAtTime(120, now) | | |
| 61 | - thump.frequency.exponentialRampToValueAtTime(50, now + 0.06) | | |
| 62 | - | | |
| 63 | - const thumpGain = ctx.createGain() | | |
| 64 | - thumpGain.gain.setValueAtTime(0, now) | | |
| 65 | - thumpGain.gain.linearRampToValueAtTime(0.3, now + 0.005) | | |
| 66 | - thumpGain.gain.exponentialRampToValueAtTime(0.01, now + 0.08) | | |
| 67 | - | | |
| 68 | - // Secondary squelch - adds wetness | | |
| 69 | - const squelch = ctx.createOscillator() | | |
| 70 | - squelch.type = 'triangle' | | |
| 71 | - squelch.frequency.setValueAtTime(200, now) | | |
| 72 | - squelch.frequency.exponentialRampToValueAtTime(80, now + 0.05) | | |
| 73 | - | | |
| 74 | - const squelchGain = ctx.createGain() | | |
| 75 | - squelchGain.gain.setValueAtTime(0, now + 0.01) | | |
| 76 | - squelchGain.gain.linearRampToValueAtTime(0.15, now + 0.02) | | |
| 77 | - squelchGain.gain.exponentialRampToValueAtTime(0.01, now + 0.07) | | |
| 78 | - | | |
| 79 | - // Master output with slight compression feel | | |
| 80 | - const master = ctx.createGain() | | |
| 81 | - master.gain.value = 0.6 | | |
| 82 | | 82 | |
| 83 | - // Connect noise chain | 83 | + const body = ctx.createBufferSource() |
| 84 | - noise.connect(dampFilter) | 84 | + body.buffer = bodyBuffer |
| 85 | - dampFilter.connect(crunchFilter) | | |
| 86 | - crunchFilter.connect(noiseGain) | | |
| 87 | - noiseGain.connect(master) | | |
| 88 | | 85 | |
| 89 | - // Connect thump | 86 | + // Heavy lowpass for muffled wet sound |
| 90 | - thump.connect(thumpGain) | 87 | + const wetFilter = ctx.createBiquadFilter() |
| 91 | - thumpGain.connect(master) | 88 | + wetFilter.type = 'lowpass' |
| | 89 | + wetFilter.frequency.setValueAtTime(600, now) |
| | 90 | + wetFilter.frequency.linearRampToValueAtTime(200, now + 0.1) |
| | 91 | + wetFilter.Q.value = 1 |
| 92 | | 92 | |
| 93 | - // Connect squelch | 93 | + const bodyEnv = ctx.createGain() |
| 94 | - squelch.connect(squelchGain) | 94 | + bodyEnv.gain.setValueAtTime(0, now) |
| 95 | - squelchGain.connect(master) | 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) |
| 96 | | 98 | |
| 97 | - // Output | 99 | + body.connect(wetFilter) |
| 98 | - master.connect(ctx.destination) | 100 | + wetFilter.connect(bodyEnv) |
| | 101 | + bodyEnv.connect(highPass) |
| 99 | | 102 | |
| 100 | - // Play | 103 | + body.start(now) |
| 101 | - noise.start(now) | 104 | + body.stop(now + bodyLength) |
| 102 | - noise.stop(now + noiseLength) | | |
| 103 | - thump.start(now) | | |
| 104 | - thump.stop(now + 0.1) | | |
| 105 | - squelch.start(now) | | |
| 106 | - squelch.stop(now + 0.08) | | |
| 107 | } | 105 | } |