Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | /** * Audio utilities for timer completion alerts using Web Audio API * Uses programmatic tone generation (no external audio files needed) */ // Singleton AudioContext - created lazily on first use let audioContext: AudioContext | null = null // Track if audio has been unlocked (iOS requires user interaction) let audioUnlocked = false /** * Get or create the AudioContext singleton * Uses webkitAudioContext fallback for older Safari */ function getAudioContext(): AudioContext | null { if (audioContext) { return audioContext } try { const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext if (AudioContextClass) { audioContext = new AudioContextClass() return audioContext } } catch { // Web Audio API not supported } return null } /** * Unlock audio playback on iOS/Safari * Must be called from a user interaction event (click, touch) * Plays a silent buffer to enable future audio playback */ export function unlockAudio(): void { if (audioUnlocked) { return } const ctx = getAudioContext() if (!ctx) { return } // Resume suspended context (Chrome autoplay policy) if (ctx.state === 'suspended') { ctx.resume().catch(() => { // Ignore resume errors }) } // Play a silent buffer to unlock iOS audio try { const buffer = ctx.createBuffer(1, 1, 22050) const source = ctx.createBufferSource() source.buffer = buffer source.connect(ctx.destination) source.start(0) audioUnlocked = true } catch { // Silent fail - audio may still work } } /** * Play a pleasant beep tone for timer completion * Generates a two-tone alert sound using Web Audio API */ export function playTimerAlert(): void { const ctx = getAudioContext() if (!ctx) { return } // Resume context if suspended if (ctx.state === 'suspended') { ctx.resume().catch(() => {}) } try { const now = ctx.currentTime // Create a pleasant two-tone beep pattern // First beep: higher pitch playTone(ctx, 880, now, 0.15) // A5 // Short pause // Second beep: same pitch playTone(ctx, 880, now + 0.2, 0.15) // A5 // Third beep: lower pitch (confirmation) playTone(ctx, 660, now + 0.45, 0.25) // E5 } catch { // Audio playback failed - silent fail } } /** * Play a single tone at the specified frequency */ function playTone( ctx: AudioContext, frequency: number, startTime: number, duration: number ): void { // Create oscillator for the tone const oscillator = ctx.createOscillator() oscillator.type = 'sine' oscillator.frequency.setValueAtTime(frequency, startTime) // Create gain node for volume envelope (prevents clicks) const gainNode = ctx.createGain() gainNode.gain.setValueAtTime(0, startTime) // Quick attack gainNode.gain.linearRampToValueAtTime(0.3, startTime + 0.01) // Sustain gainNode.gain.setValueAtTime(0.3, startTime + duration - 0.05) // Quick release (prevents click) gainNode.gain.linearRampToValueAtTime(0, startTime + duration) // Connect oscillator -> gain -> output oscillator.connect(gainNode) gainNode.connect(ctx.destination) // Schedule playback oscillator.start(startTime) oscillator.stop(startTime + duration) } |