Direct Sequence Spread Spectrum Modulation#4
Conversation
…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
…gth PN sequence needed to hide data
…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
… and DSSS modulation
📝 WalkthroughWalkthroughThis 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🔴 CriticalCritical:
frameCountis incremented twice per frame iteration.
frameCount++appears on both line 53 (insidemap) and line 61 (insideoutSpectra.push). This causes:
- Frame indices to skip (1, 3, 5... instead of 0, 1, 2...)
BinMapper.mapToBinson line 47 receives inconsistent seed values- 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
mapobject 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
0xace1is hardcoded here and stored privately inPNGeneratorwithout 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:
- Adding a
getSeed()method toPNGenerator- 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.tsandrecorder.ts, the actual architecture usesPNGenerator+DSSS_Spreader, not thisSpreaderclass. It's never imported anywhere.Additionally, this implementation has several bugs:
- Line 40:
this.currentBit === 0should be=== null(0 is a valid bit value)- Line 48:
chipIndex > SFproduces 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 withPNGeneratorConsider 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 whensafeBins.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. WhileprocessFrame.tsguards withsafeBins.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
📒 Files selected for processing (8)
package.jsonsrc/core/modulator/dsss.tssrc/core/modulator/embedChips.tssrc/core/modulator/mapping/binMapper.tssrc/core/modulator/pnGen.tssrc/core/modulator/spreader.tssrc/core/profiler/processFrame.tssrc/core/profiler/recorder.ts
| export function embedFrameChips( | ||
| fftComplex: Float32Array, | ||
| chipMap: Map<number, number>, | ||
| N: number, | ||
| delta: number = 0.02, | ||
| ) { |
There was a problem hiding this comment.
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:
- Out-of-bounds access:
rIndex = binIndex * 2can exceed the 512-element array - Incorrect data interpretation: Magnitude values are read as real/imaginary pairs
- 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); |
There was a problem hiding this comment.
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.
| 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.
| const mirrorBin = N - binIndex; | ||
| if (mirrorBin > binIndex && mirrorBin < N) { | ||
| const mirrorRIndex = mirrorBin * 2; | ||
| const mirrorIIndex = mirrorRIndex + 1; | ||
|
|
||
| modifiedFFT[mirrorRIndex] = newReal; | ||
| modifiedFFT[mirrorIIndex] = -newImag; | ||
| } |
There was a problem hiding this comment.
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).
| private nextChip(): number { | ||
| const lsb = this.state & 1; | ||
| this.state >>> 1; | ||
|
|
||
| if (lsb === 1) this.state ^= this.mask; | ||
| return lsb === 1 ? 1 : -1; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| const fftComplex = processFFT(frames); | ||
| const bandEnergy = computeBarkEnergy(fftComplex); | ||
| const maskingThresholds = estimateMasking(bandEnergy); | ||
| const safeBins = identifySafeBins(fftComplex, maskingThresholds); |
There was a problem hiding this comment.
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.
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
2. Deterministic Bin Mapping
3. Differential Phase Modulation
4. Frame-Based Scheduler
Current Status & Logs
Next Steps
Summary by CodeRabbit
New Features
Chores