UNPKG

mic-ts

Version:

A simple stream wrapper for arecord (Linux (including Raspbian)) and sox (Mac/Windows). Returns a Passthrough stream object so that stream control like pause(), resume(), pipe(), etc. are all available.

323 lines (266 loc) 9.12 kB
import { describe, it, expect, vi, beforeEach } from "vitest"; import { MicImpl } from "./Mic.js"; import { IsSilence } from "./IsSilence.js"; describe("MicImpl", () => { let mic: MicImpl; beforeEach(() => { mic = new MicImpl(); }); it("should emit startComplete when start is called", () => { const audioStream = mic.getAudioStream(); const startComplete = vi.fn(); audioStream.on("startComplete", startComplete); mic.start(); expect(startComplete).toHaveBeenCalledOnce(); }); it("should throw an error if start is called multiple times", () => { const debugMic = new MicImpl({ debug: true }); debugMic.start(); expect(() => debugMic.start()).toThrowError( "Duplicate calls to start(): Microphone already started!", ); }); it("should emit stopComplete when stop is called", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); const stopComplete = vi.fn(); audioStream.on("stopComplete", stopComplete); mic.start(); mic.stop(); expect(stopComplete).toHaveBeenCalledOnce(); }); it("should emit pauseComplete when pause is called", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); const pauseComplete = vi.fn(); audioStream.on("pauseComplete", pauseComplete); mic.start(); mic.pause(); expect(pauseComplete).toHaveBeenCalledOnce(); }); it("should emit resumeComplete when resume is called", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); const resumeComplete = vi.fn(); audioStream.on("resumeComplete", resumeComplete); mic.start(); mic.pause(); mic.resume(); expect(resumeComplete).toHaveBeenCalledOnce(); }); it("should return the audioStream instance", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); expect(audioStream).toBeInstanceOf(IsSilence); }); it("should emit silence and sound events on audioStream", () => { const mic = new MicImpl({ exitOnSilence: 1 }); const audioStream = mic.getAudioStream(); const silenceEvent = vi.fn(); const soundEvent = vi.fn(); audioStream.on("silence", silenceEvent); audioStream.on("sound", soundEvent); mic.start(); audioStream.write(Buffer.from([0, 0])); // Silent frame audioStream.write(Buffer.from([0, 0])); // Silent frame audioStream.write(Buffer.from([10, 10])); // Sound frame expect(silenceEvent).toHaveBeenCalledOnce(); expect(soundEvent).toHaveBeenCalledOnce(); }); it("should handle stop without starting", () => { const mic = new MicImpl(); expect(() => mic.stop()).not.toThrow(); }); it("should handle pause and resume without starting", () => { const mic = new MicImpl(); expect(() => mic.pause()).not.toThrow(); expect(() => mic.resume()).not.toThrow(); }); describe("Constructor options", () => { it("should use default options when none provided", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); expect(audioStream.getNumSilenceFramesExitThresh()).toBe(0); }); it("should use custom exitOnSilence option", () => { const mic = new MicImpl({ exitOnSilence: 5 }); const audioStream = mic.getAudioStream(); expect(audioStream.getNumSilenceFramesExitThresh()).toBe(5); }); it("should use debug option", () => { const mic = new MicImpl({ debug: true }); expect(() => mic.start()).not.toThrow(); }); it("should use custom audio format options", () => { const mic = new MicImpl({ bitwidth: "24", endian: "big", channels: "2", rate: "44100", encoding: "unsigned-integer", fileType: "wav", }); expect(() => mic.start()).not.toThrow(); }); }); describe("Audio process error handling", () => { it("should handle audio process error with ENOENT code", () => { const mic = new MicImpl({ debug: false }); const audioStream = mic.getAudioStream(); const errorSpy = vi.fn(); audioStream.on("error", errorSpy); mic.start(); // Simulate error event (this would normally come from the actual process) const audioProcess = ( mic as unknown as { audioProcess: { emit: ( event: string, error: { code: string; message: string }, ) => void; } | null; } ).audioProcess; if (audioProcess) { audioProcess.emit("error", { code: "ENOENT", message: "Command not found", }); } // Note: In real scenario, this would test the error handling logic // For unit tests, we verify the method exists and doesn't throw expect(errorSpy).toBeDefined(); }); it("should handle audio process error with other codes", () => { const mic = new MicImpl({ debug: false }); const audioStream = mic.getAudioStream(); const errorSpy = vi.fn(); audioStream.on("error", errorSpy); mic.start(); // Simulate error event const audioProcess = ( mic as unknown as { audioProcess: { emit: ( event: string, error: { code: string; message: string }, ) => void; } | null; } ).audioProcess; if (audioProcess) { audioProcess.emit("error", { code: "EACCES", message: "Permission denied", }); } expect(errorSpy).toBeDefined(); }); }); describe("Audio process exit handling", () => { it("should emit audioProcessExitComplete on normal exit", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); const exitSpy = vi.fn(); audioStream.on("audioProcessExitComplete", exitSpy); mic.start(); // Simulate process exit const audioProcess = ( mic as unknown as { audioProcess: { emit: ( event: string, code: number | null, signal: string | null, ) => void; } | null; } ).audioProcess; if (audioProcess) { audioProcess.emit("exit", 0, null); } expect(exitSpy).toHaveBeenCalled(); }); it("should not emit audioProcessExitComplete when killed by signal", () => { const mic = new MicImpl(); const audioStream = mic.getAudioStream(); const exitSpy = vi.fn(); audioStream.on("audioProcessExitComplete", exitSpy); mic.start(); // Simulate process killed by signal const audioProcess = ( mic as unknown as { audioProcess: { emit: ( event: string, code: number | null, signal: string | null, ) => void; } | null; } ).audioProcess; if (audioProcess) { audioProcess.emit("exit", null, "SIGTERM"); } expect(exitSpy).not.toHaveBeenCalled(); }); }); describe("Info stream handling", () => { it("should handle info stream data in debug mode", () => { const mic = new MicImpl({ debug: true }); const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); mic.start(); // Simulate info stream data const infoStream = ( mic as unknown as { infoStream: { emit: (event: string, data: Buffer) => void }; } ).infoStream; infoStream.emit("data", Buffer.from("test info")); expect(consoleSpy).toHaveBeenCalledWith("Received Info: test info"); consoleSpy.mockRestore(); }); it("should handle info stream error in debug mode", () => { const mic = new MicImpl({ debug: true }); const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); mic.start(); // Simulate info stream error const infoStream = ( mic as unknown as { infoStream: { emit: (event: string, error: Error) => void }; } ).infoStream; infoStream.emit("error", new Error("test error")); expect(consoleSpy).toHaveBeenCalledWith( "Error in Info Stream: Error: test error", ); consoleSpy.mockRestore(); }); }); describe("Edge cases", () => { it("should handle multiple stop calls", () => { const mic = new MicImpl(); mic.start(); mic.stop(); expect(() => mic.stop()).not.toThrow(); }); it("should handle multiple pause calls", () => { const mic = new MicImpl(); mic.start(); mic.pause(); expect(() => mic.pause()).not.toThrow(); }); it("should handle multiple resume calls", () => { const mic = new MicImpl(); mic.start(); mic.pause(); mic.resume(); expect(() => mic.resume()).not.toThrow(); }); it("should handle resume without pause", () => { const mic = new MicImpl(); mic.start(); expect(() => mic.resume()).not.toThrow(); }); }); });