2025-03-16 17:45:29 +01:00
|
|
|
import { isOptionOn } from "./options";
|
2025-03-18 14:16:12 +01:00
|
|
|
import { GameState } from "./types";
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-18 14:16:12 +01:00
|
|
|
let lastPlay = Date.now();
|
2025-03-17 19:47:16 +01:00
|
|
|
|
2025-03-18 14:16:12 +01:00
|
|
|
export function playPendingSounds(gameState: GameState) {
|
|
|
|
if (lastPlay > Date.now() - 60) {
|
|
|
|
return;
|
2025-03-17 19:47:16 +01:00
|
|
|
}
|
2025-03-18 14:16:12 +01:00
|
|
|
lastPlay = Date.now();
|
|
|
|
for (let key in gameState.aboutToPlaySound) {
|
|
|
|
const soundName = key as keyof GameState["aboutToPlaySound"];
|
|
|
|
const ex = gameState.aboutToPlaySound[soundName] as {
|
|
|
|
vol: number;
|
|
|
|
x: number;
|
|
|
|
};
|
|
|
|
if (ex.vol) {
|
|
|
|
sounds[soundName](
|
|
|
|
Math.min(2, ex.vol),
|
|
|
|
pixelsToPan(gameState, ex.x),
|
|
|
|
gameState.combo,
|
|
|
|
);
|
|
|
|
ex.vol = 0;
|
2025-03-17 19:47:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-03-12 15:15:30 +01:00
|
|
|
export const sounds = {
|
2025-03-18 14:16:12 +01:00
|
|
|
wallBeep: (vol: number, pan: number, combo: number) => {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-17 19:47:16 +01:00
|
|
|
createSingleBounceSound(800, pan, vol);
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
|
|
|
|
2025-03-18 14:16:12 +01:00
|
|
|
comboIncreaseMaybe: (volume: number, pan: number, combo: number) => {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-15 10:34:01 +01:00
|
|
|
let delta = 0;
|
|
|
|
if (!isNaN(lastComboPlayed)) {
|
|
|
|
if (lastComboPlayed < combo) delta = 1;
|
|
|
|
if (lastComboPlayed > combo) delta = -1;
|
2025-03-13 08:53:02 +01:00
|
|
|
}
|
2025-03-17 19:47:16 +01:00
|
|
|
playShepard(delta, pan, volume);
|
2025-03-15 10:34:01 +01:00
|
|
|
lastComboPlayed = combo;
|
|
|
|
},
|
|
|
|
|
2025-03-18 14:16:12 +01:00
|
|
|
comboDecrease(volume: number, pan: number, combo: number) {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-17 19:47:16 +01:00
|
|
|
playShepard(-1, pan, volume);
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
2025-03-18 14:16:12 +01:00
|
|
|
coinBounce: (volume: number, pan: number, combo: number) => {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-17 19:47:16 +01:00
|
|
|
createSingleBounceSound(1200, pan, volume, 0.1, "triangle");
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
2025-03-18 14:16:12 +01:00
|
|
|
explode: (volume: number, pan: number, combo: number) => {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-17 19:47:16 +01:00
|
|
|
createExplosionSound(pan);
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
2025-03-18 14:16:12 +01:00
|
|
|
lifeLost(volume: number, pan: number, combo: number) {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-17 19:47:16 +01:00
|
|
|
createShatteredGlassSound(pan);
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
|
|
|
|
2025-03-18 14:16:12 +01:00
|
|
|
coinCatch(volume: number, pan: number, combo: number) {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return;
|
2025-03-18 14:16:12 +01:00
|
|
|
createSingleBounceSound(900, pan, volume, 0.1, "triangle");
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
2025-03-18 14:16:12 +01:00
|
|
|
colorChange(volume: number, pan: number, combo: number) {
|
2025-03-17 19:47:16 +01:00
|
|
|
createSingleBounceSound(400, pan, volume, 0.5, "sine");
|
|
|
|
createSingleBounceSound(800, pan, volume * 0.5, 0.2, "square");
|
2025-03-15 10:34:01 +01:00
|
|
|
},
|
2025-03-12 15:15:30 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// How to play the code on the leftconst context = new window.AudioContext();
|
2025-03-13 08:53:02 +01:00
|
|
|
let audioContext: AudioContext,
|
2025-03-15 10:34:01 +01:00
|
|
|
audioRecordingTrack: MediaStreamAudioDestinationNode;
|
2025-03-12 15:15:30 +01:00
|
|
|
|
|
|
|
export function getAudioContext() {
|
2025-03-15 10:34:01 +01:00
|
|
|
if (!audioContext) {
|
2025-03-15 21:29:38 +01:00
|
|
|
if (!isOptionOn("sound")) return null;
|
2025-03-15 10:34:01 +01:00
|
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
audioRecordingTrack = audioContext.createMediaStreamDestination();
|
|
|
|
}
|
|
|
|
return audioContext;
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function getAudioRecordingTrack() {
|
2025-03-15 10:34:01 +01:00
|
|
|
getAudioContext();
|
|
|
|
return audioRecordingTrack;
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function createSingleBounceSound(
|
2025-03-15 10:34:01 +01:00
|
|
|
baseFreq = 800,
|
|
|
|
pan = 0.5,
|
|
|
|
volume = 1,
|
|
|
|
duration = 0.1,
|
|
|
|
type: OscillatorType = "sine",
|
2025-03-12 15:15:30 +01:00
|
|
|
) {
|
2025-03-15 10:34:01 +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);
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let noiseBuffer: AudioBuffer;
|
|
|
|
|
2025-03-13 08:53:02 +01:00
|
|
|
function getNoiseBuffer(context: AudioContext) {
|
2025-03-15 10:34:01 +01:00
|
|
|
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;
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
2025-03-15 10:34:01 +01:00
|
|
|
}
|
|
|
|
return noiseBuffer;
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function createExplosionSound(pan = 0.5) {
|
2025-03-15 10:34:01 +01:00
|
|
|
const context = getAudioContext();
|
|
|
|
if (!context) return;
|
|
|
|
// Create an audio buffer
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +01:00
|
|
|
// Create a noise source
|
|
|
|
const noiseSource = context.createBufferSource();
|
|
|
|
noiseSource.buffer = getNoiseBuffer(context);
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +01:00
|
|
|
// Create a gain node to control the volume
|
|
|
|
const gainNode = context.createGain();
|
|
|
|
noiseSource.connect(gainNode);
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +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-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +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-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +01:00
|
|
|
// Connect filter to panner and then to the destination (speakers)
|
|
|
|
filter.connect(panner);
|
|
|
|
panner.connect(context.destination);
|
|
|
|
panner.connect(audioRecordingTrack);
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +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-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +01:00
|
|
|
// Lower the filter frequency over time to create the "explosive" effect
|
|
|
|
filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1);
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +01:00
|
|
|
// Start the noise source
|
|
|
|
noiseSource.start(context.currentTime);
|
2025-03-12 15:15:30 +01:00
|
|
|
|
2025-03-15 10:34:01 +01:00
|
|
|
// Stop the noise source after the sound has played
|
|
|
|
noiseSource.stop(context.currentTime + 1);
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
2025-03-18 14:16:12 +01:00
|
|
|
function pixelsToPan(gameState: GameState, pan: number) {
|
2025-03-15 10:34:01 +01:00
|
|
|
return Math.max(
|
|
|
|
0,
|
|
|
|
Math.min(
|
|
|
|
1,
|
|
|
|
(pan - gameState.offsetXRoundedDown) / gameState.gameZoneWidthRoundedUp,
|
|
|
|
),
|
|
|
|
);
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let lastComboPlayed = NaN,
|
2025-03-15 10:34:01 +01:00
|
|
|
shepard = 6;
|
2025-03-12 15:15:30 +01:00
|
|
|
|
|
|
|
function playShepard(delta: number, pan: number, volume: number) {
|
2025-03-15 10:34:01 +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-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
2025-03-13 08:53:02 +01:00
|
|
|
function createShatteredGlassSound(pan: number) {
|
2025-03-15 10:34:01 +01:00
|
|
|
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);
|
2025-03-12 15:15:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Helper function to create an oscillator with a specific frequency
|
2025-03-13 08:53:02 +01:00
|
|
|
function createOscillator(
|
2025-03-15 10:34:01 +01:00
|
|
|
context: AudioContext,
|
|
|
|
frequency: number,
|
|
|
|
type: OscillatorType,
|
2025-03-13 08:53:02 +01:00
|
|
|
) {
|
2025-03-15 10:34:01 +01:00
|
|
|
const oscillator = context.createOscillator();
|
|
|
|
oscillator.type = type;
|
|
|
|
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
|
|
|
return oscillator;
|
2025-03-13 08:53:02 +01:00
|
|
|
}
|