Skip to content

Audio Reconstruction Pipeline#5

Merged
Forgata merged 14 commits into
mainfrom
feature/reconstruction
Mar 10, 2026
Merged

Audio Reconstruction Pipeline#5
Forgata merged 14 commits into
mainfrom
feature/reconstruction

Conversation

@Forgata
Copy link
Copy Markdown
Owner

@Forgata Forgata commented Mar 10, 2026

DeepHide Audio Reconstruction Pipeline

Key Changes

  • Dual-Output FFT Engine: Updated processFFT to return both fftComplex and powerSpectrum.
  • IFFT Implementation: Added processIFFT to transform modified spectra back into 1024-sample time-domain frames while maintaining symmetry.
  • Overlap-Add (OLA) Synthesis: Implemented 50% hop-size stitching with Hamming COLA normalization to eliminate boundary clicks and amplitude doubling.
  • PCM Pipeline: Added floatToInt16 conversionWaveFile writer to export the final stealth carrier as a standard 16-bit .wav.

Technical Verification

  • Hermitian Integrity: Verified that IFFT produces real-valued PCM with minimal imaginary leakage.
  • Signal Continuity: Confirmed that the overlapBuffer correctly preserves frame tails for seamless playback.
  • Acoustic Transparency.

Summary by CodeRabbit

  • New Features
    • Added WAV audio file output support for recorded and processed audio
    • Introduced inverse FFT processing for time-domain audio conversion
    • Implemented overlap-add synthesis for seamless frame reconstruction
    • Enhanced audio data type flexibility for improved compatibility

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

This PR adds an audio watermarking output synthesis pipeline that reconstructs audio from modified spectral data. It introduces inverse FFT, PCM conversion, overlap-add synthesis, and WAV file writing modules. The profiler and modulator are updated to support state-tracked spectral processing and expanded output generation.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
.gitignore, package.json
Added WAV file ignore pattern and introduced wavefile ^11.0.0 dependency for WAV file creation.
Profiler Pipeline
src/core/profiler/fft.ts, src/core/profiler/processFrame.ts, src/core/profiler/recorder.ts
Modified FFT processing to return both fftComplex and powerSpectrum; introduced FrameState for frame indexing; expanded recorder with IFFT/overlap-add/PCM conversion pipeline ending in WAV output.
Modulator
src/core/modulator/embedChips.ts
Widened embedFrameChips parameter type to accept Float32Array or number[], added bounds check for imaginary index access.
Audio Recombination Pipeline
src/core/recombination/ifft.ts, src/core/recombination/reconstructor.ts, src/core/recombination/pcmConverter.ts, src/core/recombination/writer.ts
Four new modules implementing inverse FFT transformation, overlap-add synthesis with buffer management, Float32 to Int16 PCM conversion, and WAV file writing with PCM accumulation.

Sequence Diagram

sequenceDiagram
    participant Profiler as Profiler<br/>(processFrame)
    participant FFT as FFT Analysis
    participant Modulator as Modulator<br/>(embedChips)
    participant IFFT as IFFT<br/>(Recombination)
    participant OLA as Overlap-Add<br/>(Reconstruction)
    participant PCM as PCM Converter
    participant Writer as WAV Writer

    Profiler->>FFT: windowedFrame
    FFT->>FFT: realTransform +<br/>spectrum completion
    FFT-->>Profiler: {fftComplex,<br/>powerSpectrum}
    
    Profiler->>Modulator: fftComplex +<br/>chipMap
    Modulator->>Modulator: embed chips<br/>into spectrum
    Modulator-->>Profiler: modifiedFFT
    
    Profiler->>IFFT: modifiedFFT<br/>(complex spectrum)
    IFFT->>IFFT: inverseTransform
    IFFT-->>OLA: timeFrame (PCM)
    
    OLA->>OLA: overlap +<br/>add with buffer
    OLA-->>PCM: hopFrame<br/>(512 samples)
    
    PCM->>PCM: scale & clamp<br/>to [-32768,32767]
    PCM-->>Writer: int16Frame
    
    Writer->>Writer: accumulate<br/>PCM frames
    Writer->>Writer: create WAV<br/>(16-bit, 16kHz)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feature/acoustic analysis #1: Modifies the same profiler pipeline, specifically FFT processing (processFFT return type and powerSpectrum pathway) and processFrame state handling.
  • Direct Sequence Spread Spectrum Modulation #4: Updates the same embedding and synthesis flow, including embedFrameChips modifications, processSTFT/processFrame refactoring, and recorder IFFT/overlap-add wiring.
  • DeepHide Embedding Engine #2: Changes src/core/profiler/recorder.ts frame-processing behavior; this PR extends it further with output synthesis (IFFT/overlap-add/WAV).

Poem

🐰 A watermarked song takes shape and sound,
Through FFT's realm and back around,
With overlap magic and buffers aligned,
The spectrum's secret now redefined,
WAV files hop forth—the pipeline's complete! 🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature of this changeset: a complete audio reconstruction pipeline implementation enabling conversion of modified FFT spectra back to time-domain audio and WAV export.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/reconstruction

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
src/core/recombination/writer.ts (1)

7-10: Remove dead commented-out code.

The commented concatenation approach (lines 7-10) and reset (line 20) should either be implemented or removed to avoid confusion. If the current array-push approach is intentional, clean up the dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/recombination/writer.ts` around lines 7 - 10, The file contains dead
commented code that attempts to concatenate Int16Array buffers (newBuffer,
newBuffer.set) and a commented reset of pcmAccumulator; remove these commented
lines or restore a real implementation so the codebase isn't confusing.
Specifically, either delete the commented block referencing
newBuffer/newBuffer.set and the commented reset of pcmAccumulator in
src/core/recombination/writer.ts, or replace the current array-push approach
with a working concatenation function that updates pcmAccumulator by creating a
new Int16Array, copying pcmAccumulator and pcmFrame into it (using set) and
assigning it back to pcmAccumulator; ensure references to pcmAccumulator and
pcmFrame are updated consistently.
src/core/modulator/embedChips.ts (1)

18-20: Consider logging or throwing on out-of-bounds bin indices.

The bounds check silently skips bins with iIndex >= fftComplex.length. While this prevents crashes, it may mask bugs in the upstream chipMap generation (e.g., BinMapper.mapToBins producing invalid indices).

Consider logging a warning or throwing an error to surface configuration issues during development.

💡 Optional: Add diagnostic logging
     if (iIndex >= fftComplex.length) {
+      console.warn(`embedFrameChips: binIndex ${binIndex} out of bounds, skipping`);
       continue;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/modulator/embedChips.ts` around lines 18 - 20, The loop silently
skips out-of-bounds bin indices (iIndex >= fftComplex.length); update the code
in embedChips.ts where iIndex and fftComplex are used (and where chipMap /
BinMapper.mapToBins feed indices) to surface this issue by either logging a
warning with context (include iIndex, fftComplex.length, chipId/chipMap entry
and any upstream mapping result) or throwing an Error during development; ensure
you reference the same variables (iIndex, fftComplex, chipMap) so the
log/exception clearly shows the invalid index and the source mapping to help
debug upstream BinMapper.mapToBins output.
src/core/profiler/processFrame.ts (1)

33-40: Move per-frame logging off the hot path.

These console.log calls run for every 1024-sample frame in the recording loop. In real-time capture, stdout I/O can become the bottleneck and introduce avoidable latency/dropouts. Please gate them behind a debug flag or remove them before shipping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/profiler/processFrame.ts` around lines 33 - 40, The per-frame
console.log calls in processFrame.ts (the two logs around Math.min/Math.max and
Found ${safeBins.length}) are on the hot path and must be gated or removed;
update the code that calls Math.min/Math.max and the safeBins length log to only
run when a debug flag is enabled (e.g., a module-level DEBUG constant or a
logger.isDebugEnabled check) so normal real-time capture bypasses stdout I/O,
leaving the core functions processFFT, computeBarkEnergy, estimateMasking, and
identifySafeBins unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 33: The dependency "wavefile" in package.json is old (last updated May
2022) and may be a maintenance risk; review its usage (search for
imports/references to "wavefile" in the codebase), decide whether to keep it,
pin a specific version, or replace it with an actively maintained alternative
(e.g., a maintained WAV library) and update package.json accordingly, and add a
short comment in the repo README or a dependencies.md noting the decision and
any rationale for choosing to keep or replace "wavefile".

In `@src/core/profiler/processFrame.ts`:
- Around line 17-23: The function processSTFT currently defaults frameState to a
new { count: 0 } each call which resets frameIndex/PN seed; remove the default
parameter so frameState is required in processSTFT's signature and update all
callers (notably the calls in recorder.ts) to create one shared const frameState
= { count: 0 } in recorder.ts and pass that single object into every
processSTFT(AudioRingBuffer, bitstream, bitPtr, pnGen, frameState) invocation so
the count advances across buffers and the BinMapper seed progresses with the
stream.

In `@src/core/profiler/recorder.ts`:
- Around line 31-35: The recorder() function should reset module-level state
before starting a new session to avoid cross-session contamination: add and
export functions like resetOverlapBuffer() in reconstructor.ts (to clear
overlapBuffer) and resetAccumulator() in writer.ts (to reinitialize
pcmAccumulator), then import and call resetOverlapBuffer() and
resetAccumulator() at the start of recorder() (before calling
processIFFT/overlapAdd/collectOutput) and/or immediately after saveWAV() to
ensure overlapAdd and collectOutput/saveWAV don’t carry state between
recordings.

In `@src/core/recombination/ifft.ts`:
- Line 6: The constant outputGain = 0.5 lacks context: update the code comments
near the outputGain declaration in src/core/recombination/ifft.ts to explain why
the extra 0.5 scaling is applied on top of fft.js's inverseTransform() (which
already applies 1/N), stating whether this is intentional attenuation, dynamic
range control, or clipping prevention, and justify the specific value (e.g.,
empirical clipping reduction, headroom requirements, or matching downstream
expected amplitude); reference the inverseTransform() normalization and mention
the resulting net scaling (1/2048) and any tests/measurements used to choose 0.5
so future maintainers can understand and adjust outputGain if needed.

In `@src/core/recombination/pcmConverter.ts`:
- Around line 6-19: The floatToInt16 function assumes input is within [-1,1]
which can cause hard clipping; modify floatToInt16 to first detect the peak
absolute sample (maxAbs) across floatFrame and, if maxAbs > 1.0, compute a
uniform scale factor (1.0 / maxAbs) and multiply every sample by that factor
before the existing clamping/rounding conversion so the buffer is
gain-normalized; alternatively add an optional parameter (e.g., normalize?:
boolean) to enable this peak-scaling behavior, but ensure the peak-detection and
uniform scaling occur inside floatToInt16 before filling intFrame.

In `@src/core/recombination/reconstructor.ts`:
- Around line 4-20: The overlap-add logic treats every frame as an interior hop,
causing the first output to be attenuated and trailing samples to be left
unflushed; fix by (1) adding a local primed flag (e.g., overlapPrimed) and
update overlapAdd to skip the 1.08 division on the very first call (use raw
overlapBuffer + timeFrame for the initial hop) and set overlapPrimed=true after
that call, and (2) add an exported flushOverlapAdd (or flushOverlapBuffer)
function that returns the remaining HOP_SIZE trailing samples (applying any
final scaling if needed), clears overlapBuffer and resets overlapPrimed so
recorder.saveWAV can call it to append the final hop; reference symbols:
overlapAdd, overlapBuffer, HOP_SIZE, and create flushOverlapAdd / overlapPrimed
for use by recorder.saveWAV.

In `@src/core/recombination/writer.ts`:
- Around line 15-21: The saveWAV function is passing pcmAccumulator (an array of
Int16Array chunks) directly to wav.fromScratch which expects a single flat
TypedArray for mono audio; fix by flattening pcmAccumulator into one Int16Array
(concatenate all chunks in pcmAccumulator into a single Int16Array) and pass
that flat array to wavefile.WaveFile.fromScratch(1, 16000, "16", flatSamples),
then write the buffer as before and re-enable/reset pcmAccumulator (uncomment or
reassign it to a new empty Int16Array) so accumulated chunks are cleared after
saving.

---

Nitpick comments:
In `@src/core/modulator/embedChips.ts`:
- Around line 18-20: The loop silently skips out-of-bounds bin indices (iIndex
>= fftComplex.length); update the code in embedChips.ts where iIndex and
fftComplex are used (and where chipMap / BinMapper.mapToBins feed indices) to
surface this issue by either logging a warning with context (include iIndex,
fftComplex.length, chipId/chipMap entry and any upstream mapping result) or
throwing an Error during development; ensure you reference the same variables
(iIndex, fftComplex, chipMap) so the log/exception clearly shows the invalid
index and the source mapping to help debug upstream BinMapper.mapToBins output.

In `@src/core/profiler/processFrame.ts`:
- Around line 33-40: The per-frame console.log calls in processFrame.ts (the two
logs around Math.min/Math.max and Found ${safeBins.length}) are on the hot path
and must be gated or removed; update the code that calls Math.min/Math.max and
the safeBins length log to only run when a debug flag is enabled (e.g., a
module-level DEBUG constant or a logger.isDebugEnabled check) so normal
real-time capture bypasses stdout I/O, leaving the core functions processFFT,
computeBarkEnergy, estimateMasking, and identifySafeBins unchanged.

In `@src/core/recombination/writer.ts`:
- Around line 7-10: The file contains dead commented code that attempts to
concatenate Int16Array buffers (newBuffer, newBuffer.set) and a commented reset
of pcmAccumulator; remove these commented lines or restore a real implementation
so the codebase isn't confusing. Specifically, either delete the commented block
referencing newBuffer/newBuffer.set and the commented reset of pcmAccumulator in
src/core/recombination/writer.ts, or replace the current array-push approach
with a working concatenation function that updates pcmAccumulator by creating a
new Int16Array, copying pcmAccumulator and pcmFrame into it (using set) and
assigning it back to pcmAccumulator; ensure references to pcmAccumulator and
pcmFrame are updated consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2872ca8f-e45a-4cc1-8d52-1fed5a9a75fa

📥 Commits

Reviewing files that changed from the base of the PR and between d93640e and c00c8e8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .gitignore
  • package.json
  • src/core/modulator/embedChips.ts
  • src/core/profiler/fft.ts
  • src/core/profiler/processFrame.ts
  • src/core/profiler/recorder.ts
  • src/core/recombination/ifft.ts
  • src/core/recombination/pcmConverter.ts
  • src/core/recombination/reconstructor.ts
  • src/core/recombination/writer.ts

Comment thread package.json
"chalk": "^5.6.2",
"fft.js": "^4.0.4"
"fft.js": "^4.0.4",
"wavefile": "^11.0.0"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest wavefile version on npm
curl -s https://registry.npmjs.org/wavefile/latest | jq '.version'

# Check for security advisories
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "wavefile") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

Repository: Forgata/deephide

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Get full package info from npm registry
curl -s https://registry.npmjs.org/wavefile | jq '{
  "latest_version": .["dist-tags"].latest,
  "all_versions": (.versions | keys | sort | .[-5:]),
  "description": .description,
  "repository": .repository.url,
  "last_modified": .time.modified
}'

# Try to check for known vulnerabilities using npm advisory endpoint
curl -s https://registry.npmjs.org/-/npm/v1/security/advisories/bulk -X POST \
  -H "Content-Type: application/json" \
  -d '{"wavefile":["11.0.0"]}' 2>/dev/null | jq . || echo "Advisory endpoint not available"

Repository: Forgata/deephide

Length of output: 365


The wavefile package version 11.0.0 exists with no known vulnerabilities, but consider maintenance risk.

The package version exists on npm with no security advisories. However, wavefile hasn't been updated since May 2022 (3+ years old), which may pose a maintenance risk. Consider evaluating whether to use this unmaintained package or switch to an actively maintained alternative for WAV file handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 33, The dependency "wavefile" in package.json is old
(last updated May 2022) and may be a maintenance risk; review its usage (search
for imports/references to "wavefile" in the codebase), decide whether to keep
it, pin a specific version, or replace it with an actively maintained
alternative (e.g., a maintained WAV library) and update package.json
accordingly, and add a short comment in the repo README or a dependencies.md
noting the decision and any rationale for choosing to keep or replace
"wavefile".

Comment on lines 17 to 23
export function processSTFT(
audioBuffer: AudioRingBuffer,
bitstream: Uint8Array,
bitPtr: { index: number },
pnGen: PNGenerator,
frameState: FrameState = { count: 0 },
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make frameState mandatory instead of defaulting it.

src/core/profiler/recorder.ts still calls processSTFT() without this argument, so the default { count: 0 } is recreated on every invocation. That resets frameIndex and keeps the BinMapper seed at 0, causing the same safe-bin shuffle to repeat across buffers instead of advancing with the stream.

Suggested change
 interface FrameState {
   count: number;
 }

 export function processSTFT(
   audioBuffer: AudioRingBuffer,
   bitstream: Uint8Array,
   bitPtr: { index: number },
   pnGen: PNGenerator,
-  frameState: FrameState = { count: 0 },
+  frameState: FrameState,
 ) {

Create one shared const frameState = { count: 0 }; in src/core/profiler/recorder.ts and pass it to every call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/profiler/processFrame.ts` around lines 17 - 23, The function
processSTFT currently defaults frameState to a new { count: 0 } each call which
resets frameIndex/PN seed; remove the default parameter so frameState is
required in processSTFT's signature and update all callers (notably the calls in
recorder.ts) to create one shared const frameState = { count: 0 } in recorder.ts
and pass that single object into every processSTFT(AudioRingBuffer, bitstream,
bitPtr, pnGen, frameState) invocation so the count advances across buffers and
the BinMapper seed progresses with the stream.

Comment on lines +31 to +35
const timeframe = processIFFT(frame.spectrum);
const synthFrame = overlapAdd(timeframe);
const pcmFrame = floatToInt16(synthFrame);

collectOutput(pcmFrame);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Module-level state is not reset between recording sessions.

Both overlapAdd (in reconstructor.ts) and collectOutput/saveWAV (in writer.ts) use module-level state (overlapBuffer and pcmAccumulator) that persists across calls to recorder(). If this function is invoked multiple times:

  1. overlapBuffer retains samples from the previous session, corrupting the first output frame(s)
  2. pcmAccumulator accumulates indefinitely (the reset on line 20 of writer.ts is commented out), causing subsequent WAV files to contain all prior recordings

Consider adding and calling reset functions at the start of recorder() or after saveWAV().

🛠️ Suggested approach

Add reset functions to both modules and call them appropriately:

In reconstructor.ts:

export function resetOverlapBuffer(): void {
  overlapBuffer.fill(0);
}

In writer.ts:

export function resetAccumulator(): void {
  pcmAccumulator = [];
}

Then in recorder.ts, import and call these at the start:

 export async function recorder(bitstream: Uint8Array) {
+  resetOverlapBuffer();
+  resetAccumulator();
   const frameSize = 512;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/profiler/recorder.ts` around lines 31 - 35, The recorder() function
should reset module-level state before starting a new session to avoid
cross-session contamination: add and export functions like resetOverlapBuffer()
in reconstructor.ts (to clear overlapBuffer) and resetAccumulator() in writer.ts
(to reinitialize pcmAccumulator), then import and call resetOverlapBuffer() and
resetAccumulator() at the start of recorder() (before calling
processIFFT/overlapAdd/collectOutput) and/or immediately after saveWAV() to
ensure overlapAdd and collectOutput/saveWAV don’t carry state between
recordings.

const FRAME_SIZE = 1024;
const f = new FFT(FRAME_SIZE);

const outputGain = 0.5;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

fft.js npm package inverseTransform normalization

💡 Result:

In the fft.js npm package (Fedor Indutny’s fft.js), inverseTransform(out, data) already applies normalization: after running the inverse FFT, it divides every element in the complex output buffer by this.size (i.e., 1/N scaling, applied to both real and imaginary interleaved slots). So transform() is unscaled, and inverseTransform() is scaled by 1/N. [1][2]

Implications:

  • inverseTransform(transform(x)) returns (approximately) the original signal without you doing extra scaling. [1][2]
  • If you want an unnormalized inverse, multiply the inverseTransform output by N (or alternatively divide the forward transform by N). [2]

Sources:
[1] fft.js README usage/API (shows inverseTransform as the inverse FFT entry point) (github.com)
[2] fft.js/lib/fft.js implementation of inverseTransform (out[i] /= this.size) (github.com)

Citations:


🏁 Script executed:

cat src/core/recombination/ifft.ts

Repository: Forgata/deephide

Length of output: 781


Add documentation explaining the outputGain = 0.5 scaling factor.

The hardcoded gain value lacks context. Since fft.js's inverseTransform() already applies 1/N normalization (dividing by 1024), the additional 0.5 multiplier results in total scaling of 1/2048 on the real output. This combined scaling is unusual and should be documented: explain whether it's intentional signal attenuation, dynamic range control, or clipping prevention, and justify why this specific value was chosen.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/recombination/ifft.ts` at line 6, The constant outputGain = 0.5
lacks context: update the code comments near the outputGain declaration in
src/core/recombination/ifft.ts to explain why the extra 0.5 scaling is applied
on top of fft.js's inverseTransform() (which already applies 1/N), stating
whether this is intentional attenuation, dynamic range control, or clipping
prevention, and justify the specific value (e.g., empirical clipping reduction,
headroom requirements, or matching downstream expected amplitude); reference the
inverseTransform() normalization and mention the resulting net scaling (1/2048)
and any tests/measurements used to choose 0.5 so future maintainers can
understand and adjust outputGain if needed.

Comment on lines +6 to +19
export function floatToInt16(floatFrame: Float32Array): Int16Array {
const intFrame = new Int16Array(floatFrame.length);

for (let i = 0; i < floatFrame.length; i++) {
let s = floatFrame[i]! * 32767.0;

if (s > 32767.0) s = 32767.0;
if (s < -32768.0) s = -32768.0;

intFrame[i] = Math.round(s);
}

return intFrame;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Input range assumption may cause hard clipping.

This function assumes floatFrame values are in the range [-1.0, 1.0]. Based on the upstream pipeline (IFFT with outputGain=0.5 and overlap-add dividing by 1.08), the effective gain is ~0.463, but the raw IFFT output magnitude depends on the input spectrum and is not inherently normalized.

If the modified spectrum produces time-domain values exceeding ±1.0 after scaling, clamping at lines 12-13 will hard-clip the audio, causing audible distortion. Consider adding a normalization step upstream or implementing peak detection to scale the entire buffer before conversion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/recombination/pcmConverter.ts` around lines 6 - 19, The floatToInt16
function assumes input is within [-1,1] which can cause hard clipping; modify
floatToInt16 to first detect the peak absolute sample (maxAbs) across floatFrame
and, if maxAbs > 1.0, compute a uniform scale factor (1.0 / maxAbs) and multiply
every sample by that factor before the existing clamping/rounding conversion so
the buffer is gain-normalized; alternatively add an optional parameter (e.g.,
normalize?: boolean) to enable this peak-scaling behavior, but ensure the
peak-detection and uniform scaling occur inside floatToInt16 before filling
intFrame.

Comment on lines +4 to +20
let overlapBuffer = new Float32Array(HOP_SIZE).fill(0);

/**
* Stitches IFFT frames using Overlap-Add logic.
* @param timeFrame 1024 samples
* @returns 512 samples of finished PCM
*/

export function overlapAdd(timeFrame: Float32Array): Float32Array {
const output = new Float32Array(HOP_SIZE);

for (let i = 0; i < HOP_SIZE; i++) {
output[i] = (overlapBuffer[i]! + timeFrame[i]!) / 1.08;
overlapBuffer[i] = timeFrame[i + HOP_SIZE]!;
}

return output;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle the first/last OLA hops explicitly.

On the first call, overlapBuffer is still all zeros, so Line 16 attenuates the opening hop as if a previous frame existed. On the last call, Lines 17-20 keep the trailing 512 samples in overlapBuffer, but src/core/profiler/recorder.ts never flushes them before saveWAV(). The reconstructed output will start with incorrect gain and end one hop short. Please add explicit priming/finalization handling for the overlap buffer instead of treating every frame as an interior frame.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/recombination/reconstructor.ts` around lines 4 - 20, The overlap-add
logic treats every frame as an interior hop, causing the first output to be
attenuated and trailing samples to be left unflushed; fix by (1) adding a local
primed flag (e.g., overlapPrimed) and update overlapAdd to skip the 1.08
division on the very first call (use raw overlapBuffer + timeFrame for the
initial hop) and set overlapPrimed=true after that call, and (2) add an exported
flushOverlapAdd (or flushOverlapBuffer) function that returns the remaining
HOP_SIZE trailing samples (applying any final scaling if needed), clears
overlapBuffer and resets overlapPrimed so recorder.saveWAV can call it to append
the final hop; reference symbols: overlapAdd, overlapBuffer, HOP_SIZE, and
create flushOverlapAdd / overlapPrimed for use by recorder.saveWAV.

Comment on lines +15 to +21
export function saveWAV(filename: string = "output.wav"): void {
const wav = new wavefile.WaveFile();
wav.fromScratch(1, 16000, "16", pcmAccumulator);
fs.writeFileSync(filename, wav.toBuffer());
console.log(`\n stealth audio saved: ${filename}`);
// pcmAccumulator = new Int16Array(0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

wavefile npm fromScratch samples parameter type

💡 Result:

In wavefile’s WaveFile.fromScratch(numChannels, sampleRate, bitDepthCode, samples, options?), the samples parameter type is Array | TypedArray. [1]

For multi-channel audio, samples can be provided either:

  • Interleaved (single TypedArray/Array of numbers), or
  • De-interleaved (array of per-channel arrays); wavefile will interleave them for you (as shown in the stereo example). [2]

Sample values are plain numbers, expected to match the chosen bitDepthCode range (e.g., -32768..32767 for "16", -1.0..1.0 for "32f"/"64"). [3]

Sources: [1] (rochars.github.io) [2] (npm.io) [3] (npm.io)

Citations:


Flatten pcmAccumulator chunks before passing to wavefile.

Line 17 passes pcmAccumulator (an Int16Array[] of accumulated chunks) directly to wav.fromScratch(). Since this is mono audio (1 channel), wavefile expects a single flat TypedArray or an array with one channel. Passing an array of chunks will either fail or be misinterpreted as multiple channels.

Combine the chunks into a single Int16Array:

Fix
 export function saveWAV(filename: string = "output.wav"): void {
+  const totalLength = pcmAccumulator.reduce((sum, chunk) => sum + chunk.length, 0);
+  const flatSamples = new Int16Array(totalLength);
+  let offset = 0;
+  for (const chunk of pcmAccumulator) {
+    flatSamples.set(chunk, offset);
+    offset += chunk.length;
+  }
+
   const wav = new wavefile.WaveFile();
-  wav.fromScratch(1, 16000, "16", pcmAccumulator);
+  wav.fromScratch(1, 16000, "16", flatSamples);
   fs.writeFileSync(filename, wav.toBuffer());
   console.log(`\n stealth audio saved: ${filename}`);

Also uncomment the reset (line 21) if pcmAccumulator should be cleared after saving.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/recombination/writer.ts` around lines 15 - 21, The saveWAV function
is passing pcmAccumulator (an array of Int16Array chunks) directly to
wav.fromScratch which expects a single flat TypedArray for mono audio; fix by
flattening pcmAccumulator into one Int16Array (concatenate all chunks in
pcmAccumulator into a single Int16Array) and pass that flat array to
wavefile.WaveFile.fromScratch(1, 16000, "16", flatSamples), then write the
buffer as before and re-enable/reset pcmAccumulator (uncomment or reassign it to
a new empty Int16Array) so accumulated chunks are cleared after saving.

@Forgata Forgata merged commit 8b4fe54 into main Mar 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant