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(); } } };