{"record":{"id":"cf2cf72e44ced42c","repo":"can1357/oh-my-pi","slug":"audio-gain-must-be-a-finite-non-negative-number","errorCode":null,"errorMessage":"Audio gain must be a finite non-negative number","messagePattern":"Audio gain must be a finite non-negative number","errorType":"exception","errorClass":"RangeError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tts/streaming-player.ts","lineNumber":65,"sourceCode":"\t\t}\n\t}\n\n\t/** Queues one mono `f32` PCM chunk without copying it in TypeScript. */\n\twrite(pcm: Float32Array): void {\n\t\tif (this.#inputClosed || this.#stopped || pcm.length === 0) return;\n\t\tif (!this.#native && !this.#error) this.start(this.#sampleRate);\n\t\tconst native = this.#native;\n\t\tif (!native) return;\n\t\ttry {\n\t\t\tnative.write(pcm);\n\t\t} catch (cause) {\n\t\t\tthis.#failNative(native, cause);\n\t\t}\n\t}\n\n\t/** Applies gain at render time, including to samples already queued natively. */\n\tsetGain(gain: number): void {\n\t\tif (!Number.isFinite(gain) || gain < 0) throw new RangeError(\"Audio gain must be a finite non-negative number\");\n\t\tthis.#gain = gain;\n\t\tconst native = this.#native;\n\t\tif (!native) return;\n\t\ttry {\n\t\t\tnative.setGain(gain);\n\t\t} catch (cause) {\n\t\t\tthis.#failNative(native, cause);\n\t\t}\n\t}\n\n\t/** Closes input and resolves after every queued sample reaches the speaker. */\n\tend(): Promise<void> {\n\t\tif (this.#ending) return this.#ending;\n\t\tif (this.#stopped) return Promise.resolve();\n\t\tthis.#inputClosed = true;\n\t\tif (this.#error) {\n\t\t\tthis.#stopped = true;\n\t\t\treturn Promise.reject(this.#error);","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tts/streaming-player.ts#L47-L83","documentation":"The streaming audio player validates gain before applying it: it must be a finite, non-negative number. NaN, Infinity, negative values, or non-numbers throw a RangeError from setGain, which is called both directly and from start().","triggerScenarios":"Calling player.setGain(NaN), setGain(-0.5), setGain(Infinity), or setGain(undefined as any); start() propagates the same error when it applies an initial invalid gain.","commonSituations":"A UI slider produces NaN from an empty input or division by zero; a config value is negative ('quiet down' attempts); a computation like volume * multiplier overflows to Infinity or yields NaN from undefined operands.","solutions":["Pass a finite number >= 0, e.g. Math.max(0, Number.isFinite(v) ? v : defaultGain)","Coerce and clamp user/config input before calling setGain or start()","Fix the upstream computation producing NaN/Infinity (check for undefined operands or divide-by-zero)"],"exampleFix":"// before\nplayer.setGain(userVolume * multiplier); // may be NaN/negative\n// after\nconst gain = Math.max(0, userVolume * multiplier);\nplayer.setGain(Number.isFinite(gain) ? gain : 1);","handlingStrategy":"validation","validationCode":"const g = Number(gain);\nif (!Number.isFinite(g) || g < 0) throw new TypeError(`invalid gain: ${gain}`);\nplayer.setGain(g);","typeGuard":"function isValidGain(v) {\n  return typeof v === 'number' && Number.isFinite(v) && v >= 0;\n}","tryCatchPattern":"try {\n  player.setGain(gain);\n} catch (err) {\n  if (err instanceof RangeError && err.message.includes('gain')) {\n    player.setGain(1); // safe default\n  } else throw err;\n}","preventionTips":["Clamp all user/config gain values with Math.max(0, value) before applying","Sanitize UI slider inputs (empty fields produce NaN)","Guard multiplicative gain computations against undefined operands and Infinity"],"tags":["audio","tts","range-error","validation","input-validation"],"backgroundTag":"invalid-number-parameter","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}