Skip to content

Direct Sequence Spread Spectrum Modulation#4

Merged
Forgata merged 8 commits into
mainfrom
feature/modulator
Mar 8, 2026
Merged

Direct Sequence Spread Spectrum Modulation#4
Forgata merged 8 commits into
mainfrom
feature/modulator

Conversation

@Forgata
Copy link
Copy Markdown
Owner

@Forgata Forgata commented Mar 8, 2026

PR: DSSS Modulation Engine & Spectral Embedding

This PR implements the core transmission engine. It bridges the gap between the encrypted/encoded bitstream and the psychoacoustic analysis by transforming logical bits into sub-perceptual spectral disturbances.

The system now successfully modulates data into the frequency domain using Direct Sequence Spread Spectrum (DSSS) at a spreading factor of 64 chips per bit.

Key Changes

1. DSSS Spreading Engine

  • PN Generator: Implemented a deterministic 16-bit Galois LFSR. This ensures the "noise" used to spread the signal is identical for both sender and receiver.
  • Symbol Mapping: Added bipolar NRZ mapping to ensure symmetrical energy distribution and signal cancellation during despreading.
  • Processing Gain: Locked the spreading factor to 64, providing high robustness against environmental noise and compression.

2. Deterministic Bin Mapping

  • Safe Bin Selection: Integrated the psychoacoustic masking thresholds identify safe FFT bins.
  • PN-Based Shuffling: Implemented a deterministic bin shuffler. Instead of linear mapping, chips are scattered across safe bins based on a frame-indexed seed, preventing periodic spectral artifacts.

3. Differential Phase Modulation

  • Phase Rotation: Implemented the core modulation logic. Data is encoded by rotating the phase of complex FFT vectors by a delta = 0.02 rad
  • Conjugate Symmetry: Applied strict Hermitian symmetry enforcement to prevent imaginary leakage and ensures the resulting signal is valid real-valued PCM audio.

4. Frame-Based Scheduler

  • Implemented a bit scheduler that orchestrates 1-bit-per-frame modulation.
  • Added a capacity check that skips frames with insufficient safe bins ensuring the system is content-adaptive and only hides data when it's psychoacoustically safe.

Current Status & Logs

  • Initial testing shows a stable bitrate of ~86 bps (at 44.1kHz/1024-frame).
  • Stability: Verified pointer synchronization over 5,000+ frames with no drift in the PN state.

Next Steps

  • Implement Inverse FFT (IFFT) and Overlap-Add (OLA) reconstruction to output the final .wav.

Summary by CodeRabbit

  • New Features

    • Added audio data embedding capabilities using spread spectrum modulation
    • Integrated pseudo-noise sequence generation for signal processing
    • Enhanced spectral analysis with deterministic chip mapping and embedding into audio frames
  • Chores

    • Updated build process to include pre-compilation step

Forgata added 8 commits March 8, 2026 15:25
…h processing gain to pull out injected signal.

initialised with SF = 64
… to 64 different freq components to produce a 'spread' chip stream by multiplying with the PN sequence
…ch the safe bins are filled to avoid predictable linear mapping
…es a phase shift to the corresponding bins and simultaneously conjugates symmetry
add bitstream, bit pointer and a PN generator instance as args
@Forgata Forgata self-assigned this Mar 8, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

This PR introduces a DSSS (Direct Sequence Spread Spectrum) modulation system for embedding data into audio spectrograms. It adds PN sequence generation, DSSS spreading, secure bin mapping, and FFT-level chip embedding, then integrates these into the existing processing pipeline to embed bitstreams into spectra during frame analysis.

Changes

Cohort / File(s) Summary
Build Configuration
package.json
Added build script for TypeScript compilation and updated dev script to run build before nodemon.
PN Sequence Generation
src/core/modulator/pnGen.ts
Added PNGenerator class implementing LFSR-based pseudo-noise sequence generation with 16-bit state and fixed feedback mask; exports public methods for constructing and generating sequences.
DSSS Core Modulation
src/core/modulator/dsss.ts, src/core/modulator/spreader.ts
Added DSSS_Spreader class with a static spread method and Spreader class with streaming chip generation; Spreader uses PN sequences and spreading factors to modulate bits over 64 chips.
Data Embedding and Bin Mapping
src/core/modulator/embedChips.ts, src/core/modulator/mapping/binMapper.ts
Added embedFrameChips function for phase-modulating FFT bins via chipMap with mirrored-bin symmetry; added BinMapper class with deterministic PRNG-based shuffling to securely assign chips to safe frequency bins.
Processing Pipeline Integration
src/core/profiler/processFrame.ts, src/core/profiler/recorder.ts
Extended processSTFT signature to accept bitstream, bitPtr object, and PNGenerator; added conditional data embedding path when sufficient safe bins exist; updated recorder to initialize PNGenerator, track bitPtr, and integrate embedded spectra collection.

Sequence Diagram(s)

sequenceDiagram
    participant PF as ProcessFrame
    participant PG as PNGenerator
    participant SM as BinMapper
    participant SP as Spreader
    participant EC as embedChips
    participant FFT as FFT Array

    PF->>PG: generateSequence(64)
    PG-->>PF: pnSequence
    PF->>SM: mapToBins(chips, safeBins, seed)
    SM-->>PF: chipMap (bin→chip)
    PF->>EC: embedFrameChips(fftResult, chipMap, N, delta)
    EC->>FFT: read magnitude & phase
    FFT-->>EC: complex values
    EC->>EC: increment phase by (chip + delta)
    EC->>FFT: write updated real/imag & mirror
    FFT-->>EC: modified spectrum
    EC-->>PF: embedded spectrum
    PF->>PF: advance bitPtr
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • DeepHide Embedding Engine #2: Modifies recorder.ts and bitstream/bitPtr tracking to enable payload integration into the recording pipeline.
  • Feature/acoustic analysis #1: Extends the same STFT/recorder processing pipeline by updating processSTFT signature and adding PN-based DSSS spreading, bin mapping, and spectral embedding.

Poem

🐰 Hark! The PN spreads so wide,
Chips dance in bins with careful pride,
FFT's phase now twirls and bends,
Data hides where spectrum ends!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 'Direct Sequence Spread Spectrum Modulation' directly corresponds to the main feature implemented in this PR—a DSSS modulation engine with spreading factor 64, PN generation, bin mapping, and phase modulation.

✏️ 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/modulator

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/profiler/processFrame.ts (1)

52-62: ⚠️ Potential issue | 🔴 Critical

Critical: frameCount is incremented twice per frame iteration.

frameCount++ appears on both line 53 (inside map) and line 61 (inside outSpectra.push). This causes:

  1. Frame indices to skip (1, 3, 5... instead of 0, 1, 2...)
  2. BinMapper.mapToBins on line 47 receives inconsistent seed values
  3. PN-based shuffling becomes unpredictable across frames
🐛 Fix: Increment frameCount once
     const map = {
-      frameIndex: frameCount++,
+      frameIndex: frameCount,
       safeBins,
       bandEnergy: new Float32Array(bandEnergy),
       maskingThresholds: new Float32Array(maskingThresholds),
     };

     outSpectra.push({
       spectrum: finalSpectrum,
       frameIndex: frameCount++,
     });

Also note: the map object is created but never used—consider removing it if not needed.

🤖 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 52 - 62, frameCount is being
incremented twice (once in the map object and once in outSpectra.push), causing
skipped indices and inconsistent seeds for BinMapper.mapToBins; fix by
incrementing frameCount exactly once per frame: capture the current frame index
into a local variable (e.g., const frameIndex = frameCount++) and use that
single frameIndex wherever you currently use frameCount++ (in the map
construction and in outSpectra.push), and if the map object is truly unused
remove it to avoid dead code.
🧹 Nitpick comments (4)
src/core/profiler/recorder.ts (2)

9-10: Consider exposing PN seed for decoder synchronization.

The seed 0xace1 is hardcoded here and stored privately in PNGenerator without a getter. For the receiver/decoder to regenerate the same PN sequence, you'll need a way to transmit or retrieve the seed. Consider either:

  1. Adding a getSeed() method to PNGenerator
  2. Storing the seed in a header/metadata structure that gets embedded
🤖 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 9 - 10, The PN seed (0xace1) is
hardcoded when instantiating PNGenerator (pnGen) and not exposed, preventing
decoder synchronization; either add a getSeed() accessor on PNGenerator and call
pnGen.getSeed() where the recorder constructs outgoing data, or ensure the
recorder embeds the seed into the existing header/metadata structure sent with
the recording (e.g., add a seed field to the metadata creation code and populate
it from PNGenerator or a stored seed variable) so the receiver/decoder can
regenerate the same PN sequence.

31-52: Remove commented-out dead code.

This commented block is no longer used and clutters the codebase. Remove it before merging.

🧹 Proposed fix
        if (bitPtr.index >= bitstream.length) {
          console.log("SUCCESS! Payload fully modulated into spectra.");
        }
-
-        // if (maskingMap.length > 0) {
-        //   const latestFrame = maskingMap[maskingMap.length - 1]!;
-        //   const safebins = latestFrame.safeBins;
-
-        //   if (safebins.length > 0 && bitPtr < bitstream.length) {
-        //     for (const bitIndex of safebins) {
-        //       if (bitPtr >= bitstream.length) break;
-
-        //       const currentBit = bitstream[bitPtr];
-
-        //       bitPtr++;
-        //     }
-        //   }
-
-        //   console.log(
-        //     `Frame: ${latestFrame.frameIndex} | Safe Bins: ${safebins.length} | Progress: ${bitPtr}/${bitstream.length} bits`,
-        //   );
-        //   if (bitPtr >= bitstream.length) {
-        //     console.log("SUCCESS! entire bitstream injected");
-        //     // break;
-        //   }
-        // }
      }
🤖 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 - 52, Remove the entire
commented-out block (the unused maskingMap/bitstream debug code) in recorder.ts
to eliminate dead code; locate the commented section that references maskingMap,
latestFrame.safeBins, bitPtr, bitstream and the console.log debug statements and
delete it wholesale, then run lint/format and tests to ensure no remaining
references or side-effects.
src/core/modulator/spreader.ts (1)

6-54: This class appears to be unused dead code.

According to the context from processFrame.ts and recorder.ts, the actual architecture uses PNGenerator + DSSS_Spreader, not this Spreader class. It's never imported anywhere.

Additionally, this implementation has several bugs:

  • Line 40: this.currentBit === 0 should be === null (0 is a valid bit value)
  • Line 48: chipIndex > SF produces 65 chips per bit (should be >= SF)
  • Line 50: bitPtr.index++ double-increments (already incremented on line 41)
  • Different LFSR: Uses xorshift algorithm and seed 0xaec2, incompatible with PNGenerator

Consider removing this file to avoid confusion, or clarify if it's intended for future use.

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

In `@src/core/modulator/spreader.ts` around lines 6 - 54, Spreader class appears
to be dead/unused and also contains multiple bugs; either delete Spreader
entirely or fix it: in class Spreader (fields SF, chipIndex, currentBit,
pnSequence and methods generatePN and getNextChip) replace the xorshift LFSR in
generatePN with the same PNGenerator algorithm/seed used elsewhere, ensure
getNextChip checks currentBit === null (not === 0), use chipIndex >= SF (not >)
to yield exactly SF chips per bit, and remove the extra bitPtr.index++ so the
bit pointer is incremented only once; if you choose to keep it, add
imports/usage or document intent to avoid confusion.
src/core/modulator/mapping/binMapper.ts (1)

32-35: Chips may overwrite each other when safeBins.length < chips.length.

The modulo cycling on line 33 (i % shuffledBins.length) causes multiple chips to map to the same bin index, with later chips overwriting earlier ones in the Map. While processFrame.ts guards with safeBins.length >= 64, this function silently loses data if called with fewer safe bins than chips.

Consider adding a defensive check or logging a warning:

💡 Optional: Add validation
 static mapToBins(
   chips: Float32Array,
   safeBins: number[],
   seed: number,
 ): Map<number, number> {
+  if (safeBins.length < chips.length) {
+    console.warn(`BinMapper: ${chips.length} chips but only ${safeBins.length} safe bins`);
+  }
   const map = new Map<number, number>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/modulator/mapping/binMapper.ts` around lines 32 - 35, The loop in
binMapper.ts that assigns chips to bins (the for loop using shuffledBins[i %
shuffledBins.length] and map.set) can overwrite earlier chips when
shuffledBins.length < chips.length; add a defensive validation at the start of
the mapping routine (or in the function that contains this loop) to check that
shuffledBins.length (or safeBins.length) is >= chips.length and if not either
throw an error or log a clear warning and return/abort the mapping to avoid
silent data loss; update any callers (e.g., processFrame.ts expectations) only
if you choose to throw so behavior is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/modulator/embedChips.ts`:
- Around line 31-38: The mirror bin calculation is producing out-of-range
indices (mirrorBin = N - binIndex); change it to use the half-size (halfN = N/2)
and compute mirrorBin = halfN - binIndex (and only apply for binIndex in
1..halfN-1) so Hermitian symmetry matches fft.js realTransform expectations;
keep using modifiedFFT with mirrorRIndex = mirrorBin * 2 and mirrorIIndex =
mirrorRIndex + 1 and set modifiedFFT[mirrorRIndex] = newReal and
modifiedFFT[mirrorIIndex] = -newImag (or skip mirror assignment for DC and
Nyquist bins).
- Line 23: The current update in embedChips.ts computes newPhase as
originalPhase + (chipValue + delta) which incorrectly adds the chip sign and
delta producing ~±1.02 rad shifts; change the formula to apply the small
rotation scaled by the chip sign (e.g., newPhase = originalPhase + chipValue *
delta) so chipValue (±1) only selects the ±0.02 rad perturbation; update any
tests/uses of newPhase/originalPhase accordingly to reflect the smaller ±delta
rotation.
- Around line 6-11: embedFrameChips expects an interleaved complex FFT array
(real,imag pairs) but processFFT currently returns a powerSpectrum
(magnitude-only) which causes out-of-bounds and wrong-phase calculations; fix by
changing processFFT in src/core/profiler/fft.ts to return (or additionally
export) the interleaved complex Float32Array used to compute powerSpectrum, then
update the call site in processFrame.ts (where fftComplex is assigned) to pass
the interleaved complex array to embedFrameChips (or adjust embedFrameChips to
accept magnitude-only and remove the rIndex/cIndex logic); ensure the returned
array length matches the expected interleaved size (FRAME_SIZE or
2*FRAME_SIZE/2) and update types/variable names accordingly so embedFrameChips
reads real/imag pairs correctly.

In `@src/core/modulator/pnGen.ts`:
- Around line 19-25: The LFSR in nextChip() never advances because the
right-shift result is discarded; change the shift to assign back to the state
(e.g., use an unsigned right-shift assignment on this.state) so the register
actually moves before applying the feedback mask; ensure the code still tests
lsb from the old state, then if (lsb === 1) apply this.state ^= this.mask to
inject feedback, so nextChip() produces the intended PN sequence.

In `@src/core/profiler/processFrame.ts`:
- Around line 32-35: The variable fftComplex is misnamed and misleading because
processFFT actually returns a magnitude/power Float32Array (length 512) rather
than a complex FFT; update the code so downstream callers use the correct data
type: either change processFFT to return the complex FFT (if computeBarkEnergy,
embedFrameChips, identifySafeBins expect complex) or rename fftComplex to
something like powerSpectrum and convert/derive a complex representation before
calling functions that require complex input (or adapt computeBarkEnergy and
embedFrameChips to accept magnitude arrays). Specifically, adjust the call site
around processFFT, computeBarkEnergy, estimateMasking, identifySafeBins and the
functions embedFrameChips/computeBarkEnergy to agree on a single format
(magnitude vs complex) and update variable names (fftComplex -> powerSpectrum or
complexFFT) to reflect the chosen format.

---

Outside diff comments:
In `@src/core/profiler/processFrame.ts`:
- Around line 52-62: frameCount is being incremented twice (once in the map
object and once in outSpectra.push), causing skipped indices and inconsistent
seeds for BinMapper.mapToBins; fix by incrementing frameCount exactly once per
frame: capture the current frame index into a local variable (e.g., const
frameIndex = frameCount++) and use that single frameIndex wherever you currently
use frameCount++ (in the map construction and in outSpectra.push), and if the
map object is truly unused remove it to avoid dead code.

---

Nitpick comments:
In `@src/core/modulator/mapping/binMapper.ts`:
- Around line 32-35: The loop in binMapper.ts that assigns chips to bins (the
for loop using shuffledBins[i % shuffledBins.length] and map.set) can overwrite
earlier chips when shuffledBins.length < chips.length; add a defensive
validation at the start of the mapping routine (or in the function that contains
this loop) to check that shuffledBins.length (or safeBins.length) is >=
chips.length and if not either throw an error or log a clear warning and
return/abort the mapping to avoid silent data loss; update any callers (e.g.,
processFrame.ts expectations) only if you choose to throw so behavior is
consistent.

In `@src/core/modulator/spreader.ts`:
- Around line 6-54: Spreader class appears to be dead/unused and also contains
multiple bugs; either delete Spreader entirely or fix it: in class Spreader
(fields SF, chipIndex, currentBit, pnSequence and methods generatePN and
getNextChip) replace the xorshift LFSR in generatePN with the same PNGenerator
algorithm/seed used elsewhere, ensure getNextChip checks currentBit === null
(not === 0), use chipIndex >= SF (not >) to yield exactly SF chips per bit, and
remove the extra bitPtr.index++ so the bit pointer is incremented only once; if
you choose to keep it, add imports/usage or document intent to avoid confusion.

In `@src/core/profiler/recorder.ts`:
- Around line 9-10: The PN seed (0xace1) is hardcoded when instantiating
PNGenerator (pnGen) and not exposed, preventing decoder synchronization; either
add a getSeed() accessor on PNGenerator and call pnGen.getSeed() where the
recorder constructs outgoing data, or ensure the recorder embeds the seed into
the existing header/metadata structure sent with the recording (e.g., add a seed
field to the metadata creation code and populate it from PNGenerator or a stored
seed variable) so the receiver/decoder can regenerate the same PN sequence.
- Around line 31-52: Remove the entire commented-out block (the unused
maskingMap/bitstream debug code) in recorder.ts to eliminate dead code; locate
the commented section that references maskingMap, latestFrame.safeBins, bitPtr,
bitstream and the console.log debug statements and delete it wholesale, then run
lint/format and tests to ensure no remaining references or side-effects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49da172e-b8ed-4b40-86f9-9de99123116a

📥 Commits

Reviewing files that changed from the base of the PR and between fc1a090 and b12dd14.

📒 Files selected for processing (8)
  • package.json
  • src/core/modulator/dsss.ts
  • src/core/modulator/embedChips.ts
  • src/core/modulator/mapping/binMapper.ts
  • src/core/modulator/pnGen.ts
  • src/core/modulator/spreader.ts
  • src/core/profiler/processFrame.ts
  • src/core/profiler/recorder.ts

Comment on lines +6 to +11
export function embedFrameChips(
fftComplex: Float32Array,
chipMap: Map<number, number>,
N: number,
delta: number = 0.02,
) {
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

Critical: fftComplex receives power spectrum instead of interleaved complex array.

According to src/core/profiler/fft.ts, processFFT returns powerSpectrum (a magnitude-only Float32Array of length FRAME_SIZE/2 = 512), not the interleaved complex array [real0, imag0, real1, imag1, ...] that this function expects.

In processFrame.ts line 32, const fftComplex = processFFT(frames) passes this power spectrum to embedFrameChips. This causes:

  1. Out-of-bounds access: rIndex = binIndex * 2 can exceed the 512-element array
  2. Incorrect data interpretation: Magnitude values are read as real/imaginary pairs
  3. Silent corruption: Phase calculations on magnitude values produce garbage
🔧 Fix: Return the complex FFT array from processFFT

In src/core/profiler/fft.ts, export the complex array alongside or instead of power spectrum:

 export function processFFT(windowedFrame: Float32Array) {
   f.realTransform(out, windowedFrame);
+  f.completeSpectrum(out);  // If needed for full spectrum
   const powerSpectrum = new Float32Array(FRAME_SIZE / 2);

   for (let i = 0; i < FRAME_SIZE / 2; i++) {
     const real = out[2 * i]!;
     const imag = out[2 * i + 1]!;
     powerSpectrum[i] = real * real + imag * imag;
   }

-  return powerSpectrum;
+  return { complex: new Float32Array(out), powerSpectrum };
 }

Then update callers to use the appropriate field.

🤖 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 6 - 11, embedFrameChips
expects an interleaved complex FFT array (real,imag pairs) but processFFT
currently returns a powerSpectrum (magnitude-only) which causes out-of-bounds
and wrong-phase calculations; fix by changing processFFT in
src/core/profiler/fft.ts to return (or additionally export) the interleaved
complex Float32Array used to compute powerSpectrum, then update the call site in
processFrame.ts (where fftComplex is assigned) to pass the interleaved complex
array to embedFrameChips (or adjust embedFrameChips to accept magnitude-only and
remove the rIndex/cIndex logic); ensure the returned array length matches the
expected interleaved size (FRAME_SIZE or 2*FRAME_SIZE/2) and update
types/variable names accordingly so embedFrameChips reads real/imag pairs
correctly.

const magnitude = Math.sqrt(real * real + imag * imag);
const originalPhase = Math.atan2(imag, real);

const newPhase = originalPhase + (chipValue + delta);
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

Phase modification formula may not match intended design.

The phase update originalPhase + (chipValue + delta) adds the chip value (+1 or -1) and delta (0.02), resulting in phase shifts of ~+1.02 or ~-0.98 radians.

The PR description mentions "delta = 0.02 rad" for phase rotation. If the intent is a small phase perturbation modulated by the chip sign, consider:

-    const newPhase = originalPhase + (chipValue + delta);
+    const newPhase = originalPhase + (chipValue * delta);

This would produce ±0.02 rad shifts, which is sub-perceptual as described.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const newPhase = originalPhase + (chipValue + delta);
const newPhase = originalPhase + (chipValue * delta);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/modulator/embedChips.ts` at line 23, The current update in
embedChips.ts computes newPhase as originalPhase + (chipValue + delta) which
incorrectly adds the chip sign and delta producing ~±1.02 rad shifts; change the
formula to apply the small rotation scaled by the chip sign (e.g., newPhase =
originalPhase + chipValue * delta) so chipValue (±1) only selects the ±0.02 rad
perturbation; update any tests/uses of newPhase/originalPhase accordingly to
reflect the smaller ±delta rotation.

Comment on lines +31 to +38
const mirrorBin = N - binIndex;
if (mirrorBin > binIndex && mirrorBin < N) {
const mirrorRIndex = mirrorBin * 2;
const mirrorIIndex = mirrorRIndex + 1;

modifiedFFT[mirrorRIndex] = newReal;
modifiedFFT[mirrorIIndex] = -newImag;
}
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

Mirror bin index calculation is out-of-bounds for real FFT.

For a real FFT of size N=1024, valid bin indices are 0 to 511 (FRAME_SIZE/2). The formula mirrorBin = N - binIndex computes indices like 1023, 1022, etc., which exceed the valid range.

For Hermitian symmetry with fft.js's realTransform, the conjugate bin for index k in [1, N/2-1] is at index N/2 - k (or you work with the full N complex array after calling completeSpectrum).

🤖 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 31 - 38, The mirror bin
calculation is producing out-of-range indices (mirrorBin = N - binIndex); change
it to use the half-size (halfN = N/2) and compute mirrorBin = halfN - binIndex
(and only apply for binIndex in 1..halfN-1) so Hermitian symmetry matches fft.js
realTransform expectations; keep using modifiedFFT with mirrorRIndex = mirrorBin
* 2 and mirrorIIndex = mirrorRIndex + 1 and set modifiedFFT[mirrorRIndex] =
newReal and modifiedFFT[mirrorIIndex] = -newImag (or skip mirror assignment for
DC and Nyquist bins).

Comment on lines +19 to +25
private nextChip(): number {
const lsb = this.state & 1;
this.state >>> 1;

if (lsb === 1) this.state ^= this.mask;
return lsb === 1 ? 1 : -1;
}
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

Critical: LFSR shift result is not assigned, producing a degenerate constant sequence.

Line 21 this.state >>> 1 computes the right-shift but discards the result. The state never advances, causing the LFSR to output a constant or 2-state sequence instead of the expected maximal-length PN sequence.

This completely breaks the DSSS spreading—all chips become identical, eliminating the processing gain.

🐛 Fix: Assign the shift result
 private nextChip(): number {
   const lsb = this.state & 1;
-  this.state >>> 1;
+  this.state = this.state >>> 1;

   if (lsb === 1) this.state ^= this.mask;
   return lsb === 1 ? 1 : -1;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private nextChip(): number {
const lsb = this.state & 1;
this.state >>> 1;
if (lsb === 1) this.state ^= this.mask;
return lsb === 1 ? 1 : -1;
}
private nextChip(): number {
const lsb = this.state & 1;
this.state = this.state >>> 1;
if (lsb === 1) this.state ^= this.mask;
return lsb === 1 ? 1 : -1;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/modulator/pnGen.ts` around lines 19 - 25, The LFSR in nextChip()
never advances because the right-shift result is discarded; change the shift to
assign back to the state (e.g., use an unsigned right-shift assignment on
this.state) so the register actually moves before applying the feedback mask;
ensure the code still tests lsb from the old state, then if (lsb === 1) apply
this.state ^= this.mask to inject feedback, so nextChip() produces the intended
PN sequence.

Comment on lines +32 to +35
const fftComplex = processFFT(frames);
const bandEnergy = computeBarkEnergy(fftComplex);
const maskingThresholds = estimateMasking(bandEnergy);
const safeBins = identifySafeBins(fftComplex, maskingThresholds);
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

Type mismatch: processFFT returns power spectrum, not complex FFT array.

processFFT returns a magnitude-only Float32Array of length 512, but downstream code (embedFrameChips, computeBarkEnergy) may expect different formats. The variable name fftComplex is misleading.

See the related comment on embedChips.ts for the critical impact on spectral embedding.

🤖 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 32 - 35, The variable
fftComplex is misnamed and misleading because processFFT actually returns a
magnitude/power Float32Array (length 512) rather than a complex FFT; update the
code so downstream callers use the correct data type: either change processFFT
to return the complex FFT (if computeBarkEnergy, embedFrameChips,
identifySafeBins expect complex) or rename fftComplex to something like
powerSpectrum and convert/derive a complex representation before calling
functions that require complex input (or adapt computeBarkEnergy and
embedFrameChips to accept magnitude arrays). Specifically, adjust the call site
around processFFT, computeBarkEnergy, estimateMasking, identifySafeBins and the
functions embedFrameChips/computeBarkEnergy to agree on a single format
(magnitude vs complex) and update variable names (fftComplex -> powerSpectrum or
complexFFT) to reflect the chosen format.

@Forgata Forgata merged commit 6e049d2 into main Mar 8, 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