shithub: m8c

Download patch

ref: 121fbe147ae54f0e397035769dc3f4a1f509b9b9
parent: ffbdb212f0a58ea4a73f149cfc5d29357342905e
parent: 8e12a3b61591876f32786a8e0dcf5dc08b65655f
author: Jonne Kokkonen <jonne.kokkonen@gmail.com>
date: Wed Aug 30 16:45:12 EDT 2023

Merge pull request #125 from laamaa/rpmpackaging

Move source files to their own subdirectory, add a subdirectory for packaging related things

--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -55,7 +55,7 @@
           cp /mingw64/bin/SDL2.dll .
           cp /mingw64/bin/libserialport-0.dll .
         fi
-        unix2dos README.md config.ini.sample LICENSE AUDIOGUIDE.md
+        unix2dos README.md LICENSE AUDIOGUIDE.md
     - name: 'Upload artifact (win32)'
       if: matrix.win == 'win32'
       uses: actions/upload-artifact@v3
@@ -71,7 +71,6 @@
           LICENSE
           README.md
           AUDIOGUIDE.md
-          config.ini.sample
     - name: 'Upload artifact (win64)'
       if: matrix.win == 'win64'
       uses: actions/upload-artifact@v3
@@ -85,7 +84,6 @@
           LICENSE
           README.md
           AUDIOGUIDE.md
-          config.ini.sample
 
   build-linux:
     runs-on: ubuntu-latest
@@ -114,7 +112,6 @@
             AUDIOGUIDE.md
             m8c
             gamecontrollerdb.txt
-            config.ini.sample
       
   build-macos:
     runs-on: macos-latest
@@ -139,7 +136,7 @@
           codesign --deep --force --verify --verbose --timestamp --sign - "$INSTALL_PREFIX/m8c.app" "$INSTALL_PREFIX/m8c.app/Contents/Frameworks/libSDL2-2.0.0.dylib" "$INSTALL_PREFIX/m8c.app/Contents/Frameworks/libserialport.0.dylib"
           cd ..
           cp -r /tmp/m8c.app .
-          zip -r m8c.zip m8c.app LICENSE README.md AUDIOGUIDE.md config.ini.sample gamecontrollerdb.txt
+          zip -r m8c.zip m8c.app LICENSE README.md AUDIOGUIDE.md gamecontrollerdb.txt
       - name: 'Upload artifact'
         uses: actions/upload-artifact@v3
         with:
--- a/AUDIOGUIDE.md
+++ b/AUDIOGUIDE.md
@@ -1,5 +1,10 @@
 # Audio setup for M8 headless 
 
+Please note that the program includes SDL based audio routing built-in nowadays.
+
+Experimental audio routing support can be enabled by setting the config value `"audio_enabled"` to `"true"`. The audio buffer size can also be tweaked from the config file for possible lower latencies.
+If the right audio device is not picked up by default, you can use a specific audio device by using `"audio_output_device"` config parameter.
+
 ## Windows
 
 * Right click Sound in Taskbar
--- a/Android.mk
+++ b/Android.mk
@@ -4,9 +4,9 @@
 
 LOCAL_MODULE := m8c
 
-LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.c)
+LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/src/*.c)
 
-LOCAL_EXPORT_C_INCLUDES := $(wildcard $(LOCAL_PATH)/*.h)
+LOCAL_EXPORT_C_INCLUDES := $(wildcard $(LOCAL_PATH)/src/*.h)
 
 LOCAL_CFLAGS += -DUSE_LIBUSB
 
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,9 +16,9 @@
     link_directories(${SDL2_LIBRARY_DIRS} ${LIBSERIALPORT_LIBRARY_DIRS})
 endif (USE_LIBUSB)
 
-file(GLOB m8c_SRC "*.h" "*.c")
+file(GLOB m8c_SRC "src/*.h" "src/*.c")
 
-set(MACOS_CONTENTS "${CMAKE_CURRENT_SOURCE_DIR}/macos/m8c.app/Contents")
+set(MACOS_CONTENTS "${CMAKE_CURRENT_SOURCE_DIR}/package/macos/m8c.app/Contents")
 
 set(APP_ICON ${MACOS_CONTENTS}/Resources/m8c.icns)
 set_source_files_properties(${APP_ICON} PROPERTIES
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
 #Set all your object files (the object files of all the .c files in your project, e.g. main.o my_sub_functions.o )
-OBJ = main.o serial.o slip.o command.o render.o ini.o config.o input.o fx_cube.o usb.o audio.o usb_audio.o ringbuffer.o inprint2.o
+OBJ = src/main.o src/serial.o src/slip.o src/command.o src/render.o src/ini.o src/config.o src/input.o src/fx_cube.o src/usb.o src/audio.o src/usb_audio.o src/ringbuffer.o src/inprint2.o
 
 #Set any dependant header files so that if they are edited they cause a complete re-compile (e.g. main.h some_subfunctions.h some_definitions_file.h ), or leave blank
-DEPS = serial.h slip.h command.h render.h ini.h config.h input.h fx_cube.h audio.h ringbuffer.h inline_font.h
+DEPS = src/serial.h src/slip.h src/command.h src/render.h src/ini.h src/config.h src/input.h src/fx_cube.h src/audio.h src/ringbuffer.h src/inline_font.h
 
 #Any special libraries you are using in your project (e.g. -lbcm2835 -lrt `pkg-config --libs gtk+-3.0` ), or leave blank
 INCLUDES = $(shell pkg-config --libs sdl2 libserialport)
@@ -16,6 +16,8 @@
 #Set the filename extensiton of your C files (e.g. .c or .cpp )
 EXTENSION = .c
 
+SOURCE_DIR = src/
+
 #define a rule that applies to all files ending in the .o suffix, which says that the .o file depends upon the .c version of the file and all the .h files included in the DEPS macro.  Compile each object file
 %.o: %$(EXTENSION) $(DEPS)
 	$(CC) -c -o $@ $< $(local_CFLAGS)
@@ -33,7 +35,7 @@
 .PHONY: clean
 
 clean:
-	rm -f *.o *~ m8c
+	rm -f src/*.o *~ m8c
 
 # PREFIX is environment variable, but if it is not set, then set default value
 ifeq ($(PREFIX),)
--- a/SDL2_inprint.h
+++ /dev/null
@@ -1,22 +1,0 @@
-// Bitmap font routine by driedfruit, https://github.com/driedfruit/SDL_inprint
-// Released into public domain.
-// Modified to support adding a background to text.
-
-#ifndef SDL2_inprint_h
-#define SDL2_inprint_h
-
-#include <SDL.h>
-
-extern void prepare_inline_font(unsigned char bits[],int font_width, int font_height);
-extern void kill_inline_font(void);
-
-extern void inrenderer(SDL_Renderer *renderer);
-extern void infont(SDL_Texture *font);
-extern void incolor1(SDL_Color *color);
-extern void incolor(Uint32 color, Uint32 unused); /* Color must be in 0x00RRGGBB format ! */
-extern void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y,
-             Uint32 fgcolor, Uint32 bgcolor);
-
-extern SDL_Texture *get_inline_font(void);
-
-#endif /* SDL2_inprint_h */
--- a/audio.c
+++ /dev/null
@@ -1,90 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-#ifndef USE_LIBUSB
-#include "audio.h"
-#include <SDL.h>
-#include <stdint.h>
-
-static SDL_AudioDeviceID devid_in = 0;
-static SDL_AudioDeviceID devid_out = 0;
-
-void audio_cb_in(void *userdata, uint8_t *stream, int len) {
-  SDL_QueueAudio(devid_out, stream, len);
-}
-
-int audio_init(int audio_buffer_size, const char* output_device_name) {
-
-  int i = 0;
-  int m8_device_id = -1;
-  int devcount_in = 0; // audio input device count
-
-  // wait for system to initialize possible new audio devices
-  SDL_Delay(500);
-
-  devcount_in = SDL_GetNumAudioDevices(SDL_TRUE);
-
-  if (devcount_in < 1) {
-    SDL_Log("No audio capture devices, SDL Error: %s", SDL_GetError());
-    return 0;
-  } else {
-    for (i = 0; i < devcount_in; i++) {
-      // Check if input device exists before doing anything else
-      SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "%s", SDL_GetAudioDeviceName(i, SDL_TRUE));
-      if (SDL_strstr(SDL_GetAudioDeviceName(i, SDL_TRUE), "M8") != NULL) {
-        SDL_Log("M8 Audio Input device found: %s",
-                SDL_GetAudioDeviceName(i, SDL_TRUE));
-        m8_device_id = i;
-      }
-    }
-    if (m8_device_id == -1) {
-      // forget about it
-      SDL_Log("Cannot find M8 audio input device");
-      return 0;
-    }
-
-    SDL_AudioSpec want_in, have_in, want_out, have_out;
-
-    // Open output device first to avoid possible Directsound errors
-    SDL_zero(want_out);
-    want_out.freq = 44100;
-    want_out.format = AUDIO_S16;
-    want_out.channels = 2;
-    want_out.samples = audio_buffer_size;
-    devid_out = SDL_OpenAudioDevice(output_device_name, 0, &want_out, &have_out,
-                                    SDL_AUDIO_ALLOW_ANY_CHANGE);
-    if (devid_out == 0) {
-      SDL_Log("Failed to open output: %s", SDL_GetError());
-      return 0;
-    }
-
-    SDL_zero(want_in);
-    want_in.freq = 44100;
-    want_in.format = AUDIO_S16;
-    want_in.channels = 2;
-    want_in.samples = audio_buffer_size;
-    want_in.callback = audio_cb_in;
-    devid_in = SDL_OpenAudioDevice(
-        SDL_GetAudioDeviceName(m8_device_id, SDL_TRUE), SDL_TRUE, &want_in,
-        &have_in, SDL_AUDIO_ALLOW_ANY_CHANGE);
-    if (devid_in == 0) {
-      SDL_Log("Failed to open M8 audio device, SDL Error: %s", SDL_GetError());
-      return 0;
-    }
-  }
-
-  // Start audio processing
-  SDL_Log("Opening audio devices");
-  SDL_PauseAudioDevice(devid_in, 0);
-  SDL_PauseAudioDevice(devid_out, 0);
-
-  return 1;
-}
-
-void audio_destroy() {
-  SDL_Log("Closing audio devices");
-  SDL_PauseAudioDevice(devid_in, 1);
-  SDL_PauseAudioDevice(devid_out, 1);
-  SDL_CloseAudioDevice(devid_in);
-  SDL_CloseAudioDevice(devid_out);
-}
-#endif
--- a/audio.h
+++ /dev/null
@@ -1,9 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-#ifndef AUDIO_H
-#define AUDIO_H
-
-int audio_init(int audio_buffer_size, const char *output_device_name);
-void audio_destroy();
-
-#endif
--- a/command.c
+++ /dev/null
@@ -1,174 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#include <SDL_log.h>
-
-#include "command.h"
-#include "render.h"
-
-// Convert 2 little-endian 8bit bytes to a 16bit integer
-static uint16_t decodeInt16(uint8_t *data, uint8_t start) {
-  return data[start] | (uint16_t)data[start + 1] << 8;
-}
-
-enum m8_command_bytes {
-  draw_rectangle_command = 0xFE,
-  draw_rectangle_command_datalength = 12,
-  draw_character_command = 0xFD,
-  draw_character_command_datalength = 12,
-  draw_oscilloscope_waveform_command = 0xFC,
-  draw_oscilloscope_waveform_command_mindatalength = 1 + 3,
-  draw_oscilloscope_waveform_command_maxdatalength = 1 + 3 + 320,
-  joypad_keypressedstate_command = 0xFB,
-  joypad_keypressedstate_command_datalength = 3,
-  system_info_command = 0xFF,
-  system_info_command_datalength = 6
-};
-
-static inline void dump_packet(uint32_t size, uint8_t *recv_buf) {
-  for (uint16_t a = 0; a < size; a++) {
-    SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "0x%02X ", recv_buf[a]);
-  }
-  SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "\n");
-}
-
-int process_command(uint8_t *data, uint32_t size) {
-
-  uint8_t recv_buf[size + 1];
-
-  memcpy(recv_buf, data, size);
-  recv_buf[size] = 0;
-
-  switch (recv_buf[0]) {
-
-  case draw_rectangle_command:
-
-    if (size != draw_rectangle_command_datalength) {
-      SDL_LogError(
-          SDL_LOG_CATEGORY_ERROR,
-          "Invalid draw rectangle packet: expected length %d, got %d\n",
-          draw_rectangle_command_datalength, size);
-      dump_packet(size, recv_buf);
-      return 0;
-      break;
-    } else {
-
-      struct draw_rectangle_command rectcmd = {
-          {decodeInt16(recv_buf, 1), decodeInt16(recv_buf, 3)}, // position x/y
-          {decodeInt16(recv_buf, 5), decodeInt16(recv_buf, 7)}, // size w/h
-          {recv_buf[9], recv_buf[10], recv_buf[11]}};           // color r/g/b
-
-      draw_rectangle(&rectcmd);
-      return 1;
-    }
-
-    break;
-
-  case draw_character_command:
-
-    if (size != draw_character_command_datalength) {
-      SDL_LogError(
-          SDL_LOG_CATEGORY_ERROR,
-          "Invalid draw character packet: expected length %d, got %d\n",
-          draw_character_command_datalength, size);
-      dump_packet(size, recv_buf);
-      return 0;
-      break;
-    } else {
-
-      struct draw_character_command charcmd = {
-          recv_buf[1],                                          // char
-          {decodeInt16(recv_buf, 2), decodeInt16(recv_buf, 4)}, // position x/y
-          {recv_buf[6], recv_buf[7], recv_buf[8]},    // foreground r/g/b
-          {recv_buf[9], recv_buf[10], recv_buf[11]}}; // background r/g/b
-      draw_character(&charcmd);
-      return 1;
-    }
-
-    break;
-
-  case draw_oscilloscope_waveform_command:
-
-    if (size < draw_oscilloscope_waveform_command_mindatalength ||
-        size > draw_oscilloscope_waveform_command_maxdatalength) {
-      SDL_LogError(
-          SDL_LOG_CATEGORY_ERROR,
-          "Invalid draw oscilloscope packet: expected length between %d "
-          "and %d, got "
-          "%d\n",
-          draw_oscilloscope_waveform_command_mindatalength,
-          draw_oscilloscope_waveform_command_maxdatalength, size);
-      dump_packet(size, recv_buf);
-      return 0;
-      break;
-    } else {
-
-      struct draw_oscilloscope_waveform_command osccmd;
-
-      osccmd.color =
-          (struct color){recv_buf[1], recv_buf[2], recv_buf[3]}; // color r/g/b
-      memcpy(osccmd.waveform, &recv_buf[4], size - 4);
-
-      osccmd.waveform_size = size - 4;
-
-      draw_waveform(&osccmd);
-      return 1;
-    }
-
-    break;
-
-  case joypad_keypressedstate_command: {
-    if (size != joypad_keypressedstate_command_datalength) {
-      SDL_LogError(
-          SDL_LOG_CATEGORY_ERROR,
-          "Invalid joypad keypressed state packet: expected length %d, "
-          "got %d\n",
-          joypad_keypressedstate_command_datalength, size);
-      dump_packet(size, recv_buf);
-      return 0;
-      break;
-    }
-
-    // nothing is done with joypad key pressed packets for now
-    return 1;
-    break;
-  }
-
-  case system_info_command: {
-    if (size != system_info_command_datalength) {
-      SDL_LogError(SDL_LOG_CATEGORY_ERROR,
-                   "Invalid system info packet: expected length %d, "
-                   "got %d\n",
-                   system_info_command_datalength, size);
-      dump_packet(size, recv_buf);
-      break;
-    }
-
-    char *hwtype[3] = {"Headless", "Beta M8", "Production M8"};
-
-    static int system_info_printed = 0;
-
-    if (system_info_printed == 0) {
-      SDL_Log("** Hardware info ** Device type: %s, Firmware ver %d.%d.%d",
-              hwtype[recv_buf[1]], recv_buf[2], recv_buf[3], recv_buf[4]);
-      system_info_printed = 1;
-    }
-
-    if (recv_buf[5] == 0x01) {
-      set_large_mode(1);
-    } else {
-      set_large_mode(0);
-    }
-    return 1;
-    break;
-  }
-
-  default:
-
-    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Invalid packet\n");
-    dump_packet(size, recv_buf);
-    return 0;
-    break;
-  }
-  return 1;
-}
--- a/command.h
+++ /dev/null
@@ -1,46 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#ifndef COMMAND_H_
-#define COMMAND_H_
-
-#include <stdint.h>
-
-struct position {
-  uint16_t x;
-  uint16_t y;
-};
-
-struct size {
-  uint16_t width;
-  uint16_t height;
-};
-
-struct color {
-  uint8_t r;
-  uint8_t g;
-  uint8_t b;
-};
-
-struct draw_rectangle_command {
-  struct position pos;
-  struct size size;
-  struct color color;
-};
-
-struct draw_character_command {
-  int c;
-  struct position pos;
-  struct color foreground;
-  struct color background;
-};
-
-struct draw_oscilloscope_waveform_command {
-  struct color color;
-  uint8_t waveform[320];
-  uint16_t waveform_size;
-};
-
-int process_command(uint8_t *data, uint32_t size);
-
-#endif
--- a/config.c
+++ /dev/null
@@ -1,394 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#include "config.h"
-#include "ini.h"
-#include <SDL.h>
-#include <assert.h>
-#include <stdio.h>
-
-/* Case insensitive string compare from ini.h library */
-static int strcmpci(const char *a, const char *b) {
-  for (;;) {
-    int d = tolower(*a) - tolower(*b);
-    if (d != 0 || !*a) {
-      return d;
-    }
-    a++, b++;
-  }
-}
-
-config_params_s init_config() {
-  config_params_s c;
-
-  c.filename = "config.ini"; // default config file to load
-
-  c.init_fullscreen = 0; // default fullscreen state at load
-  c.init_use_gpu = 1;    // default to use hardware acceleration
-  c.idle_ms = 10;        // default to high performance
-  c.wait_for_device = 1; // default to exit if device disconnected
-  c.wait_packets = 1024; // default zero-byte attempts to disconnect (about 2
-  // sec for default idle_ms)
-  c.audio_enabled = 0;   // route M8 audio to default output
-  c.audio_buffer_size = 1024; // requested audio buffer size in samples
-  c.audio_device_name = NULL; // Use this device, leave NULL to use the default output device
-
-  c.key_up = SDL_SCANCODE_UP;
-  c.key_left = SDL_SCANCODE_LEFT;
-  c.key_down = SDL_SCANCODE_DOWN;
-  c.key_right = SDL_SCANCODE_RIGHT;
-  c.key_select = SDL_SCANCODE_LSHIFT;
-  c.key_select_alt = SDL_SCANCODE_Z;
-  c.key_start = SDL_SCANCODE_SPACE;
-  c.key_start_alt = SDL_SCANCODE_X;
-  c.key_opt = SDL_SCANCODE_LALT;
-  c.key_opt_alt = SDL_SCANCODE_A;
-  c.key_edit = SDL_SCANCODE_LCTRL;
-  c.key_edit_alt = SDL_SCANCODE_S;
-  c.key_delete = SDL_SCANCODE_DELETE;
-  c.key_reset = SDL_SCANCODE_R;
-
-  c.gamepad_up = SDL_CONTROLLER_BUTTON_DPAD_UP;
-  c.gamepad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
-  c.gamepad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
-  c.gamepad_right = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
-  c.gamepad_select = SDL_CONTROLLER_BUTTON_BACK;
-  c.gamepad_start = SDL_CONTROLLER_BUTTON_START;
-  c.gamepad_opt = SDL_CONTROLLER_BUTTON_B;
-  c.gamepad_edit = SDL_CONTROLLER_BUTTON_A;
-  c.gamepad_quit = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
-  c.gamepad_reset = SDL_CONTROLLER_BUTTON_LEFTSTICK;
-
-  c.gamepad_analog_threshold = 32766;
-  c.gamepad_analog_invert = 0;
-  c.gamepad_analog_axis_updown = SDL_CONTROLLER_AXIS_LEFTY;
-  c.gamepad_analog_axis_leftright = SDL_CONTROLLER_AXIS_LEFTX;
-  c.gamepad_analog_axis_start = SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
-  c.gamepad_analog_axis_select = SDL_CONTROLLER_AXIS_TRIGGERLEFT;
-  c.gamepad_analog_axis_opt = SDL_CONTROLLER_AXIS_INVALID;
-  c.gamepad_analog_axis_edit = SDL_CONTROLLER_AXIS_INVALID;
-
-  return c;
-}
-
-// Write config to file
-void write_config(config_params_s *conf) {
-
-  // Open the default config file for writing
-  char config_path[1024] = {0};
-  snprintf(config_path, sizeof(config_path), "%s%s", SDL_GetPrefPath("", "m8c"),
-           conf->filename);
-  SDL_RWops *rw = SDL_RWFromFile(config_path, "w");
-
-  SDL_Log("Writing config file to %s", config_path);
-
-  const unsigned int INI_LINE_COUNT = 44;
-  const unsigned int LINELEN = 50;
-
-  // Entries for the config file
-  char ini_values[INI_LINE_COUNT][LINELEN];
-  int initPointer = 0;
-  snprintf(ini_values[initPointer++], LINELEN, "[graphics]\n");
-  snprintf(ini_values[initPointer++], LINELEN, "fullscreen=%s\n",
-           conf->init_fullscreen ? "true" : "false");
-  snprintf(ini_values[initPointer++], LINELEN, "use_gpu=%s\n",
-           conf->init_use_gpu ? "true" : "false");
-  snprintf(ini_values[initPointer++], LINELEN, "idle_ms=%d\n", conf->idle_ms);
-  snprintf(ini_values[initPointer++], LINELEN, "wait_for_device=%s\n",
-           conf->wait_for_device ? "true" : "false");
-  snprintf(ini_values[initPointer++], LINELEN, "wait_packets=%d\n",
-           conf->wait_packets);
-  snprintf(ini_values[initPointer++], LINELEN, "[audio]\n");
-  snprintf(ini_values[initPointer++], LINELEN, "audio_enabled=%s\n",
-           conf->audio_enabled ? "true" : "false");
-  snprintf(ini_values[initPointer++], LINELEN, "audio_buffer_size=%d\n",
-           conf->audio_buffer_size);
-  snprintf(ini_values[initPointer++], LINELEN, "audio_device_name=%s\n",
-           conf->audio_device_name ? conf->audio_device_name : "Default");
-  snprintf(ini_values[initPointer++], LINELEN, "[keyboard]\n");
-  snprintf(ini_values[initPointer++], LINELEN, "key_up=%d\n", conf->key_up);
-  snprintf(ini_values[initPointer++], LINELEN, "key_left=%d\n", conf->key_left);
-  snprintf(ini_values[initPointer++], LINELEN, "key_down=%d\n", conf->key_down);
-  snprintf(ini_values[initPointer++], LINELEN, "key_right=%d\n",
-           conf->key_right);
-  snprintf(ini_values[initPointer++], LINELEN, "key_select=%d\n",
-           conf->key_select);
-  snprintf(ini_values[initPointer++], LINELEN, "key_select_alt=%d\n",
-           conf->key_select_alt);
-  snprintf(ini_values[initPointer++], LINELEN, "key_start=%d\n",
-           conf->key_start);
-  snprintf(ini_values[initPointer++], LINELEN, "key_start_alt=%d\n",
-           conf->key_start_alt);
-  snprintf(ini_values[initPointer++], LINELEN, "key_opt=%d\n", conf->key_opt);
-  snprintf(ini_values[initPointer++], LINELEN, "key_opt_alt=%d\n",
-           conf->key_opt_alt);
-  snprintf(ini_values[initPointer++], LINELEN, "key_edit=%d\n", conf->key_edit);
-  snprintf(ini_values[initPointer++], LINELEN, "key_edit_alt=%d\n",
-           conf->key_edit_alt);
-  snprintf(ini_values[initPointer++], LINELEN, "key_delete=%d\n",
-           conf->key_delete);
-  snprintf(ini_values[initPointer++], LINELEN, "key_reset=%d\n",
-           conf->key_reset);
-  snprintf(ini_values[initPointer++], LINELEN, "[gamepad]\n");
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_up=%d\n",
-           conf->gamepad_up);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_left=%d\n",
-           conf->gamepad_left);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_down=%d\n",
-           conf->gamepad_down);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_right=%d\n",
-           conf->gamepad_right);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_select=%d\n",
-           conf->gamepad_select);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_start=%d\n",
-           conf->gamepad_start);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_opt=%d\n",
-           conf->gamepad_opt);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_edit=%d\n",
-           conf->gamepad_edit);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_quit=%d\n",
-           conf->gamepad_quit);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_reset=%d\n",
-           conf->gamepad_reset);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_threshold=%d\n",
-           conf->gamepad_analog_threshold);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_invert=%s\n",
-           conf->gamepad_analog_invert ? "true" : "false");
-  snprintf(ini_values[initPointer++], LINELEN,
-           "gamepad_analog_axis_updown=%d\n", conf->gamepad_analog_axis_updown);
-  snprintf(ini_values[initPointer++], LINELEN,
-           "gamepad_analog_axis_leftright=%d\n",
-           conf->gamepad_analog_axis_leftright);
-  snprintf(ini_values[initPointer++], LINELEN,
-           "gamepad_analog_axis_select=%d\n", conf->gamepad_analog_axis_select);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_axis_start=%d\n",
-           conf->gamepad_analog_axis_start);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_axis_opt=%d\n",
-           conf->gamepad_analog_axis_opt);
-  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_axis_edit=%d\n",
-           conf->gamepad_analog_axis_edit);
-
-  // Ensure we aren't writing off the end of the array
-  assert(initPointer == INI_LINE_COUNT);
-
-  if (rw != NULL) {
-    // Write ini_values array to config file
-    for (int i = 0; i < INI_LINE_COUNT; i++) {
-      size_t len = SDL_strlen(ini_values[i]);
-      if (SDL_RWwrite(rw, ini_values[i], 1, len) != len) {
-        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM,
-                     "Couldn't write line into config file.");
-      } else {
-        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Wrote to config: %s",
-                     ini_values[i]);
-      }
-    }
-    SDL_RWclose(rw);
-  } else {
-    SDL_Log("Couldn't write into config file.");
-  }
-}
-
-// Read config
-void read_config(config_params_s *conf) {
-
-  char config_path[1024] = {0};
-  snprintf(config_path, sizeof(config_path), "%s%s", SDL_GetPrefPath("", "m8c"),
-           conf->filename);
-  SDL_Log("Reading config %s", config_path);
-  ini_t *ini = ini_load(config_path);
-  if (ini == NULL) {
-    SDL_Log("Could not load config.");
-    write_config(conf);
-    return;
-  }
-
-  read_audio_config(ini, conf);
-  read_graphics_config(ini, conf);
-  read_key_config(ini, conf);
-  read_gamepad_config(ini, conf);
-
-  // Frees the mem used for the config
-  ini_free(ini);
-
-  // Write any new default options after loading
-  write_config(conf);
-}
-
-void read_audio_config(ini_t *ini, config_params_s *conf) {
-  const char *param_audio_enabled = ini_get(ini, "audio", "audio_enabled");
-  const char *param_audio_buffer_size =
-          ini_get(ini, "audio", "audio_buffer_size");
-  const char *param_audio_device_name =
-          ini_get(ini, "audio", "audio_device_name");
-
-  if (param_audio_enabled != NULL) {
-    if (strcmpci(param_audio_enabled, "true") == 0) {
-      conf->audio_enabled = 1;
-    } else {
-      conf->audio_enabled = 0;
-    }
-  }
-
-  if (param_audio_device_name != NULL && SDL_strcmp(param_audio_device_name, "Default") != 0) {
-    conf->audio_device_name = SDL_strdup(param_audio_device_name);
-  }
-
-  if (param_audio_buffer_size != NULL) {
-    conf->audio_buffer_size = SDL_atoi(param_audio_buffer_size);
-  }
-}
-
-void read_graphics_config(ini_t *ini, config_params_s *conf) {
-  const char *param_fs = ini_get(ini, "graphics", "fullscreen");
-  const char *param_gpu = ini_get(ini, "graphics", "use_gpu");
-  const char *idle_ms = ini_get(ini, "graphics", "idle_ms");
-  const char *param_wait = ini_get(ini, "graphics", "wait_for_device");
-  const char *wait_packets = ini_get(ini, "graphics", "wait_packets");
-
-  if (strcmpci(param_fs, "true") == 0) {
-    conf->init_fullscreen = 1;
-  } else
-    conf->init_fullscreen = 0;
-
-  if (param_gpu != NULL) {
-    if (strcmpci(param_gpu, "true") == 0) {
-      conf->init_use_gpu = 1;
-    } else
-      conf->init_use_gpu = 0;
-  }
-
-  if (idle_ms != NULL)
-    conf->idle_ms = SDL_atoi(idle_ms);
-
-  if (param_wait != NULL) {
-    if (strcmpci(param_wait, "true") == 0) {
-      conf->wait_for_device = 1;
-    } else {
-      conf->wait_for_device = 0;
-    }
-  }
-  if (wait_packets != NULL)
-    conf->wait_packets = SDL_atoi(wait_packets);
-}
-
-void read_key_config(ini_t *ini, config_params_s *conf) {
-  // TODO: Some form of validation
-
-  const char *key_up = ini_get(ini, "keyboard", "key_up");
-  const char *key_left = ini_get(ini, "keyboard", "key_left");
-  const char *key_down = ini_get(ini, "keyboard", "key_down");
-  const char *key_right = ini_get(ini, "keyboard", "key_right");
-  const char *key_select = ini_get(ini, "keyboard", "key_select");
-  const char *key_select_alt = ini_get(ini, "keyboard", "key_select_alt");
-  const char *key_start = ini_get(ini, "keyboard", "key_start");
-  const char *key_start_alt = ini_get(ini, "keyboard", "key_start_alt");
-  const char *key_opt = ini_get(ini, "keyboard", "key_opt");
-  const char *key_opt_alt = ini_get(ini, "keyboard", "key_opt_alt");
-  const char *key_edit = ini_get(ini, "keyboard", "key_edit");
-  const char *key_edit_alt = ini_get(ini, "keyboard", "key_edit_alt");
-  const char *key_delete = ini_get(ini, "keyboard", "key_delete");
-  const char *key_reset = ini_get(ini, "keyboard", "key_reset");
-
-  if (key_up)
-    conf->key_up = SDL_atoi(key_up);
-  if (key_left)
-    conf->key_left = SDL_atoi(key_left);
-  if (key_down)
-    conf->key_down = SDL_atoi(key_down);
-  if (key_right)
-    conf->key_right = SDL_atoi(key_right);
-  if (key_select)
-    conf->key_select = SDL_atoi(key_select);
-  if (key_select_alt)
-    conf->key_select_alt = SDL_atoi(key_select_alt);
-  if (key_start)
-    conf->key_start = SDL_atoi(key_start);
-  if (key_start_alt)
-    conf->key_start_alt = SDL_atoi(key_start_alt);
-  if (key_opt)
-    conf->key_opt = SDL_atoi(key_opt);
-  if (key_opt_alt)
-    conf->key_opt_alt = SDL_atoi(key_opt_alt);
-  if (key_edit)
-    conf->key_edit = SDL_atoi(key_edit);
-  if (key_edit_alt)
-    conf->key_edit_alt = SDL_atoi(key_edit_alt);
-  if (key_delete)
-    conf->key_delete = SDL_atoi(key_delete);
-  if (key_reset)
-    conf->key_reset = SDL_atoi(key_reset);
-}
-
-void read_gamepad_config(ini_t *ini, config_params_s *conf) {
-  // TODO: Some form of validation
-
-  const char *gamepad_up = ini_get(ini, "gamepad", "gamepad_up");
-  const char *gamepad_left = ini_get(ini, "gamepad", "gamepad_left");
-  const char *gamepad_down = ini_get(ini, "gamepad", "gamepad_down");
-  const char *gamepad_right = ini_get(ini, "gamepad", "gamepad_right");
-  const char *gamepad_select = ini_get(ini, "gamepad", "gamepad_select");
-  const char *gamepad_start = ini_get(ini, "gamepad", "gamepad_start");
-  const char *gamepad_opt = ini_get(ini, "gamepad", "gamepad_opt");
-  const char *gamepad_edit = ini_get(ini, "gamepad", "gamepad_edit");
-  const char *gamepad_quit = ini_get(ini, "gamepad", "gamepad_quit");
-  const char *gamepad_reset = ini_get(ini, "gamepad", "gamepad_reset");
-  const char *gamepad_analog_threshold =
-          ini_get(ini, "gamepad", "gamepad_analog_threshold");
-  const char *gamepad_analog_invert =
-          ini_get(ini, "gamepad", "gamepad_analog_invert");
-  const char *gamepad_analog_axis_updown =
-          ini_get(ini, "gamepad", "gamepad_analog_axis_updown");
-  const char *gamepad_analog_axis_leftright =
-          ini_get(ini, "gamepad", "gamepad_analog_axis_leftright");
-  const char *gamepad_analog_axis_select =
-          ini_get(ini, "gamepad", "gamepad_analog_axis_select");
-  const char *gamepad_analog_axis_start =
-          ini_get(ini, "gamepad", "gamepad_analog_axis_start");
-  const char *gamepad_analog_axis_opt =
-          ini_get(ini, "gamepad", "gamepad_analog_axis_opt");
-  const char *gamepad_analog_axis_edit =
-          ini_get(ini, "gamepad", "gamepad_analog_axis_edit");
-
-  if (gamepad_up)
-    conf->gamepad_up = SDL_atoi(gamepad_up);
-  if (gamepad_left)
-    conf->gamepad_left = SDL_atoi(gamepad_left);
-  if (gamepad_down)
-    conf->gamepad_down = SDL_atoi(gamepad_down);
-  if (gamepad_right)
-    conf->gamepad_right = SDL_atoi(gamepad_right);
-  if (gamepad_select)
-    conf->gamepad_select = SDL_atoi(gamepad_select);
-  if (gamepad_start)
-    conf->gamepad_start = SDL_atoi(gamepad_start);
-  if (gamepad_opt)
-    conf->gamepad_opt = SDL_atoi(gamepad_opt);
-  if (gamepad_edit)
-    conf->gamepad_edit = SDL_atoi(gamepad_edit);
-  if (gamepad_quit)
-    conf->gamepad_quit = SDL_atoi(gamepad_quit);
-  if (gamepad_reset)
-    conf->gamepad_reset = SDL_atoi(gamepad_reset);
-  if (gamepad_analog_threshold)
-    conf->gamepad_analog_threshold = SDL_atoi(gamepad_analog_threshold);
-
-  if (strcmpci(gamepad_analog_invert, "true") == 0)
-    conf->gamepad_analog_invert = 1;
-  else
-    conf->gamepad_analog_invert = 0;
-
-  if (gamepad_analog_axis_updown)
-    conf->gamepad_analog_axis_updown = SDL_atoi(gamepad_analog_axis_updown);
-  if (gamepad_analog_axis_leftright)
-    conf->gamepad_analog_axis_leftright =
-            SDL_atoi(gamepad_analog_axis_leftright);
-  if (gamepad_analog_axis_select)
-    conf->gamepad_analog_axis_select = SDL_atoi(gamepad_analog_axis_select);
-  if (gamepad_analog_axis_start)
-    conf->gamepad_analog_axis_start = SDL_atoi(gamepad_analog_axis_start);
-  if (gamepad_analog_axis_opt)
-    conf->gamepad_analog_axis_opt = SDL_atoi(gamepad_analog_axis_opt);
-  if (gamepad_analog_axis_edit)
-    conf->gamepad_analog_axis_edit = SDL_atoi(gamepad_analog_axis_edit);
-}
--- a/config.h
+++ /dev/null
@@ -1,65 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#ifndef CONFIG_H_
-#define CONFIG_H_
-
-#include "ini.h"
-
-typedef struct config_params_s {
-  char *filename;
-  int init_fullscreen;
-  int init_use_gpu;
-  int idle_ms;
-  int wait_for_device;
-  int wait_packets;
-  int audio_enabled;
-  int audio_buffer_size;
-  const char *audio_device_name;
-
-  int key_up;
-  int key_left;
-  int key_down;
-  int key_right;
-  int key_select;
-  int key_select_alt;
-  int key_start;
-  int key_start_alt;
-  int key_opt;
-  int key_opt_alt;
-  int key_edit;
-  int key_edit_alt;
-  int key_delete;
-  int key_reset;
-
-  int gamepad_up;
-  int gamepad_left;
-  int gamepad_down;
-  int gamepad_right;
-  int gamepad_select;
-  int gamepad_start;
-  int gamepad_opt;
-  int gamepad_edit;
-  int gamepad_quit;
-  int gamepad_reset;
-
-  int gamepad_analog_threshold;
-  int gamepad_analog_invert;
-  int gamepad_analog_axis_updown;
-  int gamepad_analog_axis_leftright;
-  int gamepad_analog_axis_start;
-  int gamepad_analog_axis_select;
-  int gamepad_analog_axis_opt;
-  int gamepad_analog_axis_edit;
-
-} config_params_s;
-
-
-config_params_s init_config();
-void read_config(config_params_s *conf);
-void read_audio_config(ini_t *config, config_params_s *conf);
-void read_graphics_config(ini_t *config, config_params_s *conf);
-void read_key_config(ini_t *config, config_params_s *conf);
-void read_gamepad_config(ini_t *config, config_params_s *conf);
-
-#endif
--- a/config.ini.sample
+++ /dev/null
@@ -1,60 +1,0 @@
-; edit this file to change m8c defaults
-; this file is re-written every time it is read,
-; so do not expect comments or commented out values to survive!
-; valid parameter changes will be written back and persisted though.
-
-[graphics]
-; set this to true to have m8c start fullscreen
-fullscreen=false
-; set this to false to run m8c in software rendering mode (may be useful for Raspberry Pi)
-use_gpu=true
-; the delay amount in ms in the main loop, decrease value for faster operation, increase value if too much cpu usage
-idle_ms = 10
-; show a spinning cube if device is not inserted
-wait_for_device = true
-; number of zero-byte attempts to disconnect if wait_for_device = false (128 = about 2 sec for default idle_ms)
-wait_packets = 128
-
-[keyboard]
-; these need to be the decimal value of the SDL scancodes.
-; a table exists here: https://github.com/libsdl-org/sdlwiki/blob/main/SDLScancodeLookup.mediawiki
-key_up=82
-key_left=80
-key_down=81
-key_right=79
-key_select=225
-key_select_alt=29
-key_start=44
-key_start_alt=27
-key_opt=226
-key_opt_alt=4
-key_edit=224
-key_edit_alt=22
-key_delete=76
-key_reset=21
-
-[gamepad]
-; these need to be the decimal value of the SDL Controller buttons.
-; a table exists here: https://wiki.libsdl.org/SDL_GameControllerButton
-gamepad_up=11
-gamepad_left=13
-gamepad_down=12
-gamepad_right=14
-gamepad_select=4
-gamepad_start=6
-gamepad_opt=1
-gamepad_edit=0
-gamepad_quit=8
-gamepad_reset=7
-
-gamepad_analog_threshold=32766 ;the threshold for analog sticks to trigger cursor movement (working values: 1-32766)
-gamepad_analog_invert=false ;NOT IMPLEMENTED YET: invert up/down and left/right axis (true/false)
-
-; these need to be the decimal value of the controller axis
-; you can use -1 if you do not wish to map the function to an analog axis
-gamepad_analog_axis_updown=1
-gamepad_analog_axis_leftright=0
-gamepad_analog_axis_start=5
-gamepad_analog_axis_select=4
-gamepad_analog_axis_opt=-1
-gamepad_analog_axis_edit=-1
--- a/fx_cube.c
+++ /dev/null
@@ -1,123 +1,0 @@
-#include <SDL.h>
-#include "SDL2_inprint.h"
-#include "SDL_pixels.h"
-
-#define target_width 320
-#define target_height 240
-static SDL_Texture *texture_cube;
-static SDL_Texture *texture_text;
-static SDL_Renderer *fx_renderer;
-static SDL_Color line_color;
-
-const char *text_m8c = "M8C";
-const char *text_disconnected = "DEVICE DISCONNECTED";
-
-static const float center_x = (float)target_width / 2;
-static const float center_y = (float)target_height / 2;
-
-static const float default_nodes[8][3] = {
-    {-1, -1, -1}, {-1, -1, 1}, {-1, 1, -1}, {-1, 1, 1},
-    {1, -1, -1},  {1, -1, 1},  {1, 1, -1},  {1, 1, 1}};
-
-static int edges[12][2] = {{0, 1}, {1, 3}, {3, 2}, {2, 0}, {4, 5}, {5, 7},
-                           {7, 6}, {6, 4}, {0, 4}, {1, 5}, {2, 6}, {3, 7}};
-
-static float nodes[8][3];
-
-static void scale(float factor0, float factor1, float factor2) {
-  for (int i = 0; i < 8; i++) {
-    nodes[i][0] *= factor0;
-    nodes[i][1] *= factor1;
-    nodes[i][2] *= factor2;
-  }
-}
-
-static void rotate_cube(float angle_x, float angle_y) {
-  float sin_x = SDL_sin(angle_x);
-  float cos_x = SDL_cos(angle_x);
-  float sin_y = SDL_sin(angle_y);
-  float cos_y = SDL_cos(angle_y);
-  for (int i = 0; i < 8; i++) {
-    float x = nodes[i][0];
-    float y = nodes[i][1];
-    float z = nodes[i][2];
-
-    nodes[i][0] = x * cos_x - z * sin_x;
-    nodes[i][2] = z * cos_x + x * sin_x;
-
-    z = nodes[i][2];
-
-    nodes[i][1] = y * cos_y - z * sin_y;
-    nodes[i][2] = z * cos_y + y * sin_y;
-  }
-}
-
-void fx_cube_init(SDL_Renderer *target_renderer, SDL_Color foreground_color) {
-
-  fx_renderer = target_renderer;
-  line_color = foreground_color;
-
-  texture_cube =
-      SDL_CreateTexture(fx_renderer, SDL_PIXELFORMAT_ARGB8888,
-                        SDL_TEXTUREACCESS_TARGET, target_width, target_height);
-  texture_text =
-      SDL_CreateTexture(fx_renderer, SDL_PIXELFORMAT_ARGB8888,
-                        SDL_TEXTUREACCESS_TARGET, target_width, target_height);
-
-  SDL_Texture *og_target = SDL_GetRenderTarget(fx_renderer);                        
-
-  SDL_SetRenderTarget(fx_renderer, texture_text);
-  SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
-  SDL_RenderClear(fx_renderer);
-
-  inprint(fx_renderer, text_disconnected, 150, 228, 0xFFFFFF, 0x000000);
-  inprint(fx_renderer, text_m8c, 2, 2, 0xFFFFFF, 0x000000);
-
-  SDL_SetRenderTarget(fx_renderer, og_target);
-
-  // Initialize default nodes
-  SDL_memcpy(nodes, default_nodes, sizeof(default_nodes));
-
-  scale(50, 50, 50);
-  rotate_cube(M_PI / 4, SDL_atan(SDL_sqrt(2)));
-
-  SDL_SetTextureBlendMode(texture_cube, SDL_BLENDMODE_BLEND);
-  SDL_SetTextureBlendMode(texture_text, SDL_BLENDMODE_BLEND);
-}
-
-void fx_cube_destroy() {
-  SDL_DestroyTexture(texture_cube);
-  SDL_DestroyTexture(texture_text);
-}
-
-void fx_cube_update() {
-  SDL_Point points[24];
-  int points_counter = 0;
-  SDL_Texture *og_texture = SDL_GetRenderTarget(fx_renderer);
-
-  SDL_SetRenderTarget(fx_renderer, texture_cube);
-  SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
-  SDL_RenderClear(fx_renderer);
-
-  int seconds = SDL_GetTicks() / 1000;
-  float scalefactor = 1 + (SDL_sin(seconds) * 0.01);
-
-  scale(scalefactor, scalefactor, scalefactor);
-  rotate_cube(M_PI / 180, M_PI / 270);
-
-  for (int i = 0; i < 12; i++) {
-    float *p1 = nodes[edges[i][0]];
-    float *p2 = nodes[edges[i][1]];
-    points[points_counter++] =
-        (SDL_Point){p1[0] + center_x, nodes[edges[i][0]][1] + center_y};
-    points[points_counter++] = (SDL_Point){p2[0] + center_x, p2[1] + center_y};
-  }
-
-  SDL_RenderCopy(fx_renderer, texture_text, NULL, NULL);
-  SDL_SetRenderDrawColor(fx_renderer, line_color.r, line_color.g, line_color.b,
-                         line_color.a);
-  SDL_RenderDrawLines(fx_renderer, points, 24);
-
-  SDL_SetRenderTarget(fx_renderer, og_texture);
-  SDL_RenderCopy(fx_renderer, texture_cube, NULL, NULL);
-}
--- a/fx_cube.h
+++ /dev/null
@@ -1,8 +1,0 @@
-#ifndef FX_CUBE_H_
-#define FX_CUBE_H_
-
-#include "SDL_render.h"
-void fx_cube_init(SDL_Renderer *target_renderer, SDL_Color foreground_color);
-void fx_cube_destroy();
-void fx_cube_update();
-#endif
\ No newline at end of file
--- a/ini.c
+++ /dev/null
@@ -1,277 +1,0 @@
-/**
- * Copyright (c) 2016 rxi
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <ctype.h>
-
-#include "ini.h"
-
-struct ini_t {
-  char *data;
-  char *end;
-};
-
-
-/* Case insensitive string compare */
-static int strcmpci(const char *a, const char *b) {
-  for (;;) {
-    int d = tolower(*a) - tolower(*b);
-    if (d != 0 || !*a) {
-      return d;
-    }
-    a++, b++;
-  }
-}
-
-/* Returns the next string in the split data */
-static char* next(ini_t *ini, char *p) {
-  p += strlen(p);
-  while (p < ini->end && *p == '\0') {
-    p++;
-  }
-  return p;
-}
-
-static void trim_back(ini_t *ini, char *p) {
-  while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) {
-    *p-- = '\0';
-  }
-}
-
-static char* discard_line(ini_t *ini, char *p) {
-  while (p < ini->end && *p != '\n') {
-    *p++ = '\0';
-  }
-  return p;
-}
-
-
-static char *unescape_quoted_value(ini_t *ini, char *p) {
-  /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q`
-   * as escape sequences are always larger than their resultant data */
-  char *q = p;
-  p++;
-  while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') {
-    if (*p == '\\') {
-      /* Handle escaped char */
-      p++;
-      switch (*p) {
-        default   : *q = *p;    break;
-        case 'r'  : *q = '\r';  break;
-        case 'n'  : *q = '\n';  break;
-        case 't'  : *q = '\t';  break;
-        case '\r' :
-        case '\n' :
-        case '\0' : goto end;
-      }
-
-    } else {
-      /* Handle normal char */
-      *q = *p;
-    }
-    q++, p++;
-  }
-end:
-  return q;
-}
-
-
-/* Splits data in place into strings containing section-headers, keys and
- * values using one or more '\0' as a delimiter. Unescapes quoted values */
-static void split_data(ini_t *ini) {
-  char *value_start, *line_start;
-  char *p = ini->data;
-
-  while (p < ini->end) {
-    switch (*p) {
-      case '\r':
-      case '\n':
-      case '\t':
-      case ' ':
-        *p = '\0';
-        /* Fall through */
-
-      case '\0':
-        p++;
-        break;
-
-      case '[':
-        p += strcspn(p, "]\n");
-        *p = '\0';
-        break;
-
-      case ';':
-        p = discard_line(ini, p);
-        break;
-
-      default:
-        line_start = p;
-        p += strcspn(p, "=\n");
-
-        /* Is line missing a '='? */
-        if (*p != '=') {
-          p = discard_line(ini, line_start);
-          break;
-        }
-        trim_back(ini, p - 1);
-
-        /* Replace '=' and whitespace after it with '\0' */
-        do {
-          *p++ = '\0';
-        } while (*p == ' ' || *p == '\r' || *p == '\t');
-
-        /* Is a value after '=' missing? */
-        if (*p == '\n' || *p == '\0') {
-          p = discard_line(ini, line_start);
-          break;
-        }
-
-        if (*p == '"') {
-          /* Handle quoted string value */
-          value_start = p;
-          p = unescape_quoted_value(ini, p);
-
-          /* Was the string empty? */
-          if (p == value_start) {
-            p = discard_line(ini, line_start);
-            break;
-          }
-
-          /* Discard the rest of the line after the string value */
-          p = discard_line(ini, p);
-
-        } else {
-          /* Handle normal value */
-          p += strcspn(p, "\n");
-          trim_back(ini, p - 1);
-        }
-        break;
-    }
-  }
-}
-
-
-
-ini_t* ini_load(const char *filename) {
-  ini_t *ini = NULL;
-  FILE *fp = NULL;
-  int n, sz;
-
-  /* Init ini struct */
-  ini = malloc(sizeof(*ini));
-  if (!ini) {
-    goto fail;
-  }
-  memset(ini, 0, sizeof(*ini));
-
-  /* Open file */
-  fp = fopen(filename, "rb");
-  if (!fp) {
-    goto fail;
-  }
-
-  /* Get file size */
-  fseek(fp, 0, SEEK_END);
-  sz = ftell(fp);
-  if (sz==0) {
-    goto fail;
-  }
-  rewind(fp);
-
-  /* Load file content into memory, null terminate, init end var */
-  ini->data = malloc(sz + 1);
-  ini->data[sz] = '\0';
-  ini->end = ini->data  + sz;
-  n = fread(ini->data, 1, sz, fp);
-  if (n != sz) {
-    goto fail;
-  }
-
-  /* Prepare data */
-  split_data(ini);
-
-  /* Clean up and return */
-  fclose(fp);
-  return ini;
-
-fail:
-  if (fp) fclose(fp);
-  if (ini) ini_free(ini);
-  return NULL;
-}
-
-
-void ini_free(ini_t *ini) {
-  free(ini->data);
-  free(ini);
-}
-
-
-const char* ini_get(ini_t *ini, const char *section, const char *key) {
-  char *current_section = "";
-  char *val;
-  char *p = ini->data;
-
-  if (*p == '\0') {
-    p = next(ini, p);
-  }
-
-  while (p < ini->end) {
-    if (*p == '[') {
-      /* Handle section */
-      current_section = p + 1;
-
-    } else {
-      /* Handle key */
-      val = next(ini, p);
-      if (!section || !strcmpci(section, current_section)) {
-        if (!strcmpci(p, key)) {
-          return val;
-        }
-      }
-      p = val;
-    }
-
-    p = next(ini, p);
-  }
-
-  return NULL;
-}
-
-
-int ini_sget(
-  ini_t *ini, const char *section, const char *key,
-  const char *scanfmt, void *dst
-) {
-  const char *val = ini_get(ini, section, key);
-  if (!val) {
-    return 0;
-  }
-  if (scanfmt) {
-    sscanf(val, scanfmt, dst);
-  } else {
-    *((const char**) dst) = val;
-  }
-  return 1;
-}
--- a/ini.h
+++ /dev/null
@@ -1,20 +1,0 @@
-/**
- * Copyright (c) 2016 rxi
- *
- * This library is free software; you can redistribute it and/or modify it
- * under the terms of the MIT license. See `ini.c` for details.
- */
-
-#ifndef INI_H
-#define INI_H
-
-#define INI_VERSION "0.1.1"
-
-typedef struct ini_t ini_t;
-
-ini_t*      ini_load(const char *filename);
-void        ini_free(ini_t *ini);
-const char* ini_get(ini_t *ini, const char *section, const char *key);
-int         ini_sget(ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst);
-
-#endif
--- a/inline_font.h
+++ /dev/null
@@ -1,10 +1,0 @@
-#ifndef INLINE_FONT_H_
-#define INLINE_FONT_H_
-
-struct inline_font {
-    int width;
-    int height;
-    unsigned char bits[];
-};
-
-#endif
--- a/inline_font_large.h
+++ /dev/null
@@ -1,133 +1,0 @@
-#ifndef INLINE_FONT_LARGE_H_
-#define INLINE_FONT_LARGE_H_
-
-#include "inline_font.h"
-
-struct inline_font inline_font_large = {
-    144,
-    88,
-    {
-0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 
-0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 0xff, 0x79, 0xf1, 
-0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 
-0x2f, 0x5e, 0xff, 0xb5, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xda, 0xb4, 
-0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xff, 0xcd, 0x99, 0x33, 0x67, 0xce, 
-0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xff, 
-0xed, 0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xf6, 0xec, 0xd9, 0xb3, 0x67, 
-0xcf, 0x9e, 0x3d, 0x7b, 0xff, 0xcd, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 
-0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xff, 0xb5, 0x69, 0xd3, 
-0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 
-0x6d, 0xff, 0x79, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 
-0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xff, 0xfd, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 
-0x1f, 0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 0xff, 0x01, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 
-0xc7, 0x8f, 0x1f, 0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 
-0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 0xe2, 
-0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 
-0x6d, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xe6, 0xcc, 0x99, 
-0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 
-0x39, 0x73, 0xf6, 0xec, 0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xf6, 0xec, 
-0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 
-0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xda, 
-0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 
-0x4d, 0x9b, 0x36, 0x6d, 0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 
-0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0x7e, 0xfc, 0xf8, 0xf1, 
-0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 
-0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xe7, 0x4f, 0xfe, 0x7f, 0xfe, 0xff, 0xff, 0xf3, 0x3f, 
-0xf3, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x4f, 0xfe, 0x0f, 
-0x70, 0xef, 0xf8, 0xf3, 0x9f, 0xe7, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 
-0xff, 0xe7, 0x4f, 0xde, 0x4e, 0xbe, 0x76, 0xf2, 0xf3, 0xcf, 0xcf, 0xcf, 0x3f, 
-0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xe7, 0xff, 0x0f, 0x4c, 0x7e, 0x7b, 0xf2, 
-0xff, 0xcf, 0xcf, 0x87, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xe7, 0xff, 
-0xdf, 0x0e, 0xf0, 0xfd, 0xf8, 0xff, 0xcf, 0xcf, 0x33, 0x0f, 0xfc, 0x3f, 0xf0, 
-0xff, 0xf3, 0xff, 0xe7, 0xff, 0xdf, 0x7e, 0xf2, 0x76, 0xf8, 0xff, 0xcf, 0xcf, 
-0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0x0f, 0x7c, 0x72, 
-0x2b, 0xc3, 0xff, 0xcf, 0xcf, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 
-0xff, 0xff, 0xdf, 0x0e, 0xb0, 0x37, 0xe7, 0xff, 0x9f, 0xe7, 0xff, 0xff, 0x3f, 
-0xff, 0xff, 0x7f, 0xfe, 0xff, 0xe7, 0xff, 0xff, 0x7f, 0xfe, 0x7f, 0xc8, 0xff, 
-0x3f, 0xf3, 0xff, 0xff, 0x9f, 0xff, 0x7f, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xc7, 0x03, 0x04, 0xcc, 0x13, 0x20, 
-0x40, 0x80, 0x03, 0x01, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3c, 0xc3, 
-0xff, 0xfc, 0xc9, 0x93, 0x3f, 0xff, 0x9f, 0x39, 0x79, 0xfe, 0xff, 0xff, 0xf3, 
-0x7f, 0x7e, 0x9e, 0x3c, 0xc9, 0xff, 0xfc, 0xc9, 0x93, 0x3f, 0xff, 0x9f, 0x3c, 
-0x79, 0xfe, 0xff, 0xff, 0x31, 0x70, 0xfc, 0x9f, 0x3c, 0xcf, 0xff, 0xfc, 0xc9, 
-0x93, 0x3f, 0xff, 0xcf, 0x3c, 0x79, 0xce, 0xcf, 0xff, 0xf0, 0x7f, 0xf8, 0x9f, 
-0x24, 0xcf, 0x03, 0x0c, 0x0c, 0x10, 0x20, 0xc0, 0xe7, 0x81, 0x01, 0xfe, 0xff, 
-0x7f, 0xf0, 0x7f, 0xf0, 0xc3, 0x3c, 0xcf, 0xf3, 0xff, 0xf9, 0xf3, 0x27, 0xcf, 
-0xf3, 0x3c, 0x7f, 0xfe, 0xff, 0xff, 0xf0, 0x7f, 0xf8, 0xf3, 0x3c, 0xcf, 0xf3, 
-0xff, 0xf9, 0xf3, 0x27, 0xcf, 0xf3, 0x3c, 0x7f, 0xce, 0xcf, 0xff, 0x31, 0x70, 
-0xfc, 0xf3, 0x3c, 0xcf, 0xf3, 0xff, 0xf9, 0xf3, 0x27, 0xcf, 0xf3, 0x3c, 0x7f, 
-0xfe, 0xcf, 0xff, 0xf3, 0x7f, 0xfe, 0xff, 0x00, 0x01, 0x02, 0x04, 0xfc, 0x13, 
-0x20, 0xc0, 0xf3, 0x80, 0x7f, 0xfe, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0x87, 0x03, 0x0e, 
-0x0c, 0x1c, 0x20, 0xc0, 0xc0, 0x3c, 0x01, 0x02, 0xe4, 0xc9, 0x9f, 0x27, 0xcf, 
-0xc0, 0x3c, 0x33, 0xf3, 0xe4, 0xc9, 0x99, 0x3f, 0x7f, 0x9e, 0x3c, 0xcf, 0xff, 
-0xe4, 0xcc, 0x1f, 0x23, 0x4e, 0x9e, 0x3c, 0x79, 0xf2, 0xe4, 0xcf, 0x93, 0x3f, 
-0x7f, 0xfe, 0x3c, 0xcf, 0xff, 0x64, 0xce, 0x1f, 0x20, 0x4c, 0x9e, 0x04, 0x79, 
-0xf2, 0xe4, 0xcf, 0x93, 0x3f, 0x7f, 0xfe, 0x3c, 0xcf, 0xff, 0x24, 0xcf, 0x9f, 
-0x24, 0x49, 0x9e, 0x24, 0x01, 0x02, 0xe6, 0xcf, 0x13, 0x38, 0x70, 0x86, 0x00, 
-0xcf, 0xff, 0x84, 0xcf, 0x9f, 0x27, 0x43, 0x9e, 0x24, 0x79, 0xf2, 0xe4, 0xcf, 
-0x93, 0x3f, 0x7f, 0x9e, 0x3c, 0xcf, 0xff, 0x24, 0xcf, 0x9f, 0x27, 0x47, 0x9e, 
-0x84, 0x79, 0xf2, 0xe4, 0xcf, 0x93, 0x3f, 0x7f, 0x9e, 0x3c, 0xcf, 0xf3, 0x64, 
-0xce, 0x9f, 0x27, 0x4f, 0x9e, 0xfc, 0x79, 0xf2, 0xe4, 0xc9, 0x99, 0x3f, 0x7f, 
-0x9e, 0x3c, 0xcf, 0xf3, 0xe4, 0xcc, 0x9f, 0x27, 0x4f, 0x9e, 0x81, 0x79, 0x02, 
-0x0e, 0x0c, 0x1c, 0x20, 0xff, 0xc0, 0x3c, 0x01, 0x06, 0xe6, 0x09, 0x90, 0x27, 
-0xcf, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 
-0x03, 0x03, 0x0e, 0x0c, 0x90, 0x27, 0x4f, 0x9e, 0x3c, 0x79, 0x02, 0x7c, 0xf8, 
-0x1f, 0xfe, 0xfc, 0xff, 0x3c, 0x79, 0xf2, 0xe4, 0x79, 0x9e, 0x27, 0x4f, 0x9e, 
-0x3c, 0x79, 0xfe, 0x7c, 0xfe, 0x7f, 0x7e, 0xf8, 0xff, 0x3c, 0x79, 0xf2, 0xe4, 
-0x7f, 0x9e, 0x27, 0x4f, 0x9e, 0x99, 0x79, 0x7e, 0x7e, 0xce, 0x7f, 0x3e, 0xf3, 
-0xff, 0x3c, 0x79, 0xf2, 0xe4, 0x7f, 0x9e, 0x27, 0x4f, 0x9e, 0xc3, 0x79, 0x3e, 
-0x7f, 0x9e, 0x7f, 0xfe, 0xff, 0xff, 0x80, 0x79, 0x02, 0x0e, 0x7c, 0x9e, 0x27, 
-0x4f, 0x9e, 0xe7, 0x03, 0x9f, 0x7f, 0x3e, 0x7f, 0xfe, 0xff, 0xff, 0xfc, 0x49, 
-0x92, 0xff, 0x79, 0x9e, 0x27, 0x4f, 0x92, 0xc3, 0xcf, 0xcf, 0x7f, 0x7e, 0x7e, 
-0xfe, 0xff, 0xff, 0xfc, 0x99, 0x32, 0xff, 0x79, 0x9e, 0x67, 0x66, 0x80, 0x99, 
-0xcf, 0xe7, 0x7f, 0xfe, 0x7c, 0xfe, 0xff, 0xff, 0xfc, 0x39, 0x73, 0xe6, 0x79, 
-0x9e, 0xe7, 0x70, 0x8c, 0x3c, 0xcf, 0xf3, 0x7f, 0xfe, 0x79, 0xfe, 0xff, 0xff, 
-0xfc, 0x43, 0xf2, 0x0c, 0x7c, 0x3e, 0xf0, 0x79, 0x9e, 0x3c, 0xcf, 0x03, 0x7c, 
-0xf8, 0x13, 0xfe, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xfc, 0xff, 0xf3, 0xff, 0xff, 0xf3, 0x7f, 0xc0, 0xff, 0xfc, 0xcf, 
-0xff, 0xe4, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xf3, 0xff, 0xff, 0xf3, 
-0x7f, 0xfe, 0xff, 0xfc, 0xff, 0xff, 0xe7, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf3, 
-0x01, 0x02, 0x04, 0x08, 0x10, 0x60, 0x7e, 0x80, 0x00, 0xc3, 0x07, 0xe4, 0x39, 
-0x1f, 0x20, 0x40, 0x80, 0xff, 0x7f, 0xf2, 0xe4, 0xcf, 0x93, 0x27, 0x40, 0x9e, 
-0x3c, 0xcf, 0xff, 0xe4, 0x3c, 0x9f, 0x24, 0x4f, 0x9e, 0xff, 0x7f, 0xf2, 0xe4, 
-0xcf, 0x93, 0x67, 0x7e, 0x9e, 0x3c, 0xcf, 0xff, 0x64, 0x3e, 0x9f, 0x24, 0x4f, 
-0x9e, 0xff, 0x01, 0xf2, 0xe4, 0xcf, 0x13, 0x60, 0x7e, 0x80, 0x3c, 0xcf, 0xff, 
-0x04, 0x3f, 0x9f, 0x24, 0x4f, 0x9e, 0xff, 0x79, 0xf2, 0xe4, 0xcf, 0x93, 0x7f, 
-0xfe, 0x9f, 0x3c, 0xcf, 0xff, 0x64, 0x3e, 0x9f, 0x24, 0x4f, 0x9e, 0xff, 0x79, 
-0xf2, 0xe4, 0xcf, 0x93, 0x7f, 0xfe, 0x9f, 0x3c, 0xcf, 0xff, 0xe4, 0x3c, 0x9f, 
-0x24, 0x4f, 0x9e, 0xff, 0x01, 0x02, 0x04, 0x08, 0x10, 0x60, 0x7e, 0x80, 0x3c, 
-0x01, 0x02, 0xe4, 0x09, 0x90, 0x24, 0x4f, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0x07, 0x78, 0x1e, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x79, 0x9e, 0xe7, 
-0xff, 0xff, 0x00, 0x01, 0x02, 0x04, 0x08, 0x90, 0x27, 0x4f, 0x92, 0x3c, 0x79, 
-0x02, 0xe4, 0x79, 0x9e, 0xe7, 0xff, 0xff, 0x3c, 0x79, 0xf2, 0xe7, 0x9f, 0x9f, 
-0x27, 0x67, 0x92, 0x99, 0x79, 0x7e, 0xe6, 0x79, 0x9e, 0x67, 0xfc, 0xff, 0x3c, 
-0x79, 0xf2, 0xe7, 0x9f, 0x9f, 0x27, 0x73, 0x92, 0xc3, 0x79, 0x3e, 0xe7, 0x79, 
-0x9e, 0x27, 0xc9, 0xff, 0x00, 0x01, 0xf2, 0x07, 0x98, 0x9f, 0x27, 0x79, 0x92, 
-0xe7, 0x01, 0x9e, 0xe7, 0x79, 0x9e, 0xe7, 0xe3, 0xff, 0xfc, 0x7f, 0xf2, 0xff, 
-0x99, 0x9f, 0x27, 0x7c, 0x92, 0xc3, 0x7f, 0xce, 0xe7, 0x79, 0x9e, 0xe7, 0xff, 
-0xff, 0xfc, 0x7f, 0xf2, 0xff, 0x99, 0x9f, 0x27, 0x7e, 0x92, 0x99, 0x7f, 0xe6, 
-0xe7, 0x79, 0x9e, 0xe7, 0xff, 0xff, 0xfc, 0x7f, 0xf2, 0x07, 0x18, 0x10, 0x20, 
-0x7f, 0x80, 0x3c, 0x01, 0x02, 0x04, 0x78, 0x1e, 0xe0, 0xff, 0xff  }};
-
-#endif
--- a/inline_font_small.h
+++ /dev/null
@@ -1,84 +1,0 @@
-/*
-The FontStruction “M8stealth57”
-(https://fontstruct.com/fontstructions/show/2043303) by “trash80” is licensed
-under a Creative Commons Attribution Share Alike license
-(http://creativecommons.org/licenses/by-sa/3.0/). “M8stealth57” was originally
-cloned (copied) from the FontStruction “stealth57”
-(https://fontstruct.com/fontstructions/show/413734) by “trash80”, which is
-licensed under a Creative Commons Attribution Share Alike license
-(http://creativecommons.org/licenses/by-sa/3.0/).
-
-Used with permission from the author.
-*/
-
-#ifndef INLINE_FONT_SMALL_H_
-#define INLINE_FONT_SMALL_H_
-
-#include "inline_font.h"
-
-struct inline_font inline_font_small = {
-    96,
-    64,
-    {
-0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 
-0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x3f, 0x45, 
-0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0xbf, 0xa6, 0x69, 
-0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x3f, 0x45, 0x51, 0x14, 
-0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0xbf, 0x65, 0x59, 0x96, 0x65, 
-0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 
-0x96, 0x65, 0x59, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 
-0x45, 0x51, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 
-0x69, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 
-0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x00, 
-0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x5f, 0xff, 
-0xfb, 0x9f, 0xef, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xbf, 0x5f, 0xd7, 0x20, 
-0x6b, 0xef, 0x77, 0x5f, 0xef, 0xff, 0xff, 0xbf, 0xbf, 0xff, 0x83, 0x3a, 0xad, 
-0xff, 0x77, 0xbf, 0xef, 0xff, 0xff, 0xdf, 0xbf, 0xff, 0xd7, 0xe0, 0xde, 0xfe, 
-0x77, 0x5f, 0x83, 0x3f, 0xf8, 0xef, 0xbf, 0xff, 0x83, 0x6b, 0xa9, 0xfe, 0x77, 
-0xff, 0xef, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xd7, 0xa0, 0x69, 0xff, 0x77, 0xff, 
-0xef, 0xfb, 0xff, 0xfb, 0xbf, 0xff, 0xff, 0xfb, 0x9f, 0xfe, 0xaf, 0xff, 0xff, 
-0xfb, 0xbf, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xe0, 0x0e, 0x82, 0x2e, 0x08, 0x82, 0x21, 0xf8, 0xff, 0xff, 0xff, 
-0xc7, 0x2e, 0xfe, 0xbe, 0xae, 0xef, 0xbf, 0xae, 0xfb, 0xff, 0xf7, 0xdf, 0xbb, 
-0xe6, 0xfe, 0xbe, 0xae, 0xef, 0xbf, 0xae, 0xdb, 0xf7, 0x33, 0x98, 0xbf, 0xea, 
-0x0e, 0x82, 0x20, 0x08, 0xde, 0x20, 0xf8, 0xff, 0xf1, 0x1f, 0xdf, 0xec, 0xee, 
-0xbf, 0xef, 0xeb, 0xee, 0xee, 0xfb, 0xff, 0x33, 0x98, 0xef, 0xee, 0xee, 0xbf, 
-0xef, 0xeb, 0xee, 0xee, 0xdb, 0xf7, 0xf7, 0xdf, 0xff, 0x20, 0x08, 0x82, 0x2f, 
-0x08, 0xee, 0xf0, 0xfb, 0xf7, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x71, 0x0c, 0xc7, 0x30, 0x08, 0xc6, 
-0x2e, 0xf8, 0xba, 0xbe, 0xeb, 0xc6, 0xae, 0xeb, 0xba, 0xae, 0xef, 0xbb, 0xee, 
-0xfe, 0xda, 0x3e, 0xc9, 0xba, 0xa2, 0xeb, 0xfa, 0xae, 0xef, 0xfb, 0xee, 0xfe, 
-0xea, 0xbe, 0xaa, 0xba, 0x2a, 0x08, 0xfb, 0x2e, 0x0c, 0xfb, 0xe0, 0xfe, 0xf2, 
-0xbe, 0x6b, 0xba, 0xa2, 0xeb, 0xfa, 0xae, 0xef, 0x9b, 0xee, 0xee, 0xea, 0xbe, 
-0xeb, 0xba, 0xbe, 0xeb, 0xba, 0xae, 0xef, 0xbb, 0xee, 0xee, 0xda, 0xbe, 0xeb, 
-0xba, 0xb1, 0x0b, 0xc7, 0x30, 0xe8, 0xc7, 0x2e, 0x18, 0xbb, 0xa0, 0xeb, 0xc6, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 
-0x0c, 0x87, 0xa0, 0xeb, 0xba, 0xae, 0x0b, 0x9e, 0x3e, 0xbf, 0xff, 0xae, 0xeb, 
-0xfa, 0xbb, 0xeb, 0xba, 0xae, 0xfb, 0xde, 0x7e, 0x5f, 0xff, 0xae, 0xeb, 0xfa, 
-0xbb, 0xeb, 0xba, 0xb5, 0x7b, 0xdf, 0x7d, 0xff, 0xff, 0xb0, 0x0b, 0xc7, 0xbb, 
-0xeb, 0xba, 0x7b, 0xb8, 0xdf, 0x7b, 0xff, 0xff, 0xbe, 0x6a, 0xbf, 0xbb, 0x5b, 
-0xab, 0xf5, 0xdb, 0xdf, 0x77, 0xff, 0xff, 0xbe, 0xed, 0xbe, 0xbb, 0x5b, 0x93, 
-0xee, 0xeb, 0xdf, 0x6f, 0xff, 0xff, 0x7e, 0xea, 0xc2, 0x7b, 0xbc, 0xbb, 0x2e, 
-0x0c, 0x9e, 0x2f, 0xff, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xfd, 0xef, 0xff, 0xef, 0x3f, 0xfe, 0xfe, 0x7e, 0xfb, 
-0xf9, 0xff, 0xff, 0xfb, 0xef, 0xff, 0xef, 0xbf, 0xff, 0xfe, 0xff, 0xfb, 0xfb, 
-0xff, 0xff, 0x3f, 0x08, 0x82, 0x20, 0x08, 0x82, 0x60, 0x7e, 0xbb, 0x3b, 0x08, 
-0x82, 0xff, 0xeb, 0xfa, 0xae, 0xbb, 0xbb, 0xee, 0x7e, 0xdb, 0xbb, 0xea, 0xba, 
-0x3f, 0xe8, 0xfa, 0x2e, 0xb8, 0x83, 0xee, 0x7e, 0xeb, 0xbb, 0xea, 0xba, 0xbf, 
-0xeb, 0xfa, 0xae, 0xbf, 0xbf, 0xee, 0x7e, 0xd3, 0xbb, 0xea, 0xba, 0x3f, 0x08, 
-0x82, 0x20, 0xb8, 0x83, 0x6e, 0x8c, 0xbb, 0xb1, 0xea, 0x82, 0xff, 0xff, 0xff, 
-0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 
-0xff, 0xff, 0xff, 0xff, 0x83, 0x3b, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 
-0xff, 0xff, 0xff, 0xbb, 0xbb, 0xdb, 0xfe, 0x20, 0x08, 0x82, 0xa0, 0xeb, 0xaa, 
-0xae, 0x0b, 0xba, 0xbb, 0x2b, 0xff, 0xae, 0xeb, 0xfb, 0xbb, 0xeb, 0xaa, 0xb5, 
-0x7b, 0xbb, 0xbb, 0xfa, 0xff, 0xae, 0xeb, 0x83, 0xbb, 0x6b, 0xab, 0x3b, 0xb8, 
-0xbb, 0xbb, 0xfb, 0xff, 0x20, 0xe8, 0xbf, 0xbb, 0xab, 0xab, 0xf5, 0xdb, 0xbb, 
-0xbb, 0xfb, 0xff, 0xfe, 0xeb, 0x83, 0x23, 0xc8, 0x83, 0x2e, 0x08, 0x82, 0x3b, 
-0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
-0xff  }};
-
-#endif
--- a/inprint2.c
+++ /dev/null
@@ -1,149 +1,0 @@
-// Bitmap font routine originally by driedfruit,
-// https://github.com/driedfruit/SDL_inprint Released into public domain.
-// Modified to support multiple fonts & adding a background to text.
-
-#include <SDL.h>
-
-#define CHARACTERS_PER_ROW 16   /* I like 16 x 8 fontsets. */
-#define CHARACTERS_PER_COLUMN 8 /* 128 x 1 is another popular format. */
-
-static SDL_Renderer *selected_renderer = NULL;
-static SDL_Texture *inline_font = NULL;
-static SDL_Texture *selected_font = NULL;
-static Uint16 selected_font_w, selected_font_h;
-
-void prepare_inline_font(unsigned char * bits, int font_width,
-                         int font_height) {
-  Uint32 *pix_ptr, tmp;
-  int i, len, j;
-  SDL_Surface *surface;
-  Uint32 colors[2];
-
-  selected_font_w = font_width;
-  selected_font_h = font_height;
-
-  if (inline_font != NULL) {
-    selected_font = inline_font;
-    return;
-  }
-
-  surface = SDL_CreateRGBSurface(0, font_width, font_height, 32,
-#if SDL_BYTEORDER == SDL_BIG_ENDIAN
-                                 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff
-#else
-                                 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000
-#endif
-  );
-  colors[0] = SDL_MapRGBA(surface->format, 0xFF, 0xFF, 0xFF, 0xFF);
-  colors[1] = SDL_MapRGBA(surface->format, 0x00, 0x00, 0x00,
-                          0x00 /* or 0xFF, to have bg-color */);
-
-  /* Get pointer to pixels and array length */
-  pix_ptr = (Uint32 *)surface->pixels;
-  len = surface->h * surface->w / 8;
-
-  /* Copy */
-  for (i = 0; i < len; i++) {
-    tmp = (Uint8)bits[i];
-    for (j = 0; j < 8; j++) {
-      Uint8 mask = (0x01 << j);
-      pix_ptr[i * 8 + j] = colors[(tmp & mask) >> j];
-    }
-  }
-
-  inline_font = SDL_CreateTextureFromSurface(selected_renderer, surface);
-  SDL_FreeSurface(surface);
-
-  selected_font = inline_font;
-}
-void kill_inline_font(void) {
-  SDL_DestroyTexture(inline_font);
-  inline_font = NULL;
-}
-void inrenderer(SDL_Renderer *renderer) { selected_renderer = renderer; }
-void infont(SDL_Texture *font) {
-  Uint32 format;
-  int access;
-  int w, h;
-
-  if (font == NULL) {
-    // prepare_inline_font();
-    return;
-  }
-
-  SDL_QueryTexture(font, &format, &access, &w, &h);
-
-  selected_font = font;
-  selected_font_w = w;
-  selected_font_h = h;
-}
-void incolor1(SDL_Color *color) {
-  SDL_SetTextureColorMod(selected_font, color->r, color->g, color->b);
-}
-void incolor(Uint32 fore,
-             Uint32 unused) /* Color must be in 0x00RRGGBB format ! */
-{
-  SDL_Color pal[1];
-  pal[0].r = (Uint8)((fore & 0x00FF0000) >> 16);
-  pal[0].g = (Uint8)((fore & 0x0000FF00) >> 8);
-  pal[0].b = (Uint8)((fore & 0x000000FF));
-  SDL_SetTextureColorMod(selected_font, pal[0].r, pal[0].g, pal[0].b);
-}
-void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y,
-             Uint32 fgcolor, Uint32 bgcolor) {
-  SDL_Rect s_rect;
-  SDL_Rect d_rect;
-  SDL_Rect bg_rect;
-
-  static uint32_t previous_fgcolor;
-
-  d_rect.x = x;
-  d_rect.y = y;
-  s_rect.w = selected_font_w / CHARACTERS_PER_ROW;
-  s_rect.h = selected_font_h / CHARACTERS_PER_COLUMN;
-  d_rect.w = s_rect.w;
-  d_rect.h = s_rect.h;
-
-  if (dst == NULL)
-    dst = selected_renderer;
-
-  for (; *str; str++) {
-    int id = (int)*str;
-#if (CHARACTERS_PER_COLUMN != 1)
-    int row = id / CHARACTERS_PER_ROW;
-    int col = id % CHARACTERS_PER_ROW;
-    s_rect.x = col * s_rect.w;
-    s_rect.y = row * s_rect.h;
-#else
-    s_rect.x = id * s_rect.w;
-    s_rect.y = 0;
-#endif
-    if (id == '\n') {
-      d_rect.x = x;
-      d_rect.y += s_rect.h;
-      continue;
-    }
-    if (fgcolor != previous_fgcolor) {
-      incolor(fgcolor, 0);
-      previous_fgcolor = fgcolor;
-    }
-
-    if (bgcolor != -1) {
-      SDL_SetRenderDrawColor(selected_renderer,
-                             (Uint8)((bgcolor & 0x00FF0000) >> 16),
-                             (Uint8)((bgcolor & 0x0000FF00) >> 8),
-                             (Uint8)((bgcolor & 0x000000FF)), 0xFF);
-      bg_rect = d_rect;
-      bg_rect.w = selected_font_w / CHARACTERS_PER_ROW - 1;
-      // Silly hack to get big font background aligned correctly.
-      if (bg_rect.h == 11) {
-        bg_rect.y++;
-      }
-
-      SDL_RenderFillRect(dst, &bg_rect);
-    }
-    SDL_RenderCopy(dst, selected_font, &s_rect, &d_rect);
-    d_rect.x += s_rect.w;
-  }
-}
-SDL_Texture *get_inline_font(void) { return selected_font; }
--- a/input.c
+++ /dev/null
@@ -1,481 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#include <SDL.h>
-#include <stdio.h>
-
-#include "SDL_timer.h"
-#include "config.h"
-#include "input.h"
-#include "render.h"
-
-#define MAX_CONTROLLERS 4
-
-SDL_GameController *game_controllers[MAX_CONTROLLERS];
-
-// Bits for M8 input messages
-enum keycodes {
-  key_left = 1 << 7,
-  key_up = 1 << 6,
-  key_down = 1 << 5,
-  key_select = 1 << 4,
-  key_start = 1 << 3,
-  key_right = 1 << 2,
-  key_opt = 1 << 1,
-  key_edit = 1
-};
-
-uint8_t keyjazz_enabled = 0;
-uint8_t keyjazz_base_octave = 2;
-uint8_t keyjazz_velocity = 0x64;
-
-static uint8_t keycode = 0; // value of the pressed key
-static int num_joysticks = 0;
-
-input_msg_s key = {normal, 0};
-
-uint8_t toggle_input_keyjazz() {
-  keyjazz_enabled = !keyjazz_enabled;
-  return keyjazz_enabled;
-}
-
-// Opens available game controllers and returns the amount of opened controllers
-int initialize_game_controllers() {
-
-  num_joysticks = SDL_NumJoysticks();
-  int controller_index = 0;
-
-  SDL_Log("Looking for game controllers\n");
-  SDL_Delay(
-      10); // Some controllers like XBone wired need a little while to get ready
-
-  // Try to load the game controller database file
-  char db_filename[1024] = {0};
-  snprintf(db_filename, sizeof(db_filename), "%sgamecontrollerdb.txt",
-           SDL_GetPrefPath("", "m8c"));
-  SDL_Log("Trying to open game controller database from %s", db_filename);
-  SDL_RWops* db_rw = SDL_RWFromFile(db_filename, "rb");
-  if (db_rw == NULL) {
-    snprintf(db_filename, sizeof(db_filename), "%sgamecontrollerdb.txt",
-    SDL_GetBasePath());
-    SDL_Log("Trying to open game controller database from %s", db_filename);
-    db_rw = SDL_RWFromFile(db_filename, "rb");
-  }
-
-  if (db_rw != NULL) {
-    int mappings = SDL_GameControllerAddMappingsFromRW(db_rw, 1);
-    if (mappings != -1)
-      SDL_Log("Found %d game controller mappings", mappings);
-    else
-      SDL_LogError(SDL_LOG_CATEGORY_INPUT,
-                   "Error loading game controller mappings.");
-  } else {
-    SDL_LogError(SDL_LOG_CATEGORY_INPUT,
-                 "Unable to open game controller database file.");
-  }
-
-  // Open all available game controllers
-  for (int i = 0; i < num_joysticks; i++) {
-    if (!SDL_IsGameController(i))
-      continue;
-    if (controller_index >= MAX_CONTROLLERS)
-      break;
-    game_controllers[controller_index] = SDL_GameControllerOpen(i);
-    SDL_Log("Controller %d: %s", controller_index + 1,
-            SDL_GameControllerName(game_controllers[controller_index]));
-    controller_index++;
-  }
-
-  return controller_index;
-}
-
-// Closes all open game controllers
-void close_game_controllers() {
-
-  for (int i = 0; i < MAX_CONTROLLERS; i++) {
-    if (game_controllers[i])
-      SDL_GameControllerClose(game_controllers[i]);
-  }
-}
-
-static input_msg_s handle_keyjazz(SDL_Event *event, uint8_t keyvalue) {
-  input_msg_s key = {keyjazz, keyvalue, keyjazz_velocity, event->type};
-  switch (event->key.keysym.scancode) {
-  case SDL_SCANCODE_Z:
-    key.value = keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_S:
-    key.value = 1 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_X:
-    key.value = 2 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_D:
-    key.value = 3 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_C:
-    key.value = 4 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_V:
-    key.value = 5 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_G:
-    key.value = 6 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_B:
-    key.value = 7 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_H:
-    key.value = 8 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_N:
-    key.value = 9 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_J:
-    key.value = 10 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_M:
-    key.value = 11 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_Q:
-    key.value = 12 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_2:
-    key.value = 13 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_W:
-    key.value = 14 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_3:
-    key.value = 15 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_E:
-    key.value = 16 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_R:
-    key.value = 17 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_5:
-    key.value = 18 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_T:
-    key.value = 19 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_6:
-    key.value = 20 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_Y:
-    key.value = 21 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_7:
-    key.value = 22 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_U:
-    key.value = 23 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_I:
-    key.value = 24 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_9:
-    key.value = 25 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_O:
-    key.value = 26 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_0:
-    key.value = 27 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_P:
-    key.value = 28 + keyjazz_base_octave * 12;
-    break;
-  case SDL_SCANCODE_KP_DIVIDE:
-    key.type = normal;
-    if (event->type == SDL_KEYDOWN && keyjazz_base_octave > 0) {
-      keyjazz_base_octave--;
-      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
-    }
-    break;
-  case SDL_SCANCODE_KP_MULTIPLY:
-    key.type = normal;
-    if (event->type == SDL_KEYDOWN && keyjazz_base_octave < 8) {
-      keyjazz_base_octave++;
-      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
-    }
-    break;
-  case SDL_SCANCODE_KP_MINUS:
-    key.type = normal;
-    if (event->type == SDL_KEYDOWN) {
-      if ((event->key.keysym.mod & KMOD_ALT) > 0) {
-        if (keyjazz_velocity > 1)
-          keyjazz_velocity -= 1;
-      } else {
-        if (keyjazz_velocity > 0x10)
-          keyjazz_velocity -= 0x10;
-      }
-      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
-    }
-    break;
-  case SDL_SCANCODE_KP_PLUS:
-    key.type = normal;
-    if (event->type == SDL_KEYDOWN) {
-      if ((event->key.keysym.mod & KMOD_ALT) > 0) {
-        if (keyjazz_velocity < 0x7F)
-          keyjazz_velocity += 1;
-      } else {
-        if (keyjazz_velocity < 0x6F)
-          keyjazz_velocity += 0x10;
-      }
-      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
-    }
-    break;
-  default:
-    key.type = normal;
-    break;
-  }
-
-  return key;
-}
-
-static input_msg_s handle_normal_keys(SDL_Event *event, config_params_s *conf,
-                                      uint8_t keyvalue) {
-  input_msg_s key = {normal, keyvalue};
-
-  if (event->key.keysym.scancode == conf->key_up) {
-    key.value = key_up;
-  } else if (event->key.keysym.scancode == conf->key_left) {
-    key.value = key_left;
-  } else if (event->key.keysym.scancode == conf->key_down) {
-    key.value = key_down;
-  } else if (event->key.keysym.scancode == conf->key_right) {
-    key.value = key_right;
-  } else if (event->key.keysym.scancode == conf->key_select ||
-             event->key.keysym.scancode == conf->key_select_alt) {
-    key.value = key_select;
-  } else if (event->key.keysym.scancode == conf->key_start ||
-             event->key.keysym.scancode == conf->key_start_alt) {
-    key.value = key_start;
-  } else if (event->key.keysym.scancode == conf->key_opt ||
-             event->key.keysym.scancode == conf->key_opt_alt) {
-    key.value = key_opt;
-  } else if (event->key.keysym.scancode == conf->key_edit ||
-             event->key.keysym.scancode == conf->key_edit_alt) {
-    key.value = key_edit;
-  } else if (event->key.keysym.scancode == conf->key_delete) {
-    key.value = key_opt | key_edit;
-  } else if (event->key.keysym.scancode == conf->key_reset) {
-    key = (input_msg_s){special, msg_reset_display};
-  } else {
-    key.value = 0;
-  }
-  return key;
-}
-
-// Check whether a button is pressed on a gamepad and return 1 if pressed.
-static int get_game_controller_button(config_params_s *conf,
-                                      SDL_GameController *controller,
-                                      int button) {
-
-  const int button_mappings[8] = {conf->gamepad_up,     conf->gamepad_down,
-                                  conf->gamepad_left,   conf->gamepad_right,
-                                  conf->gamepad_opt,    conf->gamepad_edit,
-                                  conf->gamepad_select, conf->gamepad_start};
-
-  // Check digital buttons
-  if (SDL_GameControllerGetButton(controller, button_mappings[button])) {
-    return 1;
-  } else {
-    // If digital button isn't pressed, check the corresponding analog control
-    switch (button) {
-    case INPUT_UP:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_updown) <
-             -conf->gamepad_analog_threshold;
-    case INPUT_DOWN:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_updown) >
-             conf->gamepad_analog_threshold;
-    case INPUT_LEFT:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_leftright) <
-             -conf->gamepad_analog_threshold;
-    case INPUT_RIGHT:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_leftright) >
-             conf->gamepad_analog_threshold;
-    case INPUT_OPT:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_opt) >
-             conf->gamepad_analog_threshold;
-    case INPUT_EDIT:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_edit) >
-             conf->gamepad_analog_threshold;
-    case INPUT_SELECT:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_select) >
-             conf->gamepad_analog_threshold;
-    case INPUT_START:
-      return SDL_GameControllerGetAxis(controller,
-                                       conf->gamepad_analog_axis_start) >
-             conf->gamepad_analog_threshold;
-    default:
-      return 0;
-    }
-  }
-  return 0;
-}
-
-// Handle game controllers, simply check all buttons and analog axis on every
-// cycle
-static int handle_game_controller_buttons(config_params_s *conf) {
-
-  const int keycodes[8] = {key_up,  key_down, key_left,   key_right,
-                           key_opt, key_edit, key_select, key_start};
-
-  int key = 0;
-
-  // Cycle through every active game controller
-  for (int gc = 0; gc < num_joysticks; gc++) {
-    // Cycle through all M8 buttons
-    for (int button = 0; button < (input_buttons_t)INPUT_MAX; button++) {
-      // If the button is active, add the keycode to the variable containing
-      // active keys
-      if (get_game_controller_button(conf, game_controllers[gc], button)) {
-        key |= keycodes[button];
-      }
-    }
-  }
-
-  return key;
-}
-
-// Handles SDL input events
-void handle_sdl_events(config_params_s *conf) {
-
-  static int prev_key_analog = 0;
-
-  SDL_Event event;
-
-  // Read joysticks
-  int key_analog = handle_game_controller_buttons(conf);
-  if (prev_key_analog != key_analog) {
-    keycode = key_analog;
-    prev_key_analog = key_analog;
-  }
-
-  // Read special case game controller buttons quit and reset
-  for (int gc = 0; gc < num_joysticks; gc++) {
-    if (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_quit) &&
-        (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_select) ||
-        SDL_GameControllerGetAxis(game_controllers[gc], conf->gamepad_analog_axis_select)))
-      key = (input_msg_s){special, msg_quit};
-    else if (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_reset) &&
-            (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_select) ||
-              SDL_GameControllerGetAxis(game_controllers[gc], conf->gamepad_analog_axis_select)))
-      key = (input_msg_s){special, msg_reset_display};
-  }
-
-  SDL_PollEvent(&event);
-
-  switch (event.type) {
-
-  // Reinitialize game controllers on controller add/remove/remap
-  case SDL_CONTROLLERDEVICEADDED:
-  case SDL_CONTROLLERDEVICEREMOVED:
-    initialize_game_controllers();
-    break;
-
-  // Handle SDL quit events (for example, window close)
-  case SDL_QUIT:
-    key = (input_msg_s){special, msg_quit};
-    break;
-
-  case SDL_WINDOWEVENT:
-    if (event.window.event == SDL_WINDOWEVENT_RESIZED)
-    {
-      static uint32_t ticks_window_resized = 0;
-      if (SDL_GetTicks() - ticks_window_resized > 500) {
-        SDL_Log("Resizing window...");
-        key = (input_msg_s){special, msg_reset_display};
-        ticks_window_resized = SDL_GetTicks();
-      }
-    }
-    break;
-
-  // Keyboard events. Special events are handled within SDL_KEYDOWN.
-  case SDL_KEYDOWN:
-
-    // ALT+ENTER toggles fullscreen
-    if (event.key.keysym.sym == SDLK_RETURN &&
-        (event.key.keysym.mod & KMOD_ALT) > 0) {
-      toggle_fullscreen();
-      break;
-    }
-
-    // ALT+F4 quits program
-    if (event.key.keysym.sym == SDLK_F4 &&
-        (event.key.keysym.mod & KMOD_ALT) > 0) {
-      key = (input_msg_s){special, msg_quit};
-      break;
-    }
-
-    // ESC = toggle keyjazz
-    if (event.key.keysym.sym == SDLK_ESCAPE) {
-      display_keyjazz_overlay(toggle_input_keyjazz(), keyjazz_base_octave, keyjazz_velocity);
-    }
-
-  // Normal keyboard inputs
-  case SDL_KEYUP:
-    key = handle_normal_keys(&event, conf, 0);
-
-    if (keyjazz_enabled)
-      key = handle_keyjazz(&event, key.value);
-    break;
-
-  default:
-    break;
-  }
-
-  switch (key.type) {
-  case normal:
-    if (event.type == SDL_KEYDOWN) {
-      keycode |= key.value;
-    } else {
-      keycode &= ~key.value;
-    }
-    break;
-  case keyjazz:
-    // Do not allow pressing multiple keys with keyjazz
-  case special:
-    if (event.type == SDL_KEYDOWN) {
-      keycode = key.value;
-    } else {
-      keycode = 0;
-    }
-    break;
-  default:
-    break;
-  }
-}
-
-// Returns the currently pressed keys to main
-input_msg_s get_input_msg(config_params_s *conf) {
-
-  key = (input_msg_s){normal, 0};
-
-  // Query for SDL events
-  handle_sdl_events(conf);
-
-  if (keycode == (key_start | key_select | key_opt | key_edit)) {
-    key = (input_msg_s){special, msg_reset_display};
-  }
-
-  if (key.type == normal) {
-    /* Normal input keys go through some event-based manipulation in
-       handle_sdl_events(), the value is stored in keycode variable */
-    return (input_msg_s){key.type, keycode};
-  } else {
-    // Special event keys already have the correct keycode baked in
-    return key;
-  }
-}
--- a/input.h
+++ /dev/null
@@ -1,44 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#ifndef INPUT_H_
-#define INPUT_H_
-
-#include <stdint.h>
-#include "config.h"
-
-typedef enum input_buttons_t {
-    INPUT_UP,
-    INPUT_DOWN,
-    INPUT_LEFT,
-    INPUT_RIGHT,
-    INPUT_OPT,
-    INPUT_EDIT,
-    INPUT_SELECT,
-    INPUT_START,
-    INPUT_MAX
-} input_buttons_t;
-
-typedef enum input_type_t {
-  normal,
-  keyjazz,
-  special
-} input_type_t;
-
-typedef enum special_messages_t {
-  msg_quit = 1,
-  msg_reset_display = 2
-} special_messages_t;
-
-typedef struct input_msg_s {
-  input_type_t type;
-  uint8_t value;
-  uint8_t value2;
-  uint32_t eventType;
-} input_msg_s;
-
-int initialize_game_controllers();
-void close_game_controllers();
-input_msg_s get_input_msg(config_params_s *conf);
-
-#endif
--- a/macos/m8c.app/Contents/Info.plist
+++ /dev/null
@@ -1,54 +1,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>BuildMachineOSBuild</key>
-	<string>17G11023</string>
-	<key>CFBundleDevelopmentRegion</key>
-	<string>en</string>
-	<key>CFBundleIconFile</key>
-	<string>m8c</string>
-	<key>CFBundleExecutable</key>
-	<string>m8c</string>
-	<key>CFBundleIdentifier</key>
-	<string>com.laamaa.m8c</string>
-	<key>CFBundleInfoDictionaryVersion</key>
-	<string>6.0</string>
-	<key>CFBundleName</key>
-	<string>m8c</string>
-	<key>CFBundlePackageType</key>
-	<string>APPL</string>
-	<key>CFBundleShortVersionString</key>
-	<string>1.0</string>
-	<key>CFBundleSupportedPlatforms</key>
-	<array>
-		<string>MacOSX</string>
-	</array>
-	<key>CFBundleVersion</key>
-	<string>1</string>
-	<key>DTCompiler</key>
-	<string>com.apple.compilers.llvm.clang.1_0</string>
-	<key>DTPlatformBuild</key>
-	<string>10B61</string>
-	<key>DTPlatformVersion</key>
-	<string>GM</string>
-	<key>DTSDKBuild</key>
-	<string>18B71</string>
-	<key>DTSDKName</key>
-	<string>macosx10.14</string>
-	<key>DTXcode</key>
-	<string>1010</string>
-	<key>DTXcodeBuild</key>
-	<string>10B61</string>
-	<key>LSApplicationCategoryType</key>
-	<string>public.app-category.utilities</string>
-	<key>LSMinimumSystemVersion</key>
-	<string>10.13</string>
-	<key>NSHumanReadableCopyright</key>
-	<string>Copyright © 2021 laamaa. All rights reserved.</string>
-	<key>NSMainNibFile</key>
-	<string>MainMenu</string>
-	<key>NSPrincipalClass</key>
-	<string>NSApplication</string>
-</dict>
-</plist>
--- a/macos/m8c.app/Contents/PkgInfo
+++ /dev/null
@@ -1,1 +1,0 @@
-APPL????
binary files a/macos/m8c.app/Contents/Resources/m8c.icns /dev/null differ
--- a/main.c
+++ /dev/null
@@ -1,285 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-/* Uncomment this line to enable debug messages or call make with `make
-   CFLAGS=-DDEBUG_MSG` */
-// #define DEBUG_MSG
-
-#include <SDL.h>
-#include <signal.h>
-
-#include "SDL2_inprint.h"
-#include "audio.h"
-#include "command.h"
-#include "config.h"
-#include "input.h"
-#include "render.h"
-#include "serial.h"
-#include "slip.h"
-
-enum state { QUIT, WAIT_FOR_DEVICE, RUN };
-
-enum state run = WAIT_FOR_DEVICE;
-uint8_t need_display_reset = 0;
-
-// Handles CTRL+C / SIGINT
-void intHandler(int dummy) { run = QUIT; }
-
-void close_serial_port() { disconnect(); }
-
-int main(int argc, char *argv[]) {
-  // Initialize the config to defaults read in the params from the
-  // configfile if present
-  config_params_s conf = init_config();
-
-  // TODO: take cli parameter to override default configfile location
-  read_config(&conf);
-
-  // allocate memory for serial buffer
-  uint8_t *serial_buf = SDL_malloc(serial_read_size);
-
-  static uint8_t slip_buffer[serial_read_size]; // SLIP command buffer
-
-  SDL_zero(slip_buffer);
-
-  // settings for the slip packet handler
-  static const slip_descriptor_s slip_descriptor = {
-      .buf = slip_buffer,
-      .buf_size = sizeof(slip_buffer),
-      .recv_message = process_command, // the function where complete slip
-                                       // packets are processed further
-  };
-
-  static slip_handler_s slip;
-
-  uint8_t prev_input = 0;
-  uint8_t prev_note = 0;
-  uint16_t zerobyte_packets = 0; // used to detect device disconnection
-
-  signal(SIGINT, intHandler);
-  signal(SIGTERM, intHandler);
-#ifdef SIGQUIT
-  signal(SIGQUIT, intHandler);
-#endif
-  slip_init(&slip, &slip_descriptor);
-
-  // First device detection to avoid SDL init if it isn't necessary. To be run
-  // only if we shouldn't wait for M8 to be connected.
-  if (conf.wait_for_device == 0) {
-    if (init_serial(1) == 0) {
-      SDL_free(serial_buf);
-      return -1;
-    }
-  }
-
-  // initialize all SDL systems
-  if (initialize_sdl(conf.init_fullscreen, conf.init_use_gpu) == -1)
-    run = QUIT;
-
-  // initial scan for (existing) game controllers
-  initialize_game_controllers();
-
-#ifdef DEBUG_MSG
-  SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
-#endif
-
-  // main loop begin
-  do {
-    // try to init serial port
-    int port_inited = init_serial(1);
-    // if port init was successful, try to enable and reset display
-    if (port_inited == 1 && enable_and_reset_display(0) == 1) {
-      // if audio routing is enabled, try to initialize audio devices
-      if (conf.audio_enabled == 1) {
-        audio_init(conf.audio_buffer_size, conf.audio_device_name);
-        // if audio is enabled, reset the display for second time to avoid glitches
-        reset_display();
-      }
-      run = RUN;
-    } else {
-      SDL_LogCritical(SDL_LOG_CATEGORY_ERROR,
-                      "Device not detected on begin loop.");
-      if (conf.wait_for_device == 1) {
-        run = WAIT_FOR_DEVICE;
-      } else {
-        run = QUIT;
-      }
-    }
-
-    // wait until device is connected
-    if (conf.wait_for_device == 1) {
-      static uint32_t ticks_poll_device = 0;
-      static uint32_t ticks_update_screen = 0;
-
-      if (port_inited == 0) {
-        screensaver_init();
-      }
-
-      while (run == WAIT_FOR_DEVICE) {
-        // get current input
-        input_msg_s input = get_input_msg(&conf);
-        if (input.type == special && input.value == msg_quit) {
-          SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Input message QUIT.");
-          run = QUIT;
-        }
-
-        if (SDL_GetTicks() - ticks_update_screen > 16) {
-          ticks_update_screen = SDL_GetTicks();
-          screensaver_draw();
-          render_screen();
-        }
-
-        // Poll for M8 device every second
-        if (port_inited == 0 && (SDL_GetTicks() - ticks_poll_device > 1000)) {
-          ticks_poll_device = SDL_GetTicks();
-          if (run == WAIT_FOR_DEVICE && init_serial(0) == 1) {
-
-            if (conf.audio_enabled == 1) {
-              if (audio_init(conf.audio_buffer_size, conf.audio_device_name) ==
-                  0) {
-                SDL_Log("Cannot initialize audio");
-                conf.audio_enabled = 0;
-              }
-            }
-
-            int result = enable_and_reset_display();
-            // Device was found; enable display and proceed to the main loop
-            if (result == 1) {
-              run = RUN;
-              port_inited = 1;
-              screensaver_destroy();
-            } else {
-              SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Device not detected.");
-              run = QUIT;
-              screensaver_destroy();
-            }
-          }
-        }
-        SDL_Delay(conf.idle_ms);
-      }
-    } else {
-      // classic startup behaviour, exit if device is not found
-      if (port_inited == 0) {
-        if (conf.audio_enabled == 1) {
-          audio_destroy();
-        }
-        close_game_controllers();
-        close_renderer();
-        kill_inline_font();
-        SDL_free(serial_buf);
-        SDL_Quit();
-        return -1;
-      }
-    }
-
-    // main loop
-    while (run == RUN) {
-
-      // get current inputs
-      input_msg_s input = get_input_msg(&conf);
-
-      switch (input.type) {
-      case normal:
-        if (input.value != prev_input) {
-          prev_input = input.value;
-          send_msg_controller(input.value);
-        }
-        break;
-      case keyjazz:
-        if (input.value != 0) {
-          if (input.eventType == SDL_KEYDOWN && input.value != prev_input) {
-            send_msg_keyjazz(input.value, input.value2);
-            prev_note = input.value;
-          } else if (input.eventType == SDL_KEYUP && input.value == prev_note) {
-            send_msg_keyjazz(0xFF, 0);
-          }
-        }
-        prev_input = input.value;
-        break;
-      case special:
-        if (input.value != prev_input) {
-          prev_input = input.value;
-          switch (input.value) {
-          case msg_quit:
-            SDL_Log("Received msg_quit from input device.");
-            run = 0;
-            break;
-          case msg_reset_display:
-            reset_display();
-            break;
-          default:
-            break;
-          }
-          break;
-        }
-      }
-
-      while (1) {
-        // read serial port
-        int bytes_read = serial_read(serial_buf, serial_read_size);
-        if (bytes_read < 0) {
-          SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Error %d reading serial. \n",
-                          (int)bytes_read);
-          run = QUIT;
-          break;
-        } else if (bytes_read > 0) {
-          // input from device: reset the zero byte counter and create a
-          // pointer to the serial buffer
-          zerobyte_packets = 0;
-          uint8_t *cur = serial_buf;
-          const uint8_t *end = serial_buf + bytes_read;
-          while (cur < end) {
-            // process the incoming bytes into commands and draw them
-            int n = slip_read_byte(&slip, *(cur++));
-            if (n != SLIP_NO_ERROR) {
-              if (n == SLIP_ERROR_INVALID_PACKET) {
-                reset_display();
-              } else {
-                SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SLIP error %d\n", n);
-              }
-            }
-          }
-        } else {
-          // zero byte packet, increment counter
-          zerobyte_packets++;
-          if (zerobyte_packets > conf.wait_packets) {
-            zerobyte_packets = 0;
-
-            // try opening the serial port to check if it's alive
-            if (check_serial_port()) {
-              // the device is still there, carry on
-              break;
-            } else {
-              port_inited = 0;
-              run = WAIT_FOR_DEVICE;
-              close_serial_port();
-              if (conf.audio_enabled == 1) {
-                audio_destroy();
-              }
-              /* we'll make one more loop to see if the device is still there
-               * but just sending zero bytes. if it doesn't get detected when
-               * resetting the port, it will disconnect */
-            }
-          }
-          break;
-        }
-      }
-      render_screen();
-      SDL_Delay(conf.idle_ms);
-    }
-  } while (run > QUIT);
-  // main loop end
-
-  // exit, clean up
-  SDL_Log("Shutting down\n");
-  if (conf.audio_enabled == 1) {
-    audio_destroy();
-  }
-  close_game_controllers();
-  close_renderer();
-  close_serial_port();
-  SDL_free(serial_buf);
-  kill_inline_font();
-  SDL_Quit();
-  return 0;
-}
--- /dev/null
+++ b/package/macos/m8c.app/Contents/Info.plist
@@ -1,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>BuildMachineOSBuild</key>
+	<string>17G11023</string>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleIconFile</key>
+	<string>m8c</string>
+	<key>CFBundleExecutable</key>
+	<string>m8c</string>
+	<key>CFBundleIdentifier</key>
+	<string>com.laamaa.m8c</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>m8c</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>MacOSX</string>
+	</array>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>DTCompiler</key>
+	<string>com.apple.compilers.llvm.clang.1_0</string>
+	<key>DTPlatformBuild</key>
+	<string>10B61</string>
+	<key>DTPlatformVersion</key>
+	<string>GM</string>
+	<key>DTSDKBuild</key>
+	<string>18B71</string>
+	<key>DTSDKName</key>
+	<string>macosx10.14</string>
+	<key>DTXcode</key>
+	<string>1010</string>
+	<key>DTXcodeBuild</key>
+	<string>10B61</string>
+	<key>LSApplicationCategoryType</key>
+	<string>public.app-category.utilities</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>10.13</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>Copyright © 2021 laamaa. All rights reserved.</string>
+	<key>NSMainNibFile</key>
+	<string>MainMenu</string>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+</dict>
+</plist>
--- /dev/null
+++ b/package/macos/m8c.app/Contents/PkgInfo
@@ -1,0 +1,1 @@
+APPL????
binary files /dev/null b/package/macos/m8c.app/Contents/Resources/m8c.icns differ
--- /dev/null
+++ b/package/rpm/m8c.spec
@@ -1,0 +1,39 @@
+Name:           m8c
+Version:        1.5.4
+Release:        1%{?dist}
+Summary:        m8c is a client for Dirtywave M8 music tracker's headless mode
+
+License:        MIT
+URL:            https://github.com/laamaa/%{name}
+Source0:        https://github.com/laamaa/m8c/archive/refs/tags/v%{version}.tar.gz
+
+BuildRequires:  gcc
+BuildRequires:  make
+BuildRequires:  SDL2-devel
+BuildRequires:  libserialport-devel
+Requires:       SDL2
+Requires:       libserialport
+
+%description
+m8c is a client for Dirtywave M8 music tracker's headless mode
+
+%prep
+%autosetup
+
+
+%build
+make %{?_smp_mflags}
+
+%install
+PREFIX=/usr %make_install
+
+
+%files
+%license LICENSE
+%{_bindir}/%{name}
+
+
+
+%changelog
+* Tue Aug 29 2023 Jonne Kokkonen <jonne.kokkonen@ambientia.fi>
+- First m8c package
--- a/render.c
+++ /dev/null
@@ -1,311 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#include "render.h"
-
-#include <SDL.h>
-#include <stdio.h>
-
-#include "SDL2_inprint.h"
-#include "command.h"
-#include "fx_cube.h"
-
-#include "inline_font.h"
-#include "inline_font_large.h"
-#include "inline_font_small.h"
-
-SDL_Window *win;
-SDL_Renderer *rend;
-SDL_Texture *maintexture;
-SDL_Color background_color =
-    (SDL_Color){.r = 0x00, .g = 0x00, .b = 0x00, .a = 0x00};
-
-static uint32_t ticks_fps;
-static int fps;
-static int large_font_enabled = 0;
-static int screen_offset_y = 0;
-
-uint8_t fullscreen = 0;
-
-static uint8_t dirty = 0;
-
-// Initializes SDL and creates a renderer and required surfaces
-int initialize_sdl(int init_fullscreen, int init_use_gpu) {
-  // ticks = SDL_GetTicks();
-
-  const int window_width = 640;  // SDL window width
-  const int window_height = 480; // SDL window height
-
-  if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
-    SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "SDL_Init: %s\n", SDL_GetError());
-    return -1;
-  }
-  // SDL documentation recommends this
-  atexit(SDL_Quit);
-
-  win = SDL_CreateWindow("m8c", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
-                         window_width, window_height,
-                         SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL |
-                             SDL_WINDOW_RESIZABLE | init_fullscreen);
-
-  rend = SDL_CreateRenderer(
-      win, -1, init_use_gpu ? SDL_RENDERER_ACCELERATED : SDL_RENDERER_SOFTWARE);
-
-  SDL_RenderSetLogicalSize(rend, 320, 240);
-
-  maintexture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888,
-                                  SDL_TEXTUREACCESS_TARGET, 320, 240);
-
-  SDL_SetRenderTarget(rend, maintexture);
-
-  SDL_SetRenderDrawColor(rend, background_color.r, background_color.g,
-                         background_color.b, background_color.a);
-
-  SDL_RenderClear(rend);
-
-  // Initialize a texture for the font and read the inline font bitmap
-  inrenderer(rend);
-  struct inline_font *font = &inline_font_small;
-  prepare_inline_font(font->bits, font->width, font->height);
-
-  SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO);
-
-  dirty = 1;
-
-  return 1;
-}
-
-static void change_font(struct inline_font *font) {
-  kill_inline_font();
-  prepare_inline_font(font->bits, font->width, font->height);
-}
-
-void set_large_mode(int enabled) {
-  if (enabled) {
-    large_font_enabled = 1;
-    screen_offset_y = 40;
-    change_font(&inline_font_large);
-  } else {
-    large_font_enabled = 0;
-    screen_offset_y = 0;
-    change_font(&inline_font_small);
-  }
-}
-
-void close_renderer() {
-  kill_inline_font();
-  SDL_DestroyTexture(maintexture);
-  SDL_DestroyRenderer(rend);
-  SDL_DestroyWindow(win);
-}
-
-void toggle_fullscreen() {
-
-  int fullscreen_state = SDL_GetWindowFlags(win) & SDL_WINDOW_FULLSCREEN;
-
-  SDL_SetWindowFullscreen(win,
-                          fullscreen_state ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP);
-  SDL_ShowCursor(fullscreen_state);
-
-  dirty = 1;
-}
-
-int draw_character(struct draw_character_command *command) {
-
-  uint32_t fgcolor = (command->foreground.r << 16) |
-                     (command->foreground.g << 8) | command->foreground.b;
-  uint32_t bgcolor = (command->background.r << 16) |
-                     (command->background.g << 8) | command->background.b;
-
-  /* Notes:
-     If large font is enabled, offset the screen elements by a fixed amount.
-     If background and foreground colors are the same, draw transparent
-     background. Due to the font bitmaps, a different pixel offset is needed for
-     both*/
-
-  inprint(rend, (char *)&command->c, command->pos.x,
-          command->pos.y + (large_font_enabled ? 2 : 3) - screen_offset_y,
-          fgcolor, (bgcolor == fgcolor) ? -1 : bgcolor);
-
-  dirty = 1;
-
-  return 1;
-}
-
-void draw_rectangle(struct draw_rectangle_command *command) {
-
-  SDL_Rect render_rect;
-
-  render_rect.x = command->pos.x;
-  if (large_font_enabled == 1) {
-    render_rect.y = command->pos.y - screen_offset_y;
-  } else {
-    render_rect.y = command->pos.y;
-  }
-  render_rect.h = command->size.height;
-  render_rect.w = command->size.width;
-
-  // Background color changed
-  if (render_rect.x == 0 && render_rect.y <= 0 && render_rect.w == 320 &&
-      render_rect.h >= 240) {
-    SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "BG color change: %d %d %d",command->color.r,command->color.g,command->color.b);
-    background_color.r = command->color.r;
-    background_color.g = command->color.g;
-    background_color.b = command->color.b;
-    background_color.a = 0xFF;
-
-#ifdef __ANDROID__
-    int bgcolor =
-        (command->color.r << 16) | (command->color.g << 8) | command->color.b;
-    SDL_AndroidSendMessage(0x8001, bgcolor);
-#endif
-  }
-
-  SDL_SetRenderDrawColor(rend, command->color.r, command->color.g,
-                         command->color.b, 0xFF);
-  SDL_RenderFillRect(rend, &render_rect);
-
-  dirty = 1;
-}
-
-void draw_waveform(struct draw_oscilloscope_waveform_command *command) {
-
-  static uint8_t wfm_cleared = 0;
-  static int prev_waveform_size = 0;
-
-  // If the waveform is not being displayed and it's already been cleared, skip
-  // rendering it
-  if (!(wfm_cleared && command->waveform_size == 0)) {
-
-    SDL_Rect wf_rect;
-    if (command->waveform_size > 0) {
-      wf_rect.x = 320 - command->waveform_size;
-      wf_rect.y = 0;
-      wf_rect.w = command->waveform_size;
-      wf_rect.h = 21;
-    } else {
-      wf_rect.x = 320 - prev_waveform_size;
-      wf_rect.y = 0;
-      wf_rect.w = prev_waveform_size;
-      wf_rect.h = 21;
-    }
-    prev_waveform_size = command->waveform_size;
-
-    SDL_SetRenderDrawColor(rend, background_color.r, background_color.g,
-                           background_color.b, background_color.a);
-    SDL_RenderFillRect(rend, &wf_rect);
-
-    SDL_SetRenderDrawColor(rend, command->color.r, command->color.g,
-                           command->color.b, 255);
-
-    // Create a SDL_Point array of the waveform pixels for batch drawing
-    SDL_Point waveform_points[command->waveform_size];
-
-    for (int i = 0; i < command->waveform_size; i++) {
-      // Limit value because the oscilloscope commands seem to glitch
-      // occasionally
-      if (command->waveform[i] > 20) {
-        command->waveform[i] = 20;
-      }
-      waveform_points[i].x = i + wf_rect.x;
-      waveform_points[i].y = command->waveform[i];
-    }
-
-    SDL_RenderDrawPoints(rend, waveform_points, command->waveform_size);
-
-    // The packet we just drew was an empty waveform
-    if (command->waveform_size == 0) {
-      wfm_cleared = 1;
-    } else {
-      wfm_cleared = 0;
-    }
-
-    dirty = 1;
-  }
-}
-
-void display_keyjazz_overlay(uint8_t show, uint8_t base_octave,
-                             uint8_t velocity) {
-
-  if (show) {
-    struct draw_rectangle_command drc;
-    drc.color = (struct color){255, 0, 0};
-    drc.pos.x = 310;
-    drc.pos.y = 230;
-    drc.size.width = 5;
-    drc.size.height = 5;
-
-    draw_rectangle(&drc);
-
-    struct draw_character_command dcc;
-    dcc.background = (struct color){background_color.r, background_color.g,
-                                    background_color.b};
-    dcc.foreground = (struct color){200, 200, 200};
-    dcc.pos.x = 296;
-    dcc.pos.y = 226;
-
-    draw_character(&dcc);
-
-    char buf[8];
-    snprintf(buf, sizeof(buf), "%02X %u", velocity, base_octave);
-
-    for (int i = 3; i >= 0; i--) {
-      dcc.c = buf[i];
-      draw_character(&dcc);
-      dcc.pos.x -= 8;
-    }
-
-  } else {
-    struct draw_rectangle_command drc;
-    drc.color = (struct color){background_color.r, background_color.g,
-                               background_color.b};
-    drc.pos.x = 272;
-    drc.pos.y = 226;
-    drc.size.width = 45;
-    drc.size.height = 14;
-
-    draw_rectangle(&drc);
-  }
-
-  dirty = 1;
-}
-
-void render_screen() {
-  if (dirty) {
-    dirty = 0;
-    // ticks = SDL_GetTicks();
-    SDL_SetRenderTarget(rend, NULL);
-
-    SDL_SetRenderDrawColor(rend, background_color.r, background_color.g,
-                           background_color.b, background_color.a);
-
-    SDL_RenderClear(rend);
-    SDL_RenderCopy(rend, maintexture, NULL, NULL);
-    SDL_RenderPresent(rend);
-    SDL_SetRenderTarget(rend, maintexture);
-
-    fps++;
-
-    if (SDL_GetTicks() - ticks_fps > 5000) {
-      ticks_fps = SDL_GetTicks();
-      SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "%.1f fps\n", (float)fps / 5);
-      fps = 0;
-    }
-  }
-}
-
-void screensaver_init() {
-  set_large_mode(1);
-  fx_cube_init(rend, (SDL_Color){255, 255, 255, 255});
-  SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Screensaver initialized");
-}
-
-void screensaver_draw() {
-  fx_cube_update();
-  dirty = 1;
-}
-
-void screensaver_destroy() {
-  fx_cube_destroy();
-  SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Screensaver destroyed");
-}
--- a/render.h
+++ /dev/null
@@ -1,26 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#ifndef RENDER_H_
-#define RENDER_H_
-
-#include "command.h"
-
-int initialize_sdl(int init_fullscreen, int init_use_gpu);
-void close_renderer();
-
-void draw_waveform(struct draw_oscilloscope_waveform_command *command);
-void draw_rectangle(struct draw_rectangle_command *command);
-int draw_character(struct draw_character_command *command);
-void set_large_mode(int enabled);
-void view_changed(int view);
-
-void render_screen();
-void toggle_fullscreen();
-void display_keyjazz_overlay(uint8_t show, uint8_t base_octave, uint8_t velocity);
-
-void screensaver_init();
-void screensaver_draw();
-void screensaver_destroy();
-
-#endif
--- a/ringbuffer.c
+++ /dev/null
@@ -1,61 +1,0 @@
-#include "ringbuffer.h"
-#include <SDL.h>
-
-RingBuffer *ring_buffer_create(uint32_t size) {
-  RingBuffer *rb = SDL_malloc(sizeof(*rb));
-  rb->buffer = SDL_malloc(sizeof(*(rb->buffer)) * size);
-  rb->head = 0;
-  rb->tail = 0;
-  rb->max_size = size;
-  rb->size = 0;
-  return rb;
-}
-
-void ring_buffer_free(RingBuffer *rb) {
-  free(rb->buffer);
-  free(rb);
-}
-
-uint32_t ring_buffer_empty(RingBuffer *rb) {
-  return (rb->size == 0);
-}
-
-uint32_t ring_buffer_full(RingBuffer *rb) {
-  return (rb->size == rb->max_size);
-}
-
-uint32_t ring_buffer_push(RingBuffer *rb, const uint8_t *data, uint32_t length) {
-  if (ring_buffer_full(rb)) {
-    return -1; // buffer full, push fails
-  } else {
-    uint32_t space1 = rb->max_size - rb->tail;
-    uint32_t n = (length <= rb->max_size - rb->size) ? length : (rb->max_size - rb->size);
-    if (n <= space1) {
-      SDL_memcpy(rb->buffer + rb->tail, data, n);
-    } else {
-      SDL_memcpy(rb->buffer + rb->tail, data, space1);
-      SDL_memcpy(rb->buffer, data + space1, n - space1);
-    }
-    rb->tail = (rb->tail + n) % rb->max_size;
-    rb->size += n;
-    return n; // push successful, returns number of bytes pushed
-  }
-}
-
-uint32_t ring_buffer_pop(RingBuffer *rb, uint8_t *data, uint32_t length) {
-  if (ring_buffer_empty(rb)) {
-    return -1; // buffer empty, pop fails
-  } else {
-    uint32_t space1 = rb->max_size - rb->head;
-    uint32_t n = (length <= rb->size) ? length : rb->size;
-    if (n <= space1) {
-      SDL_memcpy(data, rb->buffer + rb->head, n);
-    } else {
-      SDL_memcpy(data, rb->buffer + rb->head, space1);
-      SDL_memcpy(data + space1, rb->buffer, n - space1);
-    }
-    rb->head = (rb->head + n) % rb->max_size;
-    rb->size -= n;
-    return n; // pop successful, returns number of bytes popped
-  }
-}
--- a/ringbuffer.h
+++ /dev/null
@@ -1,24 +1,0 @@
-#ifndef M8C_RINGBUFFER_H
-#define M8C_RINGBUFFER_H
-
-#include <stdint.h>
-
-typedef struct {
-    uint8_t *buffer;
-    uint32_t head;
-    uint32_t tail;
-    uint32_t max_size;
-    uint32_t size;
-} RingBuffer;
-
-RingBuffer *ring_buffer_create(uint32_t size);
-
-uint32_t ring_buffer_empty(RingBuffer *rb);
-
-uint32_t ring_buffer_pop(RingBuffer *rb, uint8_t *data, uint32_t length);
-
-uint32_t ring_buffer_push(RingBuffer *rb, const uint8_t *data, uint32_t length);
-
-void ring_buffer_free(RingBuffer *rb);
-
-#endif //M8C_RINGBUFFER_H
--- a/serial.c
+++ /dev/null
@@ -1,256 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-// Contains portions of code from libserialport's examples released to the
-// public domain
-
-#ifndef USE_LIBUSB
-#include <SDL.h>
-#include <libserialport.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "serial.h"
-
-struct sp_port *m8_port = NULL;
-
-// Helper function for error handling
-static int check(enum sp_return result);
-
-static int detect_m8_serial_device(struct sp_port *m8_port) {
-  // Check the connection method - we want USB serial devices
-  enum sp_transport transport = sp_get_port_transport(m8_port);
-
-  if (transport == SP_TRANSPORT_USB) {
-    // Get the USB vendor and product IDs.
-    int usb_vid, usb_pid;
-    sp_get_port_usb_vid_pid(m8_port, &usb_vid, &usb_pid);
-
-    if (usb_vid == 0x16C0 && usb_pid == 0x048A)
-      return 1;
-  }
-
-  return 0;
-}
-
-// Checks for connected devices and whether the specified device still exists
-int check_serial_port() {
-
-  int device_found = 0;
-
-  /* A pointer to a null-terminated array of pointers to
-   * struct sp_port, which will contain the ports found.*/
-  struct sp_port **port_list;
-
-  /* Call sp_list_ports() to get the ports. The port_list
-   * pointer will be updated to refer to the array created. */
-  enum sp_return result = sp_list_ports(&port_list);
-
-  if (result != SP_OK) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!\n");
-    abort();
-  }
-
-  /* Iterate through the ports. When port_list[i] is NULL
-   * this indicates the end of the list. */
-  for (int i = 0; port_list[i] != NULL; i++) {
-    struct sp_port *port = port_list[i];
-
-    if (detect_m8_serial_device(port)) {
-      if (strcmp(sp_get_port_name(port), sp_get_port_name(m8_port)) == 0)
-        device_found = 1;
-    }
-  }
-
-  sp_free_port_list(port_list);
-  return device_found;
-}
-
-int init_serial(int verbose) {
-  if (m8_port != NULL) {
-    // Port is already initialized
-    return 1;
-  }
-  /* A pointer to a null-terminated array of pointers to
-   * struct sp_port, which will contain the ports found.*/
-  struct sp_port **port_list;
-
-  if (verbose)
-    SDL_Log("Looking for USB serial devices.\n");
-
-  /* Call sp_list_ports() to get the ports. The port_list
-   * pointer will be updated to refer to the array created. */
-  enum sp_return result = sp_list_ports(&port_list);
-
-  if (result != SP_OK) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!\n");
-    abort();
-  }
-
-  /* Iterate through the ports. When port_list[i] is NULL
-   * this indicates the end of the list. */
-  for (int i = 0; port_list[i] != NULL; i++) {
-    struct sp_port *port = port_list[i];
-
-    if (detect_m8_serial_device(port)) {
-      SDL_Log("Found M8 in %s.\n", sp_get_port_name(port));
-      sp_copy_port(port, &m8_port);
-    }
-  }
-
-  sp_free_port_list(port_list);
-
-  if (m8_port != NULL) {
-    // Open the serial port and configure it
-    SDL_Log("Opening port.\n");
-    enum sp_return result;
-
-    result = sp_open(m8_port, SP_MODE_READ_WRITE);
-    if (check(result) != SP_OK)
-      return 0;
-
-    result = sp_set_baudrate(m8_port, 115200);
-    if (check(result) != SP_OK)
-      return 0;
-
-    result = sp_set_bits(m8_port, 8);
-    if (check(result) != SP_OK)
-      return 0;
-
-    result = sp_set_parity(m8_port, SP_PARITY_NONE);
-    if (check(result) != SP_OK)
-      return 0;
-
-    result = sp_set_stopbits(m8_port, 1);
-    if (check(result) != SP_OK)
-      return 0;
-
-    result = sp_set_flowcontrol(m8_port, SP_FLOWCONTROL_NONE);
-    if (check(result) != SP_OK)
-      return 0;
-  } else {
-    if (verbose) {
-      SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Cannot find a M8.\n");
-    }
-    return 0;
-  }
-
-  return 1;
-}
-
-// Helper function for error handling.
-static int check(enum sp_return result) {
-
-  char *error_message;
-
-  switch (result) {
-  case SP_ERR_ARG:
-    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid argument.\n");
-    break;
-  case SP_ERR_FAIL:
-    error_message = sp_last_error_message();
-    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Failed: %s\n",
-                 error_message);
-    sp_free_error_message(error_message);
-    break;
-  case SP_ERR_SUPP:
-    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Not supported.\n");
-    break;
-  case SP_ERR_MEM:
-    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
-                 "Error: Couldn't allocate memory.\n");
-    break;
-  case SP_OK:
-  default:
-    break;
-  }
-  return result;
-}
-
-int reset_display() {
-  int result;
-
-  SDL_Log("Reset display\n");
-
-  char buf[1] = {'R'};
-  result = sp_blocking_write(m8_port, buf, 1, 5);
-  if (result != 1) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error resetting M8 display, code %d",
-                 result);
-    return 0;
-  }
-  return 1;
-}
-
-int enable_and_reset_display() {
-  int result;
-
-  SDL_Log("Enabling and resetting M8 display\n");
-
-  char buf[1] = {'E'};
-  result = sp_blocking_write(m8_port, buf, 1, 5);
-  if (result != 1) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error enabling M8 display, code %d",
-                 result);
-    return 0;
-  }
-
-  result = reset_display();
-
-  return result;
-}
-
-int disconnect() {
-  int result;
-
-  SDL_Log("Disconnecting M8\n");
-
-  char buf[1] = {'D'};
-  
-  result = sp_blocking_write(m8_port, buf, 1, 5);
-  if (result != 1) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending disconnect, code %d",
-                 result);
-    result = 0;
-  }
-  sp_close(m8_port);
-  sp_free_port(m8_port);
-  m8_port = NULL;
-  return result;
-}
-
-int serial_read(uint8_t *serial_buf, int count) {
-  return sp_nonblocking_read(m8_port, serial_buf, count);
-}
-
-int send_msg_controller(uint8_t input) {
-  char buf[2] = {'C', input};
-  size_t nbytes = 2;
-  int result;
-  result = sp_blocking_write(m8_port, buf, nbytes, 5);
-  if (result != nbytes) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending input, code %d",
-                 result);
-    return -1;
-  }
-  return 1;
-}
-
-int send_msg_keyjazz(uint8_t note, uint8_t velocity) {
-  if (velocity > 0x7F)
-    velocity = 0x7F;
-  char buf[3] = {'K', note, velocity};
-  size_t nbytes = 3;
-  int result;
-  result = sp_blocking_write(m8_port, buf, nbytes, 5);
-  if (result != nbytes) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending keyjazz, code %d",
-                 result);
-    return -1;
-  }
-
-  return 1;
-}
-#endif
--- a/serial.h
+++ /dev/null
@@ -1,25 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-#ifndef _SERIAL_H_
-#define _SERIAL_H_
-
-#ifdef USE_LIBUSB
-// Max packet length of the USB endpoint
-#define serial_read_size 512
-int init_serial_with_file_descriptor(int file_descriptor);
-#else
-// maximum amount of bytes to read from the serial in one read()
-#define serial_read_size 512
-#endif
-
-int init_serial(int verbose);
-int check_serial_port();
-int reset_display();
-int enable_and_reset_display();
-int disconnect();
-int serial_read(uint8_t *serial_buf, int count);
-int send_msg_controller(uint8_t input);
-int send_msg_keyjazz(uint8_t note, uint8_t velocity);
-
-#endif
--- a/slip.c
+++ /dev/null
@@ -1,116 +1,0 @@
-/*
-MIT License
-
-Copyright (c) 2018 Marcin Borowicz
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-/* This code is originally by marcinbor85, https://github.com/marcinbor85/slip
-It has been simplified a bit as CRC checking etc. is not required in this
-program. */
-
-#include "slip.h"
-#include "command.h"
-
-#include <assert.h>
-#include <stddef.h>
-
-static void reset_rx(slip_handler_s *slip) {
-  assert(slip != NULL);
-
-  slip->state = SLIP_STATE_NORMAL;
-  slip->size = 0;
-}
-
-slip_error_t slip_init(slip_handler_s *slip,
-                       const slip_descriptor_s *descriptor) {
-  assert(slip != NULL);
-  assert(descriptor != NULL);
-  assert(descriptor->buf != NULL);
-  assert(descriptor->recv_message != NULL);
-
-  slip->descriptor = descriptor;
-  reset_rx(slip);
-
-  return SLIP_NO_ERROR;
-}
-
-static slip_error_t put_byte_to_buffer(slip_handler_s *slip, uint8_t byte) {
-  slip_error_t error = SLIP_NO_ERROR;
-
-  assert(slip != NULL);
-
-  if (slip->size >= slip->descriptor->buf_size) {
-    error = SLIP_ERROR_BUFFER_OVERFLOW;
-    reset_rx(slip);
-  } else {
-    slip->descriptor->buf[slip->size++] = byte;
-    slip->state = SLIP_STATE_NORMAL;
-  }
-
-  return error;
-}
-
-slip_error_t slip_read_byte(slip_handler_s *slip, uint8_t byte) {
-  slip_error_t error = SLIP_NO_ERROR;
-
-  assert(slip != NULL);
-
-  switch (slip->state) {
-  case SLIP_STATE_NORMAL:
-    switch (byte) {
-    case SLIP_SPECIAL_BYTE_END:
-      if (!slip->descriptor->recv_message(slip->descriptor->buf, slip->size)){
-        error = SLIP_ERROR_INVALID_PACKET;
-      }
-      reset_rx(slip);
-      break;
-    case SLIP_SPECIAL_BYTE_ESC:
-      slip->state = SLIP_STATE_ESCAPED;
-      break;
-    default:
-      error = put_byte_to_buffer(slip, byte);
-      break;
-    }
-    break;
-
-  case SLIP_STATE_ESCAPED:
-    switch (byte) {
-    case SLIP_ESCAPED_BYTE_END:
-      byte = SLIP_SPECIAL_BYTE_END;
-      break;
-    case SLIP_ESCAPED_BYTE_ESC:
-      byte = SLIP_SPECIAL_BYTE_ESC;
-      break;
-    default:
-      error = SLIP_ERROR_UNKNOWN_ESCAPED_BYTE;
-      reset_rx(slip);
-      break;
-    }
-
-    if (error != SLIP_NO_ERROR)
-      break;
-
-    error = put_byte_to_buffer(slip, byte);
-    break;
-  }
-
-  return error;
-}
--- a/slip.h
+++ /dev/null
@@ -1,68 +1,0 @@
-/*
-MIT License
-
-Copyright (c) 2018 Marcin Borowicz
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-/* This code is originally by marcinbor85, https://github.com/marcinbor85/slip
-It has been simplified a bit as CRC checking etc. is not required in this
-program. */
-
-#ifndef SLIP_H_
-#define SLIP_H_
-
-#include "command.h"
-#include <stdint.h>
-
-#define SLIP_SPECIAL_BYTE_END           0xC0
-#define SLIP_SPECIAL_BYTE_ESC           0xDB
-
-#define SLIP_ESCAPED_BYTE_END           0xDC
-#define SLIP_ESCAPED_BYTE_ESC           0xDD
-
-typedef enum {
-        SLIP_STATE_NORMAL = 0x00,
-        SLIP_STATE_ESCAPED
-} slip_state_t;
-
-typedef struct {
-        uint8_t *buf;
-        uint32_t buf_size;
-        int (*recv_message)(uint8_t *data, uint32_t size);
-} slip_descriptor_s;
-
-typedef struct {
-        slip_state_t state;
-        uint32_t size;
-        const slip_descriptor_s *descriptor;
-} slip_handler_s;
-
-typedef enum {
-        SLIP_NO_ERROR = 0x00,
-        SLIP_ERROR_BUFFER_OVERFLOW,
-        SLIP_ERROR_UNKNOWN_ESCAPED_BYTE,
-        SLIP_ERROR_INVALID_PACKET
-} slip_error_t;
-
-slip_error_t slip_init(slip_handler_s *slip, const slip_descriptor_s *descriptor);
-slip_error_t slip_read_byte(slip_handler_s *slip, uint8_t byte);
-
-#endif
\ No newline at end of file
--- /dev/null
+++ b/src/SDL2_inprint.h
@@ -1,0 +1,22 @@
+// Bitmap font routine by driedfruit, https://github.com/driedfruit/SDL_inprint
+// Released into public domain.
+// Modified to support adding a background to text.
+
+#ifndef SDL2_inprint_h
+#define SDL2_inprint_h
+
+#include <SDL.h>
+
+extern void prepare_inline_font(unsigned char bits[],int font_width, int font_height);
+extern void kill_inline_font(void);
+
+extern void inrenderer(SDL_Renderer *renderer);
+extern void infont(SDL_Texture *font);
+extern void incolor1(SDL_Color *color);
+extern void incolor(Uint32 color, Uint32 unused); /* Color must be in 0x00RRGGBB format ! */
+extern void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y,
+             Uint32 fgcolor, Uint32 bgcolor);
+
+extern SDL_Texture *get_inline_font(void);
+
+#endif /* SDL2_inprint_h */
--- /dev/null
+++ b/src/audio.c
@@ -1,0 +1,90 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+#ifndef USE_LIBUSB
+#include "audio.h"
+#include <SDL.h>
+#include <stdint.h>
+
+static SDL_AudioDeviceID devid_in = 0;
+static SDL_AudioDeviceID devid_out = 0;
+
+void audio_cb_in(void *userdata, uint8_t *stream, int len) {
+  SDL_QueueAudio(devid_out, stream, len);
+}
+
+int audio_init(int audio_buffer_size, const char* output_device_name) {
+
+  int i = 0;
+  int m8_device_id = -1;
+  int devcount_in = 0; // audio input device count
+
+  // wait for system to initialize possible new audio devices
+  SDL_Delay(500);
+
+  devcount_in = SDL_GetNumAudioDevices(SDL_TRUE);
+
+  if (devcount_in < 1) {
+    SDL_Log("No audio capture devices, SDL Error: %s", SDL_GetError());
+    return 0;
+  } else {
+    for (i = 0; i < devcount_in; i++) {
+      // Check if input device exists before doing anything else
+      SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "%s", SDL_GetAudioDeviceName(i, SDL_TRUE));
+      if (SDL_strstr(SDL_GetAudioDeviceName(i, SDL_TRUE), "M8") != NULL) {
+        SDL_Log("M8 Audio Input device found: %s",
+                SDL_GetAudioDeviceName(i, SDL_TRUE));
+        m8_device_id = i;
+      }
+    }
+    if (m8_device_id == -1) {
+      // forget about it
+      SDL_Log("Cannot find M8 audio input device");
+      return 0;
+    }
+
+    SDL_AudioSpec want_in, have_in, want_out, have_out;
+
+    // Open output device first to avoid possible Directsound errors
+    SDL_zero(want_out);
+    want_out.freq = 44100;
+    want_out.format = AUDIO_S16;
+    want_out.channels = 2;
+    want_out.samples = audio_buffer_size;
+    devid_out = SDL_OpenAudioDevice(output_device_name, 0, &want_out, &have_out,
+                                    SDL_AUDIO_ALLOW_ANY_CHANGE);
+    if (devid_out == 0) {
+      SDL_Log("Failed to open output: %s", SDL_GetError());
+      return 0;
+    }
+
+    SDL_zero(want_in);
+    want_in.freq = 44100;
+    want_in.format = AUDIO_S16;
+    want_in.channels = 2;
+    want_in.samples = audio_buffer_size;
+    want_in.callback = audio_cb_in;
+    devid_in = SDL_OpenAudioDevice(
+        SDL_GetAudioDeviceName(m8_device_id, SDL_TRUE), SDL_TRUE, &want_in,
+        &have_in, SDL_AUDIO_ALLOW_ANY_CHANGE);
+    if (devid_in == 0) {
+      SDL_Log("Failed to open M8 audio device, SDL Error: %s", SDL_GetError());
+      return 0;
+    }
+  }
+
+  // Start audio processing
+  SDL_Log("Opening audio devices");
+  SDL_PauseAudioDevice(devid_in, 0);
+  SDL_PauseAudioDevice(devid_out, 0);
+
+  return 1;
+}
+
+void audio_destroy() {
+  SDL_Log("Closing audio devices");
+  SDL_PauseAudioDevice(devid_in, 1);
+  SDL_PauseAudioDevice(devid_out, 1);
+  SDL_CloseAudioDevice(devid_in);
+  SDL_CloseAudioDevice(devid_out);
+}
+#endif
--- /dev/null
+++ b/src/audio.h
@@ -1,0 +1,9 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+#ifndef AUDIO_H
+#define AUDIO_H
+
+int audio_init(int audio_buffer_size, const char *output_device_name);
+void audio_destroy();
+
+#endif
--- /dev/null
+++ b/src/command.c
@@ -1,0 +1,174 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#include <SDL_log.h>
+
+#include "command.h"
+#include "render.h"
+
+// Convert 2 little-endian 8bit bytes to a 16bit integer
+static uint16_t decodeInt16(uint8_t *data, uint8_t start) {
+  return data[start] | (uint16_t)data[start + 1] << 8;
+}
+
+enum m8_command_bytes {
+  draw_rectangle_command = 0xFE,
+  draw_rectangle_command_datalength = 12,
+  draw_character_command = 0xFD,
+  draw_character_command_datalength = 12,
+  draw_oscilloscope_waveform_command = 0xFC,
+  draw_oscilloscope_waveform_command_mindatalength = 1 + 3,
+  draw_oscilloscope_waveform_command_maxdatalength = 1 + 3 + 320,
+  joypad_keypressedstate_command = 0xFB,
+  joypad_keypressedstate_command_datalength = 3,
+  system_info_command = 0xFF,
+  system_info_command_datalength = 6
+};
+
+static inline void dump_packet(uint32_t size, uint8_t *recv_buf) {
+  for (uint16_t a = 0; a < size; a++) {
+    SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "0x%02X ", recv_buf[a]);
+  }
+  SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "\n");
+}
+
+int process_command(uint8_t *data, uint32_t size) {
+
+  uint8_t recv_buf[size + 1];
+
+  memcpy(recv_buf, data, size);
+  recv_buf[size] = 0;
+
+  switch (recv_buf[0]) {
+
+  case draw_rectangle_command:
+
+    if (size != draw_rectangle_command_datalength) {
+      SDL_LogError(
+          SDL_LOG_CATEGORY_ERROR,
+          "Invalid draw rectangle packet: expected length %d, got %d\n",
+          draw_rectangle_command_datalength, size);
+      dump_packet(size, recv_buf);
+      return 0;
+      break;
+    } else {
+
+      struct draw_rectangle_command rectcmd = {
+          {decodeInt16(recv_buf, 1), decodeInt16(recv_buf, 3)}, // position x/y
+          {decodeInt16(recv_buf, 5), decodeInt16(recv_buf, 7)}, // size w/h
+          {recv_buf[9], recv_buf[10], recv_buf[11]}};           // color r/g/b
+
+      draw_rectangle(&rectcmd);
+      return 1;
+    }
+
+    break;
+
+  case draw_character_command:
+
+    if (size != draw_character_command_datalength) {
+      SDL_LogError(
+          SDL_LOG_CATEGORY_ERROR,
+          "Invalid draw character packet: expected length %d, got %d\n",
+          draw_character_command_datalength, size);
+      dump_packet(size, recv_buf);
+      return 0;
+      break;
+    } else {
+
+      struct draw_character_command charcmd = {
+          recv_buf[1],                                          // char
+          {decodeInt16(recv_buf, 2), decodeInt16(recv_buf, 4)}, // position x/y
+          {recv_buf[6], recv_buf[7], recv_buf[8]},    // foreground r/g/b
+          {recv_buf[9], recv_buf[10], recv_buf[11]}}; // background r/g/b
+      draw_character(&charcmd);
+      return 1;
+    }
+
+    break;
+
+  case draw_oscilloscope_waveform_command:
+
+    if (size < draw_oscilloscope_waveform_command_mindatalength ||
+        size > draw_oscilloscope_waveform_command_maxdatalength) {
+      SDL_LogError(
+          SDL_LOG_CATEGORY_ERROR,
+          "Invalid draw oscilloscope packet: expected length between %d "
+          "and %d, got "
+          "%d\n",
+          draw_oscilloscope_waveform_command_mindatalength,
+          draw_oscilloscope_waveform_command_maxdatalength, size);
+      dump_packet(size, recv_buf);
+      return 0;
+      break;
+    } else {
+
+      struct draw_oscilloscope_waveform_command osccmd;
+
+      osccmd.color =
+          (struct color){recv_buf[1], recv_buf[2], recv_buf[3]}; // color r/g/b
+      memcpy(osccmd.waveform, &recv_buf[4], size - 4);
+
+      osccmd.waveform_size = size - 4;
+
+      draw_waveform(&osccmd);
+      return 1;
+    }
+
+    break;
+
+  case joypad_keypressedstate_command: {
+    if (size != joypad_keypressedstate_command_datalength) {
+      SDL_LogError(
+          SDL_LOG_CATEGORY_ERROR,
+          "Invalid joypad keypressed state packet: expected length %d, "
+          "got %d\n",
+          joypad_keypressedstate_command_datalength, size);
+      dump_packet(size, recv_buf);
+      return 0;
+      break;
+    }
+
+    // nothing is done with joypad key pressed packets for now
+    return 1;
+    break;
+  }
+
+  case system_info_command: {
+    if (size != system_info_command_datalength) {
+      SDL_LogError(SDL_LOG_CATEGORY_ERROR,
+                   "Invalid system info packet: expected length %d, "
+                   "got %d\n",
+                   system_info_command_datalength, size);
+      dump_packet(size, recv_buf);
+      break;
+    }
+
+    char *hwtype[3] = {"Headless", "Beta M8", "Production M8"};
+
+    static int system_info_printed = 0;
+
+    if (system_info_printed == 0) {
+      SDL_Log("** Hardware info ** Device type: %s, Firmware ver %d.%d.%d",
+              hwtype[recv_buf[1]], recv_buf[2], recv_buf[3], recv_buf[4]);
+      system_info_printed = 1;
+    }
+
+    if (recv_buf[5] == 0x01) {
+      set_large_mode(1);
+    } else {
+      set_large_mode(0);
+    }
+    return 1;
+    break;
+  }
+
+  default:
+
+    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Invalid packet\n");
+    dump_packet(size, recv_buf);
+    return 0;
+    break;
+  }
+  return 1;
+}
--- /dev/null
+++ b/src/command.h
@@ -1,0 +1,46 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#ifndef COMMAND_H_
+#define COMMAND_H_
+
+#include <stdint.h>
+
+struct position {
+  uint16_t x;
+  uint16_t y;
+};
+
+struct size {
+  uint16_t width;
+  uint16_t height;
+};
+
+struct color {
+  uint8_t r;
+  uint8_t g;
+  uint8_t b;
+};
+
+struct draw_rectangle_command {
+  struct position pos;
+  struct size size;
+  struct color color;
+};
+
+struct draw_character_command {
+  int c;
+  struct position pos;
+  struct color foreground;
+  struct color background;
+};
+
+struct draw_oscilloscope_waveform_command {
+  struct color color;
+  uint8_t waveform[320];
+  uint16_t waveform_size;
+};
+
+int process_command(uint8_t *data, uint32_t size);
+
+#endif
--- /dev/null
+++ b/src/config.c
@@ -1,0 +1,394 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#include "config.h"
+#include "ini.h"
+#include <SDL.h>
+#include <assert.h>
+#include <stdio.h>
+
+/* Case insensitive string compare from ini.h library */
+static int strcmpci(const char *a, const char *b) {
+  for (;;) {
+    int d = tolower(*a) - tolower(*b);
+    if (d != 0 || !*a) {
+      return d;
+    }
+    a++, b++;
+  }
+}
+
+config_params_s init_config() {
+  config_params_s c;
+
+  c.filename = "config.ini"; // default config file to load
+
+  c.init_fullscreen = 0; // default fullscreen state at load
+  c.init_use_gpu = 1;    // default to use hardware acceleration
+  c.idle_ms = 10;        // default to high performance
+  c.wait_for_device = 1; // default to exit if device disconnected
+  c.wait_packets = 1024; // default zero-byte attempts to disconnect (about 2
+  // sec for default idle_ms)
+  c.audio_enabled = 0;   // route M8 audio to default output
+  c.audio_buffer_size = 1024; // requested audio buffer size in samples
+  c.audio_device_name = NULL; // Use this device, leave NULL to use the default output device
+
+  c.key_up = SDL_SCANCODE_UP;
+  c.key_left = SDL_SCANCODE_LEFT;
+  c.key_down = SDL_SCANCODE_DOWN;
+  c.key_right = SDL_SCANCODE_RIGHT;
+  c.key_select = SDL_SCANCODE_LSHIFT;
+  c.key_select_alt = SDL_SCANCODE_Z;
+  c.key_start = SDL_SCANCODE_SPACE;
+  c.key_start_alt = SDL_SCANCODE_X;
+  c.key_opt = SDL_SCANCODE_LALT;
+  c.key_opt_alt = SDL_SCANCODE_A;
+  c.key_edit = SDL_SCANCODE_LCTRL;
+  c.key_edit_alt = SDL_SCANCODE_S;
+  c.key_delete = SDL_SCANCODE_DELETE;
+  c.key_reset = SDL_SCANCODE_R;
+
+  c.gamepad_up = SDL_CONTROLLER_BUTTON_DPAD_UP;
+  c.gamepad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
+  c.gamepad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
+  c.gamepad_right = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
+  c.gamepad_select = SDL_CONTROLLER_BUTTON_BACK;
+  c.gamepad_start = SDL_CONTROLLER_BUTTON_START;
+  c.gamepad_opt = SDL_CONTROLLER_BUTTON_B;
+  c.gamepad_edit = SDL_CONTROLLER_BUTTON_A;
+  c.gamepad_quit = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
+  c.gamepad_reset = SDL_CONTROLLER_BUTTON_LEFTSTICK;
+
+  c.gamepad_analog_threshold = 32766;
+  c.gamepad_analog_invert = 0;
+  c.gamepad_analog_axis_updown = SDL_CONTROLLER_AXIS_LEFTY;
+  c.gamepad_analog_axis_leftright = SDL_CONTROLLER_AXIS_LEFTX;
+  c.gamepad_analog_axis_start = SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
+  c.gamepad_analog_axis_select = SDL_CONTROLLER_AXIS_TRIGGERLEFT;
+  c.gamepad_analog_axis_opt = SDL_CONTROLLER_AXIS_INVALID;
+  c.gamepad_analog_axis_edit = SDL_CONTROLLER_AXIS_INVALID;
+
+  return c;
+}
+
+// Write config to file
+void write_config(config_params_s *conf) {
+
+  // Open the default config file for writing
+  char config_path[1024] = {0};
+  snprintf(config_path, sizeof(config_path), "%s%s", SDL_GetPrefPath("", "m8c"),
+           conf->filename);
+  SDL_RWops *rw = SDL_RWFromFile(config_path, "w");
+
+  SDL_Log("Writing config file to %s", config_path);
+
+  const unsigned int INI_LINE_COUNT = 44;
+  const unsigned int LINELEN = 50;
+
+  // Entries for the config file
+  char ini_values[INI_LINE_COUNT][LINELEN];
+  int initPointer = 0;
+  snprintf(ini_values[initPointer++], LINELEN, "[graphics]\n");
+  snprintf(ini_values[initPointer++], LINELEN, "fullscreen=%s\n",
+           conf->init_fullscreen ? "true" : "false");
+  snprintf(ini_values[initPointer++], LINELEN, "use_gpu=%s\n",
+           conf->init_use_gpu ? "true" : "false");
+  snprintf(ini_values[initPointer++], LINELEN, "idle_ms=%d\n", conf->idle_ms);
+  snprintf(ini_values[initPointer++], LINELEN, "wait_for_device=%s\n",
+           conf->wait_for_device ? "true" : "false");
+  snprintf(ini_values[initPointer++], LINELEN, "wait_packets=%d\n",
+           conf->wait_packets);
+  snprintf(ini_values[initPointer++], LINELEN, "[audio]\n");
+  snprintf(ini_values[initPointer++], LINELEN, "audio_enabled=%s\n",
+           conf->audio_enabled ? "true" : "false");
+  snprintf(ini_values[initPointer++], LINELEN, "audio_buffer_size=%d\n",
+           conf->audio_buffer_size);
+  snprintf(ini_values[initPointer++], LINELEN, "audio_device_name=%s\n",
+           conf->audio_device_name ? conf->audio_device_name : "Default");
+  snprintf(ini_values[initPointer++], LINELEN, "[keyboard]\n");
+  snprintf(ini_values[initPointer++], LINELEN, "key_up=%d\n", conf->key_up);
+  snprintf(ini_values[initPointer++], LINELEN, "key_left=%d\n", conf->key_left);
+  snprintf(ini_values[initPointer++], LINELEN, "key_down=%d\n", conf->key_down);
+  snprintf(ini_values[initPointer++], LINELEN, "key_right=%d\n",
+           conf->key_right);
+  snprintf(ini_values[initPointer++], LINELEN, "key_select=%d\n",
+           conf->key_select);
+  snprintf(ini_values[initPointer++], LINELEN, "key_select_alt=%d\n",
+           conf->key_select_alt);
+  snprintf(ini_values[initPointer++], LINELEN, "key_start=%d\n",
+           conf->key_start);
+  snprintf(ini_values[initPointer++], LINELEN, "key_start_alt=%d\n",
+           conf->key_start_alt);
+  snprintf(ini_values[initPointer++], LINELEN, "key_opt=%d\n", conf->key_opt);
+  snprintf(ini_values[initPointer++], LINELEN, "key_opt_alt=%d\n",
+           conf->key_opt_alt);
+  snprintf(ini_values[initPointer++], LINELEN, "key_edit=%d\n", conf->key_edit);
+  snprintf(ini_values[initPointer++], LINELEN, "key_edit_alt=%d\n",
+           conf->key_edit_alt);
+  snprintf(ini_values[initPointer++], LINELEN, "key_delete=%d\n",
+           conf->key_delete);
+  snprintf(ini_values[initPointer++], LINELEN, "key_reset=%d\n",
+           conf->key_reset);
+  snprintf(ini_values[initPointer++], LINELEN, "[gamepad]\n");
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_up=%d\n",
+           conf->gamepad_up);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_left=%d\n",
+           conf->gamepad_left);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_down=%d\n",
+           conf->gamepad_down);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_right=%d\n",
+           conf->gamepad_right);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_select=%d\n",
+           conf->gamepad_select);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_start=%d\n",
+           conf->gamepad_start);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_opt=%d\n",
+           conf->gamepad_opt);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_edit=%d\n",
+           conf->gamepad_edit);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_quit=%d\n",
+           conf->gamepad_quit);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_reset=%d\n",
+           conf->gamepad_reset);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_threshold=%d\n",
+           conf->gamepad_analog_threshold);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_invert=%s\n",
+           conf->gamepad_analog_invert ? "true" : "false");
+  snprintf(ini_values[initPointer++], LINELEN,
+           "gamepad_analog_axis_updown=%d\n", conf->gamepad_analog_axis_updown);
+  snprintf(ini_values[initPointer++], LINELEN,
+           "gamepad_analog_axis_leftright=%d\n",
+           conf->gamepad_analog_axis_leftright);
+  snprintf(ini_values[initPointer++], LINELEN,
+           "gamepad_analog_axis_select=%d\n", conf->gamepad_analog_axis_select);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_axis_start=%d\n",
+           conf->gamepad_analog_axis_start);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_axis_opt=%d\n",
+           conf->gamepad_analog_axis_opt);
+  snprintf(ini_values[initPointer++], LINELEN, "gamepad_analog_axis_edit=%d\n",
+           conf->gamepad_analog_axis_edit);
+
+  // Ensure we aren't writing off the end of the array
+  assert(initPointer == INI_LINE_COUNT);
+
+  if (rw != NULL) {
+    // Write ini_values array to config file
+    for (int i = 0; i < INI_LINE_COUNT; i++) {
+      size_t len = SDL_strlen(ini_values[i]);
+      if (SDL_RWwrite(rw, ini_values[i], 1, len) != len) {
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM,
+                     "Couldn't write line into config file.");
+      } else {
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Wrote to config: %s",
+                     ini_values[i]);
+      }
+    }
+    SDL_RWclose(rw);
+  } else {
+    SDL_Log("Couldn't write into config file.");
+  }
+}
+
+// Read config
+void read_config(config_params_s *conf) {
+
+  char config_path[1024] = {0};
+  snprintf(config_path, sizeof(config_path), "%s%s", SDL_GetPrefPath("", "m8c"),
+           conf->filename);
+  SDL_Log("Reading config %s", config_path);
+  ini_t *ini = ini_load(config_path);
+  if (ini == NULL) {
+    SDL_Log("Could not load config.");
+    write_config(conf);
+    return;
+  }
+
+  read_audio_config(ini, conf);
+  read_graphics_config(ini, conf);
+  read_key_config(ini, conf);
+  read_gamepad_config(ini, conf);
+
+  // Frees the mem used for the config
+  ini_free(ini);
+
+  // Write any new default options after loading
+  write_config(conf);
+}
+
+void read_audio_config(ini_t *ini, config_params_s *conf) {
+  const char *param_audio_enabled = ini_get(ini, "audio", "audio_enabled");
+  const char *param_audio_buffer_size =
+          ini_get(ini, "audio", "audio_buffer_size");
+  const char *param_audio_device_name =
+          ini_get(ini, "audio", "audio_device_name");
+
+  if (param_audio_enabled != NULL) {
+    if (strcmpci(param_audio_enabled, "true") == 0) {
+      conf->audio_enabled = 1;
+    } else {
+      conf->audio_enabled = 0;
+    }
+  }
+
+  if (param_audio_device_name != NULL && SDL_strcmp(param_audio_device_name, "Default") != 0) {
+    conf->audio_device_name = SDL_strdup(param_audio_device_name);
+  }
+
+  if (param_audio_buffer_size != NULL) {
+    conf->audio_buffer_size = SDL_atoi(param_audio_buffer_size);
+  }
+}
+
+void read_graphics_config(ini_t *ini, config_params_s *conf) {
+  const char *param_fs = ini_get(ini, "graphics", "fullscreen");
+  const char *param_gpu = ini_get(ini, "graphics", "use_gpu");
+  const char *idle_ms = ini_get(ini, "graphics", "idle_ms");
+  const char *param_wait = ini_get(ini, "graphics", "wait_for_device");
+  const char *wait_packets = ini_get(ini, "graphics", "wait_packets");
+
+  if (strcmpci(param_fs, "true") == 0) {
+    conf->init_fullscreen = 1;
+  } else
+    conf->init_fullscreen = 0;
+
+  if (param_gpu != NULL) {
+    if (strcmpci(param_gpu, "true") == 0) {
+      conf->init_use_gpu = 1;
+    } else
+      conf->init_use_gpu = 0;
+  }
+
+  if (idle_ms != NULL)
+    conf->idle_ms = SDL_atoi(idle_ms);
+
+  if (param_wait != NULL) {
+    if (strcmpci(param_wait, "true") == 0) {
+      conf->wait_for_device = 1;
+    } else {
+      conf->wait_for_device = 0;
+    }
+  }
+  if (wait_packets != NULL)
+    conf->wait_packets = SDL_atoi(wait_packets);
+}
+
+void read_key_config(ini_t *ini, config_params_s *conf) {
+  // TODO: Some form of validation
+
+  const char *key_up = ini_get(ini, "keyboard", "key_up");
+  const char *key_left = ini_get(ini, "keyboard", "key_left");
+  const char *key_down = ini_get(ini, "keyboard", "key_down");
+  const char *key_right = ini_get(ini, "keyboard", "key_right");
+  const char *key_select = ini_get(ini, "keyboard", "key_select");
+  const char *key_select_alt = ini_get(ini, "keyboard", "key_select_alt");
+  const char *key_start = ini_get(ini, "keyboard", "key_start");
+  const char *key_start_alt = ini_get(ini, "keyboard", "key_start_alt");
+  const char *key_opt = ini_get(ini, "keyboard", "key_opt");
+  const char *key_opt_alt = ini_get(ini, "keyboard", "key_opt_alt");
+  const char *key_edit = ini_get(ini, "keyboard", "key_edit");
+  const char *key_edit_alt = ini_get(ini, "keyboard", "key_edit_alt");
+  const char *key_delete = ini_get(ini, "keyboard", "key_delete");
+  const char *key_reset = ini_get(ini, "keyboard", "key_reset");
+
+  if (key_up)
+    conf->key_up = SDL_atoi(key_up);
+  if (key_left)
+    conf->key_left = SDL_atoi(key_left);
+  if (key_down)
+    conf->key_down = SDL_atoi(key_down);
+  if (key_right)
+    conf->key_right = SDL_atoi(key_right);
+  if (key_select)
+    conf->key_select = SDL_atoi(key_select);
+  if (key_select_alt)
+    conf->key_select_alt = SDL_atoi(key_select_alt);
+  if (key_start)
+    conf->key_start = SDL_atoi(key_start);
+  if (key_start_alt)
+    conf->key_start_alt = SDL_atoi(key_start_alt);
+  if (key_opt)
+    conf->key_opt = SDL_atoi(key_opt);
+  if (key_opt_alt)
+    conf->key_opt_alt = SDL_atoi(key_opt_alt);
+  if (key_edit)
+    conf->key_edit = SDL_atoi(key_edit);
+  if (key_edit_alt)
+    conf->key_edit_alt = SDL_atoi(key_edit_alt);
+  if (key_delete)
+    conf->key_delete = SDL_atoi(key_delete);
+  if (key_reset)
+    conf->key_reset = SDL_atoi(key_reset);
+}
+
+void read_gamepad_config(ini_t *ini, config_params_s *conf) {
+  // TODO: Some form of validation
+
+  const char *gamepad_up = ini_get(ini, "gamepad", "gamepad_up");
+  const char *gamepad_left = ini_get(ini, "gamepad", "gamepad_left");
+  const char *gamepad_down = ini_get(ini, "gamepad", "gamepad_down");
+  const char *gamepad_right = ini_get(ini, "gamepad", "gamepad_right");
+  const char *gamepad_select = ini_get(ini, "gamepad", "gamepad_select");
+  const char *gamepad_start = ini_get(ini, "gamepad", "gamepad_start");
+  const char *gamepad_opt = ini_get(ini, "gamepad", "gamepad_opt");
+  const char *gamepad_edit = ini_get(ini, "gamepad", "gamepad_edit");
+  const char *gamepad_quit = ini_get(ini, "gamepad", "gamepad_quit");
+  const char *gamepad_reset = ini_get(ini, "gamepad", "gamepad_reset");
+  const char *gamepad_analog_threshold =
+          ini_get(ini, "gamepad", "gamepad_analog_threshold");
+  const char *gamepad_analog_invert =
+          ini_get(ini, "gamepad", "gamepad_analog_invert");
+  const char *gamepad_analog_axis_updown =
+          ini_get(ini, "gamepad", "gamepad_analog_axis_updown");
+  const char *gamepad_analog_axis_leftright =
+          ini_get(ini, "gamepad", "gamepad_analog_axis_leftright");
+  const char *gamepad_analog_axis_select =
+          ini_get(ini, "gamepad", "gamepad_analog_axis_select");
+  const char *gamepad_analog_axis_start =
+          ini_get(ini, "gamepad", "gamepad_analog_axis_start");
+  const char *gamepad_analog_axis_opt =
+          ini_get(ini, "gamepad", "gamepad_analog_axis_opt");
+  const char *gamepad_analog_axis_edit =
+          ini_get(ini, "gamepad", "gamepad_analog_axis_edit");
+
+  if (gamepad_up)
+    conf->gamepad_up = SDL_atoi(gamepad_up);
+  if (gamepad_left)
+    conf->gamepad_left = SDL_atoi(gamepad_left);
+  if (gamepad_down)
+    conf->gamepad_down = SDL_atoi(gamepad_down);
+  if (gamepad_right)
+    conf->gamepad_right = SDL_atoi(gamepad_right);
+  if (gamepad_select)
+    conf->gamepad_select = SDL_atoi(gamepad_select);
+  if (gamepad_start)
+    conf->gamepad_start = SDL_atoi(gamepad_start);
+  if (gamepad_opt)
+    conf->gamepad_opt = SDL_atoi(gamepad_opt);
+  if (gamepad_edit)
+    conf->gamepad_edit = SDL_atoi(gamepad_edit);
+  if (gamepad_quit)
+    conf->gamepad_quit = SDL_atoi(gamepad_quit);
+  if (gamepad_reset)
+    conf->gamepad_reset = SDL_atoi(gamepad_reset);
+  if (gamepad_analog_threshold)
+    conf->gamepad_analog_threshold = SDL_atoi(gamepad_analog_threshold);
+
+  if (strcmpci(gamepad_analog_invert, "true") == 0)
+    conf->gamepad_analog_invert = 1;
+  else
+    conf->gamepad_analog_invert = 0;
+
+  if (gamepad_analog_axis_updown)
+    conf->gamepad_analog_axis_updown = SDL_atoi(gamepad_analog_axis_updown);
+  if (gamepad_analog_axis_leftright)
+    conf->gamepad_analog_axis_leftright =
+            SDL_atoi(gamepad_analog_axis_leftright);
+  if (gamepad_analog_axis_select)
+    conf->gamepad_analog_axis_select = SDL_atoi(gamepad_analog_axis_select);
+  if (gamepad_analog_axis_start)
+    conf->gamepad_analog_axis_start = SDL_atoi(gamepad_analog_axis_start);
+  if (gamepad_analog_axis_opt)
+    conf->gamepad_analog_axis_opt = SDL_atoi(gamepad_analog_axis_opt);
+  if (gamepad_analog_axis_edit)
+    conf->gamepad_analog_axis_edit = SDL_atoi(gamepad_analog_axis_edit);
+}
--- /dev/null
+++ b/src/config.h
@@ -1,0 +1,65 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#ifndef CONFIG_H_
+#define CONFIG_H_
+
+#include "ini.h"
+
+typedef struct config_params_s {
+  char *filename;
+  int init_fullscreen;
+  int init_use_gpu;
+  int idle_ms;
+  int wait_for_device;
+  int wait_packets;
+  int audio_enabled;
+  int audio_buffer_size;
+  const char *audio_device_name;
+
+  int key_up;
+  int key_left;
+  int key_down;
+  int key_right;
+  int key_select;
+  int key_select_alt;
+  int key_start;
+  int key_start_alt;
+  int key_opt;
+  int key_opt_alt;
+  int key_edit;
+  int key_edit_alt;
+  int key_delete;
+  int key_reset;
+
+  int gamepad_up;
+  int gamepad_left;
+  int gamepad_down;
+  int gamepad_right;
+  int gamepad_select;
+  int gamepad_start;
+  int gamepad_opt;
+  int gamepad_edit;
+  int gamepad_quit;
+  int gamepad_reset;
+
+  int gamepad_analog_threshold;
+  int gamepad_analog_invert;
+  int gamepad_analog_axis_updown;
+  int gamepad_analog_axis_leftright;
+  int gamepad_analog_axis_start;
+  int gamepad_analog_axis_select;
+  int gamepad_analog_axis_opt;
+  int gamepad_analog_axis_edit;
+
+} config_params_s;
+
+
+config_params_s init_config();
+void read_config(config_params_s *conf);
+void read_audio_config(ini_t *config, config_params_s *conf);
+void read_graphics_config(ini_t *config, config_params_s *conf);
+void read_key_config(ini_t *config, config_params_s *conf);
+void read_gamepad_config(ini_t *config, config_params_s *conf);
+
+#endif
--- /dev/null
+++ b/src/fx_cube.c
@@ -1,0 +1,123 @@
+#include <SDL.h>
+#include "SDL2_inprint.h"
+#include "SDL_pixels.h"
+
+#define target_width 320
+#define target_height 240
+static SDL_Texture *texture_cube;
+static SDL_Texture *texture_text;
+static SDL_Renderer *fx_renderer;
+static SDL_Color line_color;
+
+const char *text_m8c = "M8C";
+const char *text_disconnected = "DEVICE DISCONNECTED";
+
+static const float center_x = (float)target_width / 2;
+static const float center_y = (float)target_height / 2;
+
+static const float default_nodes[8][3] = {
+    {-1, -1, -1}, {-1, -1, 1}, {-1, 1, -1}, {-1, 1, 1},
+    {1, -1, -1},  {1, -1, 1},  {1, 1, -1},  {1, 1, 1}};
+
+static int edges[12][2] = {{0, 1}, {1, 3}, {3, 2}, {2, 0}, {4, 5}, {5, 7},
+                           {7, 6}, {6, 4}, {0, 4}, {1, 5}, {2, 6}, {3, 7}};
+
+static float nodes[8][3];
+
+static void scale(float factor0, float factor1, float factor2) {
+  for (int i = 0; i < 8; i++) {
+    nodes[i][0] *= factor0;
+    nodes[i][1] *= factor1;
+    nodes[i][2] *= factor2;
+  }
+}
+
+static void rotate_cube(float angle_x, float angle_y) {
+  float sin_x = SDL_sin(angle_x);
+  float cos_x = SDL_cos(angle_x);
+  float sin_y = SDL_sin(angle_y);
+  float cos_y = SDL_cos(angle_y);
+  for (int i = 0; i < 8; i++) {
+    float x = nodes[i][0];
+    float y = nodes[i][1];
+    float z = nodes[i][2];
+
+    nodes[i][0] = x * cos_x - z * sin_x;
+    nodes[i][2] = z * cos_x + x * sin_x;
+
+    z = nodes[i][2];
+
+    nodes[i][1] = y * cos_y - z * sin_y;
+    nodes[i][2] = z * cos_y + y * sin_y;
+  }
+}
+
+void fx_cube_init(SDL_Renderer *target_renderer, SDL_Color foreground_color) {
+
+  fx_renderer = target_renderer;
+  line_color = foreground_color;
+
+  texture_cube =
+      SDL_CreateTexture(fx_renderer, SDL_PIXELFORMAT_ARGB8888,
+                        SDL_TEXTUREACCESS_TARGET, target_width, target_height);
+  texture_text =
+      SDL_CreateTexture(fx_renderer, SDL_PIXELFORMAT_ARGB8888,
+                        SDL_TEXTUREACCESS_TARGET, target_width, target_height);
+
+  SDL_Texture *og_target = SDL_GetRenderTarget(fx_renderer);                        
+
+  SDL_SetRenderTarget(fx_renderer, texture_text);
+  SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
+  SDL_RenderClear(fx_renderer);
+
+  inprint(fx_renderer, text_disconnected, 150, 228, 0xFFFFFF, 0x000000);
+  inprint(fx_renderer, text_m8c, 2, 2, 0xFFFFFF, 0x000000);
+
+  SDL_SetRenderTarget(fx_renderer, og_target);
+
+  // Initialize default nodes
+  SDL_memcpy(nodes, default_nodes, sizeof(default_nodes));
+
+  scale(50, 50, 50);
+  rotate_cube(M_PI / 4, SDL_atan(SDL_sqrt(2)));
+
+  SDL_SetTextureBlendMode(texture_cube, SDL_BLENDMODE_BLEND);
+  SDL_SetTextureBlendMode(texture_text, SDL_BLENDMODE_BLEND);
+}
+
+void fx_cube_destroy() {
+  SDL_DestroyTexture(texture_cube);
+  SDL_DestroyTexture(texture_text);
+}
+
+void fx_cube_update() {
+  SDL_Point points[24];
+  int points_counter = 0;
+  SDL_Texture *og_texture = SDL_GetRenderTarget(fx_renderer);
+
+  SDL_SetRenderTarget(fx_renderer, texture_cube);
+  SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
+  SDL_RenderClear(fx_renderer);
+
+  int seconds = SDL_GetTicks() / 1000;
+  float scalefactor = 1 + (SDL_sin(seconds) * 0.01);
+
+  scale(scalefactor, scalefactor, scalefactor);
+  rotate_cube(M_PI / 180, M_PI / 270);
+
+  for (int i = 0; i < 12; i++) {
+    float *p1 = nodes[edges[i][0]];
+    float *p2 = nodes[edges[i][1]];
+    points[points_counter++] =
+        (SDL_Point){p1[0] + center_x, nodes[edges[i][0]][1] + center_y};
+    points[points_counter++] = (SDL_Point){p2[0] + center_x, p2[1] + center_y};
+  }
+
+  SDL_RenderCopy(fx_renderer, texture_text, NULL, NULL);
+  SDL_SetRenderDrawColor(fx_renderer, line_color.r, line_color.g, line_color.b,
+                         line_color.a);
+  SDL_RenderDrawLines(fx_renderer, points, 24);
+
+  SDL_SetRenderTarget(fx_renderer, og_texture);
+  SDL_RenderCopy(fx_renderer, texture_cube, NULL, NULL);
+}
--- /dev/null
+++ b/src/fx_cube.h
@@ -1,0 +1,8 @@
+#ifndef FX_CUBE_H_
+#define FX_CUBE_H_
+
+#include "SDL_render.h"
+void fx_cube_init(SDL_Renderer *target_renderer, SDL_Color foreground_color);
+void fx_cube_destroy();
+void fx_cube_update();
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/ini.c
@@ -1,0 +1,277 @@
+/**
+ * Copyright (c) 2016 rxi
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "ini.h"
+
+struct ini_t {
+  char *data;
+  char *end;
+};
+
+
+/* Case insensitive string compare */
+static int strcmpci(const char *a, const char *b) {
+  for (;;) {
+    int d = tolower(*a) - tolower(*b);
+    if (d != 0 || !*a) {
+      return d;
+    }
+    a++, b++;
+  }
+}
+
+/* Returns the next string in the split data */
+static char* next(ini_t *ini, char *p) {
+  p += strlen(p);
+  while (p < ini->end && *p == '\0') {
+    p++;
+  }
+  return p;
+}
+
+static void trim_back(ini_t *ini, char *p) {
+  while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) {
+    *p-- = '\0';
+  }
+}
+
+static char* discard_line(ini_t *ini, char *p) {
+  while (p < ini->end && *p != '\n') {
+    *p++ = '\0';
+  }
+  return p;
+}
+
+
+static char *unescape_quoted_value(ini_t *ini, char *p) {
+  /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q`
+   * as escape sequences are always larger than their resultant data */
+  char *q = p;
+  p++;
+  while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') {
+    if (*p == '\\') {
+      /* Handle escaped char */
+      p++;
+      switch (*p) {
+        default   : *q = *p;    break;
+        case 'r'  : *q = '\r';  break;
+        case 'n'  : *q = '\n';  break;
+        case 't'  : *q = '\t';  break;
+        case '\r' :
+        case '\n' :
+        case '\0' : goto end;
+      }
+
+    } else {
+      /* Handle normal char */
+      *q = *p;
+    }
+    q++, p++;
+  }
+end:
+  return q;
+}
+
+
+/* Splits data in place into strings containing section-headers, keys and
+ * values using one or more '\0' as a delimiter. Unescapes quoted values */
+static void split_data(ini_t *ini) {
+  char *value_start, *line_start;
+  char *p = ini->data;
+
+  while (p < ini->end) {
+    switch (*p) {
+      case '\r':
+      case '\n':
+      case '\t':
+      case ' ':
+        *p = '\0';
+        /* Fall through */
+
+      case '\0':
+        p++;
+        break;
+
+      case '[':
+        p += strcspn(p, "]\n");
+        *p = '\0';
+        break;
+
+      case ';':
+        p = discard_line(ini, p);
+        break;
+
+      default:
+        line_start = p;
+        p += strcspn(p, "=\n");
+
+        /* Is line missing a '='? */
+        if (*p != '=') {
+          p = discard_line(ini, line_start);
+          break;
+        }
+        trim_back(ini, p - 1);
+
+        /* Replace '=' and whitespace after it with '\0' */
+        do {
+          *p++ = '\0';
+        } while (*p == ' ' || *p == '\r' || *p == '\t');
+
+        /* Is a value after '=' missing? */
+        if (*p == '\n' || *p == '\0') {
+          p = discard_line(ini, line_start);
+          break;
+        }
+
+        if (*p == '"') {
+          /* Handle quoted string value */
+          value_start = p;
+          p = unescape_quoted_value(ini, p);
+
+          /* Was the string empty? */
+          if (p == value_start) {
+            p = discard_line(ini, line_start);
+            break;
+          }
+
+          /* Discard the rest of the line after the string value */
+          p = discard_line(ini, p);
+
+        } else {
+          /* Handle normal value */
+          p += strcspn(p, "\n");
+          trim_back(ini, p - 1);
+        }
+        break;
+    }
+  }
+}
+
+
+
+ini_t* ini_load(const char *filename) {
+  ini_t *ini = NULL;
+  FILE *fp = NULL;
+  int n, sz;
+
+  /* Init ini struct */
+  ini = malloc(sizeof(*ini));
+  if (!ini) {
+    goto fail;
+  }
+  memset(ini, 0, sizeof(*ini));
+
+  /* Open file */
+  fp = fopen(filename, "rb");
+  if (!fp) {
+    goto fail;
+  }
+
+  /* Get file size */
+  fseek(fp, 0, SEEK_END);
+  sz = ftell(fp);
+  if (sz==0) {
+    goto fail;
+  }
+  rewind(fp);
+
+  /* Load file content into memory, null terminate, init end var */
+  ini->data = malloc(sz + 1);
+  ini->data[sz] = '\0';
+  ini->end = ini->data  + sz;
+  n = fread(ini->data, 1, sz, fp);
+  if (n != sz) {
+    goto fail;
+  }
+
+  /* Prepare data */
+  split_data(ini);
+
+  /* Clean up and return */
+  fclose(fp);
+  return ini;
+
+fail:
+  if (fp) fclose(fp);
+  if (ini) ini_free(ini);
+  return NULL;
+}
+
+
+void ini_free(ini_t *ini) {
+  free(ini->data);
+  free(ini);
+}
+
+
+const char* ini_get(ini_t *ini, const char *section, const char *key) {
+  char *current_section = "";
+  char *val;
+  char *p = ini->data;
+
+  if (*p == '\0') {
+    p = next(ini, p);
+  }
+
+  while (p < ini->end) {
+    if (*p == '[') {
+      /* Handle section */
+      current_section = p + 1;
+
+    } else {
+      /* Handle key */
+      val = next(ini, p);
+      if (!section || !strcmpci(section, current_section)) {
+        if (!strcmpci(p, key)) {
+          return val;
+        }
+      }
+      p = val;
+    }
+
+    p = next(ini, p);
+  }
+
+  return NULL;
+}
+
+
+int ini_sget(
+  ini_t *ini, const char *section, const char *key,
+  const char *scanfmt, void *dst
+) {
+  const char *val = ini_get(ini, section, key);
+  if (!val) {
+    return 0;
+  }
+  if (scanfmt) {
+    sscanf(val, scanfmt, dst);
+  } else {
+    *((const char**) dst) = val;
+  }
+  return 1;
+}
--- /dev/null
+++ b/src/ini.h
@@ -1,0 +1,20 @@
+/**
+ * Copyright (c) 2016 rxi
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT license. See `ini.c` for details.
+ */
+
+#ifndef INI_H
+#define INI_H
+
+#define INI_VERSION "0.1.1"
+
+typedef struct ini_t ini_t;
+
+ini_t*      ini_load(const char *filename);
+void        ini_free(ini_t *ini);
+const char* ini_get(ini_t *ini, const char *section, const char *key);
+int         ini_sget(ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst);
+
+#endif
--- /dev/null
+++ b/src/inline_font.h
@@ -1,0 +1,10 @@
+#ifndef INLINE_FONT_H_
+#define INLINE_FONT_H_
+
+struct inline_font {
+    int width;
+    int height;
+    unsigned char bits[];
+};
+
+#endif
--- /dev/null
+++ b/src/inline_font_large.h
@@ -1,0 +1,133 @@
+#ifndef INLINE_FONT_LARGE_H_
+#define INLINE_FONT_LARGE_H_
+
+#include "inline_font.h"
+
+struct inline_font inline_font_large = {
+    144,
+    88,
+    {
+0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 
+0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 0xff, 0x79, 0xf1, 
+0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 
+0x2f, 0x5e, 0xff, 0xb5, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xda, 0xb4, 
+0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xff, 0xcd, 0x99, 0x33, 0x67, 0xce, 
+0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xff, 
+0xed, 0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xf6, 0xec, 0xd9, 0xb3, 0x67, 
+0xcf, 0x9e, 0x3d, 0x7b, 0xff, 0xcd, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 
+0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xff, 0xb5, 0x69, 0xd3, 
+0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 
+0x6d, 0xff, 0x79, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 
+0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xff, 0xfd, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 
+0x1f, 0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 0xff, 0x01, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 
+0xc7, 0x8f, 0x1f, 0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 
+0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 0xe2, 
+0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 
+0x6d, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xe6, 0xcc, 0x99, 
+0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 
+0x39, 0x73, 0xf6, 0xec, 0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xf6, 0xec, 
+0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 
+0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x99, 0x33, 0x67, 0xce, 0x9c, 0x39, 0x73, 0xda, 
+0xb4, 0x69, 0xd3, 0xa6, 0x4d, 0x9b, 0x36, 0x6d, 0xda, 0xb4, 0x69, 0xd3, 0xa6, 
+0x4d, 0x9b, 0x36, 0x6d, 0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 
+0xbc, 0x78, 0xf1, 0xe2, 0xc5, 0x8b, 0x17, 0x2f, 0x5e, 0x7e, 0xfc, 0xf8, 0xf1, 
+0xe3, 0xc7, 0x8f, 0x1f, 0x3f, 0x7e, 0xfc, 0xf8, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 
+0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xe7, 0x4f, 0xfe, 0x7f, 0xfe, 0xff, 0xff, 0xf3, 0x3f, 
+0xf3, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x4f, 0xfe, 0x0f, 
+0x70, 0xef, 0xf8, 0xf3, 0x9f, 0xe7, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 
+0xff, 0xe7, 0x4f, 0xde, 0x4e, 0xbe, 0x76, 0xf2, 0xf3, 0xcf, 0xcf, 0xcf, 0x3f, 
+0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xe7, 0xff, 0x0f, 0x4c, 0x7e, 0x7b, 0xf2, 
+0xff, 0xcf, 0xcf, 0x87, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xe7, 0xff, 
+0xdf, 0x0e, 0xf0, 0xfd, 0xf8, 0xff, 0xcf, 0xcf, 0x33, 0x0f, 0xfc, 0x3f, 0xf0, 
+0xff, 0xf3, 0xff, 0xe7, 0xff, 0xdf, 0x7e, 0xf2, 0x76, 0xf8, 0xff, 0xcf, 0xcf, 
+0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0x0f, 0x7c, 0x72, 
+0x2b, 0xc3, 0xff, 0xcf, 0xcf, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 
+0xff, 0xff, 0xdf, 0x0e, 0xb0, 0x37, 0xe7, 0xff, 0x9f, 0xe7, 0xff, 0xff, 0x3f, 
+0xff, 0xff, 0x7f, 0xfe, 0xff, 0xe7, 0xff, 0xff, 0x7f, 0xfe, 0x7f, 0xc8, 0xff, 
+0x3f, 0xf3, 0xff, 0xff, 0x9f, 0xff, 0x7f, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xc7, 0x03, 0x04, 0xcc, 0x13, 0x20, 
+0x40, 0x80, 0x03, 0x01, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3c, 0xc3, 
+0xff, 0xfc, 0xc9, 0x93, 0x3f, 0xff, 0x9f, 0x39, 0x79, 0xfe, 0xff, 0xff, 0xf3, 
+0x7f, 0x7e, 0x9e, 0x3c, 0xc9, 0xff, 0xfc, 0xc9, 0x93, 0x3f, 0xff, 0x9f, 0x3c, 
+0x79, 0xfe, 0xff, 0xff, 0x31, 0x70, 0xfc, 0x9f, 0x3c, 0xcf, 0xff, 0xfc, 0xc9, 
+0x93, 0x3f, 0xff, 0xcf, 0x3c, 0x79, 0xce, 0xcf, 0xff, 0xf0, 0x7f, 0xf8, 0x9f, 
+0x24, 0xcf, 0x03, 0x0c, 0x0c, 0x10, 0x20, 0xc0, 0xe7, 0x81, 0x01, 0xfe, 0xff, 
+0x7f, 0xf0, 0x7f, 0xf0, 0xc3, 0x3c, 0xcf, 0xf3, 0xff, 0xf9, 0xf3, 0x27, 0xcf, 
+0xf3, 0x3c, 0x7f, 0xfe, 0xff, 0xff, 0xf0, 0x7f, 0xf8, 0xf3, 0x3c, 0xcf, 0xf3, 
+0xff, 0xf9, 0xf3, 0x27, 0xcf, 0xf3, 0x3c, 0x7f, 0xce, 0xcf, 0xff, 0x31, 0x70, 
+0xfc, 0xf3, 0x3c, 0xcf, 0xf3, 0xff, 0xf9, 0xf3, 0x27, 0xcf, 0xf3, 0x3c, 0x7f, 
+0xfe, 0xcf, 0xff, 0xf3, 0x7f, 0xfe, 0xff, 0x00, 0x01, 0x02, 0x04, 0xfc, 0x13, 
+0x20, 0xc0, 0xf3, 0x80, 0x7f, 0xfe, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0x87, 0x03, 0x0e, 
+0x0c, 0x1c, 0x20, 0xc0, 0xc0, 0x3c, 0x01, 0x02, 0xe4, 0xc9, 0x9f, 0x27, 0xcf, 
+0xc0, 0x3c, 0x33, 0xf3, 0xe4, 0xc9, 0x99, 0x3f, 0x7f, 0x9e, 0x3c, 0xcf, 0xff, 
+0xe4, 0xcc, 0x1f, 0x23, 0x4e, 0x9e, 0x3c, 0x79, 0xf2, 0xe4, 0xcf, 0x93, 0x3f, 
+0x7f, 0xfe, 0x3c, 0xcf, 0xff, 0x64, 0xce, 0x1f, 0x20, 0x4c, 0x9e, 0x04, 0x79, 
+0xf2, 0xe4, 0xcf, 0x93, 0x3f, 0x7f, 0xfe, 0x3c, 0xcf, 0xff, 0x24, 0xcf, 0x9f, 
+0x24, 0x49, 0x9e, 0x24, 0x01, 0x02, 0xe6, 0xcf, 0x13, 0x38, 0x70, 0x86, 0x00, 
+0xcf, 0xff, 0x84, 0xcf, 0x9f, 0x27, 0x43, 0x9e, 0x24, 0x79, 0xf2, 0xe4, 0xcf, 
+0x93, 0x3f, 0x7f, 0x9e, 0x3c, 0xcf, 0xff, 0x24, 0xcf, 0x9f, 0x27, 0x47, 0x9e, 
+0x84, 0x79, 0xf2, 0xe4, 0xcf, 0x93, 0x3f, 0x7f, 0x9e, 0x3c, 0xcf, 0xf3, 0x64, 
+0xce, 0x9f, 0x27, 0x4f, 0x9e, 0xfc, 0x79, 0xf2, 0xe4, 0xc9, 0x99, 0x3f, 0x7f, 
+0x9e, 0x3c, 0xcf, 0xf3, 0xe4, 0xcc, 0x9f, 0x27, 0x4f, 0x9e, 0x81, 0x79, 0x02, 
+0x0e, 0x0c, 0x1c, 0x20, 0xff, 0xc0, 0x3c, 0x01, 0x06, 0xe6, 0x09, 0x90, 0x27, 
+0xcf, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 
+0x03, 0x03, 0x0e, 0x0c, 0x90, 0x27, 0x4f, 0x9e, 0x3c, 0x79, 0x02, 0x7c, 0xf8, 
+0x1f, 0xfe, 0xfc, 0xff, 0x3c, 0x79, 0xf2, 0xe4, 0x79, 0x9e, 0x27, 0x4f, 0x9e, 
+0x3c, 0x79, 0xfe, 0x7c, 0xfe, 0x7f, 0x7e, 0xf8, 0xff, 0x3c, 0x79, 0xf2, 0xe4, 
+0x7f, 0x9e, 0x27, 0x4f, 0x9e, 0x99, 0x79, 0x7e, 0x7e, 0xce, 0x7f, 0x3e, 0xf3, 
+0xff, 0x3c, 0x79, 0xf2, 0xe4, 0x7f, 0x9e, 0x27, 0x4f, 0x9e, 0xc3, 0x79, 0x3e, 
+0x7f, 0x9e, 0x7f, 0xfe, 0xff, 0xff, 0x80, 0x79, 0x02, 0x0e, 0x7c, 0x9e, 0x27, 
+0x4f, 0x9e, 0xe7, 0x03, 0x9f, 0x7f, 0x3e, 0x7f, 0xfe, 0xff, 0xff, 0xfc, 0x49, 
+0x92, 0xff, 0x79, 0x9e, 0x27, 0x4f, 0x92, 0xc3, 0xcf, 0xcf, 0x7f, 0x7e, 0x7e, 
+0xfe, 0xff, 0xff, 0xfc, 0x99, 0x32, 0xff, 0x79, 0x9e, 0x67, 0x66, 0x80, 0x99, 
+0xcf, 0xe7, 0x7f, 0xfe, 0x7c, 0xfe, 0xff, 0xff, 0xfc, 0x39, 0x73, 0xe6, 0x79, 
+0x9e, 0xe7, 0x70, 0x8c, 0x3c, 0xcf, 0xf3, 0x7f, 0xfe, 0x79, 0xfe, 0xff, 0xff, 
+0xfc, 0x43, 0xf2, 0x0c, 0x7c, 0x3e, 0xf0, 0x79, 0x9e, 0x3c, 0xcf, 0x03, 0x7c, 
+0xf8, 0x13, 0xfe, 0xff, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xfc, 0xff, 0xf3, 0xff, 0xff, 0xf3, 0x7f, 0xc0, 0xff, 0xfc, 0xcf, 
+0xff, 0xe4, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xf3, 0xff, 0xff, 0xf3, 
+0x7f, 0xfe, 0xff, 0xfc, 0xff, 0xff, 0xe7, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf3, 
+0x01, 0x02, 0x04, 0x08, 0x10, 0x60, 0x7e, 0x80, 0x00, 0xc3, 0x07, 0xe4, 0x39, 
+0x1f, 0x20, 0x40, 0x80, 0xff, 0x7f, 0xf2, 0xe4, 0xcf, 0x93, 0x27, 0x40, 0x9e, 
+0x3c, 0xcf, 0xff, 0xe4, 0x3c, 0x9f, 0x24, 0x4f, 0x9e, 0xff, 0x7f, 0xf2, 0xe4, 
+0xcf, 0x93, 0x67, 0x7e, 0x9e, 0x3c, 0xcf, 0xff, 0x64, 0x3e, 0x9f, 0x24, 0x4f, 
+0x9e, 0xff, 0x01, 0xf2, 0xe4, 0xcf, 0x13, 0x60, 0x7e, 0x80, 0x3c, 0xcf, 0xff, 
+0x04, 0x3f, 0x9f, 0x24, 0x4f, 0x9e, 0xff, 0x79, 0xf2, 0xe4, 0xcf, 0x93, 0x7f, 
+0xfe, 0x9f, 0x3c, 0xcf, 0xff, 0x64, 0x3e, 0x9f, 0x24, 0x4f, 0x9e, 0xff, 0x79, 
+0xf2, 0xe4, 0xcf, 0x93, 0x7f, 0xfe, 0x9f, 0x3c, 0xcf, 0xff, 0xe4, 0x3c, 0x9f, 
+0x24, 0x4f, 0x9e, 0xff, 0x01, 0x02, 0x04, 0x08, 0x10, 0x60, 0x7e, 0x80, 0x3c, 
+0x01, 0x02, 0xe4, 0x09, 0x90, 0x24, 0x4f, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0x07, 0x78, 0x1e, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x79, 0x9e, 0xe7, 
+0xff, 0xff, 0x00, 0x01, 0x02, 0x04, 0x08, 0x90, 0x27, 0x4f, 0x92, 0x3c, 0x79, 
+0x02, 0xe4, 0x79, 0x9e, 0xe7, 0xff, 0xff, 0x3c, 0x79, 0xf2, 0xe7, 0x9f, 0x9f, 
+0x27, 0x67, 0x92, 0x99, 0x79, 0x7e, 0xe6, 0x79, 0x9e, 0x67, 0xfc, 0xff, 0x3c, 
+0x79, 0xf2, 0xe7, 0x9f, 0x9f, 0x27, 0x73, 0x92, 0xc3, 0x79, 0x3e, 0xe7, 0x79, 
+0x9e, 0x27, 0xc9, 0xff, 0x00, 0x01, 0xf2, 0x07, 0x98, 0x9f, 0x27, 0x79, 0x92, 
+0xe7, 0x01, 0x9e, 0xe7, 0x79, 0x9e, 0xe7, 0xe3, 0xff, 0xfc, 0x7f, 0xf2, 0xff, 
+0x99, 0x9f, 0x27, 0x7c, 0x92, 0xc3, 0x7f, 0xce, 0xe7, 0x79, 0x9e, 0xe7, 0xff, 
+0xff, 0xfc, 0x7f, 0xf2, 0xff, 0x99, 0x9f, 0x27, 0x7e, 0x92, 0x99, 0x7f, 0xe6, 
+0xe7, 0x79, 0x9e, 0xe7, 0xff, 0xff, 0xfc, 0x7f, 0xf2, 0x07, 0x18, 0x10, 0x20, 
+0x7f, 0x80, 0x3c, 0x01, 0x02, 0x04, 0x78, 0x1e, 0xe0, 0xff, 0xff  }};
+
+#endif
--- /dev/null
+++ b/src/inline_font_small.h
@@ -1,0 +1,84 @@
+/*
+The FontStruction “M8stealth57”
+(https://fontstruct.com/fontstructions/show/2043303) by “trash80” is licensed
+under a Creative Commons Attribution Share Alike license
+(http://creativecommons.org/licenses/by-sa/3.0/). “M8stealth57” was originally
+cloned (copied) from the FontStruction “stealth57”
+(https://fontstruct.com/fontstructions/show/413734) by “trash80”, which is
+licensed under a Creative Commons Attribution Share Alike license
+(http://creativecommons.org/licenses/by-sa/3.0/).
+
+Used with permission from the author.
+*/
+
+#ifndef INLINE_FONT_SMALL_H_
+#define INLINE_FONT_SMALL_H_
+
+#include "inline_font.h"
+
+struct inline_font inline_font_small = {
+    96,
+    64,
+    {
+0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 
+0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x3f, 0x45, 
+0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0xbf, 0xa6, 0x69, 
+0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x3f, 0x45, 0x51, 0x14, 
+0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0xbf, 0x65, 0x59, 0x96, 0x65, 
+0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 
+0x96, 0x65, 0x59, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 
+0x45, 0x51, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 0x69, 0x9a, 0xa6, 
+0x69, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 0x14, 0x45, 0x51, 
+0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x96, 0x65, 0x59, 0x00, 
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x5f, 0xff, 
+0xfb, 0x9f, 0xef, 0xaf, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xbf, 0x5f, 0xd7, 0x20, 
+0x6b, 0xef, 0x77, 0x5f, 0xef, 0xff, 0xff, 0xbf, 0xbf, 0xff, 0x83, 0x3a, 0xad, 
+0xff, 0x77, 0xbf, 0xef, 0xff, 0xff, 0xdf, 0xbf, 0xff, 0xd7, 0xe0, 0xde, 0xfe, 
+0x77, 0x5f, 0x83, 0x3f, 0xf8, 0xef, 0xbf, 0xff, 0x83, 0x6b, 0xa9, 0xfe, 0x77, 
+0xff, 0xef, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xd7, 0xa0, 0x69, 0xff, 0x77, 0xff, 
+0xef, 0xfb, 0xff, 0xfb, 0xbf, 0xff, 0xff, 0xfb, 0x9f, 0xfe, 0xaf, 0xff, 0xff, 
+0xfb, 0xbf, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xe0, 0x0e, 0x82, 0x2e, 0x08, 0x82, 0x21, 0xf8, 0xff, 0xff, 0xff, 
+0xc7, 0x2e, 0xfe, 0xbe, 0xae, 0xef, 0xbf, 0xae, 0xfb, 0xff, 0xf7, 0xdf, 0xbb, 
+0xe6, 0xfe, 0xbe, 0xae, 0xef, 0xbf, 0xae, 0xdb, 0xf7, 0x33, 0x98, 0xbf, 0xea, 
+0x0e, 0x82, 0x20, 0x08, 0xde, 0x20, 0xf8, 0xff, 0xf1, 0x1f, 0xdf, 0xec, 0xee, 
+0xbf, 0xef, 0xeb, 0xee, 0xee, 0xfb, 0xff, 0x33, 0x98, 0xef, 0xee, 0xee, 0xbf, 
+0xef, 0xeb, 0xee, 0xee, 0xdb, 0xf7, 0xf7, 0xdf, 0xff, 0x20, 0x08, 0x82, 0x2f, 
+0x08, 0xee, 0xf0, 0xfb, 0xf7, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x71, 0x0c, 0xc7, 0x30, 0x08, 0xc6, 
+0x2e, 0xf8, 0xba, 0xbe, 0xeb, 0xc6, 0xae, 0xeb, 0xba, 0xae, 0xef, 0xbb, 0xee, 
+0xfe, 0xda, 0x3e, 0xc9, 0xba, 0xa2, 0xeb, 0xfa, 0xae, 0xef, 0xfb, 0xee, 0xfe, 
+0xea, 0xbe, 0xaa, 0xba, 0x2a, 0x08, 0xfb, 0x2e, 0x0c, 0xfb, 0xe0, 0xfe, 0xf2, 
+0xbe, 0x6b, 0xba, 0xa2, 0xeb, 0xfa, 0xae, 0xef, 0x9b, 0xee, 0xee, 0xea, 0xbe, 
+0xeb, 0xba, 0xbe, 0xeb, 0xba, 0xae, 0xef, 0xbb, 0xee, 0xee, 0xda, 0xbe, 0xeb, 
+0xba, 0xb1, 0x0b, 0xc7, 0x30, 0xe8, 0xc7, 0x2e, 0x18, 0xbb, 0xa0, 0xeb, 0xc6, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x70, 
+0x0c, 0x87, 0xa0, 0xeb, 0xba, 0xae, 0x0b, 0x9e, 0x3e, 0xbf, 0xff, 0xae, 0xeb, 
+0xfa, 0xbb, 0xeb, 0xba, 0xae, 0xfb, 0xde, 0x7e, 0x5f, 0xff, 0xae, 0xeb, 0xfa, 
+0xbb, 0xeb, 0xba, 0xb5, 0x7b, 0xdf, 0x7d, 0xff, 0xff, 0xb0, 0x0b, 0xc7, 0xbb, 
+0xeb, 0xba, 0x7b, 0xb8, 0xdf, 0x7b, 0xff, 0xff, 0xbe, 0x6a, 0xbf, 0xbb, 0x5b, 
+0xab, 0xf5, 0xdb, 0xdf, 0x77, 0xff, 0xff, 0xbe, 0xed, 0xbe, 0xbb, 0x5b, 0x93, 
+0xee, 0xeb, 0xdf, 0x6f, 0xff, 0xff, 0x7e, 0xea, 0xc2, 0x7b, 0xbc, 0xbb, 0x2e, 
+0x0c, 0x9e, 0x2f, 0xff, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xfd, 0xef, 0xff, 0xef, 0x3f, 0xfe, 0xfe, 0x7e, 0xfb, 
+0xf9, 0xff, 0xff, 0xfb, 0xef, 0xff, 0xef, 0xbf, 0xff, 0xfe, 0xff, 0xfb, 0xfb, 
+0xff, 0xff, 0x3f, 0x08, 0x82, 0x20, 0x08, 0x82, 0x60, 0x7e, 0xbb, 0x3b, 0x08, 
+0x82, 0xff, 0xeb, 0xfa, 0xae, 0xbb, 0xbb, 0xee, 0x7e, 0xdb, 0xbb, 0xea, 0xba, 
+0x3f, 0xe8, 0xfa, 0x2e, 0xb8, 0x83, 0xee, 0x7e, 0xeb, 0xbb, 0xea, 0xba, 0xbf, 
+0xeb, 0xfa, 0xae, 0xbf, 0xbf, 0xee, 0x7e, 0xd3, 0xbb, 0xea, 0xba, 0x3f, 0x08, 
+0x82, 0x20, 0xb8, 0x83, 0x6e, 0x8c, 0xbb, 0xb1, 0xea, 0x82, 0xff, 0xff, 0xff, 
+0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 
+0xff, 0xff, 0xff, 0xff, 0x83, 0x3b, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xff, 
+0xff, 0xff, 0xff, 0xbb, 0xbb, 0xdb, 0xfe, 0x20, 0x08, 0x82, 0xa0, 0xeb, 0xaa, 
+0xae, 0x0b, 0xba, 0xbb, 0x2b, 0xff, 0xae, 0xeb, 0xfb, 0xbb, 0xeb, 0xaa, 0xb5, 
+0x7b, 0xbb, 0xbb, 0xfa, 0xff, 0xae, 0xeb, 0x83, 0xbb, 0x6b, 0xab, 0x3b, 0xb8, 
+0xbb, 0xbb, 0xfb, 0xff, 0x20, 0xe8, 0xbf, 0xbb, 0xab, 0xab, 0xf5, 0xdb, 0xbb, 
+0xbb, 0xfb, 0xff, 0xfe, 0xeb, 0x83, 0x23, 0xc8, 0x83, 0x2e, 0x08, 0x82, 0x3b, 
+0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
+0xff  }};
+
+#endif
--- /dev/null
+++ b/src/inprint2.c
@@ -1,0 +1,149 @@
+// Bitmap font routine originally by driedfruit,
+// https://github.com/driedfruit/SDL_inprint Released into public domain.
+// Modified to support multiple fonts & adding a background to text.
+
+#include <SDL.h>
+
+#define CHARACTERS_PER_ROW 16   /* I like 16 x 8 fontsets. */
+#define CHARACTERS_PER_COLUMN 8 /* 128 x 1 is another popular format. */
+
+static SDL_Renderer *selected_renderer = NULL;
+static SDL_Texture *inline_font = NULL;
+static SDL_Texture *selected_font = NULL;
+static Uint16 selected_font_w, selected_font_h;
+
+void prepare_inline_font(unsigned char * bits, int font_width,
+                         int font_height) {
+  Uint32 *pix_ptr, tmp;
+  int i, len, j;
+  SDL_Surface *surface;
+  Uint32 colors[2];
+
+  selected_font_w = font_width;
+  selected_font_h = font_height;
+
+  if (inline_font != NULL) {
+    selected_font = inline_font;
+    return;
+  }
+
+  surface = SDL_CreateRGBSurface(0, font_width, font_height, 32,
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+                                 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff
+#else
+                                 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000
+#endif
+  );
+  colors[0] = SDL_MapRGBA(surface->format, 0xFF, 0xFF, 0xFF, 0xFF);
+  colors[1] = SDL_MapRGBA(surface->format, 0x00, 0x00, 0x00,
+                          0x00 /* or 0xFF, to have bg-color */);
+
+  /* Get pointer to pixels and array length */
+  pix_ptr = (Uint32 *)surface->pixels;
+  len = surface->h * surface->w / 8;
+
+  /* Copy */
+  for (i = 0; i < len; i++) {
+    tmp = (Uint8)bits[i];
+    for (j = 0; j < 8; j++) {
+      Uint8 mask = (0x01 << j);
+      pix_ptr[i * 8 + j] = colors[(tmp & mask) >> j];
+    }
+  }
+
+  inline_font = SDL_CreateTextureFromSurface(selected_renderer, surface);
+  SDL_FreeSurface(surface);
+
+  selected_font = inline_font;
+}
+void kill_inline_font(void) {
+  SDL_DestroyTexture(inline_font);
+  inline_font = NULL;
+}
+void inrenderer(SDL_Renderer *renderer) { selected_renderer = renderer; }
+void infont(SDL_Texture *font) {
+  Uint32 format;
+  int access;
+  int w, h;
+
+  if (font == NULL) {
+    // prepare_inline_font();
+    return;
+  }
+
+  SDL_QueryTexture(font, &format, &access, &w, &h);
+
+  selected_font = font;
+  selected_font_w = w;
+  selected_font_h = h;
+}
+void incolor1(SDL_Color *color) {
+  SDL_SetTextureColorMod(selected_font, color->r, color->g, color->b);
+}
+void incolor(Uint32 fore,
+             Uint32 unused) /* Color must be in 0x00RRGGBB format ! */
+{
+  SDL_Color pal[1];
+  pal[0].r = (Uint8)((fore & 0x00FF0000) >> 16);
+  pal[0].g = (Uint8)((fore & 0x0000FF00) >> 8);
+  pal[0].b = (Uint8)((fore & 0x000000FF));
+  SDL_SetTextureColorMod(selected_font, pal[0].r, pal[0].g, pal[0].b);
+}
+void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y,
+             Uint32 fgcolor, Uint32 bgcolor) {
+  SDL_Rect s_rect;
+  SDL_Rect d_rect;
+  SDL_Rect bg_rect;
+
+  static uint32_t previous_fgcolor;
+
+  d_rect.x = x;
+  d_rect.y = y;
+  s_rect.w = selected_font_w / CHARACTERS_PER_ROW;
+  s_rect.h = selected_font_h / CHARACTERS_PER_COLUMN;
+  d_rect.w = s_rect.w;
+  d_rect.h = s_rect.h;
+
+  if (dst == NULL)
+    dst = selected_renderer;
+
+  for (; *str; str++) {
+    int id = (int)*str;
+#if (CHARACTERS_PER_COLUMN != 1)
+    int row = id / CHARACTERS_PER_ROW;
+    int col = id % CHARACTERS_PER_ROW;
+    s_rect.x = col * s_rect.w;
+    s_rect.y = row * s_rect.h;
+#else
+    s_rect.x = id * s_rect.w;
+    s_rect.y = 0;
+#endif
+    if (id == '\n') {
+      d_rect.x = x;
+      d_rect.y += s_rect.h;
+      continue;
+    }
+    if (fgcolor != previous_fgcolor) {
+      incolor(fgcolor, 0);
+      previous_fgcolor = fgcolor;
+    }
+
+    if (bgcolor != -1) {
+      SDL_SetRenderDrawColor(selected_renderer,
+                             (Uint8)((bgcolor & 0x00FF0000) >> 16),
+                             (Uint8)((bgcolor & 0x0000FF00) >> 8),
+                             (Uint8)((bgcolor & 0x000000FF)), 0xFF);
+      bg_rect = d_rect;
+      bg_rect.w = selected_font_w / CHARACTERS_PER_ROW - 1;
+      // Silly hack to get big font background aligned correctly.
+      if (bg_rect.h == 11) {
+        bg_rect.y++;
+      }
+
+      SDL_RenderFillRect(dst, &bg_rect);
+    }
+    SDL_RenderCopy(dst, selected_font, &s_rect, &d_rect);
+    d_rect.x += s_rect.w;
+  }
+}
+SDL_Texture *get_inline_font(void) { return selected_font; }
--- /dev/null
+++ b/src/input.c
@@ -1,0 +1,481 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#include <SDL.h>
+#include <stdio.h>
+
+#include "SDL_timer.h"
+#include "config.h"
+#include "input.h"
+#include "render.h"
+
+#define MAX_CONTROLLERS 4
+
+SDL_GameController *game_controllers[MAX_CONTROLLERS];
+
+// Bits for M8 input messages
+enum keycodes {
+  key_left = 1 << 7,
+  key_up = 1 << 6,
+  key_down = 1 << 5,
+  key_select = 1 << 4,
+  key_start = 1 << 3,
+  key_right = 1 << 2,
+  key_opt = 1 << 1,
+  key_edit = 1
+};
+
+uint8_t keyjazz_enabled = 0;
+uint8_t keyjazz_base_octave = 2;
+uint8_t keyjazz_velocity = 0x64;
+
+static uint8_t keycode = 0; // value of the pressed key
+static int num_joysticks = 0;
+
+input_msg_s key = {normal, 0};
+
+uint8_t toggle_input_keyjazz() {
+  keyjazz_enabled = !keyjazz_enabled;
+  return keyjazz_enabled;
+}
+
+// Opens available game controllers and returns the amount of opened controllers
+int initialize_game_controllers() {
+
+  num_joysticks = SDL_NumJoysticks();
+  int controller_index = 0;
+
+  SDL_Log("Looking for game controllers\n");
+  SDL_Delay(
+      10); // Some controllers like XBone wired need a little while to get ready
+
+  // Try to load the game controller database file
+  char db_filename[1024] = {0};
+  snprintf(db_filename, sizeof(db_filename), "%sgamecontrollerdb.txt",
+           SDL_GetPrefPath("", "m8c"));
+  SDL_Log("Trying to open game controller database from %s", db_filename);
+  SDL_RWops* db_rw = SDL_RWFromFile(db_filename, "rb");
+  if (db_rw == NULL) {
+    snprintf(db_filename, sizeof(db_filename), "%sgamecontrollerdb.txt",
+    SDL_GetBasePath());
+    SDL_Log("Trying to open game controller database from %s", db_filename);
+    db_rw = SDL_RWFromFile(db_filename, "rb");
+  }
+
+  if (db_rw != NULL) {
+    int mappings = SDL_GameControllerAddMappingsFromRW(db_rw, 1);
+    if (mappings != -1)
+      SDL_Log("Found %d game controller mappings", mappings);
+    else
+      SDL_LogError(SDL_LOG_CATEGORY_INPUT,
+                   "Error loading game controller mappings.");
+  } else {
+    SDL_LogError(SDL_LOG_CATEGORY_INPUT,
+                 "Unable to open game controller database file.");
+  }
+
+  // Open all available game controllers
+  for (int i = 0; i < num_joysticks; i++) {
+    if (!SDL_IsGameController(i))
+      continue;
+    if (controller_index >= MAX_CONTROLLERS)
+      break;
+    game_controllers[controller_index] = SDL_GameControllerOpen(i);
+    SDL_Log("Controller %d: %s", controller_index + 1,
+            SDL_GameControllerName(game_controllers[controller_index]));
+    controller_index++;
+  }
+
+  return controller_index;
+}
+
+// Closes all open game controllers
+void close_game_controllers() {
+
+  for (int i = 0; i < MAX_CONTROLLERS; i++) {
+    if (game_controllers[i])
+      SDL_GameControllerClose(game_controllers[i]);
+  }
+}
+
+static input_msg_s handle_keyjazz(SDL_Event *event, uint8_t keyvalue) {
+  input_msg_s key = {keyjazz, keyvalue, keyjazz_velocity, event->type};
+  switch (event->key.keysym.scancode) {
+  case SDL_SCANCODE_Z:
+    key.value = keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_S:
+    key.value = 1 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_X:
+    key.value = 2 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_D:
+    key.value = 3 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_C:
+    key.value = 4 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_V:
+    key.value = 5 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_G:
+    key.value = 6 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_B:
+    key.value = 7 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_H:
+    key.value = 8 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_N:
+    key.value = 9 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_J:
+    key.value = 10 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_M:
+    key.value = 11 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_Q:
+    key.value = 12 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_2:
+    key.value = 13 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_W:
+    key.value = 14 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_3:
+    key.value = 15 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_E:
+    key.value = 16 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_R:
+    key.value = 17 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_5:
+    key.value = 18 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_T:
+    key.value = 19 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_6:
+    key.value = 20 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_Y:
+    key.value = 21 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_7:
+    key.value = 22 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_U:
+    key.value = 23 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_I:
+    key.value = 24 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_9:
+    key.value = 25 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_O:
+    key.value = 26 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_0:
+    key.value = 27 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_P:
+    key.value = 28 + keyjazz_base_octave * 12;
+    break;
+  case SDL_SCANCODE_KP_DIVIDE:
+    key.type = normal;
+    if (event->type == SDL_KEYDOWN && keyjazz_base_octave > 0) {
+      keyjazz_base_octave--;
+      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
+    }
+    break;
+  case SDL_SCANCODE_KP_MULTIPLY:
+    key.type = normal;
+    if (event->type == SDL_KEYDOWN && keyjazz_base_octave < 8) {
+      keyjazz_base_octave++;
+      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
+    }
+    break;
+  case SDL_SCANCODE_KP_MINUS:
+    key.type = normal;
+    if (event->type == SDL_KEYDOWN) {
+      if ((event->key.keysym.mod & KMOD_ALT) > 0) {
+        if (keyjazz_velocity > 1)
+          keyjazz_velocity -= 1;
+      } else {
+        if (keyjazz_velocity > 0x10)
+          keyjazz_velocity -= 0x10;
+      }
+      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
+    }
+    break;
+  case SDL_SCANCODE_KP_PLUS:
+    key.type = normal;
+    if (event->type == SDL_KEYDOWN) {
+      if ((event->key.keysym.mod & KMOD_ALT) > 0) {
+        if (keyjazz_velocity < 0x7F)
+          keyjazz_velocity += 1;
+      } else {
+        if (keyjazz_velocity < 0x6F)
+          keyjazz_velocity += 0x10;
+      }
+      display_keyjazz_overlay(1, keyjazz_base_octave, keyjazz_velocity);
+    }
+    break;
+  default:
+    key.type = normal;
+    break;
+  }
+
+  return key;
+}
+
+static input_msg_s handle_normal_keys(SDL_Event *event, config_params_s *conf,
+                                      uint8_t keyvalue) {
+  input_msg_s key = {normal, keyvalue};
+
+  if (event->key.keysym.scancode == conf->key_up) {
+    key.value = key_up;
+  } else if (event->key.keysym.scancode == conf->key_left) {
+    key.value = key_left;
+  } else if (event->key.keysym.scancode == conf->key_down) {
+    key.value = key_down;
+  } else if (event->key.keysym.scancode == conf->key_right) {
+    key.value = key_right;
+  } else if (event->key.keysym.scancode == conf->key_select ||
+             event->key.keysym.scancode == conf->key_select_alt) {
+    key.value = key_select;
+  } else if (event->key.keysym.scancode == conf->key_start ||
+             event->key.keysym.scancode == conf->key_start_alt) {
+    key.value = key_start;
+  } else if (event->key.keysym.scancode == conf->key_opt ||
+             event->key.keysym.scancode == conf->key_opt_alt) {
+    key.value = key_opt;
+  } else if (event->key.keysym.scancode == conf->key_edit ||
+             event->key.keysym.scancode == conf->key_edit_alt) {
+    key.value = key_edit;
+  } else if (event->key.keysym.scancode == conf->key_delete) {
+    key.value = key_opt | key_edit;
+  } else if (event->key.keysym.scancode == conf->key_reset) {
+    key = (input_msg_s){special, msg_reset_display};
+  } else {
+    key.value = 0;
+  }
+  return key;
+}
+
+// Check whether a button is pressed on a gamepad and return 1 if pressed.
+static int get_game_controller_button(config_params_s *conf,
+                                      SDL_GameController *controller,
+                                      int button) {
+
+  const int button_mappings[8] = {conf->gamepad_up,     conf->gamepad_down,
+                                  conf->gamepad_left,   conf->gamepad_right,
+                                  conf->gamepad_opt,    conf->gamepad_edit,
+                                  conf->gamepad_select, conf->gamepad_start};
+
+  // Check digital buttons
+  if (SDL_GameControllerGetButton(controller, button_mappings[button])) {
+    return 1;
+  } else {
+    // If digital button isn't pressed, check the corresponding analog control
+    switch (button) {
+    case INPUT_UP:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_updown) <
+             -conf->gamepad_analog_threshold;
+    case INPUT_DOWN:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_updown) >
+             conf->gamepad_analog_threshold;
+    case INPUT_LEFT:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_leftright) <
+             -conf->gamepad_analog_threshold;
+    case INPUT_RIGHT:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_leftright) >
+             conf->gamepad_analog_threshold;
+    case INPUT_OPT:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_opt) >
+             conf->gamepad_analog_threshold;
+    case INPUT_EDIT:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_edit) >
+             conf->gamepad_analog_threshold;
+    case INPUT_SELECT:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_select) >
+             conf->gamepad_analog_threshold;
+    case INPUT_START:
+      return SDL_GameControllerGetAxis(controller,
+                                       conf->gamepad_analog_axis_start) >
+             conf->gamepad_analog_threshold;
+    default:
+      return 0;
+    }
+  }
+  return 0;
+}
+
+// Handle game controllers, simply check all buttons and analog axis on every
+// cycle
+static int handle_game_controller_buttons(config_params_s *conf) {
+
+  const int keycodes[8] = {key_up,  key_down, key_left,   key_right,
+                           key_opt, key_edit, key_select, key_start};
+
+  int key = 0;
+
+  // Cycle through every active game controller
+  for (int gc = 0; gc < num_joysticks; gc++) {
+    // Cycle through all M8 buttons
+    for (int button = 0; button < (input_buttons_t)INPUT_MAX; button++) {
+      // If the button is active, add the keycode to the variable containing
+      // active keys
+      if (get_game_controller_button(conf, game_controllers[gc], button)) {
+        key |= keycodes[button];
+      }
+    }
+  }
+
+  return key;
+}
+
+// Handles SDL input events
+void handle_sdl_events(config_params_s *conf) {
+
+  static int prev_key_analog = 0;
+
+  SDL_Event event;
+
+  // Read joysticks
+  int key_analog = handle_game_controller_buttons(conf);
+  if (prev_key_analog != key_analog) {
+    keycode = key_analog;
+    prev_key_analog = key_analog;
+  }
+
+  // Read special case game controller buttons quit and reset
+  for (int gc = 0; gc < num_joysticks; gc++) {
+    if (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_quit) &&
+        (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_select) ||
+        SDL_GameControllerGetAxis(game_controllers[gc], conf->gamepad_analog_axis_select)))
+      key = (input_msg_s){special, msg_quit};
+    else if (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_reset) &&
+            (SDL_GameControllerGetButton(game_controllers[gc], conf->gamepad_select) ||
+              SDL_GameControllerGetAxis(game_controllers[gc], conf->gamepad_analog_axis_select)))
+      key = (input_msg_s){special, msg_reset_display};
+  }
+
+  SDL_PollEvent(&event);
+
+  switch (event.type) {
+
+  // Reinitialize game controllers on controller add/remove/remap
+  case SDL_CONTROLLERDEVICEADDED:
+  case SDL_CONTROLLERDEVICEREMOVED:
+    initialize_game_controllers();
+    break;
+
+  // Handle SDL quit events (for example, window close)
+  case SDL_QUIT:
+    key = (input_msg_s){special, msg_quit};
+    break;
+
+  case SDL_WINDOWEVENT:
+    if (event.window.event == SDL_WINDOWEVENT_RESIZED)
+    {
+      static uint32_t ticks_window_resized = 0;
+      if (SDL_GetTicks() - ticks_window_resized > 500) {
+        SDL_Log("Resizing window...");
+        key = (input_msg_s){special, msg_reset_display};
+        ticks_window_resized = SDL_GetTicks();
+      }
+    }
+    break;
+
+  // Keyboard events. Special events are handled within SDL_KEYDOWN.
+  case SDL_KEYDOWN:
+
+    // ALT+ENTER toggles fullscreen
+    if (event.key.keysym.sym == SDLK_RETURN &&
+        (event.key.keysym.mod & KMOD_ALT) > 0) {
+      toggle_fullscreen();
+      break;
+    }
+
+    // ALT+F4 quits program
+    if (event.key.keysym.sym == SDLK_F4 &&
+        (event.key.keysym.mod & KMOD_ALT) > 0) {
+      key = (input_msg_s){special, msg_quit};
+      break;
+    }
+
+    // ESC = toggle keyjazz
+    if (event.key.keysym.sym == SDLK_ESCAPE) {
+      display_keyjazz_overlay(toggle_input_keyjazz(), keyjazz_base_octave, keyjazz_velocity);
+    }
+
+  // Normal keyboard inputs
+  case SDL_KEYUP:
+    key = handle_normal_keys(&event, conf, 0);
+
+    if (keyjazz_enabled)
+      key = handle_keyjazz(&event, key.value);
+    break;
+
+  default:
+    break;
+  }
+
+  switch (key.type) {
+  case normal:
+    if (event.type == SDL_KEYDOWN) {
+      keycode |= key.value;
+    } else {
+      keycode &= ~key.value;
+    }
+    break;
+  case keyjazz:
+    // Do not allow pressing multiple keys with keyjazz
+  case special:
+    if (event.type == SDL_KEYDOWN) {
+      keycode = key.value;
+    } else {
+      keycode = 0;
+    }
+    break;
+  default:
+    break;
+  }
+}
+
+// Returns the currently pressed keys to main
+input_msg_s get_input_msg(config_params_s *conf) {
+
+  key = (input_msg_s){normal, 0};
+
+  // Query for SDL events
+  handle_sdl_events(conf);
+
+  if (keycode == (key_start | key_select | key_opt | key_edit)) {
+    key = (input_msg_s){special, msg_reset_display};
+  }
+
+  if (key.type == normal) {
+    /* Normal input keys go through some event-based manipulation in
+       handle_sdl_events(), the value is stored in keycode variable */
+    return (input_msg_s){key.type, keycode};
+  } else {
+    // Special event keys already have the correct keycode baked in
+    return key;
+  }
+}
--- /dev/null
+++ b/src/input.h
@@ -1,0 +1,44 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#ifndef INPUT_H_
+#define INPUT_H_
+
+#include <stdint.h>
+#include "config.h"
+
+typedef enum input_buttons_t {
+    INPUT_UP,
+    INPUT_DOWN,
+    INPUT_LEFT,
+    INPUT_RIGHT,
+    INPUT_OPT,
+    INPUT_EDIT,
+    INPUT_SELECT,
+    INPUT_START,
+    INPUT_MAX
+} input_buttons_t;
+
+typedef enum input_type_t {
+  normal,
+  keyjazz,
+  special
+} input_type_t;
+
+typedef enum special_messages_t {
+  msg_quit = 1,
+  msg_reset_display = 2
+} special_messages_t;
+
+typedef struct input_msg_s {
+  input_type_t type;
+  uint8_t value;
+  uint8_t value2;
+  uint32_t eventType;
+} input_msg_s;
+
+int initialize_game_controllers();
+void close_game_controllers();
+input_msg_s get_input_msg(config_params_s *conf);
+
+#endif
--- /dev/null
+++ b/src/main.c
@@ -1,0 +1,285 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+/* Uncomment this line to enable debug messages or call make with `make
+   CFLAGS=-DDEBUG_MSG` */
+// #define DEBUG_MSG
+
+#include <SDL.h>
+#include <signal.h>
+
+#include "SDL2_inprint.h"
+#include "audio.h"
+#include "command.h"
+#include "config.h"
+#include "input.h"
+#include "render.h"
+#include "serial.h"
+#include "slip.h"
+
+enum state { QUIT, WAIT_FOR_DEVICE, RUN };
+
+enum state run = WAIT_FOR_DEVICE;
+uint8_t need_display_reset = 0;
+
+// Handles CTRL+C / SIGINT
+void intHandler(int dummy) { run = QUIT; }
+
+void close_serial_port() { disconnect(); }
+
+int main(int argc, char *argv[]) {
+  // Initialize the config to defaults read in the params from the
+  // configfile if present
+  config_params_s conf = init_config();
+
+  // TODO: take cli parameter to override default configfile location
+  read_config(&conf);
+
+  // allocate memory for serial buffer
+  uint8_t *serial_buf = SDL_malloc(serial_read_size);
+
+  static uint8_t slip_buffer[serial_read_size]; // SLIP command buffer
+
+  SDL_zero(slip_buffer);
+
+  // settings for the slip packet handler
+  static const slip_descriptor_s slip_descriptor = {
+      .buf = slip_buffer,
+      .buf_size = sizeof(slip_buffer),
+      .recv_message = process_command, // the function where complete slip
+                                       // packets are processed further
+  };
+
+  static slip_handler_s slip;
+
+  uint8_t prev_input = 0;
+  uint8_t prev_note = 0;
+  uint16_t zerobyte_packets = 0; // used to detect device disconnection
+
+  signal(SIGINT, intHandler);
+  signal(SIGTERM, intHandler);
+#ifdef SIGQUIT
+  signal(SIGQUIT, intHandler);
+#endif
+  slip_init(&slip, &slip_descriptor);
+
+  // First device detection to avoid SDL init if it isn't necessary. To be run
+  // only if we shouldn't wait for M8 to be connected.
+  if (conf.wait_for_device == 0) {
+    if (init_serial(1) == 0) {
+      SDL_free(serial_buf);
+      return -1;
+    }
+  }
+
+  // initialize all SDL systems
+  if (initialize_sdl(conf.init_fullscreen, conf.init_use_gpu) == -1)
+    run = QUIT;
+
+  // initial scan for (existing) game controllers
+  initialize_game_controllers();
+
+#ifdef DEBUG_MSG
+  SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
+#endif
+
+  // main loop begin
+  do {
+    // try to init serial port
+    int port_inited = init_serial(1);
+    // if port init was successful, try to enable and reset display
+    if (port_inited == 1 && enable_and_reset_display(0) == 1) {
+      // if audio routing is enabled, try to initialize audio devices
+      if (conf.audio_enabled == 1) {
+        audio_init(conf.audio_buffer_size, conf.audio_device_name);
+        // if audio is enabled, reset the display for second time to avoid glitches
+        reset_display();
+      }
+      run = RUN;
+    } else {
+      SDL_LogCritical(SDL_LOG_CATEGORY_ERROR,
+                      "Device not detected on begin loop.");
+      if (conf.wait_for_device == 1) {
+        run = WAIT_FOR_DEVICE;
+      } else {
+        run = QUIT;
+      }
+    }
+
+    // wait until device is connected
+    if (conf.wait_for_device == 1) {
+      static uint32_t ticks_poll_device = 0;
+      static uint32_t ticks_update_screen = 0;
+
+      if (port_inited == 0) {
+        screensaver_init();
+      }
+
+      while (run == WAIT_FOR_DEVICE) {
+        // get current input
+        input_msg_s input = get_input_msg(&conf);
+        if (input.type == special && input.value == msg_quit) {
+          SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Input message QUIT.");
+          run = QUIT;
+        }
+
+        if (SDL_GetTicks() - ticks_update_screen > 16) {
+          ticks_update_screen = SDL_GetTicks();
+          screensaver_draw();
+          render_screen();
+        }
+
+        // Poll for M8 device every second
+        if (port_inited == 0 && (SDL_GetTicks() - ticks_poll_device > 1000)) {
+          ticks_poll_device = SDL_GetTicks();
+          if (run == WAIT_FOR_DEVICE && init_serial(0) == 1) {
+
+            if (conf.audio_enabled == 1) {
+              if (audio_init(conf.audio_buffer_size, conf.audio_device_name) ==
+                  0) {
+                SDL_Log("Cannot initialize audio");
+                conf.audio_enabled = 0;
+              }
+            }
+
+            int result = enable_and_reset_display();
+            // Device was found; enable display and proceed to the main loop
+            if (result == 1) {
+              run = RUN;
+              port_inited = 1;
+              screensaver_destroy();
+            } else {
+              SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Device not detected.");
+              run = QUIT;
+              screensaver_destroy();
+            }
+          }
+        }
+        SDL_Delay(conf.idle_ms);
+      }
+    } else {
+      // classic startup behaviour, exit if device is not found
+      if (port_inited == 0) {
+        if (conf.audio_enabled == 1) {
+          audio_destroy();
+        }
+        close_game_controllers();
+        close_renderer();
+        kill_inline_font();
+        SDL_free(serial_buf);
+        SDL_Quit();
+        return -1;
+      }
+    }
+
+    // main loop
+    while (run == RUN) {
+
+      // get current inputs
+      input_msg_s input = get_input_msg(&conf);
+
+      switch (input.type) {
+      case normal:
+        if (input.value != prev_input) {
+          prev_input = input.value;
+          send_msg_controller(input.value);
+        }
+        break;
+      case keyjazz:
+        if (input.value != 0) {
+          if (input.eventType == SDL_KEYDOWN && input.value != prev_input) {
+            send_msg_keyjazz(input.value, input.value2);
+            prev_note = input.value;
+          } else if (input.eventType == SDL_KEYUP && input.value == prev_note) {
+            send_msg_keyjazz(0xFF, 0);
+          }
+        }
+        prev_input = input.value;
+        break;
+      case special:
+        if (input.value != prev_input) {
+          prev_input = input.value;
+          switch (input.value) {
+          case msg_quit:
+            SDL_Log("Received msg_quit from input device.");
+            run = 0;
+            break;
+          case msg_reset_display:
+            reset_display();
+            break;
+          default:
+            break;
+          }
+          break;
+        }
+      }
+
+      while (1) {
+        // read serial port
+        int bytes_read = serial_read(serial_buf, serial_read_size);
+        if (bytes_read < 0) {
+          SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Error %d reading serial. \n",
+                          (int)bytes_read);
+          run = QUIT;
+          break;
+        } else if (bytes_read > 0) {
+          // input from device: reset the zero byte counter and create a
+          // pointer to the serial buffer
+          zerobyte_packets = 0;
+          uint8_t *cur = serial_buf;
+          const uint8_t *end = serial_buf + bytes_read;
+          while (cur < end) {
+            // process the incoming bytes into commands and draw them
+            int n = slip_read_byte(&slip, *(cur++));
+            if (n != SLIP_NO_ERROR) {
+              if (n == SLIP_ERROR_INVALID_PACKET) {
+                reset_display();
+              } else {
+                SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SLIP error %d\n", n);
+              }
+            }
+          }
+        } else {
+          // zero byte packet, increment counter
+          zerobyte_packets++;
+          if (zerobyte_packets > conf.wait_packets) {
+            zerobyte_packets = 0;
+
+            // try opening the serial port to check if it's alive
+            if (check_serial_port()) {
+              // the device is still there, carry on
+              break;
+            } else {
+              port_inited = 0;
+              run = WAIT_FOR_DEVICE;
+              close_serial_port();
+              if (conf.audio_enabled == 1) {
+                audio_destroy();
+              }
+              /* we'll make one more loop to see if the device is still there
+               * but just sending zero bytes. if it doesn't get detected when
+               * resetting the port, it will disconnect */
+            }
+          }
+          break;
+        }
+      }
+      render_screen();
+      SDL_Delay(conf.idle_ms);
+    }
+  } while (run > QUIT);
+  // main loop end
+
+  // exit, clean up
+  SDL_Log("Shutting down\n");
+  if (conf.audio_enabled == 1) {
+    audio_destroy();
+  }
+  close_game_controllers();
+  close_renderer();
+  close_serial_port();
+  SDL_free(serial_buf);
+  kill_inline_font();
+  SDL_Quit();
+  return 0;
+}
--- /dev/null
+++ b/src/render.c
@@ -1,0 +1,311 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#include "render.h"
+
+#include <SDL.h>
+#include <stdio.h>
+
+#include "SDL2_inprint.h"
+#include "command.h"
+#include "fx_cube.h"
+
+#include "inline_font.h"
+#include "inline_font_large.h"
+#include "inline_font_small.h"
+
+SDL_Window *win;
+SDL_Renderer *rend;
+SDL_Texture *maintexture;
+SDL_Color background_color =
+    (SDL_Color){.r = 0x00, .g = 0x00, .b = 0x00, .a = 0x00};
+
+static uint32_t ticks_fps;
+static int fps;
+static int large_font_enabled = 0;
+static int screen_offset_y = 0;
+
+uint8_t fullscreen = 0;
+
+static uint8_t dirty = 0;
+
+// Initializes SDL and creates a renderer and required surfaces
+int initialize_sdl(int init_fullscreen, int init_use_gpu) {
+  // ticks = SDL_GetTicks();
+
+  const int window_width = 640;  // SDL window width
+  const int window_height = 480; // SDL window height
+
+  if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
+    SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "SDL_Init: %s\n", SDL_GetError());
+    return -1;
+  }
+  // SDL documentation recommends this
+  atexit(SDL_Quit);
+
+  win = SDL_CreateWindow("m8c", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
+                         window_width, window_height,
+                         SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL |
+                             SDL_WINDOW_RESIZABLE | init_fullscreen);
+
+  rend = SDL_CreateRenderer(
+      win, -1, init_use_gpu ? SDL_RENDERER_ACCELERATED : SDL_RENDERER_SOFTWARE);
+
+  SDL_RenderSetLogicalSize(rend, 320, 240);
+
+  maintexture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888,
+                                  SDL_TEXTUREACCESS_TARGET, 320, 240);
+
+  SDL_SetRenderTarget(rend, maintexture);
+
+  SDL_SetRenderDrawColor(rend, background_color.r, background_color.g,
+                         background_color.b, background_color.a);
+
+  SDL_RenderClear(rend);
+
+  // Initialize a texture for the font and read the inline font bitmap
+  inrenderer(rend);
+  struct inline_font *font = &inline_font_small;
+  prepare_inline_font(font->bits, font->width, font->height);
+
+  SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO);
+
+  dirty = 1;
+
+  return 1;
+}
+
+static void change_font(struct inline_font *font) {
+  kill_inline_font();
+  prepare_inline_font(font->bits, font->width, font->height);
+}
+
+void set_large_mode(int enabled) {
+  if (enabled) {
+    large_font_enabled = 1;
+    screen_offset_y = 40;
+    change_font(&inline_font_large);
+  } else {
+    large_font_enabled = 0;
+    screen_offset_y = 0;
+    change_font(&inline_font_small);
+  }
+}
+
+void close_renderer() {
+  kill_inline_font();
+  SDL_DestroyTexture(maintexture);
+  SDL_DestroyRenderer(rend);
+  SDL_DestroyWindow(win);
+}
+
+void toggle_fullscreen() {
+
+  int fullscreen_state = SDL_GetWindowFlags(win) & SDL_WINDOW_FULLSCREEN;
+
+  SDL_SetWindowFullscreen(win,
+                          fullscreen_state ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP);
+  SDL_ShowCursor(fullscreen_state);
+
+  dirty = 1;
+}
+
+int draw_character(struct draw_character_command *command) {
+
+  uint32_t fgcolor = (command->foreground.r << 16) |
+                     (command->foreground.g << 8) | command->foreground.b;
+  uint32_t bgcolor = (command->background.r << 16) |
+                     (command->background.g << 8) | command->background.b;
+
+  /* Notes:
+     If large font is enabled, offset the screen elements by a fixed amount.
+     If background and foreground colors are the same, draw transparent
+     background. Due to the font bitmaps, a different pixel offset is needed for
+     both*/
+
+  inprint(rend, (char *)&command->c, command->pos.x,
+          command->pos.y + (large_font_enabled ? 2 : 3) - screen_offset_y,
+          fgcolor, (bgcolor == fgcolor) ? -1 : bgcolor);
+
+  dirty = 1;
+
+  return 1;
+}
+
+void draw_rectangle(struct draw_rectangle_command *command) {
+
+  SDL_Rect render_rect;
+
+  render_rect.x = command->pos.x;
+  if (large_font_enabled == 1) {
+    render_rect.y = command->pos.y - screen_offset_y;
+  } else {
+    render_rect.y = command->pos.y;
+  }
+  render_rect.h = command->size.height;
+  render_rect.w = command->size.width;
+
+  // Background color changed
+  if (render_rect.x == 0 && render_rect.y <= 0 && render_rect.w == 320 &&
+      render_rect.h >= 240) {
+    SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "BG color change: %d %d %d",command->color.r,command->color.g,command->color.b);
+    background_color.r = command->color.r;
+    background_color.g = command->color.g;
+    background_color.b = command->color.b;
+    background_color.a = 0xFF;
+
+#ifdef __ANDROID__
+    int bgcolor =
+        (command->color.r << 16) | (command->color.g << 8) | command->color.b;
+    SDL_AndroidSendMessage(0x8001, bgcolor);
+#endif
+  }
+
+  SDL_SetRenderDrawColor(rend, command->color.r, command->color.g,
+                         command->color.b, 0xFF);
+  SDL_RenderFillRect(rend, &render_rect);
+
+  dirty = 1;
+}
+
+void draw_waveform(struct draw_oscilloscope_waveform_command *command) {
+
+  static uint8_t wfm_cleared = 0;
+  static int prev_waveform_size = 0;
+
+  // If the waveform is not being displayed and it's already been cleared, skip
+  // rendering it
+  if (!(wfm_cleared && command->waveform_size == 0)) {
+
+    SDL_Rect wf_rect;
+    if (command->waveform_size > 0) {
+      wf_rect.x = 320 - command->waveform_size;
+      wf_rect.y = 0;
+      wf_rect.w = command->waveform_size;
+      wf_rect.h = 21;
+    } else {
+      wf_rect.x = 320 - prev_waveform_size;
+      wf_rect.y = 0;
+      wf_rect.w = prev_waveform_size;
+      wf_rect.h = 21;
+    }
+    prev_waveform_size = command->waveform_size;
+
+    SDL_SetRenderDrawColor(rend, background_color.r, background_color.g,
+                           background_color.b, background_color.a);
+    SDL_RenderFillRect(rend, &wf_rect);
+
+    SDL_SetRenderDrawColor(rend, command->color.r, command->color.g,
+                           command->color.b, 255);
+
+    // Create a SDL_Point array of the waveform pixels for batch drawing
+    SDL_Point waveform_points[command->waveform_size];
+
+    for (int i = 0; i < command->waveform_size; i++) {
+      // Limit value because the oscilloscope commands seem to glitch
+      // occasionally
+      if (command->waveform[i] > 20) {
+        command->waveform[i] = 20;
+      }
+      waveform_points[i].x = i + wf_rect.x;
+      waveform_points[i].y = command->waveform[i];
+    }
+
+    SDL_RenderDrawPoints(rend, waveform_points, command->waveform_size);
+
+    // The packet we just drew was an empty waveform
+    if (command->waveform_size == 0) {
+      wfm_cleared = 1;
+    } else {
+      wfm_cleared = 0;
+    }
+
+    dirty = 1;
+  }
+}
+
+void display_keyjazz_overlay(uint8_t show, uint8_t base_octave,
+                             uint8_t velocity) {
+
+  if (show) {
+    struct draw_rectangle_command drc;
+    drc.color = (struct color){255, 0, 0};
+    drc.pos.x = 310;
+    drc.pos.y = 230;
+    drc.size.width = 5;
+    drc.size.height = 5;
+
+    draw_rectangle(&drc);
+
+    struct draw_character_command dcc;
+    dcc.background = (struct color){background_color.r, background_color.g,
+                                    background_color.b};
+    dcc.foreground = (struct color){200, 200, 200};
+    dcc.pos.x = 296;
+    dcc.pos.y = 226;
+
+    draw_character(&dcc);
+
+    char buf[8];
+    snprintf(buf, sizeof(buf), "%02X %u", velocity, base_octave);
+
+    for (int i = 3; i >= 0; i--) {
+      dcc.c = buf[i];
+      draw_character(&dcc);
+      dcc.pos.x -= 8;
+    }
+
+  } else {
+    struct draw_rectangle_command drc;
+    drc.color = (struct color){background_color.r, background_color.g,
+                               background_color.b};
+    drc.pos.x = 272;
+    drc.pos.y = 226;
+    drc.size.width = 45;
+    drc.size.height = 14;
+
+    draw_rectangle(&drc);
+  }
+
+  dirty = 1;
+}
+
+void render_screen() {
+  if (dirty) {
+    dirty = 0;
+    // ticks = SDL_GetTicks();
+    SDL_SetRenderTarget(rend, NULL);
+
+    SDL_SetRenderDrawColor(rend, background_color.r, background_color.g,
+                           background_color.b, background_color.a);
+
+    SDL_RenderClear(rend);
+    SDL_RenderCopy(rend, maintexture, NULL, NULL);
+    SDL_RenderPresent(rend);
+    SDL_SetRenderTarget(rend, maintexture);
+
+    fps++;
+
+    if (SDL_GetTicks() - ticks_fps > 5000) {
+      ticks_fps = SDL_GetTicks();
+      SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "%.1f fps\n", (float)fps / 5);
+      fps = 0;
+    }
+  }
+}
+
+void screensaver_init() {
+  set_large_mode(1);
+  fx_cube_init(rend, (SDL_Color){255, 255, 255, 255});
+  SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Screensaver initialized");
+}
+
+void screensaver_draw() {
+  fx_cube_update();
+  dirty = 1;
+}
+
+void screensaver_destroy() {
+  fx_cube_destroy();
+  SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Screensaver destroyed");
+}
--- /dev/null
+++ b/src/render.h
@@ -1,0 +1,26 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#ifndef RENDER_H_
+#define RENDER_H_
+
+#include "command.h"
+
+int initialize_sdl(int init_fullscreen, int init_use_gpu);
+void close_renderer();
+
+void draw_waveform(struct draw_oscilloscope_waveform_command *command);
+void draw_rectangle(struct draw_rectangle_command *command);
+int draw_character(struct draw_character_command *command);
+void set_large_mode(int enabled);
+void view_changed(int view);
+
+void render_screen();
+void toggle_fullscreen();
+void display_keyjazz_overlay(uint8_t show, uint8_t base_octave, uint8_t velocity);
+
+void screensaver_init();
+void screensaver_draw();
+void screensaver_destroy();
+
+#endif
--- /dev/null
+++ b/src/ringbuffer.c
@@ -1,0 +1,61 @@
+#include "ringbuffer.h"
+#include <SDL.h>
+
+RingBuffer *ring_buffer_create(uint32_t size) {
+  RingBuffer *rb = SDL_malloc(sizeof(*rb));
+  rb->buffer = SDL_malloc(sizeof(*(rb->buffer)) * size);
+  rb->head = 0;
+  rb->tail = 0;
+  rb->max_size = size;
+  rb->size = 0;
+  return rb;
+}
+
+void ring_buffer_free(RingBuffer *rb) {
+  free(rb->buffer);
+  free(rb);
+}
+
+uint32_t ring_buffer_empty(RingBuffer *rb) {
+  return (rb->size == 0);
+}
+
+uint32_t ring_buffer_full(RingBuffer *rb) {
+  return (rb->size == rb->max_size);
+}
+
+uint32_t ring_buffer_push(RingBuffer *rb, const uint8_t *data, uint32_t length) {
+  if (ring_buffer_full(rb)) {
+    return -1; // buffer full, push fails
+  } else {
+    uint32_t space1 = rb->max_size - rb->tail;
+    uint32_t n = (length <= rb->max_size - rb->size) ? length : (rb->max_size - rb->size);
+    if (n <= space1) {
+      SDL_memcpy(rb->buffer + rb->tail, data, n);
+    } else {
+      SDL_memcpy(rb->buffer + rb->tail, data, space1);
+      SDL_memcpy(rb->buffer, data + space1, n - space1);
+    }
+    rb->tail = (rb->tail + n) % rb->max_size;
+    rb->size += n;
+    return n; // push successful, returns number of bytes pushed
+  }
+}
+
+uint32_t ring_buffer_pop(RingBuffer *rb, uint8_t *data, uint32_t length) {
+  if (ring_buffer_empty(rb)) {
+    return -1; // buffer empty, pop fails
+  } else {
+    uint32_t space1 = rb->max_size - rb->head;
+    uint32_t n = (length <= rb->size) ? length : rb->size;
+    if (n <= space1) {
+      SDL_memcpy(data, rb->buffer + rb->head, n);
+    } else {
+      SDL_memcpy(data, rb->buffer + rb->head, space1);
+      SDL_memcpy(data + space1, rb->buffer, n - space1);
+    }
+    rb->head = (rb->head + n) % rb->max_size;
+    rb->size -= n;
+    return n; // pop successful, returns number of bytes popped
+  }
+}
--- /dev/null
+++ b/src/ringbuffer.h
@@ -1,0 +1,24 @@
+#ifndef M8C_RINGBUFFER_H
+#define M8C_RINGBUFFER_H
+
+#include <stdint.h>
+
+typedef struct {
+    uint8_t *buffer;
+    uint32_t head;
+    uint32_t tail;
+    uint32_t max_size;
+    uint32_t size;
+} RingBuffer;
+
+RingBuffer *ring_buffer_create(uint32_t size);
+
+uint32_t ring_buffer_empty(RingBuffer *rb);
+
+uint32_t ring_buffer_pop(RingBuffer *rb, uint8_t *data, uint32_t length);
+
+uint32_t ring_buffer_push(RingBuffer *rb, const uint8_t *data, uint32_t length);
+
+void ring_buffer_free(RingBuffer *rb);
+
+#endif //M8C_RINGBUFFER_H
--- /dev/null
+++ b/src/serial.c
@@ -1,0 +1,256 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+// Contains portions of code from libserialport's examples released to the
+// public domain
+
+#ifndef USE_LIBUSB
+#include <SDL.h>
+#include <libserialport.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "serial.h"
+
+struct sp_port *m8_port = NULL;
+
+// Helper function for error handling
+static int check(enum sp_return result);
+
+static int detect_m8_serial_device(struct sp_port *m8_port) {
+  // Check the connection method - we want USB serial devices
+  enum sp_transport transport = sp_get_port_transport(m8_port);
+
+  if (transport == SP_TRANSPORT_USB) {
+    // Get the USB vendor and product IDs.
+    int usb_vid, usb_pid;
+    sp_get_port_usb_vid_pid(m8_port, &usb_vid, &usb_pid);
+
+    if (usb_vid == 0x16C0 && usb_pid == 0x048A)
+      return 1;
+  }
+
+  return 0;
+}
+
+// Checks for connected devices and whether the specified device still exists
+int check_serial_port() {
+
+  int device_found = 0;
+
+  /* A pointer to a null-terminated array of pointers to
+   * struct sp_port, which will contain the ports found.*/
+  struct sp_port **port_list;
+
+  /* Call sp_list_ports() to get the ports. The port_list
+   * pointer will be updated to refer to the array created. */
+  enum sp_return result = sp_list_ports(&port_list);
+
+  if (result != SP_OK) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!\n");
+    abort();
+  }
+
+  /* Iterate through the ports. When port_list[i] is NULL
+   * this indicates the end of the list. */
+  for (int i = 0; port_list[i] != NULL; i++) {
+    struct sp_port *port = port_list[i];
+
+    if (detect_m8_serial_device(port)) {
+      if (strcmp(sp_get_port_name(port), sp_get_port_name(m8_port)) == 0)
+        device_found = 1;
+    }
+  }
+
+  sp_free_port_list(port_list);
+  return device_found;
+}
+
+int init_serial(int verbose) {
+  if (m8_port != NULL) {
+    // Port is already initialized
+    return 1;
+  }
+  /* A pointer to a null-terminated array of pointers to
+   * struct sp_port, which will contain the ports found.*/
+  struct sp_port **port_list;
+
+  if (verbose)
+    SDL_Log("Looking for USB serial devices.\n");
+
+  /* Call sp_list_ports() to get the ports. The port_list
+   * pointer will be updated to refer to the array created. */
+  enum sp_return result = sp_list_ports(&port_list);
+
+  if (result != SP_OK) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!\n");
+    abort();
+  }
+
+  /* Iterate through the ports. When port_list[i] is NULL
+   * this indicates the end of the list. */
+  for (int i = 0; port_list[i] != NULL; i++) {
+    struct sp_port *port = port_list[i];
+
+    if (detect_m8_serial_device(port)) {
+      SDL_Log("Found M8 in %s.\n", sp_get_port_name(port));
+      sp_copy_port(port, &m8_port);
+    }
+  }
+
+  sp_free_port_list(port_list);
+
+  if (m8_port != NULL) {
+    // Open the serial port and configure it
+    SDL_Log("Opening port.\n");
+    enum sp_return result;
+
+    result = sp_open(m8_port, SP_MODE_READ_WRITE);
+    if (check(result) != SP_OK)
+      return 0;
+
+    result = sp_set_baudrate(m8_port, 115200);
+    if (check(result) != SP_OK)
+      return 0;
+
+    result = sp_set_bits(m8_port, 8);
+    if (check(result) != SP_OK)
+      return 0;
+
+    result = sp_set_parity(m8_port, SP_PARITY_NONE);
+    if (check(result) != SP_OK)
+      return 0;
+
+    result = sp_set_stopbits(m8_port, 1);
+    if (check(result) != SP_OK)
+      return 0;
+
+    result = sp_set_flowcontrol(m8_port, SP_FLOWCONTROL_NONE);
+    if (check(result) != SP_OK)
+      return 0;
+  } else {
+    if (verbose) {
+      SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Cannot find a M8.\n");
+    }
+    return 0;
+  }
+
+  return 1;
+}
+
+// Helper function for error handling.
+static int check(enum sp_return result) {
+
+  char *error_message;
+
+  switch (result) {
+  case SP_ERR_ARG:
+    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid argument.\n");
+    break;
+  case SP_ERR_FAIL:
+    error_message = sp_last_error_message();
+    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Failed: %s\n",
+                 error_message);
+    sp_free_error_message(error_message);
+    break;
+  case SP_ERR_SUPP:
+    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Not supported.\n");
+    break;
+  case SP_ERR_MEM:
+    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+                 "Error: Couldn't allocate memory.\n");
+    break;
+  case SP_OK:
+  default:
+    break;
+  }
+  return result;
+}
+
+int reset_display() {
+  int result;
+
+  SDL_Log("Reset display\n");
+
+  char buf[1] = {'R'};
+  result = sp_blocking_write(m8_port, buf, 1, 5);
+  if (result != 1) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error resetting M8 display, code %d",
+                 result);
+    return 0;
+  }
+  return 1;
+}
+
+int enable_and_reset_display() {
+  int result;
+
+  SDL_Log("Enabling and resetting M8 display\n");
+
+  char buf[1] = {'E'};
+  result = sp_blocking_write(m8_port, buf, 1, 5);
+  if (result != 1) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error enabling M8 display, code %d",
+                 result);
+    return 0;
+  }
+
+  result = reset_display();
+
+  return result;
+}
+
+int disconnect() {
+  int result;
+
+  SDL_Log("Disconnecting M8\n");
+
+  char buf[1] = {'D'};
+  
+  result = sp_blocking_write(m8_port, buf, 1, 5);
+  if (result != 1) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending disconnect, code %d",
+                 result);
+    result = 0;
+  }
+  sp_close(m8_port);
+  sp_free_port(m8_port);
+  m8_port = NULL;
+  return result;
+}
+
+int serial_read(uint8_t *serial_buf, int count) {
+  return sp_nonblocking_read(m8_port, serial_buf, count);
+}
+
+int send_msg_controller(uint8_t input) {
+  char buf[2] = {'C', input};
+  size_t nbytes = 2;
+  int result;
+  result = sp_blocking_write(m8_port, buf, nbytes, 5);
+  if (result != nbytes) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending input, code %d",
+                 result);
+    return -1;
+  }
+  return 1;
+}
+
+int send_msg_keyjazz(uint8_t note, uint8_t velocity) {
+  if (velocity > 0x7F)
+    velocity = 0x7F;
+  char buf[3] = {'K', note, velocity};
+  size_t nbytes = 3;
+  int result;
+  result = sp_blocking_write(m8_port, buf, nbytes, 5);
+  if (result != nbytes) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending keyjazz, code %d",
+                 result);
+    return -1;
+  }
+
+  return 1;
+}
+#endif
--- /dev/null
+++ b/src/serial.h
@@ -1,0 +1,25 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+#ifndef _SERIAL_H_
+#define _SERIAL_H_
+
+#ifdef USE_LIBUSB
+// Max packet length of the USB endpoint
+#define serial_read_size 512
+int init_serial_with_file_descriptor(int file_descriptor);
+#else
+// maximum amount of bytes to read from the serial in one read()
+#define serial_read_size 512
+#endif
+
+int init_serial(int verbose);
+int check_serial_port();
+int reset_display();
+int enable_and_reset_display();
+int disconnect();
+int serial_read(uint8_t *serial_buf, int count);
+int send_msg_controller(uint8_t input);
+int send_msg_keyjazz(uint8_t note, uint8_t velocity);
+
+#endif
--- /dev/null
+++ b/src/slip.c
@@ -1,0 +1,116 @@
+/*
+MIT License
+
+Copyright (c) 2018 Marcin Borowicz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/* This code is originally by marcinbor85, https://github.com/marcinbor85/slip
+It has been simplified a bit as CRC checking etc. is not required in this
+program. */
+
+#include "slip.h"
+#include "command.h"
+
+#include <assert.h>
+#include <stddef.h>
+
+static void reset_rx(slip_handler_s *slip) {
+  assert(slip != NULL);
+
+  slip->state = SLIP_STATE_NORMAL;
+  slip->size = 0;
+}
+
+slip_error_t slip_init(slip_handler_s *slip,
+                       const slip_descriptor_s *descriptor) {
+  assert(slip != NULL);
+  assert(descriptor != NULL);
+  assert(descriptor->buf != NULL);
+  assert(descriptor->recv_message != NULL);
+
+  slip->descriptor = descriptor;
+  reset_rx(slip);
+
+  return SLIP_NO_ERROR;
+}
+
+static slip_error_t put_byte_to_buffer(slip_handler_s *slip, uint8_t byte) {
+  slip_error_t error = SLIP_NO_ERROR;
+
+  assert(slip != NULL);
+
+  if (slip->size >= slip->descriptor->buf_size) {
+    error = SLIP_ERROR_BUFFER_OVERFLOW;
+    reset_rx(slip);
+  } else {
+    slip->descriptor->buf[slip->size++] = byte;
+    slip->state = SLIP_STATE_NORMAL;
+  }
+
+  return error;
+}
+
+slip_error_t slip_read_byte(slip_handler_s *slip, uint8_t byte) {
+  slip_error_t error = SLIP_NO_ERROR;
+
+  assert(slip != NULL);
+
+  switch (slip->state) {
+  case SLIP_STATE_NORMAL:
+    switch (byte) {
+    case SLIP_SPECIAL_BYTE_END:
+      if (!slip->descriptor->recv_message(slip->descriptor->buf, slip->size)){
+        error = SLIP_ERROR_INVALID_PACKET;
+      }
+      reset_rx(slip);
+      break;
+    case SLIP_SPECIAL_BYTE_ESC:
+      slip->state = SLIP_STATE_ESCAPED;
+      break;
+    default:
+      error = put_byte_to_buffer(slip, byte);
+      break;
+    }
+    break;
+
+  case SLIP_STATE_ESCAPED:
+    switch (byte) {
+    case SLIP_ESCAPED_BYTE_END:
+      byte = SLIP_SPECIAL_BYTE_END;
+      break;
+    case SLIP_ESCAPED_BYTE_ESC:
+      byte = SLIP_SPECIAL_BYTE_ESC;
+      break;
+    default:
+      error = SLIP_ERROR_UNKNOWN_ESCAPED_BYTE;
+      reset_rx(slip);
+      break;
+    }
+
+    if (error != SLIP_NO_ERROR)
+      break;
+
+    error = put_byte_to_buffer(slip, byte);
+    break;
+  }
+
+  return error;
+}
--- /dev/null
+++ b/src/slip.h
@@ -1,0 +1,68 @@
+/*
+MIT License
+
+Copyright (c) 2018 Marcin Borowicz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/* This code is originally by marcinbor85, https://github.com/marcinbor85/slip
+It has been simplified a bit as CRC checking etc. is not required in this
+program. */
+
+#ifndef SLIP_H_
+#define SLIP_H_
+
+#include "command.h"
+#include <stdint.h>
+
+#define SLIP_SPECIAL_BYTE_END           0xC0
+#define SLIP_SPECIAL_BYTE_ESC           0xDB
+
+#define SLIP_ESCAPED_BYTE_END           0xDC
+#define SLIP_ESCAPED_BYTE_ESC           0xDD
+
+typedef enum {
+        SLIP_STATE_NORMAL = 0x00,
+        SLIP_STATE_ESCAPED
+} slip_state_t;
+
+typedef struct {
+        uint8_t *buf;
+        uint32_t buf_size;
+        int (*recv_message)(uint8_t *data, uint32_t size);
+} slip_descriptor_s;
+
+typedef struct {
+        slip_state_t state;
+        uint32_t size;
+        const slip_descriptor_s *descriptor;
+} slip_handler_s;
+
+typedef enum {
+        SLIP_NO_ERROR = 0x00,
+        SLIP_ERROR_BUFFER_OVERFLOW,
+        SLIP_ERROR_UNKNOWN_ESCAPED_BYTE,
+        SLIP_ERROR_INVALID_PACKET
+} slip_error_t;
+
+slip_error_t slip_init(slip_handler_s *slip, const slip_descriptor_s *descriptor);
+slip_error_t slip_read_byte(slip_handler_s *slip, uint8_t byte);
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/usb.c
@@ -1,0 +1,295 @@
+// Copyright 2021 Jonne Kokkonen
+// Released under the MIT licence, https://opensource.org/licenses/MIT
+
+// Contains portions of code from libserialport's examples released to the
+// public domain
+#ifdef USE_LIBUSB
+
+#include <SDL.h>
+#include <stdlib.h>
+#include <string.h>
+#include <libusb.h>
+
+#include "usb.h"
+
+static int ep_out_addr = 0x03;
+static int ep_in_addr = 0x83;
+
+#define ACM_CTRL_DTR   0x01
+#define ACM_CTRL_RTS   0x02
+
+libusb_context *ctx = NULL;
+libusb_device_handle *devh = NULL;
+
+static int do_exit = 0;
+
+int usb_loop(void *data) {
+  SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
+  while (!do_exit) {
+    int rc = libusb_handle_events(ctx);
+    if (rc != LIBUSB_SUCCESS) {
+      SDL_Log("Audio loop error: %s\n", libusb_error_name(rc));
+      break;
+    }
+  }
+  return 0;
+}
+
+static SDL_Thread *usb_thread;
+
+static void LIBUSB_CALL xfr_cb_in(struct libusb_transfer *transfer) {
+  int *completed = transfer->user_data;
+  *completed = 1;
+}
+
+int bulk_transfer(int endpoint, uint8_t *serial_buf, int count, unsigned int timeout_ms) {
+  int completed = 0;
+
+  struct libusb_transfer *transfer;
+  transfer = libusb_alloc_transfer(0);
+  libusb_fill_bulk_transfer(transfer, devh, endpoint, serial_buf, count,
+                            xfr_cb_in, &completed, timeout_ms);
+  int r = libusb_submit_transfer(transfer);
+
+  if (r < 0) {
+    SDL_Log("Error");
+    libusb_free_transfer(transfer);
+    return r;
+  }
+
+  retry:
+  libusb_lock_event_waiters(ctx);
+  while (!completed) {
+    if (!libusb_event_handler_active(ctx)) {
+      libusb_unlock_event_waiters(ctx);
+      goto retry;
+    }
+    libusb_wait_for_event(ctx, NULL);
+  }
+  libusb_unlock_event_waiters(ctx);
+
+  int actual_length = transfer->actual_length;
+
+  libusb_free_transfer(transfer);
+
+  return actual_length;
+}
+
+int blocking_write(void *buf,
+                   int count, unsigned int timeout_ms) {
+  return bulk_transfer(ep_out_addr, buf, count, timeout_ms);
+}
+
+int serial_read(uint8_t *serial_buf, int count) {
+  return bulk_transfer(ep_in_addr, serial_buf, count, 1);
+}
+
+int check_serial_port() {
+  // Reading will fail anyway when the device is not present anymore
+  return 1;
+}
+
+int init_interface() {
+
+  if (devh == NULL) {
+    SDL_Log("Device not initialised!");
+    return 0;
+  }
+
+  int rc;
+
+  for (int if_num = 0; if_num < 2; if_num++) {
+    if (libusb_kernel_driver_active(devh, if_num)) {
+      SDL_Log("Detaching kernel driver for interface %d", if_num);
+      libusb_detach_kernel_driver(devh, if_num);
+    }
+    rc = libusb_claim_interface(devh, if_num);
+    if (rc < 0) {
+      SDL_Log("Error claiming interface: %s", libusb_error_name(rc));
+      return 0;
+    }
+  }
+
+  /* Start configuring the device:
+   * - set line state
+   */
+  SDL_Log("Setting line state");
+  rc = libusb_control_transfer(devh, 0x21, 0x22, ACM_CTRL_DTR | ACM_CTRL_RTS,
+                               0, NULL, 0, 0);
+  if (rc < 0) {
+    SDL_Log("Error during control transfer: %s", libusb_error_name(rc));
+    return 0;
+  }
+
+  /* - set line encoding: here 115200 8N1
+   * 115200 = 0x01C200 ~> 0x00, 0xC2, 0x01, 0x00 in little endian
+   */
+  SDL_Log("Set line encoding");
+  unsigned char encoding[] = {0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08};
+  rc = libusb_control_transfer(devh, 0x21, 0x20, 0, 0, encoding,
+                               sizeof(encoding), 0);
+  if (rc < 0) {
+    SDL_Log("Error during control transfer: %s", libusb_error_name(rc));
+    return 0;
+  }
+
+  usb_thread = SDL_CreateThread(&usb_loop, "USB", NULL);
+
+  return 1;
+}
+
+int init_serial_with_file_descriptor(int file_descriptor) {
+  SDL_Log("Initialising serial with file descriptor");
+
+  if (file_descriptor <= 0) {
+    SDL_Log("Invalid file descriptor: %d", file_descriptor);
+    return 0;
+  }
+
+  int r;
+  r = libusb_set_option(NULL, LIBUSB_OPTION_NO_DEVICE_DISCOVERY, NULL);
+  if (r != LIBUSB_SUCCESS) {
+    SDL_Log("libusb_set_option failed: %s", libusb_error_name(r));
+    return 0;
+  }
+  r = libusb_init(&ctx);
+  if (r < 0) {
+    SDL_Log("libusb_init failed: %s", libusb_error_name(r));
+    return 0;
+  }
+  r = libusb_wrap_sys_device(ctx, (intptr_t) file_descriptor, &devh);
+  if (r < 0) {
+    SDL_Log("libusb_wrap_sys_device failed: %s", libusb_error_name(r));
+    return 0;
+  } else if (devh == NULL) {
+    SDL_Log("libusb_wrap_sys_device returned invalid handle");
+    return 0;
+  }
+  SDL_Log("USB device init success");
+
+  return init_interface();
+}
+
+int init_serial(int verbose) {
+
+  if (devh != NULL) {
+    return 1;
+  }
+
+  int r;
+  r = libusb_init(&ctx);
+  if (r < 0) {
+    SDL_Log("libusb_init failed: %s", libusb_error_name(r));
+    return 0;
+  }
+  devh = libusb_open_device_with_vid_pid(ctx, 0x16c0, 0x048a);
+  if (devh == NULL) {
+    SDL_Log("libusb_open_device_with_vid_pid returned invalid handle");
+    return 0;
+  }
+  SDL_Log("USB device init success");
+
+  return init_interface();
+}
+
+int reset_display() {
+  int result;
+
+  SDL_Log("Reset display\n");
+
+  char buf[1] = {'R'};
+
+  result = blocking_write(buf, 1, 5);
+  if (result != 1) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error resetting M8 display, code %d",
+                 result);
+    return 0;
+  }
+  return 1;
+}
+
+int enable_and_reset_display() {
+  int result;
+
+  SDL_Log("Enabling and resetting M8 display\n");
+
+  char buf[1] = {'E'};
+  result = blocking_write(buf, 1, 5);
+  if (result != 1) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error enabling M8 display, code %d",
+                 result);
+    return 0;
+  }
+
+  SDL_Delay(5);
+  result = reset_display();
+  return result;
+}
+
+int disconnect() {
+
+  char buf[1] = {'D'};
+  int result;
+
+  SDL_Log("Disconnecting M8\n");
+
+  result = blocking_write(buf, 1, 5);
+  if (result != 1) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending disconnect, code %d",
+                 result);
+    return -1;
+  }
+
+  int rc;
+
+  for (int if_num = 0; if_num < 2; if_num++) {
+    rc = libusb_release_interface(devh, if_num);
+    if (rc < 0) {
+      SDL_Log("Error releasing interface: %s", libusb_error_name(rc));
+      return 0;
+    }
+  }
+
+  do_exit = 1;
+
+  if (devh != NULL) {
+    libusb_close(devh);
+  }
+
+  SDL_WaitThread(usb_thread, NULL);
+
+  libusb_exit(ctx);
+
+  return 1;
+}
+
+int send_msg_controller(uint8_t input) {
+  char buf[2] = {'C', input};
+  int nbytes = 2;
+  int result;
+  result = blocking_write(buf, nbytes, 5);
+  if (result != nbytes) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending input, code %d",
+                 result);
+    return -1;
+  }
+  return 1;
+}
+
+int send_msg_keyjazz(uint8_t note, uint8_t velocity) {
+  if (velocity > 0x7F)
+    velocity = 0x7F;
+  char buf[3] = {'K', note, velocity};
+  int nbytes = 3;
+  int result;
+  result = blocking_write(buf, nbytes, 5);
+  if (result != nbytes) {
+    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending keyjazz, code %d",
+                 result);
+    return -1;
+  }
+
+  return 1;
+}
+
+#endif
--- /dev/null
+++ b/src/usb.h
@@ -1,0 +1,9 @@
+#ifdef USE_LIBUSB
+#ifndef M8C_USB_H
+#define M8C_USB_H
+
+#include <libusb.h>
+extern libusb_device_handle *devh;
+
+#endif //M8C_USB_H
+#endif
--- /dev/null
+++ b/src/usb_audio.c
@@ -1,0 +1,196 @@
+#ifdef USE_LIBUSB
+
+#include <libusb.h>
+#include <errno.h>
+#include <SDL.h>
+#include "ringbuffer.h"
+#include "usb.h"
+
+#define EP_ISO_IN 0x85
+#define IFACE_NUM 4
+
+#define NUM_TRANSFERS 64
+#define PACKET_SIZE 180
+#define NUM_PACKETS 2
+
+SDL_AudioDeviceID sdl_audio_device_id = 0;
+RingBuffer *audio_buffer = NULL;
+
+static void audio_callback(void *userdata, Uint8 *stream,
+                           int len) {
+  uint32_t read_len = ring_buffer_pop(audio_buffer, stream, len);
+
+  if (read_len == -1) {
+    SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Buffer underflow!");
+  }
+
+  // If we didn't read the full len bytes, fill the rest with zeros
+  if (read_len < len) {
+    SDL_memset(&stream[read_len], 0, len - read_len);
+  }
+}
+
+static void cb_xfr(struct libusb_transfer *xfr) {
+  unsigned int i;
+
+  for (i = 0; i < xfr->num_iso_packets; i++) {
+    struct libusb_iso_packet_descriptor *pack = &xfr->iso_packet_desc[i];
+
+    if (pack->status != LIBUSB_TRANSFER_COMPLETED) {
+      SDL_Log("XFR callback error (status %d: %s)", pack->status,
+              libusb_error_name(pack->status));
+      /* This doesn't happen, so bail out if it does. */
+      return;
+    }
+
+    const uint8_t *data = libusb_get_iso_packet_buffer_simple(xfr, i);
+    if (sdl_audio_device_id != 0) {
+      uint32_t actual = ring_buffer_push(audio_buffer, data, pack->actual_length);
+      if (actual == -1) {
+        SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Buffer overflow!");
+      }
+    }
+  }
+
+  if (libusb_submit_transfer(xfr) < 0) {
+    SDL_Log("error re-submitting URB\n");
+    SDL_free(xfr->buffer);
+  }
+}
+
+static struct libusb_transfer *xfr[NUM_TRANSFERS];
+
+static int benchmark_in() {
+  int i;
+
+  for (i = 0; i < NUM_TRANSFERS; i++) {
+    xfr[i] = libusb_alloc_transfer(NUM_PACKETS);
+    if (!xfr[i]) {
+      SDL_Log("Could not allocate transfer");
+      return -ENOMEM;
+    }
+
+    Uint8 *buffer = SDL_malloc(PACKET_SIZE * NUM_PACKETS);
+
+    libusb_fill_iso_transfer(xfr[i], devh, EP_ISO_IN, buffer,
+                             PACKET_SIZE * NUM_PACKETS, NUM_PACKETS, cb_xfr, NULL, 0);
+    libusb_set_iso_packet_lengths(xfr[i], PACKET_SIZE);
+
+    libusb_submit_transfer(xfr[i]);
+  }
+
+  return 1;
+}
+
+int audio_init(int audio_buffer_size, const char *output_device_name) {
+  SDL_Log("USB audio setup");
+
+  int rc;
+
+  rc = libusb_kernel_driver_active(devh, IFACE_NUM);
+  if (rc == 1) {
+    SDL_Log("Detaching kernel driver");
+    rc = libusb_detach_kernel_driver(devh, IFACE_NUM);
+    if (rc < 0) {
+      SDL_Log("Could not detach kernel driver: %s\n",
+              libusb_error_name(rc));
+      return rc;
+    }
+  }
+
+  rc = libusb_claim_interface(devh, IFACE_NUM);
+  if (rc < 0) {
+    SDL_Log("Error claiming interface: %s\n", libusb_error_name(rc));
+    return rc;
+  }
+
+  rc = libusb_set_interface_alt_setting(devh, IFACE_NUM, 1);
+  if (rc < 0) {
+    SDL_Log("Error setting alt setting: %s\n", libusb_error_name(rc));
+    return rc;
+  }
+
+  if (!SDL_WasInit(SDL_INIT_AUDIO)) {
+    if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
+      SDL_Log("Init audio failed %s", SDL_GetError());
+      return -1;
+    }
+  } else {
+    SDL_Log("Audio was already initialised");
+  }
+
+  static SDL_AudioSpec audio_spec;
+  audio_spec.format = AUDIO_S16;
+  audio_spec.channels = 2;
+  audio_spec.freq = 44100;
+  audio_spec.samples = audio_buffer_size;
+  audio_spec.callback = audio_callback;
+
+  SDL_AudioSpec _obtained;
+  SDL_zero(_obtained);
+
+  SDL_Log("Current audio driver is %s and device %s", SDL_GetCurrentAudioDriver(),
+          output_device_name);
+
+  if (SDL_strcasecmp(SDL_GetCurrentAudioDriver(), "openslES") == 0 || output_device_name == NULL) {
+    SDL_Log("Using default audio device");
+    sdl_audio_device_id = SDL_OpenAudioDevice(NULL, 0, &audio_spec, &_obtained, 0);
+  } else {
+    sdl_audio_device_id = SDL_OpenAudioDevice(output_device_name, 0, &audio_spec, &_obtained, 0);
+  }
+
+  audio_buffer = ring_buffer_create(4 * _obtained.size);
+
+  SDL_Log("Obtained audio spec. Sample rate: %d, channels: %d, samples: %d, size: %d",
+          _obtained.freq,
+          _obtained.channels,
+          _obtained.samples, +_obtained.size);
+
+  SDL_PauseAudioDevice(sdl_audio_device_id, 0);
+
+  // Good to go
+  SDL_Log("Starting capture");
+  if ((rc = benchmark_in()) < 0) {
+    SDL_Log("Capture failed to start: %d", rc);
+    return rc;
+  }
+
+  SDL_Log("Successful init");
+  return 1;
+}
+
+int audio_destroy() {
+  SDL_Log("Closing audio");
+
+  int i, rc;
+
+  for (i = 0; i < NUM_TRANSFERS; i++) {
+    rc = libusb_cancel_transfer(xfr[i]);
+    if (rc < 0) {
+      SDL_Log("Error cancelling transfer: %s\n", libusb_error_name(rc));
+    }
+    SDL_free(xfr[i]->buffer);
+  }
+
+  SDL_Log("Freeing interface %d", IFACE_NUM);
+
+  rc = libusb_release_interface(devh, IFACE_NUM);
+  if (rc < 0) {
+    SDL_Log("Error releasing interface: %s\n", libusb_error_name(rc));
+    return rc;
+  }
+
+  if (sdl_audio_device_id != 0) {
+    SDL_Log("Closing audio device %d", sdl_audio_device_id);
+    SDL_AudioDeviceID device = sdl_audio_device_id;
+    sdl_audio_device_id = 0;
+    SDL_CloseAudioDevice(device);
+  }
+
+  SDL_Log("Audio closed");
+
+  ring_buffer_free(audio_buffer);
+  return 1;
+}
+
+#endif
--- a/usb.c
+++ /dev/null
@@ -1,295 +1,0 @@
-// Copyright 2021 Jonne Kokkonen
-// Released under the MIT licence, https://opensource.org/licenses/MIT
-
-// Contains portions of code from libserialport's examples released to the
-// public domain
-#ifdef USE_LIBUSB
-
-#include <SDL.h>
-#include <stdlib.h>
-#include <string.h>
-#include <libusb.h>
-
-#include "usb.h"
-
-static int ep_out_addr = 0x03;
-static int ep_in_addr = 0x83;
-
-#define ACM_CTRL_DTR   0x01
-#define ACM_CTRL_RTS   0x02
-
-libusb_context *ctx = NULL;
-libusb_device_handle *devh = NULL;
-
-static int do_exit = 0;
-
-int usb_loop(void *data) {
-  SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
-  while (!do_exit) {
-    int rc = libusb_handle_events(ctx);
-    if (rc != LIBUSB_SUCCESS) {
-      SDL_Log("Audio loop error: %s\n", libusb_error_name(rc));
-      break;
-    }
-  }
-  return 0;
-}
-
-static SDL_Thread *usb_thread;
-
-static void LIBUSB_CALL xfr_cb_in(struct libusb_transfer *transfer) {
-  int *completed = transfer->user_data;
-  *completed = 1;
-}
-
-int bulk_transfer(int endpoint, uint8_t *serial_buf, int count, unsigned int timeout_ms) {
-  int completed = 0;
-
-  struct libusb_transfer *transfer;
-  transfer = libusb_alloc_transfer(0);
-  libusb_fill_bulk_transfer(transfer, devh, endpoint, serial_buf, count,
-                            xfr_cb_in, &completed, timeout_ms);
-  int r = libusb_submit_transfer(transfer);
-
-  if (r < 0) {
-    SDL_Log("Error");
-    libusb_free_transfer(transfer);
-    return r;
-  }
-
-  retry:
-  libusb_lock_event_waiters(ctx);
-  while (!completed) {
-    if (!libusb_event_handler_active(ctx)) {
-      libusb_unlock_event_waiters(ctx);
-      goto retry;
-    }
-    libusb_wait_for_event(ctx, NULL);
-  }
-  libusb_unlock_event_waiters(ctx);
-
-  int actual_length = transfer->actual_length;
-
-  libusb_free_transfer(transfer);
-
-  return actual_length;
-}
-
-int blocking_write(void *buf,
-                   int count, unsigned int timeout_ms) {
-  return bulk_transfer(ep_out_addr, buf, count, timeout_ms);
-}
-
-int serial_read(uint8_t *serial_buf, int count) {
-  return bulk_transfer(ep_in_addr, serial_buf, count, 1);
-}
-
-int check_serial_port() {
-  // Reading will fail anyway when the device is not present anymore
-  return 1;
-}
-
-int init_interface() {
-
-  if (devh == NULL) {
-    SDL_Log("Device not initialised!");
-    return 0;
-  }
-
-  int rc;
-
-  for (int if_num = 0; if_num < 2; if_num++) {
-    if (libusb_kernel_driver_active(devh, if_num)) {
-      SDL_Log("Detaching kernel driver for interface %d", if_num);
-      libusb_detach_kernel_driver(devh, if_num);
-    }
-    rc = libusb_claim_interface(devh, if_num);
-    if (rc < 0) {
-      SDL_Log("Error claiming interface: %s", libusb_error_name(rc));
-      return 0;
-    }
-  }
-
-  /* Start configuring the device:
-   * - set line state
-   */
-  SDL_Log("Setting line state");
-  rc = libusb_control_transfer(devh, 0x21, 0x22, ACM_CTRL_DTR | ACM_CTRL_RTS,
-                               0, NULL, 0, 0);
-  if (rc < 0) {
-    SDL_Log("Error during control transfer: %s", libusb_error_name(rc));
-    return 0;
-  }
-
-  /* - set line encoding: here 115200 8N1
-   * 115200 = 0x01C200 ~> 0x00, 0xC2, 0x01, 0x00 in little endian
-   */
-  SDL_Log("Set line encoding");
-  unsigned char encoding[] = {0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08};
-  rc = libusb_control_transfer(devh, 0x21, 0x20, 0, 0, encoding,
-                               sizeof(encoding), 0);
-  if (rc < 0) {
-    SDL_Log("Error during control transfer: %s", libusb_error_name(rc));
-    return 0;
-  }
-
-  usb_thread = SDL_CreateThread(&usb_loop, "USB", NULL);
-
-  return 1;
-}
-
-int init_serial_with_file_descriptor(int file_descriptor) {
-  SDL_Log("Initialising serial with file descriptor");
-
-  if (file_descriptor <= 0) {
-    SDL_Log("Invalid file descriptor: %d", file_descriptor);
-    return 0;
-  }
-
-  int r;
-  r = libusb_set_option(NULL, LIBUSB_OPTION_NO_DEVICE_DISCOVERY, NULL);
-  if (r != LIBUSB_SUCCESS) {
-    SDL_Log("libusb_set_option failed: %s", libusb_error_name(r));
-    return 0;
-  }
-  r = libusb_init(&ctx);
-  if (r < 0) {
-    SDL_Log("libusb_init failed: %s", libusb_error_name(r));
-    return 0;
-  }
-  r = libusb_wrap_sys_device(ctx, (intptr_t) file_descriptor, &devh);
-  if (r < 0) {
-    SDL_Log("libusb_wrap_sys_device failed: %s", libusb_error_name(r));
-    return 0;
-  } else if (devh == NULL) {
-    SDL_Log("libusb_wrap_sys_device returned invalid handle");
-    return 0;
-  }
-  SDL_Log("USB device init success");
-
-  return init_interface();
-}
-
-int init_serial(int verbose) {
-
-  if (devh != NULL) {
-    return 1;
-  }
-
-  int r;
-  r = libusb_init(&ctx);
-  if (r < 0) {
-    SDL_Log("libusb_init failed: %s", libusb_error_name(r));
-    return 0;
-  }
-  devh = libusb_open_device_with_vid_pid(ctx, 0x16c0, 0x048a);
-  if (devh == NULL) {
-    SDL_Log("libusb_open_device_with_vid_pid returned invalid handle");
-    return 0;
-  }
-  SDL_Log("USB device init success");
-
-  return init_interface();
-}
-
-int reset_display() {
-  int result;
-
-  SDL_Log("Reset display\n");
-
-  char buf[1] = {'R'};
-
-  result = blocking_write(buf, 1, 5);
-  if (result != 1) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error resetting M8 display, code %d",
-                 result);
-    return 0;
-  }
-  return 1;
-}
-
-int enable_and_reset_display() {
-  int result;
-
-  SDL_Log("Enabling and resetting M8 display\n");
-
-  char buf[1] = {'E'};
-  result = blocking_write(buf, 1, 5);
-  if (result != 1) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error enabling M8 display, code %d",
-                 result);
-    return 0;
-  }
-
-  SDL_Delay(5);
-  result = reset_display();
-  return result;
-}
-
-int disconnect() {
-
-  char buf[1] = {'D'};
-  int result;
-
-  SDL_Log("Disconnecting M8\n");
-
-  result = blocking_write(buf, 1, 5);
-  if (result != 1) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending disconnect, code %d",
-                 result);
-    return -1;
-  }
-
-  int rc;
-
-  for (int if_num = 0; if_num < 2; if_num++) {
-    rc = libusb_release_interface(devh, if_num);
-    if (rc < 0) {
-      SDL_Log("Error releasing interface: %s", libusb_error_name(rc));
-      return 0;
-    }
-  }
-
-  do_exit = 1;
-
-  if (devh != NULL) {
-    libusb_close(devh);
-  }
-
-  SDL_WaitThread(usb_thread, NULL);
-
-  libusb_exit(ctx);
-
-  return 1;
-}
-
-int send_msg_controller(uint8_t input) {
-  char buf[2] = {'C', input};
-  int nbytes = 2;
-  int result;
-  result = blocking_write(buf, nbytes, 5);
-  if (result != nbytes) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending input, code %d",
-                 result);
-    return -1;
-  }
-  return 1;
-}
-
-int send_msg_keyjazz(uint8_t note, uint8_t velocity) {
-  if (velocity > 0x7F)
-    velocity = 0x7F;
-  char buf[3] = {'K', note, velocity};
-  int nbytes = 3;
-  int result;
-  result = blocking_write(buf, nbytes, 5);
-  if (result != nbytes) {
-    SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending keyjazz, code %d",
-                 result);
-    return -1;
-  }
-
-  return 1;
-}
-
-#endif
--- a/usb.h
+++ /dev/null
@@ -1,9 +1,0 @@
-#ifdef USE_LIBUSB
-#ifndef M8C_USB_H
-#define M8C_USB_H
-
-#include <libusb.h>
-extern libusb_device_handle *devh;
-
-#endif //M8C_USB_H
-#endif
--- a/usb_audio.c
+++ /dev/null
@@ -1,196 +1,0 @@
-#ifdef USE_LIBUSB
-
-#include <libusb.h>
-#include <errno.h>
-#include <SDL.h>
-#include "ringbuffer.h"
-#include "usb.h"
-
-#define EP_ISO_IN 0x85
-#define IFACE_NUM 4
-
-#define NUM_TRANSFERS 64
-#define PACKET_SIZE 180
-#define NUM_PACKETS 2
-
-SDL_AudioDeviceID sdl_audio_device_id = 0;
-RingBuffer *audio_buffer = NULL;
-
-static void audio_callback(void *userdata, Uint8 *stream,
-                           int len) {
-  uint32_t read_len = ring_buffer_pop(audio_buffer, stream, len);
-
-  if (read_len == -1) {
-    SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Buffer underflow!");
-  }
-
-  // If we didn't read the full len bytes, fill the rest with zeros
-  if (read_len < len) {
-    SDL_memset(&stream[read_len], 0, len - read_len);
-  }
-}
-
-static void cb_xfr(struct libusb_transfer *xfr) {
-  unsigned int i;
-
-  for (i = 0; i < xfr->num_iso_packets; i++) {
-    struct libusb_iso_packet_descriptor *pack = &xfr->iso_packet_desc[i];
-
-    if (pack->status != LIBUSB_TRANSFER_COMPLETED) {
-      SDL_Log("XFR callback error (status %d: %s)", pack->status,
-              libusb_error_name(pack->status));
-      /* This doesn't happen, so bail out if it does. */
-      return;
-    }
-
-    const uint8_t *data = libusb_get_iso_packet_buffer_simple(xfr, i);
-    if (sdl_audio_device_id != 0) {
-      uint32_t actual = ring_buffer_push(audio_buffer, data, pack->actual_length);
-      if (actual == -1) {
-        SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Buffer overflow!");
-      }
-    }
-  }
-
-  if (libusb_submit_transfer(xfr) < 0) {
-    SDL_Log("error re-submitting URB\n");
-    SDL_free(xfr->buffer);
-  }
-}
-
-static struct libusb_transfer *xfr[NUM_TRANSFERS];
-
-static int benchmark_in() {
-  int i;
-
-  for (i = 0; i < NUM_TRANSFERS; i++) {
-    xfr[i] = libusb_alloc_transfer(NUM_PACKETS);
-    if (!xfr[i]) {
-      SDL_Log("Could not allocate transfer");
-      return -ENOMEM;
-    }
-
-    Uint8 *buffer = SDL_malloc(PACKET_SIZE * NUM_PACKETS);
-
-    libusb_fill_iso_transfer(xfr[i], devh, EP_ISO_IN, buffer,
-                             PACKET_SIZE * NUM_PACKETS, NUM_PACKETS, cb_xfr, NULL, 0);
-    libusb_set_iso_packet_lengths(xfr[i], PACKET_SIZE);
-
-    libusb_submit_transfer(xfr[i]);
-  }
-
-  return 1;
-}
-
-int audio_init(int audio_buffer_size, const char *output_device_name) {
-  SDL_Log("USB audio setup");
-
-  int rc;
-
-  rc = libusb_kernel_driver_active(devh, IFACE_NUM);
-  if (rc == 1) {
-    SDL_Log("Detaching kernel driver");
-    rc = libusb_detach_kernel_driver(devh, IFACE_NUM);
-    if (rc < 0) {
-      SDL_Log("Could not detach kernel driver: %s\n",
-              libusb_error_name(rc));
-      return rc;
-    }
-  }
-
-  rc = libusb_claim_interface(devh, IFACE_NUM);
-  if (rc < 0) {
-    SDL_Log("Error claiming interface: %s\n", libusb_error_name(rc));
-    return rc;
-  }
-
-  rc = libusb_set_interface_alt_setting(devh, IFACE_NUM, 1);
-  if (rc < 0) {
-    SDL_Log("Error setting alt setting: %s\n", libusb_error_name(rc));
-    return rc;
-  }
-
-  if (!SDL_WasInit(SDL_INIT_AUDIO)) {
-    if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
-      SDL_Log("Init audio failed %s", SDL_GetError());
-      return -1;
-    }
-  } else {
-    SDL_Log("Audio was already initialised");
-  }
-
-  static SDL_AudioSpec audio_spec;
-  audio_spec.format = AUDIO_S16;
-  audio_spec.channels = 2;
-  audio_spec.freq = 44100;
-  audio_spec.samples = audio_buffer_size;
-  audio_spec.callback = audio_callback;
-
-  SDL_AudioSpec _obtained;
-  SDL_zero(_obtained);
-
-  SDL_Log("Current audio driver is %s and device %s", SDL_GetCurrentAudioDriver(),
-          output_device_name);
-
-  if (SDL_strcasecmp(SDL_GetCurrentAudioDriver(), "openslES") == 0 || output_device_name == NULL) {
-    SDL_Log("Using default audio device");
-    sdl_audio_device_id = SDL_OpenAudioDevice(NULL, 0, &audio_spec, &_obtained, 0);
-  } else {
-    sdl_audio_device_id = SDL_OpenAudioDevice(output_device_name, 0, &audio_spec, &_obtained, 0);
-  }
-
-  audio_buffer = ring_buffer_create(4 * _obtained.size);
-
-  SDL_Log("Obtained audio spec. Sample rate: %d, channels: %d, samples: %d, size: %d",
-          _obtained.freq,
-          _obtained.channels,
-          _obtained.samples, +_obtained.size);
-
-  SDL_PauseAudioDevice(sdl_audio_device_id, 0);
-
-  // Good to go
-  SDL_Log("Starting capture");
-  if ((rc = benchmark_in()) < 0) {
-    SDL_Log("Capture failed to start: %d", rc);
-    return rc;
-  }
-
-  SDL_Log("Successful init");
-  return 1;
-}
-
-int audio_destroy() {
-  SDL_Log("Closing audio");
-
-  int i, rc;
-
-  for (i = 0; i < NUM_TRANSFERS; i++) {
-    rc = libusb_cancel_transfer(xfr[i]);
-    if (rc < 0) {
-      SDL_Log("Error cancelling transfer: %s\n", libusb_error_name(rc));
-    }
-    SDL_free(xfr[i]->buffer);
-  }
-
-  SDL_Log("Freeing interface %d", IFACE_NUM);
-
-  rc = libusb_release_interface(devh, IFACE_NUM);
-  if (rc < 0) {
-    SDL_Log("Error releasing interface: %s\n", libusb_error_name(rc));
-    return rc;
-  }
-
-  if (sdl_audio_device_id != 0) {
-    SDL_Log("Closing audio device %d", sdl_audio_device_id);
-    SDL_AudioDeviceID device = sdl_audio_device_id;
-    sdl_audio_device_id = 0;
-    SDL_CloseAudioDevice(device);
-  }
-
-  SDL_Log("Audio closed");
-
-  ring_buffer_free(audio_buffer);
-  return 1;
-}
-
-#endif
--