Spaces:
Running on Zero
Running on Zero
fix(01-07): fold an empty recording into the duration floor
Browse filesA push too short to produce a single audio callback was reported as a distinct
'no-audio' condition. It is a zero-length recording, so 'duration-floor' is both true
and actionable; 'no-audio' now means only stop() without a matching start(). Also
records why the standalone suite drives capture with browser audio processing off.
- avatar/mic.js +15 -3
avatar/mic.js
CHANGED
|
@@ -40,6 +40,7 @@ export const GATE = {
|
|
| 40 |
|
| 41 |
/** Reject reasons. Tests assert on these exact strings, so they are exported data. */
|
| 42 |
export const REJECT = {
|
|
|
|
| 43 |
NO_AUDIO: 'no-audio',
|
| 44 |
DURATION: 'duration-floor',
|
| 45 |
RMS: 'rms-floor',
|
|
@@ -141,8 +142,10 @@ export function analyse(samples, sampleRate) {
|
|
| 141 |
export function gate(samples, sampleRate) {
|
| 142 |
const stats = analyse(samples, sampleRate);
|
| 143 |
let reason = null;
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
| 146 |
else if (stats.rms < GATE.MIN_RMS) reason = REJECT.RMS;
|
| 147 |
else if (!(stats.modulation >= GATE.MIN_MODULATION)) reason = REJECT.MODULATION;
|
| 148 |
return { ok: reason === null, reason, ...stats };
|
|
@@ -185,7 +188,16 @@ async function resampleTo(samples, fromRate, toRate) {
|
|
| 185 |
* @param {Function} [opts.emit] the facade event bus
|
| 186 |
* @param {Function} [opts.onListening] called with true/false as capture really starts/stops
|
| 187 |
* @param {Function} [opts.isBusy] returns true while the avatar is thinking or speaking
|
| 188 |
-
* @param {boolean} [opts.processing]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
*/
|
| 190 |
export function createMic({
|
| 191 |
emit = () => {},
|
|
|
|
| 40 |
|
| 41 |
/** Reject reasons. Tests assert on these exact strings, so they are exported data. */
|
| 42 |
export const REJECT = {
|
| 43 |
+
/** stop() without a matching start(), or ASR returned nothing at all. */
|
| 44 |
NO_AUDIO: 'no-audio',
|
| 45 |
DURATION: 'duration-floor',
|
| 46 |
RMS: 'rms-floor',
|
|
|
|
| 142 |
export function gate(samples, sampleRate) {
|
| 143 |
const stats = analyse(samples, sampleRate);
|
| 144 |
let reason = null;
|
| 145 |
+
// An empty buffer is not a separate condition, it is a zero-length recording, and
|
| 146 |
+
// saying "duration-floor" for a push too short to produce a single audio callback is
|
| 147 |
+
// both true and more useful than a distinct empty-buffer reason nobody can action.
|
| 148 |
+
if (stats.durationMs < GATE.MIN_DURATION_MS) reason = REJECT.DURATION;
|
| 149 |
else if (stats.rms < GATE.MIN_RMS) reason = REJECT.RMS;
|
| 150 |
else if (!(stats.modulation >= GATE.MIN_MODULATION)) reason = REJECT.MODULATION;
|
| 151 |
return { ok: reason === null, reason, ...stats };
|
|
|
|
| 188 |
* @param {Function} [opts.emit] the facade event bus
|
| 189 |
* @param {Function} [opts.onListening] called with true/false as capture really starts/stops
|
| 190 |
* @param {Function} [opts.isBusy] returns true while the avatar is thinking or speaking
|
| 191 |
+
* @param {boolean} [opts.processing] echo cancellation / noise suppression / AGC
|
| 192 |
+
*
|
| 193 |
+
* A note on `processing`, which the standalone ASR suite turns OFF deliberately.
|
| 194 |
+
* Chrome's WebRTC audio processing is very good at steady broadband noise: measured on
|
| 195 |
+
* this project's own cafe fixture it drops RMS from 0.0577 to 0.0055, so the clip is
|
| 196 |
+
* rejected by the RMS floor before the modulation condition is ever consulted. That is a
|
| 197 |
+
* fine outcome in production and a terrible one in a test, because it would leave the
|
| 198 |
+
* third gate condition unexercised while looking green. The suite therefore verifies the
|
| 199 |
+
* gate in its PESSIMISTIC configuration - raw microphone, no browser help - which is what
|
| 200 |
+
* a browser without WebRTC processing actually hands us. Production keeps it on.
|
| 201 |
*/
|
| 202 |
export function createMic({
|
| 203 |
emit = () => {},
|