All files AudioInputHandler.ts

75% Statements 39/52
58.33% Branches 7/12
100% Functions 6/6
75% Lines 39/52

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                      1x     5x   5x   5x   5x   5x                   5x 5x 5x               5x 1x 1x                       5x   4x         4x         4x   4x 4x 4x 4x     4x 4x     4x 1x   1x 1x   4x 4x 4x                     4x                     5x   1x   1x 1x   1x 1x   1x 1x 5x  
 
 
/**
 * 
 * AudioInputHandler is a microphone input handler that:
 * Captures audio from the user’s microphone.
 * Processes audio in chunks (Float32Array).
 * Sends those chunks to a callback function for further processing.
 * It also provides start/stop control and exposes the audio sample rate.
 */
 
export class AudioInputHandler {
 
  /** Media stream from users microphone */
  private stream: MediaStream | null = null;
  /** Used for processing the audio */
  private ctx: AudioContext | null = null;
  /** Used to buffer audio data*/
  private processor: ScriptProcessorNode | null = null;
  /** Flag that checks if startListening has already been called */
  public isListening: boolean = false;
  /** Callback function that receives each audio chunk captured from the microphone. */
  private onAudioChunk: (chunk: Float32Array) => void;
 
 
  /**
 * Creates a new AudioInputHandler.
 *
 * @param onAudioChunk - A callback function that is called whenever
 *                        an audio chunk is captured. Receives a Float32Array
 *                        containing the audio samples.
 */
  constructor(onAudioChunk: (chunk: Float32Array) => void) {
    this.onAudioChunk = onAudioChunk;
  }
 
 
  /**
 * Returns the sample rate of the audio context.
 *
 * @returns The sample rate in Hz, or `undefined` if the audio context is not initialized.
 */
  public getSampleRate() {
    return this.ctx?.sampleRate ?? 0;
  }
 
  /**
 * Starts capturing audio from the user's microphone.
 *
 * - Prompts the user for microphone permissions.
 * - Creates an AudioContext and a ScriptProcessorNode to process audio in chunks.
 * - Calls the `onAudioChunk` callback with a Float32Array for each audio buffer.
 * - Handles errors such as permission denial or missing microphone hardware.
 *
 * @returns A Promise that resolves when listening has started.
 */
  public async startListening(): Promise<void> {
    //bail if mic already running
    if (this.isListening) {
      console.log('already listening...');
    }
    
 
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        console.error("This device does not support microphone input.")
        return;
        }
 
    try {
      // this line asks for user perms and starts rec
      this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      this.ctx = new AudioContext();
      const source = this.ctx.createMediaStreamSource(this.stream);
      this.processor = this.ctx.createScriptProcessor(8192, 1, 1);
 
      //connect audio graph nodes together
      source.connect(this.processor);
      this.processor.connect(this.ctx.destination); // connecting to speaker even though we are not outputting audio because apparently it might crash if not?
 
      //onaudioprocess that auto exexcutes when buffer full, e is the event data itself
      this.processor.onaudioprocess = (e) => {
        const input = e.inputBuffer.getChannelData(0); // this returns a Float32Array
 
        this.onAudioChunk(new Float32Array(input));
      };
 
            this.isListening = true;
            console.log("Microphone is listening...")
        } catch (err: any) {
            //updating so we dont have generic message and we know why the catch is hitting
            if (err.name === "NotAllowedError"){
                console.error("You may have denied microphone permissions... please try again");
            } else if (err.name === "NotFoundError") {
                console.error("No microphone was found on this device");
            } else {
                console.error("Error accessing Mic: " + err);
            }
 
        }
    }
 
 
    /**
 * Stops capturing audio from the microphone and cleans up resources.
 *
 * - Disconnects the ScriptProcessorNode from the audio graph.
 * - Closes the AudioContext.
 * - Stops all tracks of the MediaStream.
 * - Updates the `isListening` flag to `false`.
 */
  public stopListening(): void {
    // if never started, bail immediately
    if (!this.isListening) return;
 
    this.processor?.disconnect();
    this.ctx?.close();
 
    this.stream?.getTracks().forEach((track) => track.stop());
    this.isListening = false;
 
    console.log('Stopped listening...');
  }
}