-
Notifications
You must be signed in to change notification settings - Fork 0
Direct Sequence Spread Spectrum Modulation #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d2bcceb
9546659
67710e6
7e71c21
f2600d2
429d468
fb7850d
b12dd14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * DSSS Spreader | ||
| * Multiplies a bipolar symbol by a PN sequence. | ||
| */ | ||
|
|
||
| export class DSSS_Spreader { | ||
| /** | ||
| * Spreads a symbol into a chip array. | ||
| * @param symbol bipolar symbol | ||
| * @param pnSequence 64-chip PN sequence | ||
| * @returns Float32Array of 64 spread chips | ||
| */ | ||
|
|
||
| static spread(symbol: number, pnSequence: Float32Array): Float32Array { | ||
| const spreadChips = new Float32Array(pnSequence.length); | ||
|
|
||
| for (let i = 0; i < pnSequence.length; i++) | ||
| spreadChips[i] = pnSequence[i]! * symbol; | ||
|
|
||
| return spreadChips; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||
| /** | ||||||
| * Spectral Embedding | ||||||
| * Injects DSSS chips into the FFT spectrum. | ||||||
| */ | ||||||
|
|
||||||
| export function embedFrameChips( | ||||||
| fftComplex: Float32Array, | ||||||
| chipMap: Map<number, number>, | ||||||
| N: number, | ||||||
| delta: number = 0.02, | ||||||
| ) { | ||||||
| const modifiedFFT = new Float32Array(fftComplex); | ||||||
|
|
||||||
| for (const [binIndex, chipValue] of chipMap.entries()) { | ||||||
| const rIndex = binIndex * 2; | ||||||
| const iIndex = rIndex + 1; | ||||||
|
|
||||||
| const real = fftComplex[rIndex]!; | ||||||
| const imag = fftComplex[iIndex]!; | ||||||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Phase modification formula may not match intended design. The phase update 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
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| const newReal = magnitude * Math.cos(newPhase); | ||||||
| const newImag = magnitude * Math.sin(newPhase); | ||||||
|
|
||||||
| modifiedFFT[rIndex] = newReal; | ||||||
| modifiedFFT[iIndex] = newImag; | ||||||
|
|
||||||
| const mirrorBin = N - binIndex; | ||||||
| if (mirrorBin > binIndex && mirrorBin < N) { | ||||||
| const mirrorRIndex = mirrorBin * 2; | ||||||
| const mirrorIIndex = mirrorRIndex + 1; | ||||||
|
|
||||||
| modifiedFFT[mirrorRIndex] = newReal; | ||||||
| modifiedFFT[mirrorIIndex] = -newImag; | ||||||
| } | ||||||
|
Comment on lines
+31
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mirror bin index calculation is out-of-bounds for real FFT. For a real FFT of size For Hermitian symmetry with 🤖 Prompt for AI Agents |
||||||
| } | ||||||
|
|
||||||
| return modifiedFFT; | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * Safe Bin Mapping | ||
| * | ||
| * Maps chips to safe FFT bins using the PN-based shuffle | ||
| */ | ||
|
|
||
| export class BinMapper { | ||
| /** | ||
| * Assigns 64 chips to a randomized subset of safe bins. | ||
| * @param chips 64 spread chips | ||
| * @param safeBins List of indices | ||
| * @param seed Frame-specific or shared seed for shuffling | ||
| */ | ||
|
|
||
| static mapToBins( | ||
| chips: Float32Array, | ||
| safeBins: number[], | ||
| seed: number, | ||
| ): Map<number, number> { | ||
| const map = new Map<number, number>(); | ||
|
|
||
| const shuffledBins = [...safeBins]; | ||
| let tempSeed = seed; | ||
|
|
||
| for (let i = shuffledBins.length - 1; i > 0; i--) { | ||
| tempSeed = (tempSeed * 1680721) % 2147483647; | ||
| const j = tempSeed % (i + 1); | ||
|
|
||
| [shuffledBins[i], shuffledBins[j]] = [shuffledBins[j]!, shuffledBins[i]!]; | ||
| } | ||
|
|
||
| for (let i = 0; i < chips.length; i++) { | ||
| const binIndex = shuffledBins[i % shuffledBins.length]!; | ||
| map.set(binIndex, chips[i]!); | ||
| } | ||
|
|
||
| return map; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * PN Sequence Generator (LFSR) | ||||||||||||||||||||||||||||||
| * Generates a deterministic maximal-length pseudo-random sequence (PN). | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export class PNGenerator { | ||||||||||||||||||||||||||||||
| private state: number; | ||||||||||||||||||||||||||||||
| private readonly mask: number = 0xb400; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| constructor(seed: number = 0xace1) { | ||||||||||||||||||||||||||||||
| this.state = (seed === 0 ? 0xace1 : seed) & 0xffff; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Generates a single chip using the LFSR. | ||||||||||||||||||||||||||||||
| * @returns +1 or -1 | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private nextChip(): number { | ||||||||||||||||||||||||||||||
| const lsb = this.state & 1; | ||||||||||||||||||||||||||||||
| this.state >>> 1; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (lsb === 1) this.state ^= this.mask; | ||||||||||||||||||||||||||||||
| return lsb === 1 ? 1 : -1; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: LFSR shift result is not assigned, producing a degenerate constant sequence. Line 21 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Generates the Spreading Sequence | ||||||||||||||||||||||||||||||
| * @param length The spreading factor | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public generateSequence(length: number): Float32Array { | ||||||||||||||||||||||||||||||
| const sequence = new Float32Array(length); | ||||||||||||||||||||||||||||||
| for (let i = 0; i < length; i++) { | ||||||||||||||||||||||||||||||
| sequence[i] = this.nextChip(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return sequence; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * DSSS Spreading Algorithm | ||
| * Transforms bits into a spreading signal (chips) for injection | ||
| */ | ||
|
|
||
| export class Spreader { | ||
| private readonly SF: number = 64; | ||
| private chipIndex: number = 0; | ||
| private currentBit: number | null = null; | ||
| private pnSequence: Int8Array; | ||
|
|
||
| constructor(seed: number = 0xaec2) { | ||
| this.pnSequence = this.generatePN(1024, seed); | ||
| } | ||
|
|
||
| private generatePN(length: number, seed: number): Int8Array { | ||
| const pn = new Int8Array(length); | ||
| let lsfr = seed; | ||
|
|
||
| for (let i = 0; i < length; i++) { | ||
| lsfr ^= lsfr >> 7; | ||
| lsfr ^= lsfr << 9; | ||
| lsfr ^= lsfr >> 13; | ||
|
|
||
| pn[i] = (lsfr & 1) === 1 ? 1 : -1; | ||
| } | ||
| return pn; | ||
| } | ||
|
|
||
| /** | ||
| * @returns the next modulation chip for the bit stream | ||
| */ | ||
|
|
||
| public getNextChip( | ||
| bitstream: Uint8Array, | ||
| bitPtr: { index: number }, | ||
| ): number | null { | ||
| if (bitPtr.index >= bitstream.length) return null; | ||
|
|
||
| if (this.currentBit === 0) | ||
| this.currentBit = bitstream[bitPtr.index++] === 1 ? 1 : -1; | ||
|
|
||
| const pnChip = this.pnSequence[this.chipIndex % this.pnSequence.length]!; | ||
|
|
||
| const modulatedChip = this.currentBit! * pnChip; | ||
| this.chipIndex++; | ||
|
|
||
| if (this.chipIndex > this.SF) { | ||
| this.chipIndex = 0; | ||
| bitPtr.index++; | ||
| } | ||
|
|
||
| return modulatedChip; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| import type { AudioRingBuffer } from "../../types/AudioRingBuffer.js"; | ||
| import { DSSS_Spreader } from "../modulator/dsss.js"; | ||
| import { embedFrameChips } from "../modulator/embedChips.js"; | ||
| import { BinMapper } from "../modulator/mapping/binMapper.js"; | ||
| import type { PNGenerator } from "../modulator/pnGen.js"; | ||
| import { processFFT } from "./fft.js"; | ||
| import { computeBarkEnergy, identifySafeBins } from "./freqBarkMap.js"; | ||
| import { estimateMasking } from "./masking.js"; | ||
|
|
@@ -9,8 +13,13 @@ const FRAME_SIZE = 1024; | |
|
|
||
| let frameCount = 0; | ||
|
|
||
| export function processSTFT(audioBuffer: AudioRingBuffer) { | ||
| const maskingMap = []; | ||
| export function processSTFT( | ||
| audioBuffer: AudioRingBuffer, | ||
| bitstream: Uint8Array, | ||
| bitPtr: { index: number }, | ||
| pnGen: PNGenerator, | ||
| ) { | ||
| const outSpectra = []; | ||
|
|
||
| while (audioBuffer.size >= FRAME_SIZE) { | ||
| const frames = audioBuffer.getFrames(FRAME_SIZE); | ||
|
|
@@ -20,15 +29,25 @@ export function processSTFT(audioBuffer: AudioRingBuffer) { | |
|
|
||
| console.log("Frame min/max:", Math.min(...frames), Math.max(...frames)); | ||
|
|
||
| const powerSpectrum = processFFT(frames); | ||
| const fftComplex = processFFT(frames); | ||
| const bandEnergy = computeBarkEnergy(fftComplex); | ||
| const maskingThresholds = estimateMasking(bandEnergy); | ||
| const safeBins = identifySafeBins(fftComplex, maskingThresholds); | ||
|
Comment on lines
+32
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type mismatch:
See the related comment on 🤖 Prompt for AI Agents |
||
|
|
||
| const bandEnergy = computeBarkEnergy(powerSpectrum); | ||
| console.log(`Found ${safeBins.length} safe bins for data injection`); | ||
|
|
||
| const maskingThresholds = estimateMasking(bandEnergy); | ||
| let finalSpectrum = fftComplex; | ||
|
|
||
| const safeBins = identifySafeBins(powerSpectrum, maskingThresholds); | ||
| if (safeBins.length >= 64 && bitPtr.index < bitstream.length) { | ||
| const bit = bitstream[bitPtr.index]!; | ||
| const symbol = (bit << 1) - 1; | ||
| const pnSequence = pnGen.generateSequence(64); | ||
| const spreadChips = DSSS_Spreader.spread(symbol, pnSequence); | ||
|
|
||
| console.log(`Found ${safeBins.length} safe bins for data injection`); | ||
| const chipMap = BinMapper.mapToBins(spreadChips, safeBins, frameCount); | ||
| finalSpectrum = embedFrameChips(fftComplex, chipMap, FRAME_SIZE, 0.02); | ||
| bitPtr.index++; | ||
| } | ||
|
|
||
| const map = { | ||
| frameIndex: frameCount++, | ||
|
|
@@ -37,10 +56,13 @@ export function processSTFT(audioBuffer: AudioRingBuffer) { | |
| maskingThresholds: new Float32Array(maskingThresholds), | ||
| }; | ||
|
|
||
| maskingMap.push(map); | ||
| outSpectra.push({ | ||
| spectrum: finalSpectrum, | ||
| frameIndex: frameCount++, | ||
| }); | ||
|
|
||
| audioBuffer.advance(HOP_SIZE); | ||
| } | ||
|
|
||
| return maskingMap; | ||
| return outSpectra; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical:
fftComplexreceives power spectrum instead of interleaved complex array.According to
src/core/profiler/fft.ts,processFFTreturnspowerSpectrum(a magnitude-onlyFloat32Arrayof lengthFRAME_SIZE/2 = 512), not the interleaved complex array[real0, imag0, real1, imag1, ...]that this function expects.In
processFrame.tsline 32,const fftComplex = processFFT(frames)passes this power spectrum toembedFrameChips. This causes:rIndex = binIndex * 2can exceed the 512-element array🔧 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