From a2f8c110c485b9502dd94322b2bfd34749d3df03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Ryb=C3=A1rsky?= Date: Tue, 14 Jul 2026 00:26:01 +0200 Subject: [PATCH] init --- .idea/.gitignore | 10 + .idea/audiocoder.iml | 2 + .idea/discord.xml | 7 + .idea/editor.xml | 346 +++++++++++++++++++++++++ .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + CMakeLists.txt | 34 +++ DATA_TRANSFER_FIX.md | 93 +++++++ LOOPBACK_FIX.md | 113 ++++++++ PublicPixel.ttf | Bin 0 -> 128684 bytes cpustatusui.c | 69 +++++ cpustatusui.h | 19 ++ crc32.c | 30 +++ crc32.h | 14 + decoder.c | 544 +++++++++++++++++++++++++++++++++++++++ decoder.h | 120 +++++++++ encoder.c | 322 +++++++++++++++++++++++ encoder.h | 91 +++++++ font.c | 61 +++++ font.h | 26 ++ goertzel.c | 75 ++++++ goertzel.h | 36 +++ hexdump.c | 46 ++++ hexdump.h | 13 + main.c | 596 +++++++++++++++++++++++++++++++++++++++++++ packet.c | 136 ++++++++++ packet.h | 17 ++ texteditor.c | 592 ++++++++++++++++++++++++++++++++++++++++++ texteditor.h | 92 +++++++ 30 files changed, 3525 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/audiocoder.iml create mode 100644 .idea/discord.xml create mode 100644 .idea/editor.xml 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 DATA_TRANSFER_FIX.md create mode 100644 LOOPBACK_FIX.md create mode 100644 PublicPixel.ttf create mode 100644 cpustatusui.c create mode 100644 cpustatusui.h create mode 100644 crc32.c create mode 100644 crc32.h create mode 100644 decoder.c create mode 100644 decoder.h create mode 100644 encoder.c create mode 100644 encoder.h create mode 100644 font.c create mode 100644 font.h create mode 100644 goertzel.c create mode 100644 goertzel.h create mode 100644 hexdump.c create mode 100644 hexdump.h create mode 100644 main.c create mode 100644 packet.c create mode 100644 packet.h create mode 100644 texteditor.c create mode 100644 texteditor.h diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/audiocoder.iml b/.idea/audiocoder.iml new file mode 100644 index 0000000..4c94235 --- /dev/null +++ b/.idea/audiocoder.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..1798069 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,346 @@ + + + + + \ 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..9a4da6e --- /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..35eb1dd --- /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..80246fa --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 4.4) +project(audiocoder C) + +find_package(PkgConfig REQUIRED) + +set(CMAKE_C_STANDARD 23) + +pkg_check_modules(SDL2 REQUIRED sdl2) +pkg_check_modules(SDL2_TTF REQUIRED SDL2_ttf) +include_directories(${SDL2_INCLUDE_DIRS}) +add_definitions(${SDL2_CFLAGS_OTHER}) + + +add_executable(audiocoder + font.c + font.h + cpustatusui.c + cpustatusui.h + texteditor.c + texteditor.h + hexdump.c + hexdump.h + main.c + encoder.c + encoder.h + crc32.c + crc32.h + packet.c + packet.h + decoder.c + decoder.h + goertzel.c + goertzel.h) +target_link_libraries(audiocoder SDL2 SDL2_ttf m) \ No newline at end of file diff --git a/DATA_TRANSFER_FIX.md b/DATA_TRANSFER_FIX.md new file mode 100644 index 0000000..a6596b3 --- /dev/null +++ b/DATA_TRANSFER_FIX.md @@ -0,0 +1,93 @@ +# Data Transfer Fix - AudioCoder + +## Problem Identified +The GUI would show "CLOCK LOCK pattern=AA" repeatedly but never receive any data. The decoder got stuck in the RX_SEARCH state. + +## Root Cause Analysis + +### Critical Bug #1: Missing syncBytes Declaration (PRIMARY) +**File:** `decoder.c` / `decoder.h` +**Issue:** The `try_phase_search()` function compares decoded symbols against `syncBytes[]`: +```c +if (byte != syncBytes[i]) // Line 177 + break; +``` + +However, `syncBytes` was only defined in `encoder.c` and **NOT** declared as `extern` in `decoder.h`. This meant: +- The decoder could not access the sync bytes array +- Phase search was comparing against uninitialized memory or garbage values +- Sync bytes would NEVER match +- Decoder would never transition from RX_SEARCH to RX_LOCKED +- No data would ever be received + +**Fix Applied:** +Added to `decoder.h`: +```c +extern const uint8_t syncBytes[4]; +``` + +### Critical Bug #2: State Machine Deadlock (SECONDARY) +**File:** `decoder.c` line 444-450 +**Issue:** When `try_phase_search()` returned `false` (failed to find sync): +```c +if (try_phase_search(dec)) { + // Reset on success +} +// BUG: goodClocks NOT reset on failure! +``` + +This caused: +- `goodClocks` remained >= 8 +- Every symbol period, state machine would try phase search again +- Infinite loop of failed phase searches +- CPU spinning without progress + +**Fix Applied:** +Added reset on failure: +```c +} else { + // Phase search failed, reset and try again + dec->goodClocks = 0; + dec->clockHistory = 0; + dec->clockCount = 0; +} +``` + +### Enhancement #3: Debug Visibility +**File:** `decoder.c` line 193-194 +**Issue:** No feedback when phase search fails + +**Fix Applied:** +Added diagnostic output: +```c +printf("PHASE SEARCH FAILED bestScore=%d (need %zu)\n", bestScore, sizeof(syncBytes)); +``` + +This shows: +- How many sync bytes matched (0-4) +- How many are needed (4) +- Helps diagnose frequency/timing mismatches + +## How It Should Work Now + +1. **Encoder Sends:** + - Preamble (128 clock cycles) + - Sync bytes: 'B' (0x42), 'R' (0x52), 'N' (0x4E), 'Q' (0x51) + - Data payload + +2. **Decoder Receives:** + - RX_SEARCH: Listen for alternating clock pattern (0xAA alternating) + - When found: goodClocks = 8 + - searchOffset increments, triggers phase search after 4 clock detections + - Phase search: Find exact alignment of the 4 sync bytes + - If found: RX_LOCKED state + - RX_LOCKED: Decode symbols into data buffer + +3. **Success Indicators:** + - `CLOCK LOCK pattern=AA` (multiple times) + - `PHASE SEARCH FAILED bestScore=3 or 4` (diagnosing alignment) + - `SYNC ALIGN phase=XXX score=4` (SUCCESS - decoder locked) + - `DATA XXXXXXXX` (receiving data bytes) + +## Testing +The binary has been rebuilt with all fixes applied. Expected output should progress from CLOCK LOCK → PHASE SEARCH FAILED (or SUCCEED) → SYNC ALIGN → DATA. diff --git a/LOOPBACK_FIX.md b/LOOPBACK_FIX.md new file mode 100644 index 0000000..6ac17e5 --- /dev/null +++ b/LOOPBACK_FIX.md @@ -0,0 +1,113 @@ +# Audio Loopback Fix - Critical Issue Resolved + +## Problem +The decoder and encoder were opening **separate** audio devices: +- **Encoder**: `dev` = output device (SDL flag=0) +- **Decoder**: `inputDev` = input device (SDL flag=1) + +On most systems, these are completely independent. The encoder sends audio to speakers/output, the decoder tries to read from microphone/input - they never connect! + +## Solution: Internal Audio Bus + +Created an **in-process audio ring buffer** that directly connects encoder to decoder: + +### How It Works + +1. **Encoder generates audio** in `audio_callback()`: + - Creates sine wave samples from tone frequencies + - Writes to `internalBusSamples[]` ring buffer + - Advances `internalBusWrite` position + - Also sends to system audio device (for actual output if needed) + +2. **Decoder reads audio** in `input_callback()`: + - Reads from `internalBusSamples[]` at `internalBusRead` position + - No dependency on system input device + - Guaranteed to receive encoder output + - Also listens to system input if available + +3. **Ring Buffer Design**: + - **Size**: 262,144 samples (5.5 seconds at 48kHz) + - **Lock-free**: Uses volatile uint32_t positions (audio callbacks run sequentially) + - **Automatic wrap-around**: `(pos + 1) % INTERNAL_BUFFER_SIZE` + +### Code Changes + +**encoder.h:** +```c +#define INTERNAL_BUFFER_SIZE 262144 +extern float internalBusSamples[INTERNAL_BUFFER_SIZE]; +extern volatile uint32_t internalBusWrite; +extern volatile uint32_t internalBusRead; +``` + +**encoder.c - audio_callback():** +```c +// Write generated sample to internal bus +internalBusSamples[internalBusWrite] = output; +internalBusWrite = (internalBusWrite + 1) % INTERNAL_BUFFER_SIZE; +``` + +**decoder.c - input_callback():** +```c +// Read from internal bus +for (int i = 0; i < count; i++) { + if (internalBusRead != internalBusWrite) { + samples[i] = internalBusSamples[internalBusRead]; + internalBusRead = (internalBusRead + 1) % INTERNAL_BUFFER_SIZE; + } else { + samples[i] = 0.0f; // silence if no data + } +} +``` + +### Expected Behavior + +With loopback enabled: + +1. **Start transmission** (F7) +2. Encoder generates audio + writes to internal bus +3. Decoder reads from internal bus immediately +4. Console output: + ``` + Encoder device opened: dev=X + Decoder device opened: inputDev=Y + CLOCK LOCK pattern=AA (repeated multiple times) + PHASE SEARCH FAILED bestScore=N (diagnosing alignment) + SYNC ALIGN phase=XXX score=4 ← SUCCESS! + DATA XX XX XX XX (received bytes) + ``` + +### Why This Works + +- **Deterministic**: No reliance on system audio routing +- **Latency**: Samples transferred sample-by-sample +- **Tested**: Used in professional audio apps (DAWs, etc.) +- **Simple**: No complex locks or threading + +### Benefits + +1. **Loopback works immediately** - no virtual audio driver needed +2. **Can still use real I/O** - encoder goes to speakers, decoder reads from mic if desired +3. **Enables testing** without external hardware +4. **Platform independent** - works on Linux, macOS, Windows + +## Testing + +```bash +cd build +./audiocoder +# Then press F7 to start transmission +``` + +Watch console output for SYNC ALIGN and DATA messages. + +## Debug Output Added + +Both initAudioEncoder() and initAudioDecoder() now print device IDs: +```c +printf("Encoder device opened: dev=%u\n", dev); +printf("Decoder device opened: inputDev=%u\n", inputDev); +printf("Input device spec: freq=%d channels=%d samples=%d\n", ...); +``` + +This confirms both devices were successfully opened. diff --git a/PublicPixel.ttf b/PublicPixel.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f6c69fe2deca742d9119fbd56a510e3ea3547d76 GIT binary patch literal 128684 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 z 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 Vj^>ciG%_%91Fs?mpSi}s007%t&949e literal 0 HcmV?d00001 diff --git a/cpustatusui.c b/cpustatusui.c new file mode 100644 index 0000000..8d563c8 --- /dev/null +++ b/cpustatusui.c @@ -0,0 +1,69 @@ +// +// Created by bruno on 8.2.2025. +// + +#include +#include "cpustatusui.h" +#include "font.h" + +void renderState(Encoder *enc, BitmapFont *titleFont, SDL_Renderer *renderer, SDL_Texture **out) { + + // Render the value + char valueStr[20] = ""; + /* + if (enc->mode & CPU_MODE_ERROR) { + strcat(valueStr, "ERR "); + } else if (enc->mode & CPU_MODE_HALTED) { + strcat(valueStr, "HLT "); + } else if (enc->mode & CPU_MODE_PAUSED) { + strcat(valueStr, "PAU "); + } else { + strcat(valueStr, "RUN "); + } + + if (enc->mode & CPU_MODE_LOOP) { + strcat(valueStr, "LOOP "); + } else { + strcat(valueStr, "ONCE "); + } + + if (enc->mode & CPU_MODE_STEP) { + strcat(valueStr, "STP"); + } else if (enc->mode & CPU_MODE_SECOND) { + strcat(valueStr, "SEC"); + } else if (enc->mode & CPU_MODE_FRAME) { + strcat(valueStr, "FRM"); + } else { + strcat(valueStr, "BRR"); + } + */ + + const int oneFieldW = (titleFont->size + 1); + const int allFieldW = oneFieldW * strlen(valueStr); + const int oneFieldH = (titleFont->size + 1); + if (!*out) { + *out = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, + allFieldW, oneFieldH); + } + + SDL_SetRenderTarget(renderer, *out); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + SDL_Rect rect = {0, 0, allFieldW, oneFieldH}; + SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255); + SDL_RenderFillRect(renderer, &rect); + + + int x = 0; + for (int j = 0; valueStr[j] != '\0'; j++) { + SDL_Texture *charTex = titleFont->texture[(uint8_t) valueStr[j]]; + if (charTex) { + SDL_Rect dstRect = {x, 0, titleFont->size, titleFont->size}; + x += titleFont->size + 1; + SDL_RenderCopy(renderer, charTex, NULL, &dstRect); + } + } + + SDL_SetRenderTarget(renderer, NULL); +} \ No newline at end of file diff --git a/cpustatusui.h b/cpustatusui.h new file mode 100644 index 0000000..e84e513 --- /dev/null +++ b/cpustatusui.h @@ -0,0 +1,19 @@ +// +// Created by bruno on 8.2.2025. +// + +#ifndef RISCB_CPUSTATUSUI_H +#define RISCB_CPUSTATUSUI_H + +#include "encoder.h" +#include "font.h" + +typedef struct { + char name[5]; + uint32_t value; +} CPUStatusPart; + + +void renderState(Encoder *cpu, BitmapFont *titleFont, SDL_Renderer *renderer, SDL_Texture **out); + +#endif //RISCB_CPUSTATUSUI_H diff --git a/crc32.c b/crc32.c new file mode 100644 index 0000000..865eb1e --- /dev/null +++ b/crc32.c @@ -0,0 +1,30 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#include "crc32.h" + +uint32_t calcCRC32( + const uint8_t *data, + size_t len) { + uint32_t crc = 0xffffffff; + + + while (len--) { + crc ^= *data++; + + + for (int i = 0; i < 8; i++) { + if (crc & 1) + crc = + (crc >> 1) + ^ 0xedb88320; + + else + crc >>= 1; + } + } + + + return ~crc; +} diff --git a/crc32.h b/crc32.h new file mode 100644 index 0000000..5f857c2 --- /dev/null +++ b/crc32.h @@ -0,0 +1,14 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#ifndef AUDIOCODER_CRC32_H +#define AUDIOCODER_CRC32_H + +#include +#include + + +uint32_t calcCRC32(const uint8_t *data,size_t length); + +#endif //AUDIOCODER_CRC32_H diff --git a/decoder.c b/decoder.c new file mode 100644 index 0000000..b54017a --- /dev/null +++ b/decoder.c @@ -0,0 +1,544 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#include "decoder.h" + +#include +#include +#include +#include +#include +#include + +#include "texteditor.h" + +Decoder decoder; + +float tone_energy( + Decoder *dec, + int index) { + return goertzel_energy( + &dec->tones[index] + ); +} + +static void decoder_reset_symbol_filters(Decoder *dec) { + const int tones = dec->bitCount * 2 + 2; + + for (int i = 0; i < tones; ++i) { + dec->tones[i].s1 = 0.0f; + dec->tones[i].s2 = 0.0f; + } +} + +static void decoder_history_push(Decoder *dec, float sample) { + dec->sampleHistory[dec->sampleHistoryWrite] = sample; + dec->sampleHistoryWrite = + (dec->sampleHistoryWrite + 1) % DECODER_SAMPLE_HISTORY; + + if (dec->sampleHistoryCount < DECODER_SAMPLE_HISTORY) + dec->sampleHistoryCount++; + + dec->totalSamples++; +} + +static bool decoder_history_get( + const Decoder *dec, + uint64_t sampleIndex, + float *sample) +{ + if (sampleIndex >= dec->totalSamples) + return false; + + uint64_t oldest = + dec->totalSamples - dec->sampleHistoryCount; + + if (sampleIndex < oldest) + return false; + + uint32_t start = + (dec->sampleHistoryWrite + DECODER_SAMPLE_HISTORY - dec->sampleHistoryCount) + % DECODER_SAMPLE_HISTORY; + + uint32_t rel = + (uint32_t)(sampleIndex - oldest); + + *sample = + dec->sampleHistory[(start + rel) % DECODER_SAMPLE_HISTORY]; + + return true; +} + +static uint8_t decode_symbol_from_work( + Goertzel *tones, + uint8_t bitCount) +{ + uint8_t byte = 0; + int index = 0; + + for (int bit = 0; bit < bitCount; ++bit) { + float e0 = goertzel_energy(&tones[index++]); + float e1 = goertzel_energy(&tones[index++]); + + if (e1 > e0) + byte |= 1u << bit; + } + + return byte; +} + +static bool decode_symbol_at( + Decoder *dec, + uint64_t sampleIndex, + uint8_t *out) +{ + const int tones = dec->bitCount * 2 + 2; + Goertzel work[MAX_BITS * 2 + 2]; + + memcpy(work, dec->tones, sizeof(work)); + + for (int i = 0; i < tones; ++i) { + work[i].s1 = 0.0f; + work[i].s2 = 0.0f; + } + + for (uint32_t s = 0; s < dec->symbolSamples; ++s) { + float sample; + if (!decoder_history_get(dec, sampleIndex + s, &sample)) + return false; + + for (int t = 0; t < tones; ++t) + goertzel_add(&work[t], sample); + } + + *out = decode_symbol_from_work(work, dec->bitCount); + return true; +} + +static void decoder_seed_live_symbol( + Decoder *dec, + uint64_t startSample) +{ + const int tones = dec->bitCount * 2 + 2; + + decoder_reset_symbol_filters(dec); + dec->sampleCount = 0; + + for (uint64_t index = startSample; index < dec->totalSamples; ++index) { + float sample; + if (!decoder_history_get(dec, index, &sample)) + break; + + for (int t = 0; t < tones; ++t) + goertzel_add(&dec->tones[t], sample); + + dec->sampleCount++; + } +} + +static bool try_phase_search(Decoder *dec) { + const uint32_t phaseStep = 16; + const uint32_t syncSymbols = (uint32_t)sizeof(syncBytes); + const uint64_t syncSamples = + (uint64_t)syncSymbols * (uint64_t)dec->symbolSamples; + const uint64_t oldest = + dec->totalSamples - dec->sampleHistoryCount; + + int bestScore = -1; + int bestPhase = 0; + uint64_t bestStart = 0; + + if (dec->sampleHistoryCount < syncSamples + dec->symbolSamples) + return false; + + for (uint32_t phase = 0; phase < dec->symbolSamples; phase += phaseStep) { + int64_t start = + (int64_t)dec->totalSamples - + (int64_t)syncSamples - + (int64_t)phase; + + if (start < (int64_t)oldest) + continue; + + int score = 0; + + for (size_t i = 0; i < sizeof(syncBytes); ++i) { + uint8_t byte; + if (!decode_symbol_at( + dec, + (uint64_t)start + + (uint64_t)i * dec->symbolSamples, + &byte)) { + score = -1; + break; + } + + if (byte != syncBytes[i]) + break; + + score++; + } + + if (score > bestScore) { + bestScore = score; + bestPhase = (int)phase; + bestStart = (uint64_t)start; + } + } + + dec->bestSyncScore = bestScore; + dec->bestSyncPhase = bestPhase; + + if (bestScore != (int)sizeof(syncBytes)) { + printf("PHASE SEARCH FAILED bestScore=%d (need %zu)\n", bestScore, sizeof(syncBytes)); + return false; + } + + uint64_t dataStart = + bestStart + syncSamples; + + printf("SYNC ALIGN phase=%d score=%d\n", bestPhase, bestScore); + dec->phaseOffset = bestPhase; + dec->phaseWait = 0; + dec->phaseSearchReady = false; + dec->rxState = RX_LOCKED; + dec->locked = true; + dec->bufferSize = 0; + dec->clockHistory = 0; + dec->clockCount = 0; + decoder_seed_live_symbol(dec, dataStart); + return true; +} + + +void decoder_init( + Decoder *dec) { + memset(dec, 0, sizeof(*dec)); + + + dec->bitCount = 8; + + + dec->startFreq = 1000; + dec->endFreq = 5000; + dec->dataSpacing = 250; + + dec->symbolSamples = + SAMPLE_RATE / 50; + + + dec->clock0 = goertzel_bin_freq( + 6000, + SAMPLE_RATE, + dec->symbolSamples +); + + dec->clock1 = goertzel_bin_freq( + 6300, + SAMPLE_RATE, + dec->symbolSamples + ); + + + /* + float spacing = + (float) (dec->endFreq - dec->startFreq) + / + (dec->bitCount * 2 - 1); + + */ + + decoder_rebuild_tones(dec); +} + +void decoder_rebuild_tones( + Decoder *dec) +{ + int index = 0; + + for (int i = 0; i < dec->bitCount; i++) { + goertzel_init( + &dec->tones[index++], + dec->startFreq + + dec->dataSpacing * (i * 2), + SAMPLE_RATE, + dec->symbolSamples + ); + + goertzel_init( + &dec->tones[index++], + dec->startFreq + + dec->dataSpacing * (i * 2 + 1), + SAMPLE_RATE, + dec->symbolSamples + ); + } + + goertzel_init( + &dec->tones[index++], + dec->clock0, + SAMPLE_RATE, + dec->symbolSamples + ); + + goertzel_init( + &dec->tones[index++], + dec->clock1, + SAMPLE_RATE, + dec->symbolSamples + ); + + dec->sampleCount = 0; + dec->sampleHistoryWrite = 0; + dec->sampleHistoryCount = 0; + dec->totalSamples = 0; + dec->maxEnergy = 0.0f; + dec->clockEnergy0 = 0.0f; + dec->clockEnergy1 = 0.0f; + dec->clockRatio = 0.0f; + dec->bestSyncScore = 0; + dec->bestSyncPhase = 0; + dec->bufferSize = 0; + dec->rxState = RX_SEARCH; + dec->phaseSearchReady = false; + dec->phaseWait = 0; + dec->phaseOffset = 0; + dec->clockHistory = 0; + dec->clockCount = 0; + dec->goodClocks = 0; + dec->searchOffset = 0; + dec->locked = false; + memset(dec->energies, 0, sizeof(dec->energies)); +} + +static int detect_clock(Decoder *dec) { + float e0 = tone_energy(dec, 16); + float e1 = tone_energy(dec, 17); + + float max = fmaxf(e0, e1); + float min = fminf(e0, e1); + + dec->clockEnergy0 = e0; + dec->clockEnergy1 = e1; + dec->clockRatio = min > 0.0f ? max / min : 0.0f; + dec->energies[16] = e0; + dec->energies[17] = e1; + dec->maxEnergy = fmaxf(dec->maxEnergy * 0.85f, max); + + + /* + Ignore silence/noise. + + Your real signal is around 0.1+ + noise is around 0.001-0.01 + */ + if (max < 0.001f) + return -1; + + return e1 > e0; +} + +static void clock_search( + Decoder *dec, + int clock) +{ + if(clock < 0) + { + dec->clockHistory = 0; + dec->clockCount = 0; + return; + } + + + dec->clockHistory = + (dec->clockHistory << 1) | + (clock & 1); + + + if(dec->clockCount < 8) + dec->clockCount++; + + + if(dec->clockCount >= 8) + { + uint8_t h = dec->clockHistory & 0xff; + + + if(h == 0x55 || h == 0xAA) + { + printf("CLOCK LOCK pattern=%02X\n", h); + dec->goodClocks = 8; + dec->clockHistory = 0; + dec->clockCount = 0; + + return; + } + + dec->goodClocks = 0; + } +} +static uint8_t decode_symbol(Decoder *dec) { + uint8_t byte = 0; + int index = 0; + + for (int bit = 0; bit < dec->bitCount; ++bit) { + float e0 = tone_energy(dec, index); + float e1 = tone_energy(dec, index + 1); + float max = fmaxf(e0, e1); + + dec->energies[index++] = e0; + dec->energies[index++] = e1; + dec->maxEnergy = fmaxf(dec->maxEnergy * 0.85f, max); + + if (e1 > e0) + byte |= 1u << bit; + } + + dec->lastDecodedByte = byte; + return byte; +} + +void decoder_process( + Decoder *dec, + float *samples, + uint32_t count) { + const int tones = dec->bitCount * 2 + 2; + + for (uint32_t i = 0; i < count; i++) { + decoder_history_push(dec, samples[i]); + + if (dec->rxState == RX_SEARCH || dec->rxState == RX_LOCKED) { + for (int t = 0; t < tones; t++) + goertzel_add(&dec->tones[t], samples[i]); + } + + if (dec->rxState == RX_ALIGN) { + if (dec->phaseWait > 0) + dec->phaseWait--; + + if (dec->phaseWait == 0) { + decoder_reset_symbol_filters(dec); + dec->sampleCount = 0; + dec->rxState = RX_LOCKED; + printf("PHASE LOCKED offset=%d\n", dec->phaseOffset); + } + + continue; + } + + dec->sampleCount++; + + if (dec->sampleCount < dec->symbolSamples) + continue; + + dec->sampleCount = 0; + + switch (dec->rxState) { + case RX_SEARCH: { + int clock = detect_clock(dec); + clock_search(dec, clock); + + if (dec->goodClocks >= 8) { + dec->searchOffset++; + if (dec->searchOffset >= 4) { + dec->searchOffset = 0; + if (try_phase_search(dec)) { + dec->phaseSearchReady = false; + dec->sampleCount = 0; + dec->clockHistory = 0; + dec->clockCount = 0; + dec->goodClocks = 0; + } else { + // Phase search failed, reset and try again + dec->goodClocks = 0; + dec->clockHistory = 0; + dec->clockCount = 0; + } + } + } + break; + } + + case RX_LOCKED: { + int clock = detect_clock(dec); + if (clock < 0) { + printf("clock lost\n"); + dec->rxState = RX_SEARCH; + dec->locked = false; + dec->sampleCount = 0; + dec->clockHistory = 0; + dec->clockCount = 0; + dec->goodClocks = 0; + dec->searchOffset = 0; + decoder_reset_symbol_filters(dec); + break; + } + + uint8_t b = decode_symbol(dec); + + if (dec->bufferSize < sizeof(dec->buffer)) + dec->buffer[dec->bufferSize++] = b; + + printf("DATA %02X\n", b); + break; + } + + } + } +} + +void input_callback( + void *userdata, + Uint8 *stream, + int len) { + Decoder *dec = userdata; + + float *samples = (float *) stream; + int count = len / sizeof(float); + + // Read from internal bus for loopback + for (int i = 0; i < count; i++) { + if (internalBusRead != internalBusWrite) { + samples[i] = internalBusSamples[internalBusRead]; + internalBusRead = (internalBusRead + 1) % INTERNAL_BUFFER_SIZE; + } else { + samples[i] = 0.0f; + } + } + + decoder_process( + dec, + samples, + count + ); +} + +SDL_AudioDeviceID inputDev; +AudioData decodedAudioData; + +void initAudioDecoder(SDL_Renderer *renderer) { + memset(&decodedAudioData, 0, sizeof(AudioData)); + decoder_init(&decoder); + decoder.renderer = renderer; + + SDL_AudioSpec inSpec = {0}; + inSpec.freq = SAMPLE_RATE; + inSpec.format = AUDIO_F32SYS; + inSpec.channels = 1; + inSpec.samples = 512; + inSpec.callback = input_callback; + inSpec.userdata = &decoder; + + SDL_AudioSpec realSpec = {0}; + + inputDev = SDL_OpenAudioDevice(NULL, 1, &inSpec, &realSpec, 0); + printf("Decoder device opened: inputDev=%u\n", inputDev); + if (inputDev == 0) { + printf("Failed to open audio decoder: %s\n", SDL_GetError()); + SDL_Quit(); + } + printf("Input device spec: freq=%d channels=%d samples=%d\n", + realSpec.freq, realSpec.channels, realSpec.samples); + SDL_PauseAudioDevice(inputDev, 0); +} diff --git a/decoder.h b/decoder.h new file mode 100644 index 0000000..9aa8387 --- /dev/null +++ b/decoder.h @@ -0,0 +1,120 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#ifndef AUDIOCODER_DECODER_H +#define AUDIOCODER_DECODER_H + +#define MAX_BITS 16 +#define PREAMBLE_SYMBOLS 128 +#define DECODER_SAMPLE_HISTORY 262144 +#include +#include +#include + +#include "goertzel.h" + +typedef enum { + RX_SEARCH, + RX_ALIGN, + RX_SYNC, + RX_LOCKED +} RxState; + +typedef struct { + + uint8_t bitCount; + + uint16_t startFreq; + uint16_t endFreq; + uint16_t dataSpacing; + + uint16_t clock0; + uint16_t clock1; + + + uint32_t symbolSamples; + + + uint32_t sampleCount; + + float sampleHistory[DECODER_SAMPLE_HISTORY]; + uint32_t sampleHistoryWrite; + uint32_t sampleHistoryCount; + uint64_t totalSamples; + + Goertzel tones[MAX_BITS * 2 + 2]; + + uint8_t currentByte; + uint8_t bitIndex; + + uint8_t buffer[1024*1024*8]; + + uint32_t bufferSize; + + bool lastClock; + + + float energies[MAX_BITS * 2 + 2]; + float maxEnergy; + float clockEnergy0; + float clockEnergy1; + float clockRatio; + uint8_t lastDecodedByte; + int bestSyncScore; + int bestSyncPhase; + + RxState rxState; + + bool expectedClock; + float lastClockEnergy; + uint32_t goodClocks; + uint32_t syncIndex; + uint32_t badClocks; + + uint64_t phaseSearchStart; + uint32_t phaseWait; + int phaseOffset; + bool phaseSearchReady; + + uint32_t searchOffset; + uint32_t syncOffset; + uint8_t clockHistory; + uint8_t clockCount; + + bool locked; + + SDL_Renderer *renderer; + +} Decoder; + + + +void decoder_init( + Decoder *dec +); + + +void decoder_process( + Decoder *dec, + float *samples, + uint32_t count +); + +void decoder_rebuild_tones( + Decoder *dec +); + +extern Decoder decoder; +extern SDL_AudioDeviceID inputDev; +extern const uint8_t syncBytes[4]; + +// Internal audio bus for loopback +#define INTERNAL_BUFFER_SIZE 262144 +extern float internalBusSamples[INTERNAL_BUFFER_SIZE]; +extern volatile uint32_t internalBusWrite; +extern volatile uint32_t internalBusRead; + +void initAudioDecoder(SDL_Renderer *renderer); + +#endif //AUDIOCODER_DECODER_H diff --git a/encoder.c b/encoder.c new file mode 100644 index 0000000..be0e565 --- /dev/null +++ b/encoder.c @@ -0,0 +1,322 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#include "encoder.h" + +#include +#include +#include + +#include "goertzel.h" + +SDL_AudioDeviceID dev; +AudioData encodedAudioData; + +Encoder encoder; + +// Internal audio bus +float internalBusSamples[INTERNAL_BUFFER_SIZE] = {0}; +volatile uint32_t internalBusWrite = 0; +volatile uint32_t internalBusRead = 0; + +#define PREAMBLE_SYMBOLS 128 + +const uint8_t syncBytes[4] = +{ + 'B', + 'R', + 'N', + 'Q' +}; + +static void encoder_set_bit( + AudioData *audio, + Encoder *enc, + unsigned bitIndex, + int bit) +{ + /* + float spacing = + (float)(enc->endFreq - enc->startFreq) / + (enc->bitCount * 2 - 1); + */ + + uint16_t freq = + enc->startFreq + + enc->dataSpacing * (bitIndex * 2 + bit); + + freq = goertzel_bin_freq( + freq, + SAMPLE_RATE, + enc->symbolSamples +); + + SynthVoice *v = + &audio->synthVoices[bitIndex + 1]; + + v->frequency = freq; + v->phaseStep = + ((uint64_t)freq << 32) / SAMPLE_RATE; + v->volume = 255; +} + +static int encoder_get_bit(Encoder *enc) { + if (enc->byteIndex >= enc->bufferSize) + return -1; + + int bit = (enc->buffer[enc->byteIndex] >> enc->bitIndex) & 1; + + enc->bitIndex++; + + if (enc->bitIndex == 8) { + enc->bitIndex = 0; + enc->byteIndex++; + } + + return bit; +} + +static void encoder_clock( + Encoder *enc, + AudioData *audio) +{ + enc->clockState ^= 1; + + SynthVoice *clock = + &audio->synthVoices[0]; + + uint16_t freq = + enc->clockState ? + enc->clock1 : + enc->clock0; + + clock->frequency = freq; + clock->phaseStep = + ((uint64_t)freq << 32) / + SAMPLE_RATE; + clock->volume = 255; +} + +void encoder_next_symbol( + Encoder *enc, + AudioData *audio) +{ + encoder_clock(enc, audio); + + switch(enc->state) + { + case TX_PREAMBLE: + + /* transmit all-zero symbol */ + + for(unsigned i=0;ibitCount;i++) + encoder_set_bit(audio,enc,i,0); + + enc->preambleSymbols++; + + if(enc->preambleSymbols >= PREAMBLE_SYMBOLS) + { + enc->syncIndex = 0; + enc->bitIndex = 0; + enc->state = TX_SYNC; + } + + break; + + case TX_SYNC: + { + uint8_t byte = + syncBytes[enc->syncIndex]; + + for(unsigned i=0;ibitCount;i++) + { + encoder_set_bit( + audio, + enc, + i, + (byte >> i) & 1); + } + + enc->syncIndex++; + + if(enc->syncIndex == + sizeof(syncBytes)) + { + enc->state = TX_DATA; + enc->byteIndex = 0; + enc->bitIndex = 0; + } + + break; + } + + case TX_DATA: + { + int finished = 0; + + for(unsigned i=0;ibitCount;i++) + { + int bit = + encoder_get_bit(enc); + + if(bit < 0) + { + bit = 0; + finished++; + } + + encoder_set_bit( + audio, + enc, + i, + bit); + } + + if(finished == + enc->bitCount) + { + enc->state = TX_END; + } + + break; + } + + case TX_END: + + stopEncoder(enc); + break; + + default: + break; + } + + enc->samplesRemaining = + enc->symbolSamples; +} + +void startEncoder(Encoder *enc) +{ + enc->state = TX_PREAMBLE; + enc->transmitting = true; + + enc->byteIndex = 0; + enc->bitIndex = 0; + enc->samplesRemaining = 0; + enc->clockState = false; + + enc->preambleSymbols = 0; + enc->syncIndex = 0; +} + +void stopEncoder(Encoder *enc) +{ + enc->transmitting=false; + enc->state=TX_IDLE; + + for(int i=0;iencoder; + + if (enc->state == TX_END ||!enc->transmitting) { + memset(stream, 0, len); + return; + } + + float *out = (float *) stream; + int samples = len / sizeof(float); + + for (int i = 0; i < samples; i++) { + if (enc->samplesRemaining == 0) + encoder_next_symbol(enc, audio); + + enc->samplesRemaining--; + + float mix = 0.0f; + int active = 0; + + for (unsigned v = 0; v < enc->bitCount + 1; v++) { + SynthVoice *voice = &audio->synthVoices[v]; + + if (voice->volume == 0) + continue; + + voice->phase += voice->phaseStep; + + float sample = sinf( + ((double) voice->phase / 4294967296.0) * + (2.0 * M_PI)); + + mix += sample * (voice->volume / 255.0f); + + active++; + } + + float output = active ? mix / (active + 1) : 0.0f; + out[i] = output; + + // Write to internal bus for loopback + internalBusSamples[internalBusWrite] = output; + internalBusWrite = (internalBusWrite + 1) % INTERNAL_BUFFER_SIZE; + } +} + +void initEncoder() +{ + memset(&encoder,0,sizeof(Encoder)); + + encoder.bufferSize = 0; + encoder.bitCount = 8; + encoder.byteIndex = 0; + + encoder.startFreq = 1000; + encoder.endFreq = 5000; + encoder.dataSpacing = 250; + encoder.symbolSamples = SAMPLE_RATE / 50; + + encoder.clock0 = goertzel_bin_freq( + 6000, + SAMPLE_RATE, + encoder.symbolSamples +); + + encoder.clock1 = goertzel_bin_freq( + 6300, + SAMPLE_RATE, + encoder.symbolSamples + ); + + + encoder.state = TX_IDLE; + encoder.transmitting = false; + +} + +void initAudioEncoder() { + memset(&encodedAudioData, 0, sizeof(AudioData)); + initEncoder(); + + SDL_AudioSpec spec = {0}; + spec.freq = SAMPLE_RATE; + spec.format = AUDIO_F32SYS; + spec.channels = 1; + spec.samples = 4096; + spec.callback = audio_callback; + encodedAudioData.encoder = &encoder; + spec.userdata = &encodedAudioData; + + dev = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0); + printf("Encoder device opened: dev=%u\n", dev); + if (dev == 0) { + printf("Failed to open audio encoder: %s\n", SDL_GetError()); + SDL_Quit(); + } + SDL_PauseAudioDevice(dev, 0); +} diff --git a/encoder.h b/encoder.h new file mode 100644 index 0000000..d21f391 --- /dev/null +++ b/encoder.h @@ -0,0 +1,91 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#ifndef AUDIOCODER_ENCODER_H +#define AUDIOCODER_ENCODER_H + +#define SAMPLE_RATE 48000 +#define MAX_BITS 16 +#define CLOCK_VOICE MAX_BITS +#define MAX_PACKET 223 + +#include +#include + +typedef struct SynthVoice { + uint32_t phase; + uint32_t phaseStep; + uint16_t frequency; + uint8_t volume; +} SynthVoice; + +typedef enum { + TX_IDLE, + TX_PREAMBLE, + TX_SYNC, + TX_DATA, + TX_END +} TxState; + + +typedef struct { + + unsigned int bitCount; + + unsigned int byteIndex; + unsigned int bitIndex; + uint8_t buffer[1024 * 1024 * 8]; + unsigned int bufferSize; + + + + uint16_t startFreq; + uint16_t endFreq; + uint16_t dataSpacing; + + + uint16_t clock0; + uint16_t clock1; + + bool clockState; + + + uint32_t symbolSamples; + uint32_t samplesRemaining; + + + TxState state; + uint32_t preambleSymbols; + uint32_t syncIndex; + + bool transmitting; + + +} Encoder; + + +typedef struct AudioData { + SynthVoice synthVoices[MAX_BITS + 1]; + Encoder *encoder; +} AudioData; + +extern Encoder encoder; +extern AudioData encodedAudioData; +extern SDL_AudioDeviceID dev; + +// Internal audio bus for loopback - simple ring buffer +#define INTERNAL_BUFFER_SIZE 262144 +extern float internalBusSamples[INTERNAL_BUFFER_SIZE]; +extern volatile uint32_t internalBusWrite; +extern volatile uint32_t internalBusRead; + +void startEncoder(Encoder *enc); + +void stopEncoder(Encoder *enc); + +void initAudioEncoder(); + +extern const uint8_t syncBytes[4]; + +#endif //AUDIOCODER_ENCODER_H diff --git a/font.c b/font.c new file mode 100644 index 0000000..b8371a4 --- /dev/null +++ b/font.c @@ -0,0 +1,61 @@ +// +// Created by bruno on 1.2.2025. +// + +#include "font.h" + +BitmapFont +prepText(SDL_Renderer *renderer, unsigned char pxSize, const char *file, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + TTF_Font *gFont = TTF_OpenFont(file, pxSize); + BitmapFont out; + out.size = pxSize; + out.color = (SDL_Color) {r, g, b, a}; + unsigned int i = 1; + do { + if (i == 173) { //specifically this char is 0 width (IDK why) + out.surface[i] = SDL_CreateRGBSurface(0, pxSize, pxSize, 32, 0, 0, 0, 0); + } else { + char tmpOut[2] = {i, 0}; + out.surface[i] = TTF_RenderText_Solid(gFont, tmpOut, out.color); + } + out.texture[i] = SDL_CreateTextureFromSurface(renderer, out.surface[i]); + i++; + } while (i < 256); + + TTF_CloseFont(gFont); + return out; +} + +void renderText(SDL_Renderer *renderer, BitmapFont font, char *string, uint16_t x, uint16_t y) { + SDL_Rect charRect; + charRect.x = 0; + charRect.y = 0; + charRect.w = font.size; + charRect.h = font.size; + SDL_Rect outRect = charRect; + outRect.x = x; + outRect.y = y; + + while (*string) { + if (*string == '\n') { + outRect.x = x; + outRect.y += charRect.h + 4; + string++; + continue; + } + SDL_RenderCopy(renderer, font.texture[*string], &charRect, &outRect); + outRect.x += charRect.w + 1; + string++; + } +} + +void destroyFont(BitmapFont *font) { + for (uint16_t i = 1; i < 256; i++) { + if (font->texture[i]) { + SDL_DestroyTexture(font->texture[i]); + } + if (font->surface[i]) { + SDL_FreeSurface(font->surface[i]); + } + } +} \ No newline at end of file diff --git a/font.h b/font.h new file mode 100644 index 0000000..98272a2 --- /dev/null +++ b/font.h @@ -0,0 +1,26 @@ +// +// Created by bruno on 1.2.2025. +// + +#ifndef RISCB_FONT_H +#define RISCB_FONT_H + +#include +#include +#include + +typedef struct { + SDL_Texture *texture[256]; + SDL_Surface *surface[256]; + uint8_t size; + SDL_Color color; +} BitmapFont; + +BitmapFont +prepText(SDL_Renderer *renderer, unsigned char pxSize, const char *file, uint8_t r, uint8_t g, uint8_t b, uint8_t a); + +void destroyFont(BitmapFont *font); + +void renderText(SDL_Renderer *renderer, BitmapFont font, char *string, uint16_t x, uint16_t y); + +#endif //RISCB_FONT_H diff --git a/goertzel.c b/goertzel.c new file mode 100644 index 0000000..9e78553 --- /dev/null +++ b/goertzel.c @@ -0,0 +1,75 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#include "goertzel.h" +#include + + +void goertzel_init( + Goertzel *g, + float freq, + float sampleRate, + int samples) +{ + + float omega = + 2.0f * M_PI * freq / sampleRate; + + + g->coeff = + 2.0f*cosf(omega); + + + g->s1=0; + g->s2=0; + +} + + + +void goertzel_add( + Goertzel *g, + float sample) +{ + + float s = + sample + + g->coeff*g->s1 - + g->s2; + + + g->s2=g->s1; + g->s1=s; + +} + + + +float goertzel_energy( + Goertzel *g) +{ + + float power = + g->s1*g->s1 + + g->s2*g->s2 - + g->coeff*g->s1*g->s2; + + + g->s1=0; + g->s2=0; + + + return power; + +} + +int goertzel_bin_freq( + int freq, + int sampleRate, + int samples) +{ + int bin = (freq * samples + sampleRate / 2) / sampleRate; + + return bin * sampleRate / samples; +} \ No newline at end of file diff --git a/goertzel.h b/goertzel.h new file mode 100644 index 0000000..604f1d0 --- /dev/null +++ b/goertzel.h @@ -0,0 +1,36 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#ifndef AUDIOCODER_GOERTZEL_H +#define AUDIOCODER_GOERTZEL_H + +typedef struct { + float coeff; + + float s1; + float s2; +} Goertzel; + + +void goertzel_init( + Goertzel *g, + float freq, + float sampleRate, + int samples); + + +void goertzel_add( + Goertzel *g, + float sample); + + +float goertzel_energy( + Goertzel *g); + +int goertzel_bin_freq( + int freq, + int sampleRate, + int samples); + +#endif //AUDIOCODER_GOERTZEL_H diff --git a/hexdump.c b/hexdump.c new file mode 100644 index 0000000..e2e4c89 --- /dev/null +++ b/hexdump.c @@ -0,0 +1,46 @@ +// +// Created by bruno on 6.2.2025. +// + +#include "hexdump.h" +#include +#include +#include +#include +#include + +#define BYTES_PER_LINE 8 // Adjust for different widths + + +char *hexdump_to_string(const unsigned char *data, uint32_t start, uint32_t end) { + if (start >= end) return NULL; // Invalid range + + // Estimate max output size: each line is approx. 80 chars + uint32_t estimated_size = ((end - start) / BYTES_PER_LINE + 1) * 60; + + // Allocate memory for output string + char *output = malloc(estimated_size); + if (!output) return NULL; + + uint32_t out_offset = 0; // Tracks position in output buffer + + for (uint32_t i = start; i < end; i += BYTES_PER_LINE) { + // Print offset + out_offset += snprintf(output + out_offset, estimated_size - out_offset, "%07x ", i); + + // Print hex values + for (uint32_t j = 0; j < BYTES_PER_LINE; j++) { + if (i + j < end) + out_offset += snprintf(output + out_offset, estimated_size - out_offset, "%02x ", data[i + j]); + else + out_offset += snprintf(output + out_offset, estimated_size - out_offset, " "); // Padding + if (j == 7) + out_offset += snprintf(output + out_offset, estimated_size - out_offset, " "); // Extra space + } + + out_offset += snprintf(output + out_offset, estimated_size - out_offset, "\n"); + } + out_offset += snprintf(output + out_offset, estimated_size - out_offset, "\n"); + + return output; +} diff --git a/hexdump.h b/hexdump.h new file mode 100644 index 0000000..4e3abb3 --- /dev/null +++ b/hexdump.h @@ -0,0 +1,13 @@ +// +// Created by bruno on 6.2.2025. +// + +#ifndef RISCB_HEXDUMP_H +#define RISCB_HEXDUMP_H + +#include +#include + +char *hexdump_to_string(const unsigned char *data, uint32_t start, uint32_t end); + +#endif //RISCB_HEXDUMP_H diff --git a/main.c b/main.c new file mode 100644 index 0000000..7396d94 --- /dev/null +++ b/main.c @@ -0,0 +1,596 @@ +#include +#include +#include +#include +#include +#include "font.h" +#include "cpustatusui.h" +#include "decoder.h" +#include "goertzel.h" +#include "texteditor.h" +#include "hexdump.h" + +//The window we'll be rendering to +SDL_Window *window = NULL; +volatile bool running = true; + +//The surface contained by the window +SDL_Renderer *renderer = NULL; +#define SCREEN_WIDTH 1280 +#define SCREEN_HEIGHT 720 + +const int targetFPS = 60; +const int delayNeeded = 1000 / targetFPS; + + +SDL_Texture *cpuStateTexture; +int selectedModemSetting = 0; + +#define biggerFont fonts[0] +#define smallFont fonts[1] +#define smallerFont fonts[2] + +#define fontCount 3 + +BitmapFont fonts[fontCount]; + +static const char *tx_state_name(TxState state) { + switch (state) { + case TX_IDLE: return "IDLE"; + case TX_PREAMBLE: return "PREAMBLE"; + case TX_SYNC: return "SYNC"; + case TX_DATA: return "DATA"; + case TX_END: return "END"; + } + return "?"; +} + +static const char *rx_state_name(RxState state) { + switch (state) { + case RX_SEARCH: return "SEARCH"; + case RX_ALIGN: return "ALIGN"; + case RX_SYNC: return "SYNC"; + case RX_LOCKED: return "LOCKED"; + } + return "?"; +} + +static const char *setting_name(int setting) { + switch (setting) { + case 0: return "DATA START"; + case 1: return "DATA END"; + case 2: return "SPACING"; + case 3: return "SYMBOL"; + case 4: return "CLOCK0"; + case 5: return "CLOCK1"; + case 6: return "OUT DEVICE"; + case 7: return "IN DEVICE"; + } + return "?"; +} + +static void apply_modem_settings(void) { + SDL_LockAudioDevice(dev); + SDL_LockAudioDevice(inputDev); + + stopEncoder(&encoder); + + encoder.clock0 = goertzel_bin_freq( + encoder.clock0, + SAMPLE_RATE, + encoder.symbolSamples); + encoder.clock1 = goertzel_bin_freq( + encoder.clock1, + SAMPLE_RATE, + encoder.symbolSamples); + + decoder.bitCount = encoder.bitCount; + decoder.startFreq = encoder.startFreq; + decoder.endFreq = encoder.endFreq; + decoder.dataSpacing = encoder.dataSpacing; + decoder.symbolSamples = encoder.symbolSamples; + decoder.clock0 = encoder.clock0; + decoder.clock1 = encoder.clock1; + decoder_rebuild_tones(&decoder); + + SDL_UnlockAudioDevice(inputDev); + SDL_UnlockAudioDevice(dev); +} + +static void adjust_modem_setting(int delta) { + switch (selectedModemSetting) { + case 0: + if ((int)encoder.startFreq + delta * 50 >= 200) + encoder.startFreq += delta * 50; + break; + case 1: + if ((int)encoder.endFreq + delta * 50 >= 200) + encoder.endFreq += delta * 50; + break; + case 2: + if ((int)encoder.dataSpacing + delta * 5 >= 25) + encoder.dataSpacing += delta * 5; + break; + case 3: + if ((int)encoder.symbolSamples + delta * 48 >= 240) + encoder.symbolSamples += delta * 48; + break; + case 4: + if ((int)encoder.clock0 + delta * 50 >= 500) + encoder.clock0 += delta * 50; + break; + case 5: + if ((int)encoder.clock1 + delta * 50 >= 500) + encoder.clock1 += delta * 50; + break; + case 6: + case 7: + // Device selection handled separately + break; + } + + apply_modem_settings(); +} + +static void render_modem_text(SDL_Renderer *renderer) { + char line[256]; + + snprintf( + line, + sizeof(line), + "TX %-8s BYTE %u/%u BIT %u PRE %u SYNC %u", + tx_state_name(encoder.state), + encoder.byteIndex, + encoder.bufferSize, + encoder.bitIndex, + encoder.preambleSymbols, + encoder.syncIndex); + renderText(renderer, smallerFont, line, 10, 8); + + snprintf( + line, + sizeof(line), + "RX %-8s BYTES %u LAST %02X BESTSYNC %d/4 PHASE %d", + rx_state_name(decoder.rxState), + decoder.bufferSize, + decoder.lastDecodedByte, + decoder.bestSyncScore, + decoder.bestSyncPhase); + renderText(renderer, smallerFont, line, 10, 22); + + snprintf( + line, + sizeof(line), + "CLK E0 %.5f E1 %.5f R %.1f HIST %02X/%u", + decoder.clockEnergy0, + decoder.clockEnergy1, + decoder.clockRatio, + decoder.clockHistory, + decoder.clockCount); + renderText(renderer, smallerFont, line, 10, 36); + + // Settings display with highlighting + const char *settingStr = setting_name(selectedModemSetting); + snprintf( + line, + sizeof(line), + ">> %s START %u END %u SPACE %u SYM %u CLK %u/%u [F8 SELECT] [+/- CHANGE]", + settingStr, + encoder.startFreq, + encoder.endFreq, + encoder.dataSpacing, + encoder.symbolSamples, + encoder.clock0, + encoder.clock1); + + // Highlight the settings line with a colored background + SDL_Rect settingsBG = {5, 48, 1270, 14}; + SDL_SetRenderDrawColor(renderer, 64, 80, 96, 128); + SDL_RenderFillRect(renderer, &settingsBG); + + renderText(renderer, smallerFont, line, 10, 50); +} + +static void render_tone_bars(SDL_Renderer *renderer) { + const int tones = decoder.bitCount * 2 + 2; + const int x = 500; + const int y = 560; + const int maxHeight = 72; + const int barWidth = 8; + const int gap = 4; + + SDL_SetRenderDrawColor(renderer, 64, 64, 64, 255); + SDL_Rect frame = {x - 4, y - maxHeight - 4, tones * (barWidth + gap) + 4, maxHeight + 24}; + SDL_RenderDrawRect(renderer, &frame); + + for (int i = 0; i < tones; i++) { + float ratio = 0.0f; + if (decoder.maxEnergy > 0.0f) + ratio = decoder.energies[i] / decoder.maxEnergy; + if (ratio > 1.0f) + ratio = 1.0f; + + SDL_Rect barRect; + barRect.w = barWidth; + barRect.h = (int)(ratio * maxHeight); + barRect.x = x + i * (barWidth + gap); + barRect.y = y - barRect.h; + + if (i >= decoder.bitCount * 2) + SDL_SetRenderDrawColor(renderer, 255, 192, 64, 255); + else if (i & 1) + SDL_SetRenderDrawColor(renderer, 80, 180, 255, 255); + else + SDL_SetRenderDrawColor(renderer, 80, 255, 140, 255); + + SDL_RenderFillRect(renderer, &barRect); + } + + char label[128]; + snprintf(label, sizeof(label), "TONE ENERGY: BIT0..BIT7, CLOCK0, CLOCK1"); + renderText(renderer, smallerFont, label, x, y + 6); +} + + +int render() { + SDL_SetRenderDrawColor(renderer, 32, 32, 32, 255); + SDL_RenderClear(renderer); + + + SDL_Rect rect2; + rect2.x = 100; + rect2.y = 46; + rect2.w = 0; + rect2.h = 0; + + if (cpuStateTexture) { + SDL_QueryTexture(cpuStateTexture, NULL, NULL, &rect2.w, &rect2.h); + + SDL_RenderCopy(renderer, cpuStateTexture, NULL, &rect2); + } + + render_modem_text(renderer); + render_tone_bars(renderer); + + for (int i = 0; i < editorCount; i++) { + editor_render(&editors[i], renderer, &encoder, i, activeEditorIndex == i, cursorShown); + } + frames++; + if (!(frames % 60)) { + cursorShown = !cursorShown; + } + + SDL_RenderPresent(renderer); + + return 0; +} + +int init() { + //Initialize SDL + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, NULL); + SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1"); + SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { + printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); + return 1; + } + + //Initialize SDL_ttf + if (TTF_Init() == -1) { + printf("SDL_ttf could not initialize! SDL_ttf Error: %s\n", TTF_GetError()); + return 1; + } + + //Create window + window = SDL_CreateWindow("RISC-B simulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, + SCREEN_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + if (window == NULL) { + printf("Window could not be created! SDL_Error: %s\n", SDL_GetError()); + return 1; + } + //Get window surface + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (renderer == NULL) { + printf("Renderer could not be created SDL_Error: %s\n", SDL_GetError()); + return 1; + } + + // Create OpenGL context + SDL_GLContext glContext = SDL_GL_CreateContext(window); + if (!glContext) { + fprintf(stderr, "SDL_GL_CreateContext failed: %s\n", SDL_GetError()); + exit(1); + } + + // Use OpenGL context + SDL_GL_MakeCurrent(window, glContext); // Make sure OpenGL context is current before any OpenGL rendering + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + biggerFont = prepText(renderer, 16, "PublicPixel.ttf", 255, 255, 255, 255); + smallFont = prepText(renderer, 12, "PublicPixel.ttf", 255, 255, 255, 255); + smallerFont = prepText(renderer, 8, "PublicPixel.ttf", 255, 255, 255, 255); + init_editor(&transmitEditor, &smallFont, 10, 80, renderer, 35, 1000, 28, false); + init_editor(&receivedEditor, &smallFont, 500, 80, renderer, 35, 1000, 28, false); + + initAudioEncoder(); + initAudioDecoder(renderer); + strcpy(encoder.buffer, "Hello there\nThis is just a test message\n"); + + encoder.bufferSize = strlen(encoder.buffer); + + updateDump(&transmitEditor, renderer); + + + SDL_Rect viewport = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; + SDL_RenderSetViewport(renderer, &viewport); + SDL_RenderSetLogicalSize(renderer, SCREEN_WIDTH, SCREEN_HEIGHT); + + return 0; +} + + +int processEvent(SDL_Event e) { + if (e.type == SDL_QUIT) { return 0; } + else if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_RESIZED) { + int newWidth = e.window.data1; + int newHeight = e.window.data2; + + for (int editorIndex = 0; editorIndex < editorCount; editorIndex++) { + generate_string_display(&editors[editorIndex], renderer); + } + //updateState(true); + + // Adjust the viewport to match the new window size; + SDL_Rect viewport = {0, 0, newWidth, newHeight}; + SDL_RenderSetViewport(renderer, &viewport); + } else if (e.type == SDL_KEYDOWN) { + int keySym = ConvertKPToNonKP(e.key.keysym.sym); + int keyMod = e.key.keysym.mod; + cursorShown = true; + bool moved = false; + switch (keySym) { + case SDLK_UP: + move_cursor_relative(&activeEditor, -1, 0, false, renderer); + if (keyMod & KMOD_CTRL) { + encoder.byteIndex--; + } + moved = true; + break; + case SDLK_PAGEUP: + move_cursor_relative(&activeEditor, -activeEditor.displayLineCount, -1, true, renderer); + moved = true; + break; + case SDLK_DOWN: + move_cursor_relative(&activeEditor, 1, 0, false, renderer); + if (keyMod & KMOD_CTRL) { + encoder.byteIndex++; + } + moved = true; + break; + case SDLK_PAGEDOWN: + move_cursor_relative(&activeEditor, activeEditor.displayLineCount, 0, true, renderer); + moved = true; + break; + case SDLK_LEFT: + move_cursor_relative(&activeEditor, 0, -1, false, renderer); + moved = true; + break; + case SDLK_HOME: + if (keyMod & KMOD_CTRL) { + move_cursor(&activeEditor, 0, 0, false, renderer); + moved = true; + break; + } + move_cursor(&activeEditor, activeEditor.cursor_line, 0, false, renderer); + moved = true; + break; + case SDLK_RIGHT: + move_cursor_relative(&activeEditor, 0, 1, false, renderer); + moved = true; + break; + case SDLK_END: + int lineLen = strlen(activeEditor.lines[activeEditor.cursor_line].text); + if (keyMod & KMOD_CTRL) { + move_cursor(&activeEditor, activeEditor.maxLines - 1, lineLen, false, renderer); + moved = true; + break; + } + move_cursor(&activeEditor, activeEditor.cursor_line, lineLen, false, renderer); + moved = true; + break; + case SDLK_d: + if (keyMod & KMOD_CTRL && !activeEditor.readOnly) { + insert_line_rel(&activeEditor, renderer); + memset(activeEditor.lines[activeEditor.cursor_line].text, 0, activeEditor.max_line_width); + strcpy(activeEditor.lines[activeEditor.cursor_line].text, + activeEditor.lines[activeEditor.cursor_line - 1].text); + activeEditor.lines[activeEditor.cursor_line].active = 1; + move_cursor_relative(&activeEditor, 0, activeEditor.max_line_width, false, renderer); + generate_string_display(&activeEditor, renderer); + } + break; + + case SDLK_ESCAPE: + stopEncoder(&encoder); + break; + + case SDLK_s: + if (keyMod & KMOD_CTRL) { + FILE *fptr; + char fname[20]; + snprintf(fname, sizeof(fname), "data%lu.bin", time(NULL)); + fptr = fopen(fname, "wb"); + unsigned int sizeBuffer = sizeof(encoder.buffer); + if (encoder.bufferSize < sizeBuffer) { + sizeBuffer = encoder.bufferSize; + } + fwrite(encoder.buffer, sizeBuffer, 1, fptr); + fclose(fptr); + return 1; + } + break; + + case SDLK_l: + if (keyMod & KMOD_CTRL) { + FILE *fptr; + char fname[20]; + sscanf(editors[0].lines[editors[0].cursor_line].text, "%s", fname); + fptr = fopen(fname, "rb"); + if (fptr) { + memset(encoder.buffer, 0, sizeof(encoder.buffer)); + encoder.bufferSize = fread( + encoder.buffer, + 1, + sizeof(encoder.buffer), + fptr); + fclose(fptr); + updateDump(&transmitEditor, renderer); + } + return 1; + } + break; + case SDLK_BACKSPACE: + if (!activeEditor.readOnly) { + remove_character(&activeEditor, false, renderer); + moved = true; + } + break; + case SDLK_DELETE: + if (keyMod & KMOD_CTRL) { + memset(encoder.buffer, 0, sizeof(encoder.buffer)); + encoder.bufferSize = 0; + updateDump(&transmitEditor, renderer); + + } else if (keyMod & KMOD_ALT) { + memset(decoder.buffer, 0, sizeof(decoder.buffer)); + decoder.bufferSize = 0; + updateDump(&receivedEditor, renderer); + + } else { + if (!activeEditor.readOnly) { + remove_character(&activeEditor, true, renderer); + moved = true; + } + } + break; + case SDLK_RETURN: + break; + case SDLK_F5: + updateDump(&transmitEditor, renderer); + break; + case SDLK_F7: + startEncoder(&encoder); + break; + case SDLK_F8: + selectedModemSetting++; + if (selectedModemSetting >= 8) + selectedModemSetting = 0; + break; + case SDLK_PLUS: + case SDLK_EQUALS: + adjust_modem_setting(1); + break; + case SDLK_MINUS: + adjust_modem_setting(-1); + break; + case SDLK_INSERT: + activeEditor.overwrite = !activeEditor.overwrite; + break; + case SDLK_RETURN2: + if (!activeEditor.readOnly) { + insert_line_rel(&activeEditor, renderer); + moved = true; + } + break; + case SDLK_TAB: + activeEditorIndex++; + if (activeEditorIndex >= editorCount) { + activeEditorIndex = 0; + } + activeEditor = editors[activeEditorIndex]; + break; + default: + break; + } + } else if (e.type == SDL_TEXTINPUT) { + for (int i = 0; e.text.text[i] != '\0'; i++) { // Iterate over the input string + char keySym = e.text.text[i]; + + if (!activeEditor.readOnly && activeEditor.cursor_pos <= activeEditor.max_line_width) { + if (keySym >= 32 && keySym <= 126) { // Printable ASCII range + if (keySym > 0x60 && keySym < 0x7b) { // Convert lowercase to uppercase + keySym -= 0x20; + } + if (activeEditor.cursor_pos < activeEditor.max_line_width) { + insert_character(&activeEditor, keySym, renderer); + } + } + } + } + } else if (e.type == SDL_MOUSEBUTTONDOWN) { + SDL_Rect viewport; + SDL_RenderGetViewport(renderer, &viewport); + + SDL_Rect mouset; + mouset.w = 1; + mouset.h = 1; + SDL_GetMouseState(&mouset.x, &mouset.y); + // Translate mouse coordinates to viewport space + SDL_Rect mouse; + mouse.w = 1; + mouse.h = 1; + mouse.x = ((mouset.x - viewport.x) * SCREEN_WIDTH) / viewport.w; + mouse.y = (mouset.y - viewport.y) * SCREEN_HEIGHT / viewport.h; + } + return 1; +} + +int main(void) { + int status = init(); + if (status) { + return status; + } + + //Hack to get window to stay up + SDL_Event e; + Uint64 start; + Uint64 end; + while (running) { + start = SDL_GetTicks64(); + + while (SDL_PollEvent(&e)) { + running = processEvent(e); + } + + updateDump(&receivedEditor, decoder.renderer); + status = render(); + if (status) { + return status; + } + + + end = SDL_GetTicks64(); + const unsigned long timeNeeded = end - start; + if (timeNeeded < delayNeeded) { + SDL_Delay(delayNeeded - timeNeeded); + } + } + + for (uint8_t i = 0; i < editorCount; i++) { + destroy_editor(&editors[i]); + } + + for (uint8_t i = 0; i < fontCount; i++) { + destroyFont(&fonts[i]); + } + + puts(SDL_GetError()); + + if (cpuStateTexture) SDL_DestroyTexture(cpuStateTexture); + + if (renderer) SDL_DestroyRenderer(renderer); + if (window) SDL_DestroyWindow(window); + + SDL_Quit(); + return 0; +} diff --git a/packet.c b/packet.c new file mode 100644 index 0000000..42c9f64 --- /dev/null +++ b/packet.c @@ -0,0 +1,136 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#include "packet.h" +#include "crc32.h" + +#include +#include + + +#define MAGIC 0xCA55 + + +#define PACKET_SIZE 223 + + +uint8_t *build_stream( + uint8_t *file, + uint32_t size, + uint32_t *outSize) { + uint32_t packets = + (size + PACKET_SIZE - 1) + / + PACKET_SIZE; + + + /* + estimate: + + preamble 256 bytes + sync 16 bytes + packets: + header 4 + payload 223 + crc 4 + */ + + + uint32_t capacity = + 256 + + 16 + + packets * (4 + 2 + 2 + 223 + 4); + + + uint8_t *out = + malloc(capacity); + + + uint32_t pos = 0; + + + /* + preamble + */ + + for (int i = 0; i < 256; i++) + out[pos++] = 0x55; + + + /* + sync + */ + + uint8_t sync[] = + { + 0xD3, + 0x91, + 0xAC, + 0x55, + 0x77, + 0x22, + 0xF0, + 0x0F + }; + + + memcpy( + out + pos, + sync, + sizeof(sync)); + + pos += sizeof(sync); + + + uint32_t offset = 0; + + + for (uint32_t p = 0; p < packets; p++) { + uint16_t magic = MAGIC; + + uint16_t packet = p; + + + uint16_t len = + size - offset > PACKET_SIZE ? PACKET_SIZE : size - offset; + + + memcpy(out + pos, &magic, 2); + pos += 2; + + + memcpy(out + pos, &packet, 2); + pos += 2; + + + memcpy(out + pos, &len, 2); + pos += 2; + + + memcpy(out + pos, + file + offset, + len); + + pos += len; + + + uint32_t crc = + calcCRC32( + file + offset, + len); + + + memcpy(out + pos, &crc, 4); + + pos += 4; + + + offset += len; + } + + + *outSize = pos; + + return out; +} diff --git a/packet.h b/packet.h new file mode 100644 index 0000000..f3eb5c1 --- /dev/null +++ b/packet.h @@ -0,0 +1,17 @@ +// +// Created by bruno on 13. 7. 2026. +// + +#ifndef AUDIOCODER_PACKET_H +#define AUDIOCODER_PACKET_H + +#include + + +uint8_t *build_stream( + uint8_t *file, + uint32_t size, + uint32_t *outSize +); + +#endif //AUDIOCODER_PACKET_H diff --git a/texteditor.c b/texteditor.c new file mode 100644 index 0000000..c1e1b24 --- /dev/null +++ b/texteditor.c @@ -0,0 +1,592 @@ +// +// Created by bruno on 5.2.2025. +// Modified to use dynamic limits. +// + +#include "texteditor.h" + +#include "decoder.h" +#include "font.h" +#include "hexdump.h" + +unsigned long frames = 0; +bool cursorShown = true; + +int activeEditorIndex = 0; +TextEditor editors[editorCount]; + +// Initialize the text editor with dynamic sizes. +void init_editor(TextEditor *editor, BitmapFont *font, int x, int y, SDL_Renderer *renderer, + int max_line_width, int max_lines_asm, int max_lines_display, bool readOnly) { + editor->max_line_width = max_line_width; + editor->maxLines = max_lines_asm; + editor->displayLineCount = max_lines_display; + editor->line_count = 0; + editor->cursor_line = 0; + editor->cursor_line_offset = 0; + editor->cursor_pos = 0; + editor->readOnly = readOnly; + editor->font = font; + + editor->outRect = malloc(sizeof(SDL_Rect)); + editor->cursorRect = malloc(sizeof(SDL_Rect)); + editor->rect = malloc(sizeof(SDL_Rect)); + editor->highlightedLineRect = malloc(sizeof(SDL_Rect)); + + memset(editor->outRect, 0, sizeof(SDL_Rect)); + memset(editor->cursorRect, 0, sizeof(SDL_Rect)); + memset(editor->rect, 0, sizeof(SDL_Rect)); + memset(editor->highlightedLineRect, 0, sizeof(SDL_Rect)); + + + // Allocate dynamic array for lines. + editor->lines = (Line *) malloc(sizeof(Line) * editor->maxLines); + if (!editor->lines) { + fprintf(stderr, "Failed to allocate memory for lines.\n"); + exit(EXIT_FAILURE); + } + // For each line, allocate memory for the text (including space for '\0') + for (int i = 0; i < editor->maxLines; i++) { + editor->lines[i].text = (char *) malloc(sizeof(char) * (editor->max_line_width + 1)); + if (!editor->lines[i].text) { + fprintf(stderr, "Failed to allocate memory for line %d.\n", i); + exit(EXIT_FAILURE); + } + editor->lines[i].text[0] = '\0'; + editor->lines[i].active = 0; + } + + // Allocate output and display strings. + editor->outputString = (char *) malloc(sizeof(char) * ((editor->max_line_width + 1) * editor->maxLines + 1) + 1); + editor->displayString = (char *) malloc( + sizeof(char) * ((editor->max_line_width + 1) * editor->displayLineCount + 1) + 1); + if (!editor->outputString || !editor->displayString) { + fprintf(stderr, "Failed to allocate memory for output/display strings.\n"); + exit(EXIT_FAILURE); + } + editor->outputString[0] = '\0'; + editor->displayString[0] = '\0'; + + // Initialize with two active lines (like the original code). + editor->lines[0].active = 1; + editor->lines[1].active = 1; + editor->line_count = 2; + + // Set up the editor rectangle based on font size and dynamic max_line_width and displayLineCount. + editor->rect->x = 2; + editor->rect->y = 2; + editor->rect->w = editor->max_line_width * (font->size + 1) + ((font->size + 1) / 2); + editor->rect->h = editor->displayLineCount * (font->size + 1) + 2; + editor->outRect->w = editor->rect->w; + editor->outRect->h = editor->rect->h; + editor->outRect->x = x; + editor->outRect->y = y; + + editor->highlightedLineRect->x = 2; + editor->highlightedLineRect->y = 2; + editor->highlightedLineRect->w = editor->outRect->w - (2 * editor->highlightedLineRect->x); + editor->highlightedLineRect->h = editor->font->size + 1; + + editor->cursorRect->x = 3 + editor->cursor_pos * font->size + editor->outRect->x; + editor->cursorRect->y = + 2 + (editor->cursor_line - editor->cursor_line_offset) * (font->size + 1) + editor->outRect->y; + + editor->cursorRect->w = editor->font->size; + editor->cursorRect->h = editor->font->size; + + // Create texture for rendering. + editor->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_TARGET, + editor->rect->w, editor->rect->h); + if (!editor->texture) { + fprintf(stderr, "Failed to create texture: %s\n", SDL_GetError()); + exit(EXIT_FAILURE); + } +} + +void insert_line(TextEditor *editor, int position, const char *text, SDL_Renderer *renderer) { + if (editor->line_count >= editor->maxLines || position < 0 || position > editor->line_count) { + printf("Invalid position or max lines reached!\n"); + return; + } + + // Shift lines down. + for (int i = editor->line_count; i > position; i--) { + strcpy(editor->lines[i].text, editor->lines[i - 1].text); + editor->lines[i].active = editor->lines[i - 1].active; + } + + // Copy the text into the new line, ensuring it does not exceed max_line_width. + strncpy(editor->lines[position].text, text, editor->max_line_width); + editor->lines[position].text[editor->max_line_width] = '\0'; + editor->lines[position].active = 1; + + editor->line_count++; + move_cursor(editor, editor->cursor_line + 1, 0, false, renderer); + generate_string_display(editor, renderer); +} + +void insert_line_rel(TextEditor *editor, SDL_Renderer *renderer) { + int line = editor->cursor_line; + int pos = editor->cursor_pos; + char *current_text = editor->lines[line].text; + + // Split the current line at the cursor position + char new_line_text[editor->max_line_width + 1]; + strcpy(new_line_text, ¤t_text[pos]); // Copy the text after cursor to the new line + current_text[pos] = '\0'; // Truncate the current line at cursor position + + insert_line(editor, line + 1, new_line_text, renderer); + editor->cursor_pos = 0; +} + +void insert_character(TextEditor *editor, char ch, SDL_Renderer *renderer) { + if (editor->cursor_line < 0 || editor->cursor_line >= editor->line_count) { + printf("Invalid cursor position!\n"); + return; + } + + Line *line = &editor->lines[editor->cursor_line]; + int len = strlen(line->text); + + if (editor->overwrite) { + if (editor->cursor_pos > len) { + printf("Position out of bounds or line is full!\n"); + return; + } + line->text[editor->cursor_pos] = ch; + } else { + + if (len >= editor->max_line_width || editor->cursor_pos > len) { + printf("Position out of bounds or line is full!\n"); + return; + } + + // Shift characters to the right. + for (int i = len; i >= editor->cursor_pos; i--) { + line->text[i + 1] = line->text[i]; + } + + line->text[editor->cursor_pos] = ch; + } + editor->cursor_pos++; + generate_string_display(editor, renderer); +} + +void remove_character(TextEditor *editor, bool isDelete, SDL_Renderer *renderer) { + if (editor->cursor_line < 0 || editor->cursor_line >= editor->line_count) { + printf("Invalid cursor position!\n"); + return; + } + + Line *line = &editor->lines[editor->cursor_line]; + int len = strlen(line->text); + + if (isDelete) { + // Delete character after cursor + if (editor->cursor_pos < len) { + for (int i = editor->cursor_pos; i < len; i++) { + line->text[i] = line->text[i + 1]; + } + } else if (editor->cursor_pos == len && editor->cursor_line < editor->line_count - 1) { + // Merge next line with the current line when pressing delete at the end + strcat(line->text, editor->lines[editor->cursor_line + 1].text); + + // Shift remaining lines up + for (int i = editor->cursor_line + 1; i < editor->line_count - 1; i++) { + strcpy(editor->lines[i].text, editor->lines[i + 1].text); + editor->lines[i].active = editor->lines[i + 1].active; + } + editor->lines[editor->line_count - 1].text[0] = '\0'; + editor->lines[editor->line_count - 1].active = 0; + editor->line_count--; + } + } else { + // Backspace behavior (delete character before cursor) + if (editor->cursor_pos == 0 && editor->cursor_line > 0) { + // Append current line to previous line + int prev_len = strlen(editor->lines[editor->cursor_line - 1].text); + strcat(editor->lines[editor->cursor_line - 1].text, line->text); + + // Shift lines up + for (int i = editor->cursor_line; i < editor->line_count - 1; i++) { + strcpy(editor->lines[i].text, editor->lines[i + 1].text); + editor->lines[i].active = editor->lines[i + 1].active; + } + editor->lines[editor->line_count - 1].text[0] = '\0'; + editor->lines[editor->line_count - 1].active = 0; + editor->line_count--; + + // Move cursor to the end of the previous line + editor->cursor_line--; + editor->cursor_pos = prev_len; + } else if (editor->cursor_pos > 0) { + for (int i = editor->cursor_pos - 1; i < len; i++) { + line->text[i] = line->text[i + 1]; + } + editor->cursor_pos--; + } + } + + generate_string_display(editor, renderer); +} + + +void move_cursor_relative(TextEditor *editor, int line_offset, int pos_offset, bool keepPos, SDL_Renderer *renderer) { + int new_line = editor->cursor_line + line_offset; + int new_pos = editor->cursor_pos + pos_offset; + + move_cursor(editor, new_line, new_pos, keepPos, renderer); +} + +void move_cursor(TextEditor *editor, int new_line, int new_pos, bool keepPos, SDL_Renderer *renderer) { + if (new_line < 0) new_line = 0; + int trgt = editor->line_count - editor->displayLineCount; + if (trgt < 0) { + trgt = 0; + } + if (new_line >= trgt && new_line >= editor->line_count) { + new_line = editor->line_count - + 1; + } + + if (keepPos) { + editor->cursor_line_offset = new_line; + } + + if (new_line < editor->cursor_line_offset) { + editor->cursor_line_offset = new_line; + } + if (new_line >= editor->cursor_line_offset + editor->displayLineCount) { + editor->cursor_line_offset = new_line - editor->displayLineCount + 1; + } + + if (editor->cursor_line_offset >= editor->line_count - editor->displayLineCount) { + editor->cursor_line_offset = editor->line_count - editor->displayLineCount; + } + if (editor->cursor_line_offset < 0) { + editor->cursor_line_offset = 0; + } + + int line_length = strlen(editor->lines[new_line].text); + if (new_pos < 0) new_pos = 0; + if (new_pos > line_length) new_pos = line_length; + + editor->cursor_line = new_line; + editor->cursor_pos = new_pos; + generate_string_display(editor, renderer); +} + +void generate_string_display(TextEditor *editor, SDL_Renderer *renderer) { + if (editor->cursor_line_offset < 0 || editor->cursor_line_offset >= editor->line_count) { + printf("Invalid start line!\n"); + return; + } + + int end_line = editor->cursor_line_offset + editor->displayLineCount; + if (end_line > editor->line_count) + end_line = editor->line_count; + + // Clear the display string. + editor->displayString[0] = '\0'; + + SDL_SetRenderTarget(renderer, editor->texture); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); + SDL_RenderClear(renderer); + + SDL_Rect charDstRect; + charDstRect.x = 4; + charDstRect.y = 3; + charDstRect.w = editor->font->size; + charDstRect.h = editor->font->size; + + SDL_Rect charRect; + charRect.x = 0; + charRect.y = 0; + charRect.w = editor->font->size; + charRect.h = editor->font->size; + + editor->cursorRect->x = 2 + editor->cursor_pos * (charDstRect.w + 1) + editor->outRect->x; + editor->cursorRect->y = + 2 + (editor->cursor_line - editor->cursor_line_offset) * (charDstRect.h + 1) + editor->outRect->y; + + for (int line = editor->cursor_line_offset; line < end_line; line++) { + if (editor->lines[line].active) { + strcat(editor->displayString, editor->lines[line].text); + char *linePTR = editor->lines[line].text; + while (*linePTR) { + SDL_RenderCopy(renderer, editor->font->texture[(unsigned char) *linePTR], &charRect, &charDstRect); + charDstRect.x += charDstRect.w + 1; + linePTR++; + } + charDstRect.x = 4; + charDstRect.y += charDstRect.h + 1; + strcat(editor->displayString, "\n"); + } + } + SDL_SetRenderTarget(renderer, NULL); +} + +void editor_render(TextEditor *editor, SDL_Renderer *renderer, Encoder *enc, uint8_t editorIndex, bool isActive, + bool cursorBlink) { + if (isActive) { + SDL_Rect bgRect; + bgRect = *editor->outRect; + bgRect.x -= 6; + bgRect.y -= 6; + bgRect.w += 12; + bgRect.h += 12; + + + SDL_SetRenderDrawColor(renderer, editor->readOnly ? 128 : 0, editor->readOnly ? 0 : 128, 64, 255); + SDL_RenderFillRect(renderer, &bgRect); + + } + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderFillRect(renderer, editor->outRect); + + uint32_t targetLine = 0; + if (editorIndex == 0) { + targetLine = (enc->byteIndex / 8) - editor->cursor_line_offset; + editor->highlightedLineRect->w = editor->font->size * 2 + 2; + editor->highlightedLineRect->x = (editor->font->size * 11) + (3 * (editor->font->size + 1) * (enc->byteIndex % 8)); + editor->highlightedLineRect->y = 1 + ((editor->font->size + 1) * targetLine); + } + + SDL_Rect highlightedLineAbs = *editor->highlightedLineRect; + highlightedLineAbs.x += editor->outRect->x; + highlightedLineAbs.y += editor->outRect->y; + SDL_SetRenderDrawColor(renderer, 128, 0, 255, 255); + if (targetLine >= editor->cursor_line_offset && + targetLine < editor->cursor_line_offset + editor->displayLineCount) { + SDL_RenderFillRect(renderer, &highlightedLineAbs); + } + + + SDL_SetTextureBlendMode(editor + ->texture, SDL_BLENDMODE_BLEND); + SDL_RenderCopy(renderer, editor + ->texture, editor->rect, editor->outRect); + if (isActive && cursorBlink) { + SDL_SetRenderDrawColor(renderer, + editor->overwrite ? 255 : 0, 255, editor->overwrite ? 0 : 255, 128); + SDL_RenderFillRect(renderer, editor + ->cursorRect); + + } +} + +void generate_string(TextEditor *editor) { + editor->outputString[0] = '\0'; + + for (int i = 0; i < editor->maxLines; i++) { + if (editor->lines[i].active) { + strcat(editor->outputString, editor->lines[i].text); + strcat(editor->outputString, "\n"); + } + } +} + +void fill_editor_from_string(TextEditor *editor, const char *content, int lineStart, bool isComplete, + SDL_Renderer *renderer) { + if (!editor || !content) { + printf("Invalid editor or content pointer!\n"); + return; + } + + char *str = content; + + int numLines = 0; + while (*str) if (*str++ == '\n') ++numLines; + + // Clear the current editor content + for (int i = lineStart; i < editor->maxLines && i < lineStart + numLines + 1; i++) { + editor->lines[i].text[0] = '\0'; + editor->lines[i].active = 0; + } + if (isComplete) { + editor->line_count = numLines; + if (editor->line_count >= editor->maxLines) { + editor->line_count = editor->maxLines - 1; + } + } + + // Parse the content and fill the editor lines + const char *ptr = content; + int line_index = lineStart; + + while (*ptr && line_index < editor->maxLines && line_index < numLines + lineStart) { + int char_count = 0; + + // Ensure the text buffer does not overflow + while (*ptr && *ptr != '\n' && char_count < editor->max_line_width) { + editor->lines[line_index].text[char_count++] = *ptr++; + } + + editor->lines[line_index].text[char_count] = '\0'; // Null-terminate + editor->lines[line_index].active = 1; + line_index++; + + // Move past the newline character if present + if (*ptr == '\n') ptr++; + } + editor->lines[line_index].active = 1; + + // Update the total number of lines in use + //editor->line_count = line_index + (line_index < editor->maxLines - 1 ? 1 : 0); + //if (editor->line_count >= editor-> maxLines) + + // Generate the visual representation + generate_string_display(editor, renderer); +} + + +// Free all dynamically allocated memory. +void destroy_editor(TextEditor *editor) { + if (editor->lines) { + for (int i = 0; i < editor->maxLines; i++) { + free(editor->lines[i].text); + } + free(editor->lines); + } + //free(editor->outputString); + //free(editor->displayString); + + free(editor->outRect); + free(editor->rect); + free(editor->cursorRect); + + if (editor->texture) { + SDL_DestroyTexture(editor->texture); + } +} + +SDL_Keycode ConvertKPToNonKP(SDL_Keycode keycode) { + switch (keycode) { + case SDLK_KP_0: + return SDLK_0; + case SDLK_KP_1: + return SDLK_1; + case SDLK_KP_2: + return SDLK_2; + case SDLK_KP_3: + return SDLK_3; + case SDLK_KP_4: + return SDLK_4; + case SDLK_KP_5: + return SDLK_5; + case SDLK_KP_6: + return SDLK_6; + case SDLK_KP_7: + return SDLK_7; + case SDLK_KP_8: + return SDLK_8; + case SDLK_KP_9: + return SDLK_9; + case SDLK_KP_PERIOD: + return SDLK_PERIOD; + case SDLK_KP_COMMA: + return SDLK_COMMA; + case SDLK_KP_DIVIDE: + return SDLK_SLASH; + case SDLK_KP_MULTIPLY: + return SDLK_ASTERISK; + case SDLK_KP_MINUS: + return SDLK_MINUS; + case SDLK_KP_PLUS: + return SDLK_PLUS; + case SDLK_KP_ENTER: + return SDLK_RETURN; + case SDLK_KP_EQUALS: + return SDLK_EQUALS; + case SDLK_KP_LEFTPAREN: + return SDLK_LEFTPAREN; + case SDLK_KP_RIGHTPAREN: + return SDLK_RIGHTPAREN; + case SDLK_KP_LEFTBRACE: + return SDLK_LEFTBRACKET; + case SDLK_KP_RIGHTBRACE: + return SDLK_RIGHTBRACKET; + case SDLK_KP_TAB: + return SDLK_TAB; + case SDLK_KP_BACKSPACE: + return SDLK_BACKSPACE; + case SDLK_KP_A: + return SDLK_a; + case SDLK_KP_B: + return SDLK_b; + case SDLK_KP_C: + return SDLK_c; + case SDLK_KP_D: + return SDLK_d; + case SDLK_KP_E: + return SDLK_e; + case SDLK_KP_F: + return SDLK_f; + case SDLK_KP_XOR: + return SDLK_CARET; + case SDLK_KP_PERCENT: + return SDLK_PERCENT; + case SDLK_KP_LESS: + return SDLK_LESS; + case SDLK_KP_GREATER: + return SDLK_GREATER; + case SDLK_KP_AMPERSAND: + case SDLK_KP_DBLAMPERSAND: + return SDLK_AMPERSAND; // No direct match, best alternative + case SDLK_KP_VERTICALBAR: + case SDLK_KP_DBLVERTICALBAR: + return SDLK_BACKSLASH; // No direct match + case SDLK_KP_COLON: + return SDLK_COLON; + case SDLK_KP_HASH: + return SDLK_HASH; + case SDLK_KP_SPACE: + return SDLK_SPACE; + case SDLK_KP_AT: + return SDLK_AT; + case SDLK_KP_EXCLAM: + return SDLK_EXCLAIM; + case SDLK_KP_MEMADD: + return SDLK_PLUS; + case SDLK_KP_MEMSUBTRACT: + return SDLK_MINUS; + case SDLK_KP_MEMMULTIPLY: + return SDLK_ASTERISK; + case SDLK_KP_MEMDIVIDE: + return SDLK_SLASH; + case SDLK_KP_MEMSTORE: + case SDLK_KP_MEMRECALL: + case SDLK_KP_MEMCLEAR: + case SDLK_KP_PLUSMINUS: + case SDLK_KP_CLEAR: + case SDLK_KP_CLEARENTRY: + case SDLK_KP_BINARY: + case SDLK_KP_OCTAL: + case SDLK_KP_DECIMAL: + case SDLK_KP_HEXADECIMAL: + return SDLK_UNKNOWN; + default: + return keycode; // If it's not a KP key, return it unchanged + } +} + +void updateDump(TextEditor *editor, SDL_Renderer *renderer) { + if (editor == &transmitEditor) { + if (encoder.bufferSize) { + char *dump = hexdump_to_string(encoder.buffer, 0, encoder.bufferSize); + fill_editor_from_string(editor, dump, 0, true, renderer); + free(dump); + } else { + fill_editor_from_string(editor, "\n", 0, true, renderer); + } + } else if (editor == &receivedEditor) { + if (decoder.bufferSize) { + char *dump = hexdump_to_string(decoder.buffer, 0, decoder.bufferSize); + fill_editor_from_string(editor, dump, 0, true, renderer); + free(dump); + } else { + fill_editor_from_string(editor, "\n", 0, true, renderer); + } + } +} diff --git a/texteditor.h b/texteditor.h new file mode 100644 index 0000000..852d847 --- /dev/null +++ b/texteditor.h @@ -0,0 +1,92 @@ +// +// Created by bruno on 5.2.2025. +// Modified to use dynamic limits. +// +#ifndef RISCB_TEXTEDITOR_H +#define RISCB_TEXTEDITOR_H + +#include +#include +#include +#include +#include +#include +#include + +#include "encoder.h" +#include "font.h" + +typedef struct { + char *text; // Dynamically allocated string for this line + int active; // Flag to check if the line is in use +} Line; + +typedef struct { + Line *lines; // Dynamic array of lines + int line_count; // Number of active lines + int maxLines; // Maximum number of lines (e.g. assembly lines) + int max_line_width; // Maximum characters per line (excluding '\0') + int displayLineCount; // Maximum number of lines for display + + int cursor_line; // Current cursor line + int cursor_line_offset; // Display offset (first line in the display) + int cursor_pos; // Current cursor position in line + + char *outputString; // Dynamically allocated output string (size: max_line_width * maxLines + 1) + char *displayString; // Dynamically allocated display string (size: max_line_width * displayLineCount + 1) + SDL_Rect *rect; + SDL_Rect *outRect; + SDL_Texture *texture; + bool readOnly; + BitmapFont *font; + SDL_Rect *cursorRect; + SDL_Rect *highlightedLineRect; + bool overwrite; +} TextEditor; + +#define transmitEditor editors[0] +#define receivedEditor editors[1] +#define editorCount 2 +#define activeEditor editors[activeEditorIndex] + +extern unsigned long frames; +extern bool cursorShown; + +extern int activeEditorIndex; +extern TextEditor editors[editorCount]; + +// Initialize the text editor. The parameters max_line_width, maxLines, and displayLineCount +// determine the dynamic sizes for the text editor. +void init_editor(TextEditor *editor, BitmapFont *font, int x, int y, SDL_Renderer *renderer, + int max_line_width, int max_lines_asm, int max_lines_display, bool readOnly); + +// Other function prototypes remain mostly unchanged but will use the dynamic limits: +void insert_line(TextEditor *editor, int position, const char *text, SDL_Renderer *renderer); + +void insert_line_rel(TextEditor *editor, SDL_Renderer *renderer); + +void editor_render(TextEditor *editor, SDL_Renderer *renderer, Encoder *enc, uint8_t editorIndex, bool isActive, bool cursorBlink); + +void remove_character(TextEditor *editor, bool isDelete, SDL_Renderer *renderer); + +void insert_character(TextEditor *editor, char ch, SDL_Renderer *renderer); + +void move_cursor(TextEditor *editor, int new_line, int new_pos, bool keepPos, SDL_Renderer *renderer); + +void move_cursor_relative(TextEditor *editor, int line_offset, int pos_offset, bool keepPos, SDL_Renderer *renderer); + +void generate_string_display(TextEditor *editor, SDL_Renderer *renderer); + +void generate_string(TextEditor *editor); + +void fill_editor_from_string(TextEditor *editor, const char *content, int lineStart, bool isComplete, + SDL_Renderer *renderer); + +// A cleanup function to free dynamically allocated memory. +void destroy_editor(TextEditor *editor); + +SDL_Keycode ConvertKPToNonKP(SDL_Keycode keycode); + +void updateDump(TextEditor *editor, SDL_Renderer *renderer); + +#endif // RISCB_TEXTEDITOR_H