From 1a5a99447426942b1755a1f2695ed487116c5168 Mon Sep 17 00:00:00 2001 From: bruno Date: Mon, 3 Nov 2025 21:07:16 +0100 Subject: [PATCH] Init --- .idea/.gitignore | 8 + .idea/audiohide.iml | 2 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + CMakeLists.txt | 7 + main.c | 479 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 517 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/audiohide.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CMakeLists.txt create mode 100644 main.c diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/audiohide.iml b/.idea/audiohide.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/audiohide.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0b76fe5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7203a55 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b09b71a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 4.1) +project(audiohide C) + +set(CMAKE_C_STANDARD 23) + +add_executable(audiohide main.c) +target_link_libraries(audiohide sndfile fftw3f m) \ No newline at end of file diff --git a/main.c b/main.c new file mode 100644 index 0000000..4e0b944 --- /dev/null +++ b/main.c @@ -0,0 +1,479 @@ +/* watermark.c + * + * DSSS-style audio watermark embedder/extractor prototype in C. + * Dependencies: libsndfile, fftw3f, math + * + * Build: + * gcc -O2 -o audio_dsss watermark.c -lsndfile -lfftw3f -lm + * + * Modes: + * embed in.wav out.wav message.txt seed + * extract in.wav out.bin seed + * test in.wav message.txt seed (creates watermarked.wav + extracted.bin and compares) + * + * Notes: + * - Mono conversion: stereo averaged to mono. + * - Adds 4-byte little-endian length prefix to payload. + * - Uses r2c/c2r FFT for correct real-frame handling and proper normalization. + */ + +#include +#include +#include +#include +#include +#include +#include + +#define FRAME_SIZE 2048 +#define HOP 512 +#define MIN_FREQ 1000.0 +#define MAX_FREQ 5000.0 +#define ALPHA 1.0f +#define FRAMES_PER_BIT 4 +#define MAX_BITS 32768 + +static float hann_window(int n, int i) { + return 0.5f * (1.0f - cosf(2.0f * M_PI * i / (n - 1))); +} + +static unsigned char *load_file(const char *path, size_t *out_size) { + FILE *f = fopen(path, "rb"); + if (!f) { perror("fopen message"); return NULL; } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz <= 0) { fclose(f); *out_size = 0; return NULL; } + unsigned char *buf = malloc(sz); + if (!buf) { fclose(f); return NULL; } + if (fread(buf, 1, sz, f) != (size_t)sz) { perror("fread"); free(buf); fclose(f); return NULL; } + fclose(f); + *out_size = sz; + return buf; +} + +static int write_file(const char *path, const unsigned char *buf, size_t sz) { + FILE *f = fopen(path, "wb"); + if (!f) { perror("fopen out"); return -1; } + if (fwrite(buf, 1, sz, f) != sz) { perror("fwrite"); fclose(f); return -1; } + fclose(f); + return 0; +} + +/* bytes -> bits LSB-first */ +static int bytes_to_bits(const unsigned char *bytes, size_t nbytes, int *bits, size_t *nbits) { + size_t maxbits = nbytes * 8; + if (maxbits > MAX_BITS) return -1; + for (size_t i = 0; i < nbytes; ++i) { + for (int b = 0; b < 8; ++b) bits[i*8 + b] = (bytes[i] >> b) & 1; + } + *nbits = maxbits; + return 0; +} + +/* bits -> bytes LSB-first */ +static size_t bits_to_bytes(const int *bits, size_t nbits, unsigned char *out) { + size_t nbytes = (nbits + 7) / 8; + memset(out, 0, nbytes); + for (size_t i = 0; i < nbits; ++i) if (bits[i]) out[i/8] |= (1u << (i%8)); + return nbytes; +} + +/* xorshift32 */ +static uint32_t prng_next(uint32_t *state) { + uint32_t x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +/* PN chips ±1 */ +static void generate_pn(int *chips, size_t len, uint32_t *seed) { + for (size_t i = 0; i < len; ++i) chips[i] = (prng_next(seed) & 1) ? 1 : -1; +} + +/* frames count */ +static int frames_count(int signal_len, int frame_size, int hop) { + if (signal_len < frame_size) return 0; + return 1 + (signal_len - frame_size) / hop; +} + +/* mono conversion */ +static float *to_mono(const float *in, sf_count_t frames, int channels) { + float *mono = malloc(sizeof(float) * frames); + if (!mono) return NULL; + if (channels == 1) { + for (sf_count_t i = 0; i < frames; ++i) mono[i] = in[i]; + } else { + for (sf_count_t i = 0; i < frames; ++i) { + float s = 0.0f; + for (int c = 0; c < channels; ++c) s += in[i*channels + c]; + mono[i] = s / channels; + } + } + return mono; +} + +/* prepend 32-bit little-endian length to message buffer */ +static unsigned char *prepend_length(const unsigned char *msg, size_t msgsz, size_t *out_sz) { + if (!msg || !out_sz) return NULL; // sanity check + size_t total = msgsz + 4; // 4 bytes for length + unsigned char *buf = malloc(total); + if (!buf) return NULL; + + // store message length in little-endian format + uint32_t len32 = (uint32_t)msgsz; + buf[0] = (unsigned char)(len32 & 0xFF); + buf[1] = (unsigned char)((len32 >> 8) & 0xFF); + buf[2] = (unsigned char)((len32 >> 16) & 0xFF); + buf[3] = (unsigned char)((len32 >> 24) & 0xFF); + + memcpy(buf + 4, msg, msgsz); // copy message after prefix + *out_sz = total; + return buf; +} + + +/* ---------- Embed ---------- */ +static int embed(const char *in_wav, const char *out_wav, const char *message_path, uint32_t seed) { + SF_INFO sfinfo; + memset(&sfinfo, 0, sizeof(sfinfo)); + SNDFILE *infile = sf_open(in_wav, SFM_READ, &sfinfo); + if (!infile) { fprintf(stderr, "sf_open input failed: %s\n", in_wav); return 1; } + + sf_count_t nframes = sfinfo.frames; + int channels = sfinfo.channels; + float *interleaved = malloc(sizeof(float) * nframes * channels); + if (!interleaved) { sf_close(infile); return 1; } + if (sf_readf_float(infile, interleaved, nframes) != nframes) { + fprintf(stderr, "sf_readf_float failed\n"); free(interleaved); sf_close(infile); return 1; + } + sf_close(infile); + + float *audio = to_mono(interleaved, nframes, channels); + free(interleaved); + if (!audio) { fprintf(stderr, "to_mono failed\n"); return 1; } + + size_t msgsz; + unsigned char *msg = load_file(message_path, &msgsz); + if (!msg) { free(audio); fprintf(stderr, "load_file failed\n"); return 1; } + + size_t msg_with_len_sz; + unsigned char *msg_with_len = prepend_length(msg, msgsz, &msg_with_len_sz); + free(msg); + if (!msg_with_len) { free(audio); fprintf(stderr, "prepend_length failed\n"); return 1; } + + int *bits = malloc(MAX_BITS * sizeof(int)); + size_t nbits; + if (bytes_to_bits(msg_with_len, msg_with_len_sz, bits, &nbits) != 0) { + fprintf(stderr, "message too large\n"); free(msg_with_len); free(audio); free(bits); return 1; + } + free(msg_with_len); + + int frame_size = FRAME_SIZE; + int hop = HOP; + int nfft = frame_size; + int nframes_stft = frames_count((int)nframes, frame_size, hop); + if (nframes_stft <= 0) { + fprintf(stderr, "audio too short for frame_size\n"); free(bits); free(audio); return 1; + } + + float *window = malloc(sizeof(float) * frame_size); + for (int i = 0; i < frame_size; ++i) window[i] = hann_window(frame_size, i); + + /* r2c: output bins = nfft/2 + 1 */ + int nbins = nfft/2 + 1; + + fftwf_plan plan_fwd = fftwf_plan_dft_r2c_1d(nfft, NULL, NULL, FFTW_ESTIMATE); + fftwf_plan plan_inv = fftwf_plan_dft_c2r_1d(nfft, NULL, NULL, FFTW_ESTIMATE); + /* We'll create per-frame buffers */ + float *frame_in = fftwf_alloc_real(nfft); + fftwf_complex *spec = fftwf_alloc_complex(nbins); + float *ifft_out = fftwf_alloc_real(nfft); + + /* Frequency bin indices for chosen band */ + double sr = sfinfo.samplerate; + int bin_min = (int)floor(MIN_FREQ / sr * nfft); + int bin_max = (int)ceil(MAX_FREQ / sr * nfft); + if (bin_min < 1) bin_min = 1; + if (bin_max > nfft/2) bin_max = nfft/2; + int target_bins = bin_max - bin_min + 1; + if (target_bins <= 0) { fprintf(stderr, "bad freq band selection\n"); goto cleanup_embed; } + + float *out_audio = calloc(nframes, sizeof(float)); + if (!out_audio) { fprintf(stderr, "alloc out_audio failed\n"); goto cleanup_embed; } + + int max_bits_possible = nframes_stft / FRAMES_PER_BIT; + if ((int)nbits > max_bits_possible) { + fprintf(stderr, "Warning: message (%zu bits) longer than capacity (%d bits). Truncating.\n", nbits, max_bits_possible); + nbits = max_bits_possible; + } + + uint32_t prng = seed; + int sample_idx = 0; + int frame_no = 0; + + /* create actual forward and inverse plans bound to buffers (since we need to execute them) */ + fftwf_destroy_plan(plan_fwd); + fftwf_destroy_plan(plan_inv); + plan_fwd = fftwf_plan_dft_r2c_1d(nfft, frame_in, spec, FFTW_MEASURE); + plan_inv = fftwf_plan_dft_c2r_1d(nfft, spec, ifft_out, FFTW_MEASURE); + + while (sample_idx + frame_size <= (int)nframes) { + /* prepare input */ + for (int i = 0; i < nfft; ++i) { + float v = 0.0f; + if (i < frame_size) v = audio[sample_idx + i] * window[i]; + frame_in[i] = v; + } + fftwf_execute(plan_fwd); + + int which_bit = frame_no / FRAMES_PER_BIT; + if (which_bit < (int)nbits) { + int b = bits[which_bit]; + int *chips = malloc(sizeof(int) * target_bins); + generate_pn(chips, target_bins, &prng); + for (int k = 0; k < target_bins; ++k) { + int bin = bin_min + k; + /* spec[bin] is complex: [real, imag] */ + float re = spec[bin][0]; + float im = spec[bin][1]; + float mag = sqrtf(re*re + im*im); + float scale = ALPHA * (1.0f + mag); + int sign = chips[k] * (b ? 1 : -1); + /* small complex perturbation */ + spec[bin][0] += sign * scale * 0.5f; + spec[bin][1] += sign * scale * 0.5f; + } + free(chips); + } + + /* inverse */ + fftwf_execute(plan_inv); + + /* normalize and overlap-add */ + for (int i = 0; i < frame_size; ++i) { + float v = ifft_out[i] / (float)nfft; /* normalization */ + out_audio[sample_idx + i] += v * window[i]; + } + + sample_idx += hop; + frame_no++; + } + + /* Fill any uncovered samples from original */ + for (int i = 0; i < (int)nframes; ++i) { + if (!isfinite(out_audio[i])) out_audio[i] = audio[i]; + } + + /* Write WAV */ + SF_INFO outinfo; + memset(&outinfo, 0, sizeof(outinfo)); + outinfo.samplerate = sfinfo.samplerate; + outinfo.channels = 1; + outinfo.format = sfinfo.format; + SNDFILE *outfile = sf_open(out_wav, SFM_WRITE, &outinfo); + if (!outfile) { fprintf(stderr, "sf_open out failed\n"); free(out_audio); goto cleanup_embed; } + if (sf_writef_float(outfile, out_audio, nframes) != nframes) { + fprintf(stderr, "sf_writef_float failed\n"); + } + sf_close(outfile); + + printf("Embedding done. Embedded %zu bits (approx %zu bytes).\n", nbits, nbits/8); + + free(out_audio); + cleanup_embed: + free(bits); + fftwf_free(frame_in); + fftwf_free(spec); + fftwf_free(ifft_out); + fftwf_destroy_plan(plan_fwd); + fftwf_destroy_plan(plan_inv); + free(window); + free(audio); + return 0; +} + +/* ---------- Extract ---------- */ +static int extract(const char *in_wav, const char *out_msg_path, uint32_t seed) { + SF_INFO sfinfo; + memset(&sfinfo, 0, sizeof(sfinfo)); + SNDFILE *infile = sf_open(in_wav, SFM_READ, &sfinfo); + if (!infile) { fprintf(stderr, "sf_open input failed: %s\n", in_wav); return 1; } + + sf_count_t nframes = sfinfo.frames; + int channels = sfinfo.channels; + float *interleaved = malloc(sizeof(float) * nframes * channels); + if (!interleaved) { sf_close(infile); return 1; } + if (sf_readf_float(infile, interleaved, nframes) != nframes) { + fprintf(stderr, "sf_readf_float failed\n"); free(interleaved); sf_close(infile); return 1; + } + sf_close(infile); + + float *audio = to_mono(interleaved, nframes, channels); + free(interleaved); + if (!audio) { fprintf(stderr, "to_mono failed\n"); return 1; } + + int frame_size = FRAME_SIZE; + int hop = HOP; + int nfft = frame_size; + int nframes_stft = frames_count((int)nframes, frame_size, hop); + if (nframes_stft <= 0) { fprintf(stderr, "audio too short for frame size\n"); free(audio); return 1; } + + float *window = malloc(sizeof(float) * frame_size); + for (int i = 0; i < frame_size; ++i) window[i] = hann_window(frame_size, i); + + int nbins = nfft/2 + 1; + fftwf_plan plan_fwd = fftwf_plan_dft_r2c_1d(nfft, NULL, NULL, FFTW_ESTIMATE); + float *frame_in = fftwf_alloc_real(nfft); + fftwf_complex *spec = fftwf_alloc_complex(nbins); + + fftwf_destroy_plan(plan_fwd); + plan_fwd = fftwf_plan_dft_r2c_1d(nfft, frame_in, spec, FFTW_MEASURE); + + double sr = sfinfo.samplerate; + int bin_min = (int)floor(MIN_FREQ / sr * nfft); + int bin_max = (int)ceil(MAX_FREQ / sr * nfft); + if (bin_min < 1) bin_min = 1; + if (bin_max > nfft/2) bin_max = nfft/2; + int target_bins = bin_max - bin_min + 1; + if (target_bins <= 0) { fprintf(stderr, "bad freq band\n"); goto cleanup_extract; } + + float *frame_corr = calloc(nframes_stft, sizeof(float)); + if (!frame_corr) { fprintf(stderr, "alloc frame_corr failed\n"); goto cleanup_extract; } + + uint32_t prng = seed; + int sample_idx = 0; + int frame_no = 0; + while (sample_idx + frame_size <= (int)nframes) { + for (int i = 0; i < nfft; ++i) frame_in[i] = (i < frame_size) ? (audio[sample_idx + i] * window[i]) : 0.0f; + fftwf_execute(plan_fwd); + + int *chips = malloc(sizeof(int) * target_bins); + generate_pn(chips, target_bins, &prng); + + double corr = 0.0; + for (int k = 0; k < target_bins; ++k) { + int bin = bin_min + k; + float re = spec[bin][0]; + float im = spec[bin][1]; + float mag = sqrtf(re*re + im*im) + 1e-9f; + /* correlate normalized real part with chip */ + corr += (re / mag) * (double)chips[k]; + } + frame_corr[frame_no] = (float)corr; + free(chips); + + sample_idx += hop; + frame_no++; + } + + int max_possible_bits = nframes_stft / FRAMES_PER_BIT; + int *recovered_bits = malloc(sizeof(int) * max_possible_bits); + if (!recovered_bits) { fprintf(stderr, "alloc recovered_bits failed\n"); goto cleanup_extract; } + + for (int b = 0; b < max_possible_bits; ++b) { + double sum = 0.0; + for (int f = 0; f < FRAMES_PER_BIT; ++f) { + int fi = b * FRAMES_PER_BIT + f; + if (fi >= nframes_stft) break; + sum += frame_corr[fi]; + } + recovered_bits[b] = (sum > 0.0) ? 1 : 0; + } + + /* Convert to bytes */ + size_t nbits_out = max_possible_bits; + unsigned char *outbytes = malloc((nbits_out/8 + 8)); + memset(outbytes, 0, (nbits_out/8 + 8)); + size_t nbytes = bits_to_bytes(recovered_bits, nbits_out, outbytes); + + /* Read first 4 bytes as length prefix (little-endian) */ + if (nbytes < 4) { + fprintf(stderr, "extracted data too small to contain length prefix\n"); + write_file(out_msg_path, outbytes, nbytes); + goto cleanup_extract; + } + uint32_t claimed_len = (uint32_t)outbytes[0] | ((uint32_t)outbytes[1] << 8) | ((uint32_t)outbytes[2] << 16) | ((uint32_t)outbytes[3] << 24); + if (claimed_len > nbytes - 4) { + fprintf(stderr, "warning: claimed length %u larger than extracted bytes %zu (truncated)\n", claimed_len, nbytes - 4); + claimed_len = (uint32_t)(nbytes - 4); + } + + if (write_file(out_msg_path, outbytes + 4, claimed_len) != 0) { + fprintf(stderr, "write_file failed\n"); + } else { + printf("Extraction produced %u bytes (after length prefix).\n", claimed_len); + } + + cleanup_extract: + if (frame_corr) free(frame_corr); + if (recovered_bits) free(recovered_bits); + if (outbytes) free(outbytes); + fftwf_free(frame_in); + fftwf_free(spec); + fftwf_destroy_plan(plan_fwd); + free(window); + free(audio); + return 0; +} + +/* ---------- main ---------- */ +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage:\n %s embed in.wav out.wav message.txt seed\n %s extract in.wav out.bin seed\n %s test in.wav message.txt seed\n", argv[0], argv[0], argv[0]); + return 1; + } + if (strcmp(argv[1], "embed") == 0) { + if (argc < 6) { fprintf(stderr, "embed needs in out message seed\n"); return 1; } + uint32_t seed = (uint32_t)atoi(argv[5]); + return embed(argv[2], argv[3], argv[4], seed); + } else if (strcmp(argv[1], "extract") == 0) { + if (argc < 5) { fprintf(stderr, "extract needs in out seed\n"); return 1; } + uint32_t seed = (uint32_t)atoi(argv[4]); + return extract(argv[2], argv[3], seed); + } else if (strcmp(argv[1], "test") == 0) { + if (argc < 5) { fprintf(stderr, "test needs in message seed\n"); return 1; } + const char *inwav = argv[2]; + const char *msg = argv[3]; + uint32_t seed = (uint32_t)atoi(argv[4]); + const char *watermarked = "watermarked.wav"; + const char *extracted = "extracted.bin"; + printf("Test mode: embedding %s into %s with seed %u -> %s\n", msg, inwav, seed, watermarked); + int r = embed(inwav, watermarked, msg, seed); + if (r != 0) { fprintf(stderr, "embed failed\n"); return r; } + printf("Now extracting from %s -> %s\n", watermarked, extracted); + r = extract(watermarked, extracted, seed); + if (r != 0) { fprintf(stderr, "extract failed\n"); return r; } + /* compare original message and extracted */ + size_t orig_sz; + unsigned char *orig = load_file(msg, &orig_sz); + size_t ext_sz; + unsigned char *ext = load_file(extracted, &ext_sz); + if (!orig || !ext) { + fprintf(stderr, "couldn't read original or extracted for comparison\n"); + free(orig); free(ext); + return 0; + } + if (orig_sz == ext_sz && memcmp(orig, ext, orig_sz) == 0) { + printf("SUCCESS: extracted matches original (%zu bytes)\n", orig_sz); + } else { + printf("MISMATCH: original %zu bytes vs extracted %zu bytes\n", orig_sz, ext_sz); + /* helpful hex diff of first 32 bytes */ + size_t show = (orig_sz < ext_sz ? orig_sz : ext_sz); + if (show > 32) show = 32; + printf("Original first %zu bytes: ", show); + for (size_t i = 0; i < show; ++i) printf("%02X ", orig[i]); + printf("\nExtracted first %zu bytes: ", show); + for (size_t i = 0; i < show; ++i) printf("%02X ", ext[i]); + printf("\n"); + } + free(orig); free(ext); + return 0; + } else { + fprintf(stderr, "unknown mode: %s\n", argv[1]); + return 1; + } +}