{"version":3,"file":"SimpleVAD-qM6aS1YK.cjs","sources":["../../lib/vowel/managers/SimpleVAD.ts"],"sourcesContent":["/**\n * SimpleVAD - Energy-based Voice Activity Detection\n * \n * This implementation uses audio energy analysis to detect speech without requiring\n * machine learning models. It's fast, requires no downloads, but less accurate than Silero VAD.\n * \n * Algorithm:\n * - Analyzes audio samples in real-time using Web Audio API\n * - Calculates RMS (Root Mean Square) energy level\n * - Detects speech when energy exceeds threshold\n * - Uses redemption frames to avoid flickering (brief silence during speech)\n */\n\n/**\n * SimpleVAD configuration\n */\nexport interface SimpleVADConfig {\n  /** MediaStream from microphone */\n  stream: MediaStream;\n  \n  /** Energy threshold for speech detection (0-1, default: 0.15) */\n  energyThreshold?: number;\n  \n  /** Sample rate in Hz (default: 16000) */\n  sampleRate?: number;\n  \n  /** FFT size for frequency analysis (default: 2048) */\n  fftSize?: number;\n  \n  /** Analysis interval in ms (default: 100) */\n  analysisInterval?: number;\n  \n  /** Minimum duration of silence (in frames) before speech is considered ended (default: 8) */\n  redemptionFrames?: number;\n  \n  /** Called when speech starts */\n  onSpeechStart?: () => void;\n  \n  /** Called when speech ends */\n  onSpeechEnd?: () => void;\n}\n\n/**\n * SimpleVAD class\n * Energy-based voice activity detection using Web Audio API\n */\nexport class SimpleVAD {\n  private audioContext: AudioContext | null = null;\n  private analyser: AnalyserNode | null = null;\n  private microphone: MediaStreamAudioSourceNode | null = null;\n  private intervalId: number | null = null;\n  \n  private config: Required<Omit<SimpleVADConfig, 'stream' | 'onSpeechStart' | 'onSpeechEnd'>> & Pick<SimpleVADConfig, 'onSpeechStart' | 'onSpeechEnd'>;\n  private stream: MediaStream;\n  \n  private isSpeaking: boolean = false;\n  private silenceFrames: number = 0;\n  private isActive: boolean = false;\n\n  constructor(config: SimpleVADConfig) {\n    this.stream = config.stream;\n    this.config = {\n      energyThreshold: config.energyThreshold ?? 0.15,\n      sampleRate: config.sampleRate ?? 16000,\n      fftSize: config.fftSize ?? 2048,\n      analysisInterval: config.analysisInterval ?? 100,\n      redemptionFrames: config.redemptionFrames ?? 8,\n      onSpeechStart: config.onSpeechStart,\n      onSpeechEnd: config.onSpeechEnd,\n    };\n  }\n\n  /**\n   * Start VAD processing\n   */\n  async start(): Promise<void> {\n    if (this.isActive) {\n      console.warn(\"SimpleVAD already started\");\n      return;\n    }\n\n    try {\n      console.log(\"🎤 Initializing Simple VAD (energy-based)...\");\n\n      // Create audio context\n      this.audioContext = new AudioContext({ sampleRate: this.config.sampleRate });\n      \n      // Create analyser node\n      this.analyser = this.audioContext.createAnalyser();\n      this.analyser.fftSize = this.config.fftSize;\n      this.analyser.smoothingTimeConstant = 0.8;\n      \n      // Connect microphone to analyser\n      this.microphone = this.audioContext.createMediaStreamSource(this.stream);\n      this.microphone.connect(this.analyser);\n      \n      // Start analysis loop\n      this.isActive = true;\n      this.startAnalysis();\n      \n      console.log(\"✅ Simple VAD initialized\");\n    } catch (error) {\n      console.error(\"❌ Failed to initialize Simple VAD:\", error);\n      throw error;\n    }\n  }\n\n  /**\n   * Stop VAD processing\n   */\n  async stop(): Promise<void> {\n    if (!this.isActive) {\n      return;\n    }\n\n    console.log(\"🛑 Stopping Simple VAD...\");\n    \n    this.isActive = false;\n    \n    // Stop analysis loop\n    if (this.intervalId !== null) {\n      clearInterval(this.intervalId);\n      this.intervalId = null;\n    }\n    \n    // Disconnect audio nodes\n    if (this.microphone) {\n      this.microphone.disconnect();\n      this.microphone = null;\n    }\n    \n    if (this.analyser) {\n      this.analyser.disconnect();\n      this.analyser = null;\n    }\n    \n    // Close audio context\n    if (this.audioContext) {\n      await this.audioContext.close();\n      this.audioContext = null;\n    }\n    \n    // Reset state\n    this.isSpeaking = false;\n    this.silenceFrames = 0;\n    \n    console.log(\"✅ Simple VAD stopped\");\n  }\n\n  /**\n   * Pause VAD processing\n   */\n  pause(): void {\n    if (this.intervalId !== null) {\n      clearInterval(this.intervalId);\n      this.intervalId = null;\n      console.log(\"⏸️ Simple VAD paused\");\n    }\n  }\n\n  /**\n   * Resume VAD processing\n   */\n  resume(): void {\n    if (this.isActive && this.intervalId === null) {\n      this.startAnalysis();\n      console.log(\"▶️ Simple VAD resumed\");\n    }\n  }\n\n  /**\n   * Check if currently speaking\n   */\n  isSpeakingNow(): boolean {\n    return this.isSpeaking;\n  }\n\n  /**\n   * Start the audio analysis loop\n   */\n  private startAnalysis(): void {\n    this.intervalId = window.setInterval(() => {\n      this.analyzeFrame();\n    }, this.config.analysisInterval);\n  }\n\n  /**\n   * Analyze a single audio frame\n   */\n  private analyzeFrame(): void {\n    if (!this.analyser || !this.isActive) {\n      return;\n    }\n\n    // Get frequency data\n    const bufferLength = this.analyser.frequencyBinCount;\n    const dataArray = new Uint8Array(bufferLength);\n    this.analyser.getByteFrequencyData(dataArray);\n\n    // Calculate RMS (Root Mean Square) energy\n    const energy = this.calculateRMS(dataArray);\n    \n    // Normalize energy to 0-1 range\n    const normalizedEnergy = energy / 255;\n\n    // Check if energy exceeds threshold\n    const hasSpeech = normalizedEnergy > this.config.energyThreshold;\n\n    if (hasSpeech) {\n      // Reset silence counter\n      this.silenceFrames = 0;\n      \n      // Trigger speech start if not already speaking\n      if (!this.isSpeaking) {\n        this.isSpeaking = true;\n        console.log(\"🗣️ User started speaking (Simple VAD)\");\n        this.config.onSpeechStart?.();\n      }\n    } else {\n      // Increment silence counter\n      if (this.isSpeaking) {\n        this.silenceFrames++;\n        \n        // Check if silence duration exceeds redemption threshold\n        if (this.silenceFrames >= this.config.redemptionFrames) {\n          this.isSpeaking = false;\n          this.silenceFrames = 0;\n          console.log(\"🔇 User stopped speaking (Simple VAD)\");\n          this.config.onSpeechEnd?.();\n        }\n      }\n    }\n  }\n\n  /**\n   * Calculate RMS (Root Mean Square) energy from frequency data\n   * \n   * @param dataArray - Frequency data array\n   * @returns RMS energy value\n   */\n  private calculateRMS(dataArray: Uint8Array): number {\n    let sum = 0;\n    \n    // Calculate sum of squares\n    for (let i = 0; i < dataArray.length; i++) {\n      sum += dataArray[i] * dataArray[i];\n    }\n    \n    // Calculate mean and return square root\n    const mean = sum / dataArray.length;\n    return Math.sqrt(mean);\n  }\n}\n\n"],"names":[],"mappings":"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"}