breakout71/src/sounds.ts

234 lines
7.1 KiB
TypeScript
Raw Normal View History

2025-03-13 08:53:02 +01:00
import {
2025-03-14 11:59:49 +01:00
gameState,
2025-03-13 08:53:02 +01:00
isSettingOn,
} from "./game";
export const sounds = {
2025-03-13 08:53:02 +01:00
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();
2025-03-13 08:53:02 +01:00
let audioContext: AudioContext,
audioRecordingTrack: MediaStreamAudioDestinationNode;
export function getAudioContext() {
2025-03-13 08:53:02 +01:00
if (!audioContext) {
if (!isSettingOn("sound")) return null;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioRecordingTrack = audioContext.createMediaStreamDestination();
}
return audioContext;
}
export function getAudioRecordingTrack() {
2025-03-13 08:53:02 +01:00
getAudioContext();
return audioRecordingTrack;
}
function createSingleBounceSound(
2025-03-13 08:53:02 +01:00
baseFreq = 800,
pan = 0.5,
volume = 1,
duration = 0.1,
type: OscillatorType = "sine",
) {
2025-03-13 08:53:02 +01:00
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;
2025-03-13 08:53:02 +01:00
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);
2025-03-13 08:53:02 +01:00
// Fill the buffer with random noise
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
2025-03-13 08:53:02 +01:00
}
return noiseBuffer;
}
function createExplosionSound(pan = 0.5) {
2025-03-13 08:53:02 +01:00
const context = getAudioContext();
if (!context) return;
// Create an audio buffer
2025-03-13 08:53:02 +01:00
// Create a noise source
const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context);
2025-03-13 08:53:02 +01:00
// Create a gain node to control the volume
const gainNode = context.createGain();
noiseSource.connect(gainNode);
2025-03-13 08:53:02 +01:00
// 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);
2025-03-13 08:53:02 +01:00
// 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
2025-03-13 08:53:02 +01:00
// Connect filter to panner and then to the destination (speakers)
filter.connect(panner);
panner.connect(context.destination);
panner.connect(audioRecordingTrack);
2025-03-13 08:53:02 +01:00
// 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);
2025-03-13 08:53:02 +01:00
// Lower the filter frequency over time to create the "explosive" effect
filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1);
2025-03-13 08:53:02 +01:00
// Start the noise source
noiseSource.start(context.currentTime);
2025-03-13 08:53:02 +01:00
// Stop the noise source after the sound has played
noiseSource.stop(context.currentTime + 1);
}
function pixelsToPan(pan: number) {
2025-03-13 08:53:02 +01:00
return Math.max(
0,
2025-03-14 11:59:49 +01:00
Math.min(1, (pan - gameState.offsetXRoundedDown) / gameState.gameZoneWidthRoundedUp),
2025-03-13 08:53:02 +01:00
);
}
let lastComboPlayed = NaN,
2025-03-13 08:53:02 +01:00
shepard = 6;
function playShepard(delta: number, pan: number, volume: number) {
2025-03-13 08:53:02 +01:00
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);
}
2025-03-13 08:53:02 +01:00
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
2025-03-13 08:53:02 +01:00
function createOscillator(
context: AudioContext,
frequency: number,
type: OscillatorType,
) {
const oscillator = context.createOscillator();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
return oscillator;
}