From 506fcbe79d2ba835ecde61e2fd167c5e5fb65b7c Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 15 Dec 2025 01:40:53 +0900 Subject: [PATCH] TAV-DT syncing on damaged stream wip --- video_encoder/Makefile | 11 +- video_encoder/src/decoder_tav_dt.c | 345 +++++++++++++++++++++-- video_encoder/tavdt_noise_injector.c | 402 +++++++++++++++++++++++++++ 3 files changed, 738 insertions(+), 20 deletions(-) create mode 100644 video_encoder/tavdt_noise_injector.c diff --git a/video_encoder/Makefile b/video_encoder/Makefile index 7794887..f485a04 100644 --- a/video_encoder/Makefile +++ b/video_encoder/Makefile @@ -47,7 +47,7 @@ TARGETS = libs encoder_tav_ref decoder_tav_ref tav_inspector tad tav_dt LIBRARIES = lib/libtavenc.a lib/libtavdec.a lib/libtadenc.a lib/libtaddec.a lib/libfec.a TAV_TARGETS = encoder_tav_ref decoder_tav_ref tav_inspector TAD_TARGETS = encoder_tad decoder_tad -DT_TARGETS = encoder_tav_dt decoder_tav_dt +DT_TARGETS = encoder_tav_dt decoder_tav_dt tavdt_noise_injector # Build all encoders (default) all: clean $(TARGETS) @@ -147,6 +147,14 @@ decoder_tav_dt: src/decoder_tav_dt.c lib/libtavdec.a lib/libtaddec.a lib/libfec. @echo "TAV-DT decoder built: decoder_tav_dt" @echo "Digital Tape format with LDPC and Reed-Solomon FEC (multithreaded)" +# TAV-DT noise injector (channel simulator) +tavdt_noise_injector: tavdt_noise_injector.c + rm -f tavdt_noise_injector + $(CC) -std=c99 -Wall -O2 -D_GNU_SOURCE -o tavdt_noise_injector tavdt_noise_injector.c -lm + @echo "" + @echo "TAV-DT noise injector built: tavdt_noise_injector" + @echo "Simulates QPSK satellite channel noise (AWGN + burst)" + # Build all TAV-DT tools tav_dt: $(DT_TARGETS) @@ -184,6 +192,7 @@ help: @echo " libs - Build all codec libraries (.a files)" @echo " tav - Build the TAV advanced video encoder" @echo " tav_dt - Build all TAV-DT (Digital Tape) tools with FEC" + @echo " tavdt_noise_injector - Build TAV-DT channel noise simulator" @echo " tad - Build all TAD audio tools (encoder, decoder)" @echo " encoder_tad - Build TAD audio encoder" @echo " decoder_tad - Build TAD audio decoder" diff --git a/video_encoder/src/decoder_tav_dt.c b/video_encoder/src/decoder_tav_dt.c index e1460e9..8ab6191 100644 --- a/video_encoder/src/decoder_tav_dt.c +++ b/video_encoder/src/decoder_tav_dt.c @@ -204,6 +204,13 @@ typedef struct { // Timing time_t start_time; + + // Error concealment + uint8_t *freeze_frame; // Last good video frame for error concealment + size_t freeze_frame_size; + uint64_t last_timecode_ns; // Last processed timecode + uint64_t audio_samples_written; // Total audio samples written + uint64_t video_frames_written; // Total video frames written (for sync check) } dt_decoder_t; // ============================================================================= @@ -240,6 +247,102 @@ static void generate_random_filename(char *filename, size_t size) { filename[prefix_len + 16] = '\0'; } +// ============================================================================= +// Error Concealment Functions +// ============================================================================= + +/** + * Write silent audio samples for error concealment. + * Generates PCMu8 silence (value 128) for the specified number of stereo samples. + */ +static int write_silent_audio(dt_decoder_t *dec, size_t num_samples) { + if (!dec->audio_temp_fp || num_samples == 0) { + return 0; + } + + // PCMu8 silence is value 128 (0x80) + uint8_t *silence = malloc(num_samples * 2); + if (!silence) { + fprintf(stderr, "Warning: Cannot allocate silence buffer\n"); + return -1; + } + + memset(silence, 128, num_samples * 2); + fwrite(silence, 1, num_samples * 2, dec->audio_temp_fp); + free(silence); + + dec->audio_samples_written += num_samples; + + if (dec->verbose) { + printf(" Error concealment: Wrote %zu samples of silent audio\n", num_samples); + } + + return 0; +} + +/** + * Write frozen video frame(s) for error concealment. + * Repeats the last good frame or writes black frame if no freeze frame exists. + */ +static int write_frozen_frames(dt_decoder_t *dec, int num_frames) { + if (!dec->video_temp_fp || num_frames <= 0) { + return 0; + } + + int internal_height = dec->is_interlaced ? dec->height / 2 : dec->height; + size_t frame_size = dec->width * internal_height * 3; + + // If no freeze frame exists, create a black frame + if (!dec->freeze_frame) { + dec->freeze_frame = calloc(1, frame_size); + if (!dec->freeze_frame) { + fprintf(stderr, "Warning: Cannot allocate freeze frame buffer\n"); + return -1; + } + dec->freeze_frame_size = frame_size; + if (dec->verbose) { + printf(" Error concealment: Using black frame (no reference frame available)\n"); + } + } + + // Write the freeze frame multiple times + for (int i = 0; i < num_frames; i++) { + fwrite(dec->freeze_frame, 1, dec->freeze_frame_size, dec->video_temp_fp); + dec->video_frames_written++; + dec->frames_decoded++; + } + + if (dec->verbose) { + printf(" Error concealment: Wrote %d frozen frame(s)\n", num_frames); + } + + return 0; +} + +/** + * Update the freeze frame buffer with the last successfully decoded frame. + */ +static int update_freeze_frame(dt_decoder_t *dec, const uint8_t *frame_data, size_t frame_size) { + if (!frame_data || frame_size == 0) { + return -1; + } + + // Allocate or reallocate freeze frame buffer + if (!dec->freeze_frame || dec->freeze_frame_size != frame_size) { + free(dec->freeze_frame); + dec->freeze_frame = malloc(frame_size); + if (!dec->freeze_frame) { + fprintf(stderr, "Warning: Cannot allocate freeze frame buffer\n"); + dec->freeze_frame_size = 0; + return -1; + } + dec->freeze_frame_size = frame_size; + } + + memcpy(dec->freeze_frame, frame_data, frame_size); + return 0; +} + // ============================================================================= // Sync Pattern Search // ============================================================================= @@ -592,7 +695,9 @@ static void cleanup_decoder_threads(dt_decoder_t *dec) { // ============================================================================= static int decode_audio_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t data_len, - size_t *consumed) { + size_t *consumed, size_t *samples_written) { + *samples_written = 0; + // Minimum: 20 byte LDPC header if (data_len < DT_TAD_HEADER_SIZE * 2) return -1; @@ -644,7 +749,7 @@ static int decode_audio_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t fprintf(stderr, "Warning: Audio packet truncated\n"); } *consumed = data_len; - return -1; + return -1; // Unrecoverable } // RS decode payload @@ -661,8 +766,12 @@ static int decode_audio_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t int rs_result = rs_decode_blocks(rs_data, rs_total, decoded_payload, compressed_size); if (rs_result < 0) { if (dec->verbose) { - fprintf(stderr, "Warning: RS decode failed for audio\n"); + fprintf(stderr, "Warning: RS decode failed for audio - UNRECOVERABLE\n"); } + free(rs_data); + free(decoded_payload); + *consumed = offset + rs_total; + return -1; // Unrecoverable - RS failed } else if (rs_result > 0) { dec->fec_corrections += rs_result; } @@ -690,6 +799,17 @@ static int decode_audio_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t if (tad_result == 0 && samples_decoded > 0 && dec->audio_temp_fp) { fwrite(pcmu8_output, 1, samples_decoded * 2, dec->audio_temp_fp); + *samples_written = samples_decoded; + dec->audio_samples_written += samples_decoded; + } else { + if (dec->verbose) { + fprintf(stderr, "Warning: TAD decode failed - UNRECOVERABLE\n"); + } + free(pcmu8_output); + free(rs_data); + free(decoded_payload); + *consumed = offset + rs_total; + return -1; // Unrecoverable - TAD decode failed } free(pcmu8_output); @@ -699,14 +819,16 @@ static int decode_audio_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t offset += rs_total; *consumed = offset; - return 0; + return 0; // Success } /** * Multithreaded video decoding - submit GOP to worker pool */ static int decode_video_subpacket_mt(dt_decoder_t *dec, const uint8_t *data, size_t data_len, - size_t *consumed) { + size_t *consumed, int *frames_written) { + *frames_written = 0; + // Minimum: 16 byte LDPC header if (data_len < DT_TAV_HEADER_SIZE * 2) return -1; @@ -740,7 +862,7 @@ static int decode_video_subpacket_mt(dt_decoder_t *dec, const uint8_t *data, siz if (offset + rs_total > data_len) { *consumed = data_len; - return -1; + return -1; // Unrecoverable } // RS decode payload @@ -755,7 +877,15 @@ static int decode_video_subpacket_mt(dt_decoder_t *dec, const uint8_t *data, siz } int rs_result = rs_decode_blocks(rs_data, rs_total, decoded_payload, compressed_size); - if (rs_result > 0) { + if (rs_result < 0) { + if (dec->verbose) { + fprintf(stderr, "Warning: RS decode failed for video (MT) - UNRECOVERABLE\n"); + } + free(rs_data); + free(decoded_payload); + *consumed = offset + rs_total; + return -1; // Unrecoverable - RS failed + } else if (rs_result > 0) { dec->fec_corrections += rs_result; } free(rs_data); @@ -788,11 +918,13 @@ static int decode_video_subpacket_mt(dt_decoder_t *dec, const uint8_t *data, siz gop_decode_job_t *job = &dec->slots[i]; pthread_mutex_unlock(&dec->mutex); - // Write frames to temp file + // Write frames to temp file and update freeze frame if (job->decode_result == 0 && dec->video_temp_fp) { for (int f = 0; f < job->gop_size; f++) { fwrite(job->rgb_frames[f], 1, job->frame_size, dec->video_temp_fp); + update_freeze_frame(dec, job->rgb_frames[f], job->frame_size); dec->frames_decoded++; + dec->video_frames_written++; } } @@ -853,12 +985,15 @@ static int decode_video_subpacket_mt(dt_decoder_t *dec, const uint8_t *data, siz offset += rs_total; *consumed = offset; + *frames_written = gop_size; // Optimistic - assume decode will succeed - return 0; + return 0; // Success - job submitted } static int decode_video_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t data_len, - size_t *consumed) { + size_t *consumed, int *frames_written) { + *frames_written = 0; + // Minimum: 16 byte LDPC header if (data_len < DT_TAV_HEADER_SIZE * 2) return -1; @@ -901,7 +1036,7 @@ static int decode_video_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t fprintf(stderr, "Warning: Video packet truncated\n"); } *consumed = data_len; - return -1; + return -1; // Unrecoverable } // RS decode payload @@ -918,8 +1053,12 @@ static int decode_video_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t int rs_result = rs_decode_blocks(rs_data, rs_total, decoded_payload, compressed_size); if (rs_result < 0) { if (dec->verbose) { - fprintf(stderr, "Warning: RS decode failed for video\n"); + fprintf(stderr, "Warning: RS decode failed for video - UNRECOVERABLE\n"); } + free(rs_data); + free(decoded_payload); + *consumed = offset + rs_total; + return -1; // Unrecoverable - RS failed } else if (rs_result > 0) { dec->fec_corrections += rs_result; } @@ -984,18 +1123,31 @@ static int decode_video_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t gop_size, rgb_frames); if (decode_result == 0) { - // Write frames to video temp file + // Write frames to video temp file and update freeze frame for (int i = 0; i < gop_size; i++) { if (dec->video_temp_fp) { fwrite(rgb_frames[i], 1, frame_size, dec->video_temp_fp); } + // Update freeze frame with last successfully decoded frame + update_freeze_frame(dec, rgb_frames[i], frame_size); dec->frames_decoded++; + dec->video_frames_written++; } + *frames_written = gop_size; } else { if (dec->verbose) { const char *err = tav_video_get_error(dec->video_ctx); - fprintf(stderr, "Warning: Video decode failed: %s\n", err ? err : "unknown error"); + fprintf(stderr, "Warning: Video decode failed: %s - UNRECOVERABLE\n", err ? err : "unknown error"); } + // Cleanup and return error + for (int i = 0; i < gop_size; i++) { + free(rgb_frames[i]); + } + free(rgb_frames); + free(rs_data); + free(decoded_payload); + *consumed = offset + rs_total; + return -1; // Unrecoverable - video decode failed } // Cleanup @@ -1009,7 +1161,7 @@ static int decode_video_subpacket(dt_decoder_t *dec, const uint8_t *data, size_t offset += rs_total; *consumed = offset; - return 0; + return 0; // Success } // ============================================================================= @@ -1187,6 +1339,146 @@ static int process_packet(dt_decoder_t *dec) { dec->packets_processed + 1, timecode_sec, header.packet_size, header.offset_to_video); } + // Calculate expected samples/frames based on timecode + // TAD audio is 32000 Hz stereo, GOP size varies + uint64_t timecode_delta_ns = 0; + size_t expected_audio_samples = 0; + int expected_video_frames = 0; + int timecode_valid = 0; + + if (dec->packets_processed > 0) { + // Sanity check: detect obviously garbage timecodes (corrupted header data) + // A timecode is "garbage" if it's impossibly large (> 24 hours) or if it went backwards + // Large forward jumps are OK - they indicate lost packets and should be trusted + uint64_t max_reasonable_timecode_ns = 86400ULL * 1000000000ULL; // 24 hours + + uint64_t reconstructed_timecode_ns = 0; + int use_reconstructed = 0; + uint64_t gop_duration_ns = (16ULL * 1000000000ULL) / dec->framerate; + + if (header.timecode_ns > max_reasonable_timecode_ns) { + // Timecode is garbage (e.g., 9007208.588s = 104 days) - reconstruct + reconstructed_timecode_ns = dec->last_timecode_ns + gop_duration_ns; + timecode_delta_ns = gop_duration_ns; + use_reconstructed = 1; + + if (dec->verbose) { + double corrupted_tc = header.timecode_ns / 1000000000.0; + double reconstructed_tc = reconstructed_timecode_ns / 1000000000.0; + fprintf(stderr, "Warning: Timecode garbage (%.3fs), reconstructed as %.3fs based on GOP size\n", + corrupted_tc, reconstructed_tc); + } + } else if (header.timecode_ns > dec->last_timecode_ns) { + // Valid timecode moving forward - trust it (even with large jumps from lost packets) + timecode_delta_ns = header.timecode_ns - dec->last_timecode_ns; + } else if (header.timecode_ns == dec->last_timecode_ns) { + // Duplicate timecode - corrupted, reconstruct + reconstructed_timecode_ns = dec->last_timecode_ns + gop_duration_ns; + timecode_delta_ns = gop_duration_ns; + use_reconstructed = 1; + + if (dec->verbose) { + fprintf(stderr, "Warning: Duplicate timecode detected, reconstructed based on GOP size\n"); + } + } else { + // Timecode went backwards - corrupted, reconstruct + reconstructed_timecode_ns = dec->last_timecode_ns + gop_duration_ns; + timecode_delta_ns = gop_duration_ns; + use_reconstructed = 1; + + if (dec->verbose) { + fprintf(stderr, "Warning: Timecode went backwards, reconstructed based on GOP size\n"); + } + } + + // Calculate expected samples/frames from (possibly reconstructed) timecode delta + // NOTE: These variables are currently unused - cumulative logic below uses absolute timecodes + expected_audio_samples = (timecode_delta_ns * 64000) / 1000000000ULL; // 32kHz stereo = 64000 samples/sec + expected_video_frames = (int)((timecode_delta_ns * dec->framerate) / 1000000000ULL); + timecode_valid = 1; + + // Store which timecode to use for next packet + if (use_reconstructed) { + // Override header timecode with reconstructed value + header.timecode_ns = reconstructed_timecode_ns; + } + } + + // Error concealment: Insert gaps BEFORE decoding current packet + // This ensures concealment data appears in the correct timeline position + + // Also handle first packet - if timecode > 0, insert concealment for missed initial data + if (dec->packets_processed == 0 && header.timecode_ns > 0) { + // First packet but timecode is not 0 - we missed the beginning + // Audio: 32000 Hz stereo = 64000 total samples per second (L+R combined) + uint64_t expected_cumulative_audio = (header.timecode_ns * 64000ULL) / 1000000000ULL; + uint64_t expected_cumulative_video = (header.timecode_ns * (uint64_t)dec->framerate) / 1000000000ULL; + + if (dec->verbose) { + printf(" FIRST PACKET CONCEALMENT: timecode=%.3fs, inserting %lu silent samples + %lu frozen frames\n", + header.timecode_ns / 1000000000.0, expected_cumulative_audio, expected_cumulative_video); + } + + if (expected_cumulative_audio > 0) { + write_silent_audio(dec, expected_cumulative_audio); + } + if (expected_cumulative_video > 0) { + write_frozen_frames(dec, (int)expected_cumulative_video); + } + } + + if (dec->packets_processed > 0 && timecode_valid) { + // Save cumulative counts BEFORE decoding this packet + uint64_t cumulative_audio_before = dec->audio_samples_written; + uint64_t cumulative_video_before = dec->video_frames_written; + + // Calculate expected CUMULATIVE samples/frames at this timecode + // Audio: 32000 Hz stereo = 64000 total samples per second (L+R combined) + uint64_t expected_cumulative_audio = (header.timecode_ns * 64000ULL) / 1000000000ULL; + uint64_t expected_cumulative_video = (header.timecode_ns * (uint64_t)dec->framerate) / 1000000000ULL; + + // Calculate gap between expected and actual (BEFORE this packet) + size_t audio_gap = 0; + int video_gap = 0; + + if (expected_cumulative_audio > cumulative_audio_before) { + audio_gap = expected_cumulative_audio - cumulative_audio_before; + } + + if (expected_cumulative_video > cumulative_video_before) { + video_gap = expected_cumulative_video - cumulative_video_before; + } + + // Insert concealment data FIRST (fills gap from lost packets) + if (audio_gap > 0 || video_gap > 0) { + if (dec->verbose) { + if (audio_gap > 0 && video_gap > 0) { + printf(" ERROR CONCEALMENT: Inserting %zu silent samples + %d frozen frames\n", + audio_gap, video_gap); + printf(" (Expected: %lu samples/%lu frames, Actual: %lu samples/%lu frames)\n", + expected_cumulative_audio, expected_cumulative_video, + cumulative_audio_before, cumulative_video_before); + } else if (audio_gap > 0) { + printf(" ERROR CONCEALMENT: Inserting %zu silent samples\n", audio_gap); + printf(" (Expected: %lu samples, Actual: %lu samples)\n", + expected_cumulative_audio, cumulative_audio_before); + } else { + printf(" ERROR CONCEALMENT: Inserting %d frozen frames\n", video_gap); + printf(" (Expected: %lu frames, Actual: %lu frames)\n", + expected_cumulative_video, cumulative_video_before); + } + } + + if (audio_gap > 0) { + write_silent_audio(dec, audio_gap); + } + if (video_gap > 0) { + write_frozen_frames(dec, video_gap); + } + } + } + + // NOW decode current packet (writes AFTER concealment) // Read packet payload (contains both TAD and TAV subpackets) uint8_t *packet_data = malloc(header.packet_size); if (!packet_data) return -1; @@ -1202,10 +1494,15 @@ static int process_packet(dt_decoder_t *dec) { } dec->bytes_read += bytes_read; + // Decode audio and video + size_t audio_samples_written = 0; + int video_frames_written = 0; + // Process TAD subpacket (audio comes first, no type byte) size_t tad_consumed = 0; if (header.offset_to_video > 0) { - decode_audio_subpacket(dec, packet_data, header.offset_to_video, &tad_consumed); + decode_audio_subpacket(dec, packet_data, header.offset_to_video, + &tad_consumed, &audio_samples_written); } // Process TAV subpacket (video comes after audio) @@ -1213,13 +1510,17 @@ static int process_packet(dt_decoder_t *dec) { size_t tav_consumed = 0; if (dec->num_threads > 1) { decode_video_subpacket_mt(dec, packet_data + header.offset_to_video, - header.packet_size - header.offset_to_video, &tav_consumed); + header.packet_size - header.offset_to_video, + &tav_consumed, &video_frames_written); } else { decode_video_subpacket(dec, packet_data + header.offset_to_video, - header.packet_size - header.offset_to_video, &tav_consumed); + header.packet_size - header.offset_to_video, + &tav_consumed, &video_frames_written); } } + // Update timecode tracking + dec->last_timecode_ns = header.timecode_ns; dec->packets_processed++; if (!dec->verbose && dec->packets_processed % 10 == 0) { @@ -1285,11 +1586,13 @@ static int run_decoder(dt_decoder_t *dec) { gop_decode_job_t *job = &dec->slots[found]; pthread_mutex_unlock(&dec->mutex); - // Write frames + // Write frames and update freeze frame if (job->decode_result == 0 && dec->video_temp_fp) { for (int f = 0; f < job->gop_size; f++) { fwrite(job->rgb_frames[f], 1, job->frame_size, dec->video_temp_fp); + update_freeze_frame(dec, job->rgb_frames[f], job->frame_size); dec->frames_decoded++; + dec->video_frames_written++; } } @@ -1351,6 +1654,10 @@ static int run_decoder(dt_decoder_t *dec) { if (dec->input_fp) { fclose(dec->input_fp); } + if (dec->freeze_frame) { + free(dec->freeze_frame); + dec->freeze_frame = NULL; + } // Remove temp files unlink(dec->audio_temp_file); diff --git a/video_encoder/tavdt_noise_injector.c b/video_encoder/tavdt_noise_injector.c new file mode 100644 index 0000000..447b03c --- /dev/null +++ b/video_encoder/tavdt_noise_injector.c @@ -0,0 +1,402 @@ +// TAV-DT Noise Injector - Simulates satellite transmission channel noise +// Models QPSK over Ku-band satellite with AWGN and burst interference +// to compile: gcc -O2 -o tavdt_noise_injector tavdt_noise_injector.c -lm +// Created by CuriousTorvald and Claude on 2025-12-14 + +#include +#include +#include +#include +#include +#include +#include + +// Buffer size for streaming processing +#define BUFFER_SIZE (1024 * 1024) // 1 MB chunks + +// Default TAV-DT bitrate for timing calculations (~2 Mbps) +#define DEFAULT_BITRATE_BPS 2000000.0 + +// Global bitrate (can be overridden by --bitrate) +static double g_bitrate_bps = DEFAULT_BITRATE_BPS; + +// Burst noise parameters +#define BURST_LENGTH_MEAN 100.0 +#define BURST_LENGTH_STDDEV 30.0 +#define BURST_LENGTH_MIN 10 + +//============================================================================= +// PRNG Functions (xorshift64) +//============================================================================= + +static uint64_t xorshift64(uint64_t *state) { + uint64_t x = *state; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + return *state = x; +} + +// Returns uniform random in [0, 1) +static double rand_uniform(uint64_t *state) { + return (double)xorshift64(state) / (double)UINT64_MAX; +} + +// Box-Muller transform for Gaussian random numbers +static double gaussian_rand(uint64_t *state, double mean, double stddev) { + double u1 = rand_uniform(state); + double u2 = rand_uniform(state); + + // Avoid log(0) + if (u1 < 1e-15) u1 = 1e-15; + + double z = sqrt(-2.0 * log(u1)) * cos(2.0 * M_PI * u2); + return mean + stddev * z; +} + +//============================================================================= +// BER Calculation +//============================================================================= + +// Calculate BER from SNR in dB for QPSK modulation +// BER = 0.5 * erfc(sqrt(Eb/N0)) +// For QPSK, Eb/N0 = SNR (2 bits per symbol) +static double snr_to_ber(double snr_db) { + double snr_linear = pow(10.0, snr_db / 10.0); + double eb_n0 = snr_linear; + return 0.5 * erfc(sqrt(eb_n0)); +} + +//============================================================================= +// Burst State Management +//============================================================================= + +typedef struct { + double current_time_sec; // Elapsed playback time + double next_burst_time; // When next burst occurs + int burst_bytes_remaining; // Bytes left in current burst (0 = no active burst) + double burst_interval; // Mean interval between bursts (60.0 / bursts_per_minute) + double burst_ber; // BER during burst + int burst_count; // Total bursts applied + int total_burst_bytes; // Total bytes affected by bursts + int verbose; // Verbose output flag +} burst_state_t; + +static void burst_state_init(burst_state_t *state, double bursts_per_minute, + double burst_ber, int verbose, uint64_t *seed) { + state->current_time_sec = 0.0; + state->burst_bytes_remaining = 0; + state->burst_ber = burst_ber; + state->burst_count = 0; + state->total_burst_bytes = 0; + state->verbose = verbose; + + if (bursts_per_minute > 0) { + state->burst_interval = 60.0 / bursts_per_minute; + // Schedule first burst using exponential distribution + state->next_burst_time = -state->burst_interval * log(rand_uniform(seed)); + } else { + state->burst_interval = 0; + state->next_burst_time = 1e30; // Never burst + } +} + +static void burst_state_advance_time(burst_state_t *state, double delta_sec, uint64_t *seed) { + double end_time = state->current_time_sec + delta_sec; + + // Check if any bursts should occur during this time span + while (state->burst_interval > 0 && state->next_burst_time < end_time) { + // A burst should start during this chunk + if (state->burst_bytes_remaining == 0) { + double length = gaussian_rand(seed, BURST_LENGTH_MEAN, BURST_LENGTH_STDDEV); + state->burst_bytes_remaining = (int)fmax(BURST_LENGTH_MIN, length); + state->burst_count++; + + if (state->verbose) { + fprintf(stderr, " [burst] time %.2fs, %d bytes\n", + state->next_burst_time, state->burst_bytes_remaining); + } + } + + // Schedule next burst + double wait = -state->burst_interval * log(rand_uniform(seed)); + if (wait < 0.001) wait = 0.001; // Minimum 1ms between bursts + state->next_burst_time += wait; + } + + state->current_time_sec = end_time; +} + +//============================================================================= +// Noise Application Functions +//============================================================================= + +// Apply AWGN-based bit errors to buffer +// Returns number of bits flipped +static int apply_background_noise(uint8_t *data, size_t len, double ber, uint64_t *seed) { + int bits_flipped = 0; + + // Optimization: if BER is extremely low, use probability-based skipping + if (ber < 1e-10) { + return 0; // Effectively no errors at this BER + } + + for (size_t i = 0; i < len; i++) { + for (int bit = 0; bit < 8; bit++) { + if (rand_uniform(seed) < ber) { + data[i] ^= (1 << bit); + bits_flipped++; + } + } + } + + return bits_flipped; +} + +// Apply burst noise to buffer (checks/updates burst state) +// Returns number of bits flipped +static int apply_burst_noise(uint8_t *data, size_t len, burst_state_t *state, uint64_t *seed) { + int bits_flipped = 0; + + if (state->burst_bytes_remaining <= 0) { + return 0; + } + + // Apply burst BER to bytes while burst is active + size_t burst_bytes = (size_t)state->burst_bytes_remaining; + if (burst_bytes > len) { + burst_bytes = len; + } + + for (size_t i = 0; i < burst_bytes; i++) { + for (int bit = 0; bit < 8; bit++) { + if (rand_uniform(seed) < state->burst_ber) { + data[i] ^= (1 << bit); + bits_flipped++; + } + } + } + + state->total_burst_bytes += burst_bytes; + state->burst_bytes_remaining -= burst_bytes; + + return bits_flipped; +} + +//============================================================================= +// Byte Position to Time Conversion +//============================================================================= + +// Convert byte position to approximate playback time based on bitrate +static double bytes_to_time(size_t byte_pos) { + return (double)(byte_pos * 8) / g_bitrate_bps; +} + +//============================================================================= +// Main Program +//============================================================================= + +static void print_usage(const char *prog) { + fprintf(stderr, "TAV-DT Noise Injector v1.0\n"); + fprintf(stderr, "Simulates QPSK satellite transmission channel noise\n\n"); + fprintf(stderr, "Usage: %s -i input.tavdt -o output.tavdt --snr N [options]\n\n", prog); + fprintf(stderr, "Required:\n"); + fprintf(stderr, " -i, --input FILE Input TAV-DT file\n"); + fprintf(stderr, " -o, --output FILE Output corrupted file\n"); + fprintf(stderr, " --snr N Signal-to-noise ratio in dB (0-30)\n"); + fprintf(stderr, "\nOptional:\n"); + fprintf(stderr, " --burst N Burst events per minute (default: 0)\n"); + fprintf(stderr, " --burst-ber N BER during burst events (default: 0.5)\n"); + fprintf(stderr, " --bitrate N Stream bitrate in Mbps for timing (default: 2.0)\n"); + fprintf(stderr, " --seed N RNG seed for reproducibility\n"); + fprintf(stderr, " -v, --verbose Show detailed progress\n"); + fprintf(stderr, " -h, --help Show this help\n"); + fprintf(stderr, "\nSNR Reference:\n"); + fprintf(stderr, " 0 dB: Worst case (BER ~7.9e-2, 1 in 13 bits)\n"); + fprintf(stderr, " 6 dB: Poor but working (BER ~2.4e-3)\n"); + fprintf(stderr, " 9 dB: Typical working (BER ~1.9e-4)\n"); + fprintf(stderr, " 12 dB: Good condition (BER ~3.8e-6)\n"); + fprintf(stderr, " 30 dB: Near-perfect (BER ~2.9e-16)\n"); +} + +int main(int argc, char *argv[]) { + const char *input_file = NULL; + const char *output_file = NULL; + double snr_db = -1; + double bursts_per_minute = 0; + double burst_ber = 0.5; + uint64_t seed = 0; + int seed_provided = 0; + int verbose = 0; + + static struct option long_options[] = { + {"input", required_argument, 0, 'i'}, + {"output", required_argument, 0, 'o'}, + {"snr", required_argument, 0, 's'}, + {"burst", required_argument, 0, 'b'}, + {"burst-ber", required_argument, 0, 'B'}, + {"bitrate", required_argument, 0, 'r'}, + {"seed", required_argument, 0, 'S'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "i:o:vh", long_options, NULL)) != -1) { + switch (opt) { + case 'i': + input_file = optarg; + break; + case 'o': + output_file = optarg; + break; + case 's': + snr_db = atof(optarg); + break; + case 'b': + bursts_per_minute = atof(optarg); + break; + case 'B': + burst_ber = atof(optarg); + break; + case 'r': + g_bitrate_bps = atof(optarg) * 1000000.0; // Convert Mbps to bps + break; + case 'S': + seed = strtoull(optarg, NULL, 10); + seed_provided = 1; + break; + case 'v': + verbose = 1; + break; + case 'h': + default: + print_usage(argv[0]); + return opt == 'h' ? 0 : 1; + } + } + + // Validate arguments + if (!input_file || !output_file || snr_db < 0) { + fprintf(stderr, "Error: Missing required arguments\n\n"); + print_usage(argv[0]); + return 1; + } + + if (burst_ber < 0 || burst_ber > 1) { + fprintf(stderr, "Error: --burst-ber must be between 0 and 1\n"); + return 1; + } + + // Initialize RNG + if (!seed_provided) { + seed = (uint64_t)time(NULL) ^ ((uint64_t)clock() << 32); + } + // Ensure seed is not zero (xorshift64 requirement) + if (seed == 0) seed = 0x853c49e6748fea9bULL; + // Warm up the generator (small seeds produce poor initial values) + for (int i = 0; i < 10; i++) xorshift64(&seed); + + // Calculate BER from SNR + double ber = snr_to_ber(snr_db); + + // Open files + FILE *in_fp = fopen(input_file, "rb"); + if (!in_fp) { + fprintf(stderr, "Error: Cannot open input file: %s\n", input_file); + return 1; + } + + FILE *out_fp = fopen(output_file, "wb"); + if (!out_fp) { + fprintf(stderr, "Error: Cannot open output file: %s\n", output_file); + fclose(in_fp); + return 1; + } + + // Print header info + fprintf(stderr, "TAV-DT Noise Injector v1.0\n"); + fprintf(stderr, "Input: %s\n", input_file); + fprintf(stderr, "Output: %s\n", output_file); + fprintf(stderr, "SNR: %.1f dB (BER: %.2e)\n", snr_db, ber); + if (bursts_per_minute > 0) { + fprintf(stderr, "Burst: %.1f events/minute (burst BER: %.2f)\n", + bursts_per_minute, burst_ber); + } else { + fprintf(stderr, "Burst: disabled\n"); + } + if (seed_provided) { + fprintf(stderr, "Seed: %llu\n", (unsigned long long)seed); + } + fprintf(stderr, "\n"); + + // Initialize burst state + burst_state_t burst; + burst_state_init(&burst, bursts_per_minute, burst_ber, verbose, &seed); + + // Allocate buffer for streaming processing + uint8_t *buffer = malloc(BUFFER_SIZE); + if (!buffer) { + fprintf(stderr, "Error: Cannot allocate buffer\n"); + fclose(in_fp); + fclose(out_fp); + return 1; + } + + // Processing statistics + long long total_bytes = 0; + long long bits_flipped_bg = 0; + long long bits_flipped_burst = 0; + int chunk_count = 0; + + // Process file in chunks + size_t bytes_read; + while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, in_fp)) > 0) { + // Calculate time delta for this chunk (for burst scheduling) + double delta_sec = bytes_to_time(bytes_read); + burst_state_advance_time(&burst, delta_sec, &seed); + + // Apply noise to chunk + bits_flipped_bg += apply_background_noise(buffer, bytes_read, ber, &seed); + bits_flipped_burst += apply_burst_noise(buffer, bytes_read, &burst, &seed); + + // Write corrupted chunk + fwrite(buffer, 1, bytes_read, out_fp); + + total_bytes += bytes_read; + chunk_count++; + + if (verbose && chunk_count % 10 == 0) { + double time_pos = bytes_to_time(total_bytes); + fprintf(stderr, "\rProcessed %.1f MB (%.1f sec)...", + total_bytes / (1024.0 * 1024.0), time_pos); + } + } + + if (verbose) { + fprintf(stderr, "\r \r"); + } + + // Clean up + free(buffer); + fclose(in_fp); + fclose(out_fp); + + // Print summary + double duration_sec = bytes_to_time(total_bytes); + long long total_bits = total_bytes * 8; + + fprintf(stderr, "Complete.\n"); + fprintf(stderr, " Total bytes: %lld (%.1f sec @ ~%.1f Mbps)\n", + total_bytes, duration_sec, g_bitrate_bps / 1000000.0); + fprintf(stderr, " Background bits flipped: %lld (%.4f%%)\n", + bits_flipped_bg, 100.0 * bits_flipped_bg / total_bits); + if (bursts_per_minute > 0) { + fprintf(stderr, " Burst events: %d (%d bytes total)\n", + burst.burst_count, burst.total_burst_bytes); + fprintf(stderr, " Burst bits flipped: %lld\n", bits_flipped_burst); + } + + return 0; +}