606 lines
19 KiB
C
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;
|
|
}
|