Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/lib/audio/AudioEngine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AudioEngine as IAudioEngine, AudioEngineConfig, AudioSegment, IRingBuffer, AudioMetrics } from './types';
import { RingBuffer } from './RingBuffer';
import { AudioSegmentProcessor, ProcessedSegment } from './AudioSegmentProcessor';
import { AudioSegmentProcessor, ProcessedSegment, CurrentStats } from './AudioSegmentProcessor';
import { resampleLinear } from './utils';

/** Duration of the visualization buffer in seconds */
Expand Down Expand Up @@ -74,6 +74,17 @@ export class AudioEngine implements IAudioEngine {
isSpeaking: false,
};

// Cached objects to avoid GC churn in hot paths
private cachedStats: CurrentStats = {
silence: { avgDuration: 0, avgEnergy: 0, avgEnergyIntegral: 0 },
speech: { avgDuration: 0, avgEnergy: 0, avgEnergyIntegral: 0 },
noiseFloor: 0.01,
snr: 0,
snrThreshold: 3.0,
minSnrThreshold: 1.0,
energyRiseThreshold: 0.08
};

// Subscribers for visualization updates
private visualizationCallbacks: Array<(data: Float32Array, metrics: AudioMetrics, bufferEndTime: number) => void> = [];
private lastVisualizationNotifyTime: number = 0;
Expand Down Expand Up @@ -422,7 +433,7 @@ export class AudioEngine implements IAudioEngine {
}

getSignalMetrics(): { noiseFloor: number; snr: number; threshold: number; snrThreshold: number } {
const stats = this.audioProcessor.getStats();
const stats = this.audioProcessor.getStats(this.cachedStats);
return {
noiseFloor: stats.noiseFloor ?? 0.0001,
snr: stats.snr ?? 0,
Expand Down Expand Up @@ -598,7 +609,7 @@ export class AudioEngine implements IAudioEngine {
this.updateVisualizationBuffer(chunk);

// 2.6 Update metrics
const stats = this.audioProcessor.getStats();
const stats = this.audioProcessor.getStats(this.cachedStats);
const stateInfo = this.audioProcessor.getStateInfo();

this.metrics.currentEnergy = energy;
Expand Down
24 changes: 22 additions & 2 deletions src/lib/audio/AudioSegmentProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface StatsSummary {
}

/** Current stats snapshot */
interface CurrentStats {
export interface CurrentStats {
silence: StatsSummary;
speech: StatsSummary;
noiseFloor: number;
Expand Down Expand Up @@ -524,9 +524,29 @@ export class AudioSegmentProcessor {

/**
* Get current statistics.
* @param out Optional pre-allocated object to avoid GC overhead
*/
getStats(): CurrentStats {
getStats(out?: CurrentStats): CurrentStats {
const stats = this.state.currentStats;

if (out) {
out.noiseFloor = stats.noiseFloor;
out.snr = stats.snr;
out.snrThreshold = stats.snrThreshold;
out.minSnrThreshold = stats.minSnrThreshold;
out.energyRiseThreshold = stats.energyRiseThreshold;

out.silence.avgDuration = stats.silence.avgDuration;
out.silence.avgEnergy = stats.silence.avgEnergy;
out.silence.avgEnergyIntegral = stats.silence.avgEnergyIntegral;

out.speech.avgDuration = stats.speech.avgDuration;
out.speech.avgEnergy = stats.speech.avgEnergy;
out.speech.avgEnergyIntegral = stats.speech.avgEnergyIntegral;
Comment on lines +533 to +545
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better maintainability and conciseness, you can use object destructuring and Object.assign to copy the properties. This avoids manually listing each property and will automatically handle new properties if the CurrentStats interface is extended in the future.

Suggested change
out.noiseFloor = stats.noiseFloor;
out.snr = stats.snr;
out.snrThreshold = stats.snrThreshold;
out.minSnrThreshold = stats.minSnrThreshold;
out.energyRiseThreshold = stats.energyRiseThreshold;
out.silence.avgDuration = stats.silence.avgDuration;
out.silence.avgEnergy = stats.silence.avgEnergy;
out.silence.avgEnergyIntegral = stats.silence.avgEnergyIntegral;
out.speech.avgDuration = stats.speech.avgDuration;
out.speech.avgEnergy = stats.speech.avgEnergy;
out.speech.avgEnergyIntegral = stats.speech.avgEnergyIntegral;
const { silence, speech, ...rest } = stats;
Object.assign(out, rest);
Object.assign(out.silence, silence);
Object.assign(out.speech, speech);


Comment on lines +532 to +546
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unsafe out dereference 🐞 Bug ⛯ Reliability

AudioSegmentProcessor.getStats(out) unconditionally writes to out.silence.* and out.speech.*
without ensuring those nested objects exist, which can throw a runtime exception if a caller passes
a partially initialized out object. Since CurrentStats is now exported, this is an
easy-to-misuse public API that can crash the audio pipeline at runtime.
Agent Prompt
### Issue description
`AudioSegmentProcessor.getStats(out)` assumes `out.silence` and `out.speech` already exist and writes into them unconditionally. If a caller passes a partially initialized object, this will throw at runtime.

### Issue Context
This method is now intended for reuse in hot paths (zero-allocation pattern) and `CurrentStats` is exported, so it’s likely other callers will adopt this API.

### Fix Focus Areas
- src/lib/audio/AudioSegmentProcessor.ts[529-548]

### Suggested fix
In the `if (out)` branch, defensively ensure nested objects exist before writing:
- Initialize `out.silence` and `out.speech` if missing (e.g., `if (!out.silence) out.silence = { avgDuration: 0, avgEnergy: 0, avgEnergyIntegral: 0 }`, same for `speech`).
- Then proceed with the field-by-field assignments as currently implemented.

(Alternative: change the API to accept a dedicated factory/initializer for `out` so callers can’t pass an invalid shape.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

return out;
}

return {
...stats,
silence: { ...stats.silence },
Expand Down
Loading