import { gameState, isSettingOn, } from "./game"; export const sounds = { wallBeep: (pan: number) => { if (!isSettingOn("sound")) return; createSingleBounceSound(800, pixelsToPan(pan)); }, comboIncreaseMaybe: (combo: number, x: number, volume: number) => { if (!isSettingOn("sound")) return; let delta = 0; if (!isNaN(lastComboPlayed)) { if (lastComboPlayed < combo) delta = 1; if (lastComboPlayed > combo) delta = -1; } playShepard(delta, pixelsToPan(x), volume); lastComboPlayed = combo; }, comboDecrease() { if (!isSettingOn("sound")) return; playShepard(-1, 0.5, 0.5); }, coinBounce: (pan: number, volume: number) => { if (!isSettingOn("sound")) return; createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle"); }, explode: (pan: number) => { if (!isSettingOn("sound")) return; createExplosionSound(pixelsToPan(pan)); }, lifeLost(pan: number) { if (!isSettingOn("sound")) return; createShatteredGlassSound(pixelsToPan(pan)); }, coinCatch(pan: number) { if (!isSettingOn("sound")) return; createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle"); }, }; // How to play the code on the leftconst context = new window.AudioContext(); let audioContext: AudioContext, audioRecordingTrack: MediaStreamAudioDestinationNode; export function getAudioContext() { if (!audioContext) { if (!isSettingOn("sound")) return null; audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioRecordingTrack = audioContext.createMediaStreamDestination(); } return audioContext; } export function getAudioRecordingTrack() { getAudioContext(); return audioRecordingTrack; } function createSingleBounceSound( baseFreq = 800, pan = 0.5, volume = 1, duration = 0.1, type: OscillatorType = "sine", ) { const context = getAudioContext(); if (!context) return; const oscillator = createOscillator(context, baseFreq, type); // Create a gain node to control the volume const gainNode = context.createGain(); oscillator.connect(gainNode); // Create a stereo panner node for left-right panning const panner = context.createStereoPanner(); panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); gainNode.connect(panner); panner.connect(context.destination); panner.connect(audioRecordingTrack); // Set up the gain envelope to simulate the impact and quick decay gainNode.gain.setValueAtTime(0.8 * volume, context.currentTime); // Initial impact gainNode.gain.exponentialRampToValueAtTime( 0.001, context.currentTime + duration, ); // Quick decay // Start the oscillator oscillator.start(context.currentTime); // Stop the oscillator after the decay oscillator.stop(context.currentTime + duration); } let noiseBuffer: AudioBuffer; function getNoiseBuffer(context: AudioContext) { if (!noiseBuffer) { const bufferSize = context.sampleRate * 2; // 2 seconds noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate); const output = noiseBuffer.getChannelData(0); // Fill the buffer with random noise for (let i = 0; i < bufferSize; i++) { output[i] = Math.random() * 2 - 1; } } return noiseBuffer; } function createExplosionSound(pan = 0.5) { const context = getAudioContext(); if (!context) return; // Create an audio buffer // Create a noise source const noiseSource = context.createBufferSource(); noiseSource.buffer = getNoiseBuffer(context); // Create a gain node to control the volume const gainNode = context.createGain(); noiseSource.connect(gainNode); // Create a filter to shape the explosion sound const filter = context.createBiquadFilter(); filter.type = "lowpass"; filter.frequency.setValueAtTime(1000, context.currentTime); // Set the initial frequency gainNode.connect(filter); // Create a stereo panner node for left-right panning const panner = context.createStereoPanner(); panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); // pan 0 to 1 maps to -1 to 1 // Connect filter to panner and then to the destination (speakers) filter.connect(panner); panner.connect(context.destination); panner.connect(audioRecordingTrack); // Ramp down the gain to simulate the explosion's fade-out gainNode.gain.setValueAtTime(1, context.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 1); // Lower the filter frequency over time to create the "explosive" effect filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1); // Start the noise source noiseSource.start(context.currentTime); // Stop the noise source after the sound has played noiseSource.stop(context.currentTime + 1); } function pixelsToPan(pan: number) { return Math.max( 0, Math.min(1, (pan - gameState.offsetXRoundedDown) / gameState.gameZoneWidthRoundedUp), ); } let lastComboPlayed = NaN, shepard = 6; function playShepard(delta: number, pan: number, volume: number) { const shepardMax = 11, factor = 1.05945594920268, baseNote = 392; shepard += delta; if (shepard > shepardMax) shepard = 0; if (shepard < 0) shepard = shepardMax; const play = (note: number) => { const freq = baseNote * Math.pow(factor, note); const diff = Math.abs(note - shepardMax * 0.5); const maxDistanceToIdeal = 1.5 * shepardMax; const vol = Math.max(0, volume * (1 - diff / maxDistanceToIdeal)); createSingleBounceSound(freq, pan, vol); return freq.toFixed(2) + " at " + Math.floor(vol * 100) + "% diff " + diff; }; play(1 + shepardMax + shepard); play(shepard); play(-1 - shepardMax + shepard); } function createShatteredGlassSound(pan: number) { const context = getAudioContext(); if (!context) return; const oscillators = [ createOscillator(context, 3000, "square"), createOscillator(context, 4500, "square"), createOscillator(context, 6000, "square"), ]; const gainNode = context.createGain(); const noiseSource = context.createBufferSource(); noiseSource.buffer = getNoiseBuffer(context); oscillators.forEach((oscillator) => oscillator.connect(gainNode)); noiseSource.connect(gainNode); gainNode.gain.setValueAtTime(0.2, context.currentTime); oscillators.forEach((oscillator) => oscillator.start()); noiseSource.start(); oscillators.forEach((oscillator) => oscillator.stop(context.currentTime + 0.2), ); noiseSource.stop(context.currentTime + 0.2); gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2); // Create a stereo panner node for left-right panning const panner = context.createStereoPanner(); panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); gainNode.connect(panner); panner.connect(context.destination); panner.connect(audioRecordingTrack); gainNode.connect(panner); } // Helper function to create an oscillator with a specific frequency function createOscillator( context: AudioContext, frequency: number, type: OscillatorType, ) { const oscillator = context.createOscillator(); oscillator.type = type; oscillator.frequency.setValueAtTime(frequency, context.currentTime); return oscillator; }