miniDSP
A small C library for audio DSP
Loading...
Searching...
No Matches
minidsp_steg.c
Go to the documentation of this file.
1
28
29#include "minidsp.h"
30#include "minidsp_internal.h"
31
32/* -----------------------------------------------------------------------
33 * Shared constants
34 * ----------------------------------------------------------------------- */
35
37#define HEADER_BITS 32
38
40#define PAYLOAD_TYPE_BIT (1u << 31)
41
43#define LENGTH_MASK 0x7FFFFFFFu
44
45/* -----------------------------------------------------------------------
46 * LSB steganography
47 *
48 * The host signal is in [-1, 1] double. We map it to a 16-bit PCM
49 * integer, flip the LSB to carry one message bit per sample, then map
50 * back. The distortion introduced is at most ±1/32768 ≈ −90 dB.
51 *
52 * Bit layout: [32 bits: length (LE)] [8*length bits: message bytes]
53 * Each bit uses one sample.
54 * ----------------------------------------------------------------------- */
55
57static double clamp(double x)
58{
59 if (x > 1.0) return 1.0;
60 if (x < -1.0) return -1.0;
61 return x;
62}
63
67static int double_to_pcm16(double sample)
68{
69 double s = clamp(sample);
70 int pcm = (s >= 0.0) ? (int)(s * 32767.0 + 0.5)
71 : (int)(s * 32767.0 - 0.5);
72 if (pcm > 32767) pcm = 32767;
73 if (pcm < -32767) pcm = -32767;
74 return pcm;
75}
76
80static double pcm16_to_double(int pcm)
81{
82 return (double)(float)((double)pcm / 32767.0);
83}
84
85static unsigned lsb_capacity(unsigned signal_len)
86{
87 if (signal_len <= HEADER_BITS)
88 return 0;
89 return (signal_len - HEADER_BITS) / 8;
90}
91
92static unsigned lsb_encode(const double *host, double *output,
93 unsigned signal_len,
94 const unsigned char *data, unsigned data_len,
95 unsigned flags)
96{
97 unsigned capacity = lsb_capacity(signal_len);
98 if (data_len == 0 || capacity == 0)
99 return 0;
100 if (data_len > capacity)
101 data_len = capacity;
102
103 /* Copy host to output. */
104 memcpy(output, host, signal_len * sizeof(double));
105
106 /* Encode the 32-bit length header (little-endian).
107 * Bit 31 carries the payload type flag. */
108 unsigned header = data_len | flags;
109 for (unsigned i = 0; i < HEADER_BITS; i++) {
110 unsigned bit = (header >> i) & 1;
111 int pcm = double_to_pcm16(output[i]);
112 /* Clear the LSB and set it to our message bit. */
113 pcm = (pcm & ~1) | (int)bit;
114 output[i] = pcm16_to_double(pcm);
115 }
116
117 /* Encode the data bytes. */
118 for (unsigned b = 0; b < data_len; b++) {
119 unsigned char ch = data[b];
120 for (unsigned bit_idx = 0; bit_idx < 8; bit_idx++) {
121 unsigned sample_idx = HEADER_BITS + b * 8 + bit_idx;
122 unsigned bit = (ch >> bit_idx) & 1;
123 int pcm = double_to_pcm16(output[sample_idx]);
124 pcm = (pcm & ~1) | (int)bit;
125 output[sample_idx] = pcm16_to_double(pcm);
126 }
127 }
128
129 return data_len;
130}
131
133static unsigned lsb_read_header(const double *signal)
134{
135 unsigned raw = 0;
136 for (unsigned i = 0; i < HEADER_BITS; i++) {
137 int pcm = double_to_pcm16(signal[i]);
138 unsigned bit = (unsigned)(pcm & 1);
139 raw |= (bit << i);
140 }
141 return raw;
142}
143
144static unsigned lsb_decode(const double *stego, unsigned signal_len,
145 unsigned char *data_out, unsigned max_len)
146{
147 if (signal_len <= HEADER_BITS || max_len == 0)
148 return 0;
149
150 unsigned msg_len = lsb_read_header(stego) & LENGTH_MASK;
151
152 /* Sanity check. */
153 unsigned capacity = lsb_capacity(signal_len);
154 if (msg_len == 0 || msg_len > capacity)
155 return 0;
156
157 unsigned decode_len = msg_len;
158 if (decode_len > max_len)
159 decode_len = max_len;
160
161 for (unsigned b = 0; b < decode_len; b++) {
162 unsigned char ch = 0;
163 for (unsigned bit_idx = 0; bit_idx < 8; bit_idx++) {
164 unsigned sample_idx = HEADER_BITS + b * 8 + bit_idx;
165 int pcm = double_to_pcm16(stego[sample_idx]);
166 unsigned bit = (unsigned)(pcm & 1);
167 ch |= (unsigned char)(bit << bit_idx);
168 }
169 data_out[b] = ch;
170 }
171 return decode_len;
172}
173
174/* -----------------------------------------------------------------------
175 * Frequency-band steganography (BFSK)
176 *
177 * Uses Binary Frequency-Shift Keying in the near-ultrasonic range:
178 * - bit 0 → tone at FREQ_LO (18.5 kHz)
179 * - bit 1 → tone at FREQ_HI (19.5 kHz)
180 *
181 * Each bit occupies CHIP_MS milliseconds of audio. The tone is mixed
182 * additively at a low amplitude so it's inaudible to most listeners.
183 *
184 * Decoding correlates each chip against both carriers and picks the
185 * stronger one.
186 *
187 * Bit layout: [32 bits: length (LE)] [8*length bits: message bytes]
188 * ----------------------------------------------------------------------- */
189
190#define FREQ_LO 18500.0
191#define FREQ_HI 19500.0
192#define CHIP_MS 3.0
193#define TONE_AMP 0.02
194
195static unsigned chip_samples(double sample_rate)
196{
197 return (unsigned)(CHIP_MS * sample_rate / 1000.0);
198}
199
200static unsigned freq_capacity_cs(unsigned signal_len, unsigned cs)
201{
202 if (cs == 0) return 0;
203 unsigned total_chips = signal_len / cs;
204 if (total_chips <= HEADER_BITS)
205 return 0;
206 return (total_chips - HEADER_BITS) / 8;
207}
208
209static unsigned freq_capacity(unsigned signal_len, double sample_rate)
210{
211 return freq_capacity_cs(signal_len, chip_samples(sample_rate));
212}
213
214/* -----------------------------------------------------------------------
215 * Cached BFSK carrier sine tables
216 *
217 * The sine lookup tables depend only on sample_rate (which determines
218 * chip_samples). We cache them across calls and only recompute when
219 * the sample rate changes. MD_shutdown() frees them via
220 * md_steg_teardown() (declared in minidsp_internal.h).
221 * ----------------------------------------------------------------------- */
222
223static double *_bfsk_sin_lo = NULL;
224static double *_bfsk_sin_hi = NULL;
225static unsigned _bfsk_cs = 0;
226
227static void _bfsk_setup(double sample_rate)
228{
229 unsigned cs = chip_samples(sample_rate);
230 if (cs == _bfsk_cs && _bfsk_sin_lo != NULL)
231 return;
232
233 free(_bfsk_sin_lo);
234 free(_bfsk_sin_hi);
235 _bfsk_sin_lo = malloc(cs * sizeof(double));
236 _bfsk_sin_hi = malloc(cs * sizeof(double));
237 MD_CHECK_VOID(_bfsk_sin_lo != NULL, MD_ERR_ALLOC_FAILED, "malloc failed");
238 MD_CHECK_VOID(_bfsk_sin_hi != NULL, MD_ERR_ALLOC_FAILED, "malloc failed");
239 for (unsigned s = 0; s < cs; s++) {
240 double t = (double)s / sample_rate;
241 _bfsk_sin_lo[s] = sin(2.0 * M_PI * FREQ_LO * t);
242 _bfsk_sin_hi[s] = sin(2.0 * M_PI * FREQ_HI * t);
243 }
244 _bfsk_cs = cs;
245}
246
248{
249 free(_bfsk_sin_lo);
250 free(_bfsk_sin_hi);
251 _bfsk_sin_lo = NULL;
252 _bfsk_sin_hi = NULL;
253 _bfsk_cs = 0;
254}
255
257static void encode_one_bit(double *output, unsigned signal_len,
258 unsigned chip_idx, unsigned cs,
259 const double *carrier)
260{
261 unsigned start = chip_idx * cs;
262 for (unsigned s = 0; s < cs && (start + s) < signal_len; s++)
263 output[start + s] += TONE_AMP * carrier[s];
264}
265
266static unsigned freq_encode(const double *host, double *output,
267 unsigned signal_len, double sample_rate,
268 const unsigned char *data, unsigned data_len,
269 unsigned flags)
270{
271 unsigned cs = chip_samples(sample_rate);
272 unsigned capacity = freq_capacity_cs(signal_len, cs);
273 if (data_len == 0 || capacity == 0)
274 return 0;
275 if (data_len > capacity)
276 data_len = capacity;
277
278 /* Copy host to output. */
279 memcpy(output, host, signal_len * sizeof(double));
280
281 /* Ensure cached sine carrier tables are current. */
282 _bfsk_setup(sample_rate);
283
284 /* Encode 32-bit length header (bit 31 carries payload type flag). */
285 unsigned header = data_len | flags;
286 for (unsigned i = 0; i < HEADER_BITS; i++) {
287 unsigned bit = (header >> i) & 1;
288 encode_one_bit(output, signal_len, i, cs,
289 bit ? _bfsk_sin_hi : _bfsk_sin_lo);
290 }
291
292 /* Encode data bytes. */
293 for (unsigned b = 0; b < data_len; b++) {
294 unsigned char ch = data[b];
295 for (unsigned bit_idx = 0; bit_idx < 8; bit_idx++) {
296 unsigned chip = HEADER_BITS + b * 8 + bit_idx;
297 unsigned bit = (ch >> bit_idx) & 1;
298 encode_one_bit(output, signal_len, chip, cs,
299 bit ? _bfsk_sin_hi : _bfsk_sin_lo);
300 }
301 }
302 return data_len;
303}
304
307static unsigned decode_one_bit(const double *stego, unsigned signal_len,
308 unsigned chip_idx, unsigned cs,
309 const double *sin_lo, const double *sin_hi,
310 double *corr_out)
311{
312 unsigned start = chip_idx * cs;
313 double corr_lo = 0.0, corr_hi = 0.0;
314 for (unsigned s = 0; s < cs && (start + s) < signal_len; s++) {
315 corr_lo += stego[start + s] * sin_lo[s];
316 corr_hi += stego[start + s] * sin_hi[s];
317 }
318 if (fabs(corr_hi) > fabs(corr_lo)) {
319 if (corr_out) *corr_out = fabs(corr_hi);
320 return 1u;
321 }
322 if (corr_out) *corr_out = fabs(corr_lo);
323 return 0u;
324}
325
326static unsigned freq_decode(const double *stego, unsigned signal_len,
327 double sample_rate,
328 unsigned char *data_out, unsigned max_len)
329{
330 unsigned cs = chip_samples(sample_rate);
331 if (cs == 0 || max_len == 0)
332 return 0;
333
334 unsigned total_chips = signal_len / cs;
335 if (total_chips <= HEADER_BITS)
336 return 0;
337
338 /* Ensure cached sine carrier tables are current. */
339 _bfsk_setup(sample_rate);
340
341 /* Read the 32-bit length header (bit 31 is payload type flag). */
342 unsigned raw_header = 0;
343 for (unsigned i = 0; i < HEADER_BITS; i++) {
344 unsigned bit = decode_one_bit(stego, signal_len, i, cs,
345 _bfsk_sin_lo, _bfsk_sin_hi,
346 NULL);
347 raw_header |= (bit << i);
348 }
349 unsigned msg_len = raw_header & LENGTH_MASK;
350
351 /* Sanity check. */
352 unsigned capacity = freq_capacity_cs(signal_len, cs);
353 if (msg_len == 0 || msg_len > capacity)
354 return 0;
355
356 unsigned decode_len = msg_len;
357 if (decode_len > max_len)
358 decode_len = max_len;
359
360 for (unsigned b = 0; b < decode_len; b++) {
361 unsigned char ch = 0;
362 for (unsigned bit_idx = 0; bit_idx < 8; bit_idx++) {
363 unsigned chip = HEADER_BITS + b * 8 + bit_idx;
364 unsigned bit = decode_one_bit(stego, signal_len, chip, cs,
365 _bfsk_sin_lo, _bfsk_sin_hi,
366 NULL);
367 ch |= (unsigned char)(bit << bit_idx);
368 }
369 data_out[b] = ch;
370 }
371
372 return decode_len;
373}
374
375/* -----------------------------------------------------------------------
376 * Spectrogram text steganography (hybrid LSB + visual)
377 *
378 * Combines LSB encoding (for machine-readable decode) with spectrogram
379 * text art in the 18-24 kHz ultrasonic band (for visual verification).
380 * Auto-upsamples to 48 kHz so Nyquist >= 24 kHz.
381 *
382 * Each bitmap column occupies a fixed 30 ms of audio; each character
383 * is 8 columns (5 data + 3 spacing) = 240 ms. The spectrogram text
384 * signal is scaled to SPECTEXT_AMP (0.02) after generation.
385 *
386 * Bit layout: identical to LSB (32-bit header + payload).
387 * ----------------------------------------------------------------------- */
388
389#define SPECTEXT_FREQ_LO 18000.0
390#define SPECTEXT_FREQ_HI 23500.0
391#define SPECTEXT_COL_MS 30.0
392#define SPECTEXT_COLS_PER_CHAR 8
393#define SPECTEXT_TARGET_SR 48000.0
394#define SPECTEXT_AMP 0.02
395#define SPECTEXT_NORM_PEAK 0.9
396#define SPECTEXT_PAD_SEC 0.25
397
399#define SPECTEXT_SEC_PER_CHAR \
400 (SPECTEXT_COL_MS / 1000.0 * SPECTEXT_COLS_PER_CHAR)
401
404static unsigned spectext_vis_capacity(double duration_sec)
405{
406 double available = duration_sec - SPECTEXT_PAD_SEC;
407 if (available <= 0.0) return 0;
408 return (unsigned)(available / SPECTEXT_SEC_PER_CHAR);
409}
410
412static unsigned spectext_output_len(unsigned signal_len, double sample_rate)
413{
414 if (sample_rate >= SPECTEXT_TARGET_SR)
415 return signal_len;
416 return MD_resample_output_len(signal_len, sample_rate, SPECTEXT_TARGET_SR);
417}
418
419static unsigned spectext_capacity(unsigned signal_len, double sample_rate)
420{
421 double duration_sec = (double)signal_len / sample_rate;
422 unsigned vis_cap = spectext_vis_capacity(duration_sec);
423 unsigned out_len = spectext_output_len(signal_len, sample_rate);
424 unsigned lsb_cap = lsb_capacity(out_len);
425 return (vis_cap < lsb_cap) ? vis_cap : lsb_cap;
426}
427
428static unsigned spectext_encode(const double *host, double *output,
429 unsigned signal_len, double sample_rate,
430 const unsigned char *data, unsigned data_len,
431 unsigned flags)
432{
433 double duration_sec = (double)signal_len / sample_rate;
434 unsigned out_len = spectext_output_len(signal_len, sample_rate);
435
436 /* Step 1: Upsample host to 48 kHz if needed.
437 * We need a mutable copy at 48 kHz to mix spectrogram art into
438 * BEFORE LSB encoding (LSB must be the last step, because adding
439 * any signal afterwards would disturb the LSB bits). */
440 double *mixed = malloc(out_len * sizeof(double));
441 MD_CHECK(mixed != NULL, MD_ERR_ALLOC_FAILED, "malloc failed", 0);
442
443 if (sample_rate < SPECTEXT_TARGET_SR) {
444 MD_resample(host, signal_len, mixed, out_len,
445 sample_rate, SPECTEXT_TARGET_SR, 128, 10.0);
446 /* Brickwall lowpass at the original Nyquist to eliminate any
447 * residual spectral images from the resampler transition band. */
448 MD_lowpass_brickwall(mixed, out_len,
449 sample_rate / 2.0, SPECTEXT_TARGET_SR);
450 } else {
451 memcpy(mixed, host, out_len * sizeof(double));
452 }
453
454 /* Step 2: Generate spectrogram text art and mix into host
455 * (before LSB so the LSB bits remain undisturbed). */
456 unsigned vis_chars = spectext_vis_capacity(duration_sec);
457 if (vis_chars > 0) {
458 /* For text payloads, show the message.
459 * For binary payloads, show "[BIN <N>B]". */
460 char vis_label[64];
461 const char *vis_text;
462
463 if (flags & PAYLOAD_TYPE_BIT) {
464 snprintf(vis_label, sizeof(vis_label), "[BIN %uB]", data_len);
465 vis_text = vis_label;
466 } else {
467 vis_text = (const char *)data;
468 }
469
470 unsigned text_len = (unsigned)strlen(vis_text);
471 if (text_len > vis_chars)
472 text_len = vis_chars;
473
474 /* Build a null-terminated substring if truncated. */
475 char *vis_substr = malloc(text_len + 1);
476 MD_CHECK(vis_substr != NULL, MD_ERR_ALLOC_FAILED, "malloc failed", 0);
477 memcpy(vis_substr, vis_text, text_len);
478 vis_substr[text_len] = '\0';
479
480 double vis_duration = (double)text_len * SPECTEXT_SEC_PER_CHAR;
481
482 double *specbuf = calloc(out_len, sizeof(double));
483 MD_CHECK(specbuf != NULL, MD_ERR_ALLOC_FAILED, "malloc failed", 0);
484
485 MD_spectrogram_text(specbuf, out_len, vis_substr,
487 vis_duration, SPECTEXT_TARGET_SR);
488
489 /* Scale to steganographic amplitude and mix into host,
490 * offset by the leading pad so text doesn't start at t=0. */
491 double scale = SPECTEXT_AMP / SPECTEXT_NORM_PEAK;
492 unsigned pad_samples = (unsigned)(SPECTEXT_PAD_SEC * SPECTEXT_TARGET_SR);
493 unsigned spec_samples = (unsigned)(vis_duration * SPECTEXT_TARGET_SR);
494 if (pad_samples + spec_samples > out_len)
495 spec_samples = (out_len > pad_samples) ? out_len - pad_samples : 0;
496 for (unsigned i = 0; i < spec_samples; i++)
497 mixed[pad_samples + i] += specbuf[i] * scale;
498
499 free(specbuf);
500 free(vis_substr);
501 }
502
503 /* Step 3: LSB-encode the full message (last step — must not
504 * add any signal afterwards or the LSB bits get disturbed). */
505 unsigned encoded = lsb_encode(mixed, output, out_len,
506 data, data_len, flags);
507 free(mixed);
508
509 return encoded;
510}
511
515static double spectext_ultrasonic_rms(const double *signal,
516 unsigned signal_len,
517 double sample_rate)
518{
519 /* Use a short window from the signal to check for ultrasonic energy.
520 * We compute a DFT and sum energy in the 18-24 kHz bins. */
521 unsigned fft_len = 4096;
522 if (fft_len > signal_len)
523 fft_len = signal_len;
524
525 /* Compute magnitude spectrum (3-arg: signal, N, mag_out). */
526 unsigned spec_len = fft_len / 2 + 1;
527 double *mag = malloc(spec_len * sizeof(double));
528 MD_CHECK(mag != NULL, MD_ERR_ALLOC_FAILED, "malloc failed", 0.0);
529
530 /* Use a segment from the middle of the signal. */
531 unsigned offset = 0;
532 if (signal_len > fft_len)
533 offset = (signal_len - fft_len) / 2;
534
535 double *windowed = malloc(fft_len * sizeof(double));
536 double *win = malloc(fft_len * sizeof(double));
537 MD_CHECK(windowed != NULL, MD_ERR_ALLOC_FAILED, "malloc failed", 0.0);
538 MD_CHECK(win != NULL, MD_ERR_ALLOC_FAILED, "malloc failed", 0.0);
539 MD_Gen_Hann_Win(win, fft_len);
540 for (unsigned i = 0; i < fft_len; i++)
541 windowed[i] = signal[offset + i] * win[i];
542 free(win);
543
544 MD_magnitude_spectrum(windowed, fft_len, mag);
545
546 /* Sum energy in the 18-24 kHz range. */
547 double bin_hz = sample_rate / (double)fft_len;
548 unsigned bin_lo = (unsigned)(SPECTEXT_FREQ_LO / bin_hz);
549 unsigned bin_hi = (unsigned)(SPECTEXT_FREQ_HI / bin_hz);
550 if (bin_hi >= spec_len)
551 bin_hi = spec_len - 1;
552
553 double energy = 0.0;
554 unsigned count = 0;
555 for (unsigned k = bin_lo; k <= bin_hi; k++) {
556 energy += mag[k] * mag[k];
557 count++;
558 }
559
560 free(windowed);
561 free(mag);
562
563 if (count == 0) return 0.0;
564 return sqrt(energy / (double)count);
565}
566
567/* -----------------------------------------------------------------------
568 * Public API
569 * ----------------------------------------------------------------------- */
570
571unsigned MD_steg_capacity(unsigned signal_len, double sample_rate, int method)
572{
573 MD_CHECK(signal_len > 0, MD_ERR_INVALID_SIZE, "signal_len must be > 0", 0);
574 MD_CHECK(sample_rate > 0.0, MD_ERR_INVALID_RANGE, "sample_rate must be > 0", 0);
575 MD_CHECK(method == MD_STEG_LSB || method == MD_STEG_FREQ_BAND ||
576 method == MD_STEG_SPECTEXT,
577 MD_ERR_INVALID_RANGE, "invalid steganography method", 0);
578
579 if (method == MD_STEG_LSB)
580 return lsb_capacity(signal_len);
581 else if (method == MD_STEG_SPECTEXT)
582 return spectext_capacity(signal_len, sample_rate);
583 else
584 return freq_capacity(signal_len, sample_rate);
585}
586
588static unsigned encode_common(const double *host, double *output,
589 unsigned signal_len, double sample_rate,
590 const unsigned char *data, unsigned data_len,
591 int method, unsigned flags)
592{
593 MD_CHECK(host != NULL, MD_ERR_NULL_POINTER, "host must not be NULL", 0);
594 MD_CHECK(output != NULL, MD_ERR_NULL_POINTER, "output must not be NULL", 0);
595 MD_CHECK(signal_len > 0, MD_ERR_INVALID_SIZE, "signal_len must be > 0", 0);
596 MD_CHECK(sample_rate > 0.0, MD_ERR_INVALID_RANGE, "sample_rate must be > 0", 0);
597 MD_CHECK(data != NULL, MD_ERR_NULL_POINTER, "data must not be NULL", 0);
598 MD_CHECK(method == MD_STEG_LSB || method == MD_STEG_FREQ_BAND ||
599 method == MD_STEG_SPECTEXT,
600 MD_ERR_INVALID_RANGE, "invalid steganography method", 0);
601
602 if (method == MD_STEG_FREQ_BAND)
603 MD_CHECK(sample_rate >= 40000.0, MD_ERR_INVALID_RANGE,
604 "frequency-band steganography requires sample_rate >= 40 kHz", 0);
605
606 if (method == MD_STEG_LSB)
607 return lsb_encode(host, output, signal_len, data, data_len, flags);
608 else if (method == MD_STEG_SPECTEXT)
609 return spectext_encode(host, output, signal_len, sample_rate,
610 data, data_len, flags);
611 else
612 return freq_encode(host, output, signal_len, sample_rate,
613 data, data_len, flags);
614}
615
616unsigned MD_steg_encode_bytes(const double *host, double *output,
617 unsigned signal_len, double sample_rate,
618 const unsigned char *data, unsigned data_len,
619 int method)
620{
621 return encode_common(host, output, signal_len, sample_rate,
622 data, data_len, method, PAYLOAD_TYPE_BIT);
623}
624
625unsigned MD_steg_decode_bytes(const double *stego, unsigned signal_len,
626 double sample_rate,
627 unsigned char *data_out, unsigned max_len,
628 int method)
629{
630 MD_CHECK(stego != NULL, MD_ERR_NULL_POINTER, "stego must not be NULL", 0);
631 MD_CHECK(signal_len > 0, MD_ERR_INVALID_SIZE, "signal_len must be > 0", 0);
632 MD_CHECK(sample_rate > 0.0, MD_ERR_INVALID_RANGE, "sample_rate must be > 0", 0);
633 MD_CHECK(data_out != NULL, MD_ERR_NULL_POINTER, "data_out must not be NULL", 0);
634 MD_CHECK(max_len > 0, MD_ERR_INVALID_SIZE, "max_len must be > 0", 0);
635 MD_CHECK(method == MD_STEG_LSB || method == MD_STEG_FREQ_BAND ||
636 method == MD_STEG_SPECTEXT,
637 MD_ERR_INVALID_RANGE, "invalid steganography method", 0);
638
639 if (method == MD_STEG_LSB || method == MD_STEG_SPECTEXT)
640 return lsb_decode(stego, signal_len, data_out, max_len);
641 else
642 return freq_decode(stego, signal_len, sample_rate,
643 data_out, max_len);
644}
645
646unsigned MD_steg_encode(const double *host, double *output,
647 unsigned signal_len, double sample_rate,
648 const char *message, int method)
649{
650 MD_CHECK(message != NULL, MD_ERR_NULL_POINTER, "message must not be NULL", 0);
651 return encode_common(host, output, signal_len, sample_rate,
652 (const unsigned char *)message,
653 (unsigned)strlen(message), method, 0);
654}
655
656unsigned MD_steg_decode(const double *stego, unsigned signal_len,
657 double sample_rate,
658 char *message_out, unsigned max_msg_len,
659 int method)
660{
661 MD_CHECK(message_out != NULL, MD_ERR_NULL_POINTER, "message_out must not be NULL", 0);
662 MD_CHECK(max_msg_len > 0, MD_ERR_INVALID_SIZE, "max_msg_len must be > 0", 0);
663 unsigned decoded = MD_steg_decode_bytes(stego, signal_len, sample_rate,
664 (unsigned char *)message_out,
665 max_msg_len - 1, method);
666 message_out[decoded] = '\0';
667 return decoded;
668}
669
670int MD_steg_detect(const double *signal, unsigned signal_len,
671 double sample_rate, int *payload_type_out)
672{
673 MD_CHECK(signal != NULL, MD_ERR_NULL_POINTER, "signal must not be NULL", -1);
674 MD_CHECK(signal_len > 0, MD_ERR_INVALID_SIZE, "signal_len must be > 0", -1);
675 MD_CHECK(sample_rate > 0.0, MD_ERR_INVALID_RANGE, "sample_rate must be > 0", -1);
676
677 int found_method = -1;
678 unsigned found_header = 0;
679
680 /* --- BFSK probe (only when sample_rate >= 40 kHz) --- */
681 if (sample_rate >= 40000.0) {
682 unsigned cs = chip_samples(sample_rate);
683 if (cs > 0) {
684 unsigned total_chips = signal_len / cs;
685 if (total_chips > HEADER_BITS) {
686 _bfsk_setup(sample_rate);
687
688 unsigned raw_header = 0;
689 double corr_sum = 0.0;
690 for (unsigned i = 0; i < HEADER_BITS; i++) {
691 double corr;
692 unsigned bit = decode_one_bit(
693 signal, signal_len, i, cs,
694 _bfsk_sin_lo, _bfsk_sin_hi, &corr);
695 raw_header |= (bit << i);
696 corr_sum += corr;
697 }
698 unsigned msg_len = raw_header & LENGTH_MASK;
699 unsigned capacity = freq_capacity_cs(signal_len, cs);
700 double avg_corr = corr_sum / HEADER_BITS;
701 double threshold = 0.25 * TONE_AMP * (double)cs / 2.0;
702
703 if (msg_len > 0 && msg_len <= capacity &&
704 avg_corr >= threshold) {
705 found_method = MD_STEG_FREQ_BAND;
706 found_header = raw_header;
707 }
708 }
709 }
710 }
711
712 /* --- LSB probe (also covers spectext, which uses LSB + ultrasonic art) --- */
713 if (found_method < 0 && signal_len > HEADER_BITS) {
714 unsigned raw_header = lsb_read_header(signal);
715 unsigned msg_len = raw_header & LENGTH_MASK;
716 unsigned capacity = lsb_capacity(signal_len);
717
718 if (msg_len > 0 && msg_len <= capacity) {
719 /* Check for ultrasonic energy to distinguish spectext from LSB. */
720 if (sample_rate >= SPECTEXT_TARGET_SR) {
721 double ultra_rms = spectext_ultrasonic_rms(signal, signal_len,
722 sample_rate);
723 if (ultra_rms > 1e-4) {
724 found_method = MD_STEG_SPECTEXT;
725 found_header = raw_header;
726 } else {
727 found_method = MD_STEG_LSB;
728 found_header = raw_header;
729 }
730 } else {
731 found_method = MD_STEG_LSB;
732 found_header = raw_header;
733 }
734 }
735 }
736
737 if (found_method >= 0 && payload_type_out != NULL)
738 *payload_type_out = (found_header >> 31) ? MD_STEG_TYPE_BINARY
740
741 return found_method;
742}
A mini library of DSP (Digital Signal Processing) routines.
@ MD_ERR_INVALID_SIZE
A size or count argument is invalid (e.g.
Definition minidsp.h:63
@ MD_ERR_INVALID_RANGE
A range or bound is invalid (e.g.
Definition minidsp.h:64
@ MD_ERR_ALLOC_FAILED
A memory allocation failed.
Definition minidsp.h:65
@ MD_ERR_NULL_POINTER
A required pointer argument is NULL.
Definition minidsp.h:62
unsigned MD_resample_output_len(unsigned input_len, double in_rate, double out_rate)
Compute the output buffer size needed for resampling.
#define MD_STEG_SPECTEXT
Steganography method: hybrid LSB + spectrogram text art.
Definition minidsp.h:1556
unsigned MD_spectrogram_text(double *output, unsigned max_len, const char *text, double freq_lo, double freq_hi, double duration_sec, double sample_rate)
Synthesise audio that displays readable text in a spectrogram.
#define MD_STEG_TYPE_TEXT
Payload type flag: text (null-terminated string).
Definition minidsp.h:1559
void MD_Gen_Hann_Win(double *out, unsigned n)
Generate a Hanning (Hann) window of length n.
#define MD_STEG_TYPE_BINARY
Payload type flag: binary (raw byte buffer).
Definition minidsp.h:1561
unsigned MD_resample(const double *input, unsigned input_len, double *output, unsigned max_output_len, double in_rate, double out_rate, unsigned num_zero_crossings, double kaiser_beta)
Resample a signal from one sample rate to another using polyphase sinc interpolation.
void MD_lowpass_brickwall(double *signal, unsigned len, double cutoff_hz, double sample_rate)
Apply a brickwall lowpass filter to a signal in-place.
#define MD_STEG_FREQ_BAND
Steganography method: near-ultrasonic frequency-band modulation (BFSK).
Definition minidsp.h:1540
void MD_magnitude_spectrum(const double *signal, unsigned N, double *mag_out)
Compute the magnitude spectrum of a real-valued signal.
#define MD_STEG_LSB
Steganography method: least-significant-bit encoding.
Definition minidsp.h:1538
Internal header for cross-file dependencies within the minidsp module.
#define MD_CHECK(cond, code, msg, retval)
Check a precondition in a function that returns a value.
#define MD_CHECK_VOID(cond, code, msg)
Check a precondition in a void function.
#define PAYLOAD_TYPE_BIT
Bit 31 of the header: payload type flag (0 = text, 1 = binary).
unsigned MD_steg_capacity(unsigned signal_len, double sample_rate, int method)
Compute the maximum message length (in bytes) that can be hidden.
unsigned MD_steg_encode_bytes(const double *host, double *output, unsigned signal_len, double sample_rate, const unsigned char *data, unsigned data_len, int method)
Encode arbitrary binary data into a host audio signal.
#define HEADER_BITS
Bits needed for the message-length header (32-bit unsigned).
static unsigned spectext_vis_capacity(double duration_sec)
Maximum number of visually displayable characters for a given duration.
unsigned MD_steg_decode_bytes(const double *stego, unsigned signal_len, double sample_rate, unsigned char *data_out, unsigned max_len, int method)
Decode binary data from a stego audio signal.
static int double_to_pcm16(double sample)
Convert a double sample in [-1, 1] to a signed 16-bit PCM value.
#define SPECTEXT_SEC_PER_CHAR
Seconds per character in the spectrogram visual.
static double pcm16_to_double(int pcm)
Convert a signed 16-bit PCM value back to double in [-1, 1].
unsigned MD_steg_decode(const double *stego, unsigned signal_len, double sample_rate, char *message_out, unsigned max_msg_len, int method)
Decode a secret message from a stego audio signal.
void md_steg_teardown(void)
Tear down the BFSK sine carrier cache.
#define SPECTEXT_NORM_PEAK
MD_spectrogram_text peak.
#define SPECTEXT_PAD_SEC
Silence before spectrogram text (s).
#define CHIP_MS
Duration of one bit chip (ms).
#define FREQ_LO
Carrier for bit 0 (Hz).
static unsigned spectext_output_len(unsigned signal_len, double sample_rate)
Compute the output signal length at 48 kHz for a given input.
#define FREQ_HI
Carrier for bit 1 (Hz).
static unsigned encode_common(const double *host, double *output, unsigned signal_len, double sample_rate, const unsigned char *data, unsigned data_len, int method, unsigned flags)
Shared encode logic for both text and binary public API functions.
#define SPECTEXT_AMP
Spectrogram art amplitude.
static double clamp(double x)
Clamp a double to [-1, 1].
unsigned MD_steg_encode(const double *host, double *output, unsigned signal_len, double sample_rate, const char *message, int method)
Encode a secret message into a host audio signal.
#define TONE_AMP
Additive tone amplitude.
int MD_steg_detect(const double *signal, unsigned signal_len, double sample_rate, int *payload_type_out)
Detect which steganography method (if any) was used to encode a signal.
#define SPECTEXT_FREQ_HI
Top of visual band (Hz, below Nyquist).
#define SPECTEXT_TARGET_SR
Output sample rate (Hz).
#define SPECTEXT_FREQ_LO
Bottom of visual band (Hz).
static unsigned decode_one_bit(const double *stego, unsigned signal_len, unsigned chip_idx, unsigned cs, const double *sin_lo, const double *sin_hi, double *corr_out)
Decode one bit by correlating a chip against precomputed BFSK carriers.
static void encode_one_bit(double *output, unsigned signal_len, unsigned chip_idx, unsigned cs, const double *carrier)
Encode one bit by adding a precomputed BFSK tone burst at the given chip.
#define LENGTH_MASK
Mask to extract the message length from the raw header (bits 0–30).
static double spectext_ultrasonic_rms(const double *signal, unsigned signal_len, double sample_rate)
Probe for ultrasonic energy in the 18-24 kHz band.
static unsigned lsb_read_header(const double *signal)
Read the raw 32-bit LSB header from the first HEADER_BITS samples.