133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
let audioContext;
|
|
let tickSoundBuffer; // For short tick
|
|
let passTurnSoundBuffer; // For 3s pass turn alert
|
|
let isMutedGlobally = false;
|
|
let continuousTickInterval = null; // For setInterval based continuous ticking
|
|
let passTurnSoundTimeout = null;
|
|
|
|
function getAudioContext() {
|
|
if (!audioContext && (window.AudioContext || window.webkitAudioContext)) {
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
return audioContext;
|
|
}
|
|
|
|
function createBeepBuffer(frequency = 440, duration = 0.1, type = 'sine') {
|
|
const ctx = getAudioContext();
|
|
if (!ctx) return null;
|
|
const sampleRate = ctx.sampleRate;
|
|
const numFrames = duration * sampleRate;
|
|
const buffer = ctx.createBuffer(1, numFrames, sampleRate);
|
|
const data = buffer.getChannelData(0);
|
|
const gain = 0.1; // Reduce gain to make beeps softer
|
|
|
|
for (let i = 0; i < numFrames; i++) {
|
|
// Simple fade out
|
|
const currentGain = gain * (1 - (i / numFrames));
|
|
if (type === 'square') {
|
|
data[i] = (Math.sin(2 * Math.PI * frequency * (i / sampleRate)) >= 0 ? 1 : -1) * currentGain;
|
|
} else { // sine
|
|
data[i] = Math.sin(2 * Math.PI * frequency * (i / sampleRate)) * currentGain;
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
async function initSounds() {
|
|
const ctx = getAudioContext();
|
|
if (!ctx) return;
|
|
// Tick sound (shorter, slightly different pitch)
|
|
if (!tickSoundBuffer) {
|
|
// Using a square wave for a more 'digital' tick, short duration
|
|
tickSoundBuffer = createBeepBuffer(1000, 0.03, 'square');
|
|
}
|
|
// Pass turn alert sound (3 beeps)
|
|
if (!passTurnSoundBuffer) {
|
|
passTurnSoundBuffer = createBeepBuffer(660, 0.08, 'sine');
|
|
}
|
|
}
|
|
initSounds();
|
|
|
|
function playSoundBuffer(buffer) {
|
|
if (isMutedGlobally || !buffer || !audioContext || audioContext.state === 'suspended') return;
|
|
const source = audioContext.createBufferSource();
|
|
source.buffer = buffer;
|
|
source.connect(audioContext.destination);
|
|
source.start();
|
|
}
|
|
|
|
export const AudioService = {
|
|
setMuted(muted) {
|
|
isMutedGlobally = muted;
|
|
if (muted) {
|
|
this.stopContinuousTick();
|
|
this.cancelPassTurnSound();
|
|
}
|
|
},
|
|
|
|
// This is the single, short tick sound for "All Timers Running" mode.
|
|
_playSingleTick() {
|
|
playSoundBuffer(tickSoundBuffer);
|
|
},
|
|
|
|
playPassTurnAlert() {
|
|
if (isMutedGlobally || !audioContext || audioContext.state === 'suspended') return;
|
|
this.cancelPassTurnSound();
|
|
|
|
let count = 0;
|
|
const playAndSchedule = () => {
|
|
if (count < 3 && !isMutedGlobally && audioContext.state !== 'suspended') {
|
|
playSoundBuffer(passTurnSoundBuffer);
|
|
count++;
|
|
passTurnSoundTimeout = setTimeout(playAndSchedule, 1000); // Beep every second for 3s
|
|
} else {
|
|
passTurnSoundTimeout = null;
|
|
}
|
|
};
|
|
playAndSchedule();
|
|
},
|
|
|
|
cancelPassTurnSound() {
|
|
if (passTurnSoundTimeout) {
|
|
clearTimeout(passTurnSoundTimeout);
|
|
passTurnSoundTimeout = null;
|
|
}
|
|
},
|
|
|
|
startContinuousTick() {
|
|
this.stopContinuousTick(); // Clear any existing interval
|
|
if (isMutedGlobally || !audioContext || audioContext.state === 'suspended') return;
|
|
|
|
// Play immediately once, then set interval
|
|
this._playSingleTick();
|
|
continuousTickInterval = setInterval(() => {
|
|
if (!isMutedGlobally && audioContext.state !== 'suspended') {
|
|
this._playSingleTick();
|
|
} else {
|
|
this.stopContinuousTick(); // Stop if muted or context suspended during interval
|
|
}
|
|
}, 1000); // Tick every second
|
|
},
|
|
|
|
stopContinuousTick() {
|
|
if (continuousTickInterval) {
|
|
clearInterval(continuousTickInterval);
|
|
continuousTickInterval = null;
|
|
}
|
|
// Ensure no rogue oscillators are playing.
|
|
// If an oscillator was ever used directly and not disconnected, it could persist.
|
|
// The current implementation relies on BufferSource which stops automatically.
|
|
},
|
|
|
|
resumeContext() {
|
|
const ctx = getAudioContext();
|
|
if (ctx && ctx.state === 'suspended') {
|
|
ctx.resume().then(() => {
|
|
console.log("AudioContext resumed successfully.");
|
|
initSounds(); // Re-initialize sounds if context was suspended for long
|
|
}).catch(e => console.error("Error resuming AudioContext:", e));
|
|
} else if (ctx && !tickSoundBuffer) { // If context was fine but sounds not loaded
|
|
initSounds();
|
|
}
|
|
}
|
|
}; |