From d2bcceb4a827125547964d03fecc6f6e717ca5a9 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 15:25:45 +0200 Subject: [PATCH 1/8] feat: create a class for the dsss spreading algorith to provide enough processing gain to pull out injected signal. initialised with SF = 64 --- src/core/modulator/spreader.ts | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/core/modulator/spreader.ts diff --git a/src/core/modulator/spreader.ts b/src/core/modulator/spreader.ts new file mode 100644 index 0000000..de085cd --- /dev/null +++ b/src/core/modulator/spreader.ts @@ -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; + } +} From 9546659b21a7705fc1cc4d6ab691a64be64f3626 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 18:40:28 +0200 Subject: [PATCH 2/8] change dec script and build script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 77ae10c..fcdd9ae 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "type": "module", "main": "src/main.ts", "scripts": { - "dev": "nodemon" + "build": "tsc", + "dev": "npm run build && nodemon" }, "devDependencies": { "@types/node": "^25.3.3", From 67710e6f2d459378d2b6786b224f4ab0d954e831 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 18:43:58 +0200 Subject: [PATCH 3/8] feat: DSSS spreading algorithm to distribute the info of a single bit to 64 different freq components to produce a 'spread' chip stream by multiplying with the PN sequence --- src/core/modulator/dsss.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/core/modulator/dsss.ts diff --git a/src/core/modulator/dsss.ts b/src/core/modulator/dsss.ts new file mode 100644 index 0000000..8c5f797 --- /dev/null +++ b/src/core/modulator/dsss.ts @@ -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; + } +} From 7e71c217c20a2386c91ca69f2f74e67ea0361861 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 18:47:56 +0200 Subject: [PATCH 4/8] feat: implement a Galoir LFSR to generate a deterministic maximal-length PN sequence needed to hide data --- src/core/modulator/pnGen.ts | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/core/modulator/pnGen.ts diff --git a/src/core/modulator/pnGen.ts b/src/core/modulator/pnGen.ts new file mode 100644 index 0000000..316ea55 --- /dev/null +++ b/src/core/modulator/pnGen.ts @@ -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; + } + + /** + * 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; + } +} From f2600d2e7aa601eba6ddbd494e10a23a051b95b4 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 18:50:36 +0200 Subject: [PATCH 5/8] feat: implement a PN permutation to constanly change the order in which the safe bins are filled to avoid predictable linear mapping --- src/core/modulator/mapping/binMapper.ts | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/core/modulator/mapping/binMapper.ts diff --git a/src/core/modulator/mapping/binMapper.ts b/src/core/modulator/mapping/binMapper.ts new file mode 100644 index 0000000..d42409e --- /dev/null +++ b/src/core/modulator/mapping/binMapper.ts @@ -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 { + const map = new Map(); + + 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; + } +} From 429d468ea986b5e9076dc8f2838a7c391acd35aa Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 18:57:02 +0200 Subject: [PATCH 6/8] feat: add spectral injection. loop through the spread chips and applies a phase shift to the corresponding bins and simultaneously conjugates symmetry --- src/core/modulator/embedChips.ts | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/core/modulator/embedChips.ts diff --git a/src/core/modulator/embedChips.ts b/src/core/modulator/embedChips.ts new file mode 100644 index 0000000..e63d02d --- /dev/null +++ b/src/core/modulator/embedChips.ts @@ -0,0 +1,42 @@ +/** + * Spectral Embedding + * Injects DSSS chips into the FFT spectrum. + */ + +export function embedFrameChips( + fftComplex: Float32Array, + chipMap: Map, + 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); + + 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; + } + } + + return modifiedFFT; +} From fb7850df31f24e44006a1eed6024a5b4a8dd17d9 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 19:06:16 +0200 Subject: [PATCH 7/8] major feat: wire in DSS modulation into the main frame processor add bitstream, bit pointer and a PN generator instance as args --- src/core/profiler/processFrame.ts | 40 ++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/core/profiler/processFrame.ts b/src/core/profiler/processFrame.ts index d0984fb..ce1d39b 100644 --- a/src/core/profiler/processFrame.ts +++ b/src/core/profiler/processFrame.ts @@ -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); - 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; } From b12dd148995c3ac8758cb3d39f69db6bc34f84d1 Mon Sep 17 00:00:00 2001 From: Forgata Date: Sun, 8 Mar 2026 19:07:19 +0200 Subject: [PATCH 8/8] feat: pipe bitstream and pointer to the frame processor for embedding and DSSS modulation --- src/core/profiler/recorder.ts | 51 +++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/core/profiler/recorder.ts b/src/core/profiler/recorder.ts index 5003b13..b110fb1 100644 --- a/src/core/profiler/recorder.ts +++ b/src/core/profiler/recorder.ts @@ -1,11 +1,13 @@ import { PvRecorder } from "@picovoice/pvrecorder-node"; import { buffer } from "../../types/AudioRingBuffer.js"; import { processSTFT } from "./processFrame.js"; +import { PNGenerator } from "../modulator/pnGen.js"; export async function recorder(bitstream: Uint8Array) { const frameSize = 512; const pvRecorder = new PvRecorder(frameSize, -1); - let bitPtr = 0; + const pnGen = new PNGenerator(0xace1); + const bitPtr = { index: 0 }; pvRecorder.start(); @@ -15,30 +17,39 @@ export async function recorder(bitstream: Uint8Array) { buffer.push(frames); if (buffer.size >= 1024) { - const maskingMap = processSTFT(buffer); + const modifiedFrames = processSTFT(buffer, bitstream, bitPtr, pnGen); - if (maskingMap.length > 0) { - const latestFrame = maskingMap[maskingMap.length - 1]!; - const safebins = latestFrame.safeBins; + for (const frame of modifiedFrames) + console.log( + `Frame: ${frame.frameIndex} | Progress: ${bitPtr.index}/${bitstream.length}`, + ); - if (safebins.length > 0 && bitPtr < bitstream.length) { - for (const bitIndex of safebins) { - if (bitPtr >= bitstream.length) break; + if (bitPtr.index >= bitstream.length) { + console.log("SUCCESS! Payload fully modulated into spectra."); + } - const currentBit = bitstream[bitPtr]; + // if (maskingMap.length > 0) { + // const latestFrame = maskingMap[maskingMap.length - 1]!; + // const safebins = latestFrame.safeBins; - bitPtr++; - } - } + // if (safebins.length > 0 && bitPtr < bitstream.length) { + // for (const bitIndex of safebins) { + // if (bitPtr >= bitstream.length) break; - 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; - } - } + // 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; + // } + // } } } } catch (error: unknown) {