mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(voice): harden Gemini Live provider integration
This commit is contained in:
1 parent
d4492d8f8c
commit
4fde920665
102 files changed
+28528
-1581
No files matched your search
@@ -0,0 +1,125 @@
|
||||
const OUTPUT_RATE = 16000;
|
||||
const FILTER_TAPS = 64;
|
||||
const FILTER_PHASES = 128;
|
||||
|
||||
// A causal, windowed-sinc low-pass filter avoids folding device frequencies
|
||||
// above 8 kHz into speech. Precompute phases; process allocates no filter data.
|
||||
function resamplingFilters(inputRate) {
|
||||
const cutoff = 0.45 * Math.min(1, OUTPUT_RATE / inputRate);
|
||||
const filters = new Float32Array(FILTER_TAPS * FILTER_PHASES);
|
||||
for (let phase = 0; phase < FILTER_PHASES; phase++) {
|
||||
let total = 0;
|
||||
for (let tap = 0; tap < FILTER_TAPS; tap++) {
|
||||
const distance = tap - (FILTER_TAPS - 1) / 2 - phase / FILTER_PHASES;
|
||||
const sinc =
|
||||
distance === 0
|
||||
? 2 * cutoff
|
||||
: Math.sin(2 * Math.PI * cutoff * distance) / (Math.PI * distance);
|
||||
const window =
|
||||
0.5 - 0.5 * Math.cos((2 * Math.PI * tap) / (FILTER_TAPS - 1));
|
||||
const weight = sinc * window;
|
||||
filters[phase * FILTER_TAPS + tap] = weight;
|
||||
total += weight;
|
||||
}
|
||||
for (let tap = 0; tap < FILTER_TAPS; tap++)
|
||||
filters[phase * FILTER_TAPS + tap] /= total;
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
/** PCM microphone capture. Only explicitly enabled frames leave this processor. */
|
||||
class GeminiCaptureProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.enabled = false;
|
||||
this.captureId = null;
|
||||
this.samples = new Int16Array(1024);
|
||||
this.length = 0;
|
||||
this.inputRate = sampleRate;
|
||||
this.filters =
|
||||
this.inputRate === OUTPUT_RATE ? null : resamplingFilters(this.inputRate);
|
||||
this.history = new Float32Array(FILTER_TAPS);
|
||||
this.resetResampler();
|
||||
this.port.onmessage = ({ data }) => {
|
||||
if (data?.enabled === true) {
|
||||
if (
|
||||
this.enabled ||
|
||||
!Number.isSafeInteger(data.captureId) ||
|
||||
data.captureId <= 0
|
||||
)
|
||||
return;
|
||||
this.captureId = data.captureId;
|
||||
this.resetResampler();
|
||||
this.enabled = true;
|
||||
this.port.postMessage({ type: 'start', captureId: this.captureId });
|
||||
} else if (
|
||||
data?.enabled === false &&
|
||||
this.enabled &&
|
||||
data.captureId === this.captureId
|
||||
) {
|
||||
this.flush();
|
||||
this.port.postMessage({ type: 'end', captureId: this.captureId });
|
||||
this.enabled = false;
|
||||
this.captureId = null;
|
||||
this.length = 0;
|
||||
// Do not drain filter tails into a later physical microphone capture.
|
||||
this.resetResampler();
|
||||
}
|
||||
};
|
||||
}
|
||||
resetResampler() {
|
||||
this.phase = 0;
|
||||
this.historyIndex = 0;
|
||||
this.history.fill(0);
|
||||
}
|
||||
appendSample(sample) {
|
||||
const value = Math.max(-1, Math.min(1, sample));
|
||||
this.samples[this.length++] = Math.round(
|
||||
value * (value < 0 ? 32768 : 32767),
|
||||
);
|
||||
if (this.length === this.samples.length) this.flush();
|
||||
}
|
||||
flush() {
|
||||
if (!this.length) return;
|
||||
const buffer = new ArrayBuffer(this.length * 2);
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < this.length; i++)
|
||||
view.setInt16(i * 2, this.samples[i], true);
|
||||
this.port.postMessage(
|
||||
{ type: 'audio', buffer, captureId: this.captureId },
|
||||
[buffer],
|
||||
);
|
||||
this.length = 0;
|
||||
}
|
||||
process(inputs) {
|
||||
if (!this.enabled) return true;
|
||||
const channel = inputs[0]?.[0];
|
||||
if (!channel) return true;
|
||||
for (const sample of channel) {
|
||||
if (!this.filters) {
|
||||
this.appendSample(sample);
|
||||
continue;
|
||||
}
|
||||
this.history[this.historyIndex] = sample;
|
||||
this.historyIndex = (this.historyIndex + 1) % FILTER_TAPS;
|
||||
this.phase += OUTPUT_RATE;
|
||||
while (this.phase >= this.inputRate) {
|
||||
this.phase -= this.inputRate;
|
||||
const phase = Math.min(
|
||||
FILTER_PHASES - 1,
|
||||
Math.floor((this.phase * FILTER_PHASES) / OUTPUT_RATE),
|
||||
);
|
||||
let value = 0;
|
||||
for (let tap = 0; tap < FILTER_TAPS; tap++) {
|
||||
const index =
|
||||
(this.historyIndex - 1 - tap + FILTER_TAPS) % FILTER_TAPS;
|
||||
value +=
|
||||
this.history[index] * this.filters[phase * FILTER_TAPS + tap];
|
||||
}
|
||||
this.appendSample(value);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('gev-gemini-capture', GeminiCaptureProcessor);
|
||||
Reference in new issue
Block a user