All files / src/lib audio.ts

0% Statements 0/46
0% Branches 0/16
0% Functions 0/6
0% Lines 0/46

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)
}
 
← Back to Dashboard