Files
2026-07-20 23:18:26 +02:00

606 lines
19 KiB
C

#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include <SDL2/SDL.h>
#include "font.h"
#include "decoder.h"
#include "goertzel.h"
#include "texteditor.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_PACKET: return "PACKET";
case TX_END: return "END";
}
return "?";
}
static const char *rx_state_name(RxState state) {
switch (state) {
case RX_SEARCH: return "SEARCH";
case RX_SYNC: return "SYNC";
case RX_MAGIC: return "MAGIC";
case RX_DATA: return "DATA";
}
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";
default: return "UNKNOWN";
}
}
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.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];
// TX info on the left
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);
// RX info on the right
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, 640, 8);
// Clock info below RX
snprintf(
line,
sizeof(line),
"CLK E0 %08.5f E1 %08.5f R %02.1f HIST %02X/%u",
decoder.clockEnergy0,
decoder.clockEnergy1,
decoder.clockRatio,
decoder.clockHistory,
decoder.clockCount);
renderText(renderer, smallerFont, line, 640, 22);
// 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 dataTones = MAX_FSK_TONES;
const int tones = dataTones + 2; // clocks are indices 0,1
const int x = 500;
const int y = 650;
const int maxHeight = 192;
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);
// Draw clocks first (indices 0 and 1)
for (int i = 0; i < tones; i++) {
int idx;
if (i < 2) idx = i; // clock indices 0,1
else idx = 2 + (i - 2); // data indices start at 2
float ratio = 0.0f;
if (decoder.maxEnergy > 0.0f)
ratio = decoder.energies[idx] / 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 (idx < 2)
SDL_SetRenderDrawColor(renderer, 255, 192, 64, 255); // clock
else if ((idx - 2) & 1)
SDL_SetRenderDrawColor(renderer, 80, 180, 255, 255); // 1-tone
else
SDL_SetRenderDrawColor(renderer, 80, 255, 140, 255); // 0-tone
SDL_RenderFillRect(renderer, &barRect);
}
char label[128];
snprintf(label, sizeof(label), "TONE ENERGY: CLOCK0, CLOCK1, BIT0..BIT15");
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);
// Auto-start encoder for testing
//startEncoder(&encoder);
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);
updateDump(&receivedEditor, 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;
}