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", 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; + } +} 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; +} 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; + } +} 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; + } +} 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; + } +} 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; } 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) {