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.

279 lines (224 loc) 9.17 kB
import { describe, it, expect, vi, beforeEach } from "vitest"; import { IsSilence } from "./IsSilence.js"; describe("IsSilence", () => { let isSilence: IsSilence; beforeEach(() => { isSilence = new IsSilence({ debug: false }); }); it("should initialize with zero consecutive silence count and zero exit threshold", () => { expect(isSilence.getConsecSilenceCount()).toBe(0); expect(isSilence.getNumSilenceFramesExitThresh()).toBe(0); }); it("should allow setting and getting numSilenceFramesExitThresh", () => { isSilence.setNumSilenceFramesExitThresh(5); expect(isSilence.getNumSilenceFramesExitThresh()).toBe(5); }); it("should increment and reset consecutive silence count", () => { const firstCount = isSilence.incrConsecSilenceCount(); expect(firstCount).toBe(1); expect(isSilence.getConsecSilenceCount()).toBe(1); isSilence.resetConsecSilenceCount(); expect(isSilence.getConsecSilenceCount()).toBe(0); }); }); describe("IsSilence transform behavior", () => { let isSilence: IsSilence; beforeEach(() => { isSilence = new IsSilence({ debug: false }); }); it("should push data through unchanged", async () => { const input = Buffer.from([0, 1, 2, 3]); const chunks: Buffer[] = []; isSilence.on("data", (chunk: Buffer) => { chunks.push(chunk); }); const finished = new Promise<void>((resolve) => { isSilence.on("end", resolve); }); isSilence.end(input); await finished; const output = Buffer.concat(chunks); expect(output).toEqual(input); }); it("should emit silence event after threshold frames of silence", async () => { isSilence.setNumSilenceFramesExitThresh(2); const silenceEvent = new Promise<void>((resolve) => { isSilence.on("silence", resolve); }); isSilence.write(Buffer.from([0, 0])); // first silent frame isSilence.write(Buffer.from([0, 0])); // second silent frame, should trigger await silenceEvent; }); it("should emit sound event when speech is detected after threshold", async () => { isSilence.setNumSilenceFramesExitThresh(1); // simulate previous silence exceeding threshold isSilence.incrConsecSilenceCount(); isSilence.incrConsecSilenceCount(); const soundEvent = new Promise<void>((resolve) => { isSilence.on("sound", resolve); }); // speech frame: next byte 10 => speechSample = 10*256 + 10 = 2570 > 2000 isSilence.write(Buffer.from([0, 10])); await soundEvent; }); it("should handle empty input buffer", async () => { const chunks: Buffer[] = []; isSilence.on("data", (chunk: Buffer) => { chunks.push(chunk); }); const finished = new Promise<void>((resolve) => { isSilence.on("end", resolve); }); isSilence.end(Buffer.from([])); await finished; expect(chunks.length).toBe(0); }); it("should handle exactly threshold number of silent frames", async () => { isSilence.setNumSilenceFramesExitThresh(2); const silenceEvent = new Promise<void>((resolve) => { isSilence.on("silence", resolve); }); isSilence.write(Buffer.from([0, 0])); // first silent frame isSilence.write(Buffer.from([0, 0])); // second silent frame, should trigger await silenceEvent; }); it("should not emit silence event for one less than threshold", async () => { isSilence.setNumSilenceFramesExitThresh(3); let silenceTriggered = false; isSilence.on("silence", () => { silenceTriggered = true; }); isSilence.write(Buffer.from([0, 0])); // first silent frame isSilence.write(Buffer.from([0, 0])); // second silent frame await new Promise((resolve) => { setTimeout(resolve, 50); }); // wait to ensure no event expect(silenceTriggered).toBe(false); }); describe("Debug mode behavior", () => { it("should log debug messages when debug is true", () => { const debugIsSilence = new IsSilence({ debug: true }); const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); debugIsSilence.setNumSilenceFramesExitThresh(1); debugIsSilence.write(Buffer.from([10, 10])); // speech frame expect(consoleSpy).toHaveBeenCalledWith("Found speech block"); consoleSpy.mockRestore(); }); it("should log silence detection in debug mode", () => { const debugIsSilence = new IsSilence({ debug: true }); const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); debugIsSilence.setNumSilenceFramesExitThresh(1); debugIsSilence.write(Buffer.from([0, 0])); // silent frame expect(consoleSpy).toHaveBeenCalledWith( "Found silence block: %d of %d", 1, 1, ); consoleSpy.mockRestore(); }); }); describe("Silence detection edge cases", () => { it("should handle single byte buffer (odd length)", async () => { isSilence.setNumSilenceFramesExitThresh(1); const chunks: Buffer[] = []; isSilence.on("data", (chunk: Buffer) => { chunks.push(chunk); }); // Single byte should be processed without error isSilence.write(Buffer.from([0])); isSilence.end(); await new Promise((resolve) => { isSilence.on("end", resolve); }); expect(chunks.length).toBe(1); }); it("should handle mixed silence and speech in same buffer", async () => { isSilence.setNumSilenceFramesExitThresh(1); // simulate previous silence exceeding threshold isSilence.incrConsecSilenceCount(); isSilence.incrConsecSilenceCount(); const silenceSpy = vi.fn(); const soundSpy = vi.fn(); isSilence.on("silence", silenceSpy); isSilence.on("sound", soundSpy); // Buffer with both silence and speech isSilence.write(Buffer.from([0, 0, 0, 10])); // silence then speech expect(soundSpy).toHaveBeenCalled(); expect(silenceSpy).not.toHaveBeenCalled(); }); it("should not reset silence count when speech is below threshold", async () => { isSilence.setNumSilenceFramesExitThresh(3); isSilence.incrConsecSilenceCount(); isSilence.incrConsecSilenceCount(); // count = 2 // Speech below threshold - but this will increment the count isSilence.write(Buffer.from([0, 5])); // speechSample = 5*256 + 5 = 1285 < 2000 expect(isSilence.getConsecSilenceCount()).toBe(3); // increments because it's silence }); it("should reset silence count when speech exceeds threshold", async () => { isSilence.setNumSilenceFramesExitThresh(3); isSilence.incrConsecSilenceCount(); isSilence.incrConsecSilenceCount(); // count = 2 // Speech above threshold isSilence.write(Buffer.from([0, 10])); // speechSample = 10*256 + 10 = 2570 > 2000 expect(isSilence.getConsecSilenceCount()).toBe(0); // should reset }); it("should handle negative speech samples", () => { isSilence.setNumSilenceFramesExitThresh(1); const soundSpy = vi.fn(); isSilence.on("sound", soundSpy); // Negative speech sample (above threshold in absolute value) isSilence.write(Buffer.from([255, 255])); // Should be treated as -1 in signed 16-bit // The actual behavior depends on the implementation expect(soundSpy).toBeDefined(); }); it("should process all frames in buffer", async () => { isSilence.setNumSilenceFramesExitThresh(1); const silenceSpy = vi.fn(); isSilence.on("silence", silenceSpy); // Multiple frames of silence isSilence.write(Buffer.from([0, 0, 0, 0, 0, 0])); // 3 frames of silence expect(silenceSpy).toHaveBeenCalled(); }); it("should not emit events when threshold is zero", async () => { isSilence.setNumSilenceFramesExitThresh(0); const silenceSpy = vi.fn(); const soundSpy = vi.fn(); isSilence.on("silence", silenceSpy); isSilence.on("sound", soundSpy); isSilence.write(Buffer.from([0, 0])); // silence isSilence.write(Buffer.from([0, 10])); // speech expect(silenceSpy).not.toHaveBeenCalled(); expect(soundSpy).not.toHaveBeenCalled(); }); }); describe("Buffer processing", () => { it("should handle buffer with exactly 2 bytes", async () => { const chunks: Buffer[] = []; isSilence.on("data", (chunk: Buffer) => { chunks.push(chunk); }); isSilence.write(Buffer.from([1, 2])); isSilence.end(); await new Promise((resolve) => { isSilence.on("end", resolve); }); expect(chunks.length).toBe(1); expect(chunks[0]).toEqual(Buffer.from([1, 2])); }); it("should handle buffer with multiple frames", async () => { const chunks: Buffer[] = []; isSilence.on("data", (chunk: Buffer) => { chunks.push(chunk); }); // 4 frames (8 bytes) const buffer = Buffer.from([0, 0, 1, 1, 0, 0, 2, 2]); isSilence.write(buffer); isSilence.end(); await new Promise((resolve) => { isSilence.on("end", resolve); }); expect(chunks.length).toBe(1); expect(chunks[0]).toEqual(buffer); }); }); });