• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

semperai / vad-ts / 18363907063

09 Oct 2025 02:44AM UTC coverage: 73.936%. First build
18363907063

Pull #1

github

web-flow
Merge 79ce1f1ec into b562b936a
Pull Request #1: Typescript Port

187 of 199 branches covered (93.97%)

Branch coverage included in aggregate %.

460 of 637 new or added lines in 8 files covered. (72.21%)

1098 of 1539 relevant lines covered (71.35%)

893.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.74
/packages/web/src/worklet.ts
1
// VAD AudioWorklet Processor
1✔
2
// Based on @ricky0123/vad-web but fixed to ensure process() is called
3

NEW
4
const LOG_PREFIX = "[VAD Worklet]";
×
NEW
5
const log = {
×
NEW
6
  debug: (...args: unknown[]) => console.debug(LOG_PREFIX, ...args),
×
NEW
7
  info: (...args: unknown[]) => console.log(LOG_PREFIX, ...args),
×
NEW
8
  error: (...args: unknown[]) => console.error(LOG_PREFIX, ...args),
×
NEW
9
  warn: (...args: unknown[]) => console.warn(LOG_PREFIX, ...args),
×
NEW
10
};
×
11

NEW
12
const Message = {
×
NEW
13
  AudioFrame: "AUDIO_FRAME",
×
NEW
14
  SpeechStart: "SPEECH_START",
×
NEW
15
  VADMisfire: "VAD_MISFIRE",
×
NEW
16
  SpeechEnd: "SPEECH_END",
×
NEW
17
  SpeechStop: "SPEECH_STOP",
×
NEW
18
  SpeechRealStart: "SPEECH_REAL_START",
×
NEW
19
  FrameProcessed: "FRAME_PROCESSED",
×
NEW
20
};
×
21

22
interface ResamplerOptions {
23
  nativeSampleRate: number;
24
  targetSampleRate: number;
25
  targetFrameSize: number;
26
}
27

NEW
28
class Resampler {
×
29
  options: ResamplerOptions;
30
  inputBuffer: number[];
31

NEW
32
  constructor(options: ResamplerOptions) {
×
NEW
33
    this.options = options;
×
NEW
34
    if (options.nativeSampleRate < 16000) {
×
NEW
35
      log.error("nativeSampleRate is too low. Should have 16000 = targetSampleRate <= nativeSampleRate");
×
NEW
36
    }
×
NEW
37
    this.inputBuffer = [];
×
NEW
38
  }
×
39

NEW
40
  process(inputFrame: Float32Array): Float32Array[] {
×
NEW
41
    const outputFrames = [];
×
NEW
42
    for (const sample of inputFrame) {
×
NEW
43
      this.inputBuffer.push(sample);
×
NEW
44
      while (this.hasEnoughDataForFrame()) {
×
NEW
45
        const frame = this.generateOutputFrame();
×
NEW
46
        outputFrames.push(frame);
×
NEW
47
      }
×
NEW
48
    }
×
NEW
49
    return outputFrames;
×
NEW
50
  }
×
51

NEW
52
  hasEnoughDataForFrame() {
×
NEW
53
    return (
×
NEW
54
      (this.inputBuffer.length * this.options.targetSampleRate) /
×
NEW
55
        this.options.nativeSampleRate >=
×
NEW
56
      this.options.targetFrameSize
×
57
    );
NEW
58
  }
×
59

NEW
60
  generateOutputFrame() {
×
NEW
61
    const outputFrame = new Float32Array(this.options.targetFrameSize);
×
NEW
62
    let outputIndex = 0;
×
NEW
63
    let inputIndex = 0;
×
64

NEW
65
    while (outputIndex < this.options.targetFrameSize) {
×
NEW
66
      let sum = 0;
×
NEW
67
      let count = 0;
×
68

NEW
69
      while (
×
NEW
70
        inputIndex <
×
NEW
71
        Math.min(
×
NEW
72
          this.inputBuffer.length,
×
NEW
73
          ((outputIndex + 1) * this.options.nativeSampleRate) /
×
NEW
74
            this.options.targetSampleRate
×
NEW
75
        )
×
NEW
76
      ) {
×
NEW
77
        const sample = this.inputBuffer[inputIndex];
×
NEW
78
        if (sample !== undefined) {
×
NEW
79
          sum += sample;
×
NEW
80
          count++;
×
NEW
81
        }
×
NEW
82
        inputIndex++;
×
NEW
83
      }
×
84

NEW
85
      outputFrame[outputIndex] = sum / count;
×
NEW
86
      outputIndex++;
×
NEW
87
    }
×
88

NEW
89
    this.inputBuffer = this.inputBuffer.slice(inputIndex);
×
NEW
90
    return outputFrame;
×
NEW
91
  }
×
NEW
92
}
×
93

94
interface WorkletOptions {
95
  frameSamples: number;
96
}
97

NEW
98
class VadWorkletProcessor extends AudioWorkletProcessor {
×
99
  options: WorkletOptions;
100
  resampler!: Resampler;
101
  _initialized: boolean;
102
  _stopProcessing: boolean;
103
  _frameCount: number;
104

NEW
105
  constructor(options: AudioWorkletNodeOptions) {
×
NEW
106
    super();
×
NEW
107
    this._initialized = false;
×
NEW
108
    this._stopProcessing = false;
×
NEW
109
    this._frameCount = 0;
×
NEW
110
    this.options = options.processorOptions;
×
111

NEW
112
    log.debug("Worklet constructor called with options:", this.options);
×
113

114
    this.port.onmessage = (ev) => {
×
115
      if (ev.data.message === Message.SpeechStop) {
×
NEW
116
        log.debug("Received SpeechStop message");
×
NEW
117
        this._stopProcessing = true;
×
118
      }
×
NEW
119
    };
×
120

NEW
121
    this.init();
×
122
  }
×
123

NEW
124
  async init() {
×
NEW
125
    log.info("Initializing worklet, sampleRate:", sampleRate);
×
126
    this.resampler = new Resampler({
×
127
      nativeSampleRate: sampleRate,
×
128
      targetSampleRate: 16000,
×
129
      targetFrameSize: this.options.frameSamples,
×
NEW
130
    });
×
NEW
131
    this._initialized = true;
×
NEW
132
    log.info("Worklet initialized successfully");
×
133

134
    // Send initialization message to main thread
NEW
135
    this.port.postMessage({
×
NEW
136
      message: "WORKLET_INITIALIZED",
×
NEW
137
      sampleRate: sampleRate,
×
NEW
138
      frameSamples: this.options.frameSamples
×
NEW
139
    });
×
140
  }
×
141

142
  process(
×
143
    inputs: Float32Array[][],
×
144
    _outputs: Float32Array[][],
×
145
    _parameters: Record<string, Float32Array>
×
146
  ): boolean {
×
147
    if (this._stopProcessing) {
×
NEW
148
      log.debug("Stop processing flag set, returning false");
×
NEW
149
      return false;
×
NEW
150
    }
×
151

NEW
152
    const input = inputs[0];
×
NEW
153
    const channel = input ? input[0] : null;
×
154

155
    // Log first few frames to verify we're receiving audio
NEW
156
    if (this._frameCount < 5) {
×
NEW
157
      log.debug(`Process called, frame ${this._frameCount}, input:`, input, 'channel:', channel, 'initialized:', this._initialized);
×
NEW
158
      this._frameCount++;
×
159
    }
×
160

NEW
161
    if (this._initialized && channel instanceof Float32Array && channel.length > 0) {
×
NEW
162
      const frames = this.resampler.process(channel);
×
163

NEW
164
      if (this._frameCount === 5 && frames.length > 0) {
×
NEW
165
        log.debug(`Posting ${frames.length} frames back to main thread`);
×
NEW
166
      }
×
167

168
      for (const frame of frames) {
×
169
        this.port.postMessage(
×
170
          { message: Message.AudioFrame, data: frame.buffer },
×
171
          [frame.buffer]
×
NEW
172
        );
×
173
      }
×
NEW
174
    } else if (this._frameCount < 10 && this._initialized) {
×
NEW
175
      log.warn(`No valid audio data in frame ${this._frameCount}, channel:`, channel);
×
176
    }
×
177

NEW
178
    return true;
×
179
  }
×
180
}
×
181

NEW
182
registerProcessor("vad-helper-worklet", VadWorkletProcessor);
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc