├── package ├── macos │ ├── m8c.app │ │ └── Contents │ │ │ ├── PkgInfo │ │ │ ├── Resources │ │ │ └── m8c.icns │ │ │ └── Info.plist │ └── Entitlements.plist ├── share │ ├── icons │ │ └── hicolor │ │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ └── m8c.png │ │ │ ├── 48x48 │ │ │ └── apps │ │ │ │ └── m8c.png │ │ │ ├── 64x64 │ │ │ └── apps │ │ │ │ └── m8c.png │ │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ └── m8c.png │ │ │ ├── 256x256 │ │ │ └── apps │ │ │ │ └── m8c.png │ │ │ ├── 512x512 │ │ │ └── apps │ │ │ │ └── m8c.png │ │ │ └── 1024x1024 │ │ │ └── apps │ │ │ └── m8c.png │ └── applications │ │ └── m8c.desktop └── appimage │ ├── m8c.desktop │ ├── icon.svg │ ├── m8c.appdata.xml │ └── package.sh ├── .clang-format ├── src ├── events.h ├── gamepads.h ├── fx_cube.h ├── backends │ ├── audio.h │ ├── ringbuffer.h │ ├── m8.h │ ├── ringbuffer.c │ ├── queue.h │ ├── queue.c │ ├── slip.h │ ├── slip.c │ ├── audio_sdl.c │ ├── audio_libusb.c │ ├── m8_rtmidi.c │ └── m8_libserialport.c ├── common.h ├── ini.h ├── fonts │ ├── fonts.h │ ├── fonts.c │ ├── font1.h │ ├── font2.h │ ├── font3.h │ ├── font4.h │ └── font5.h ├── SDL2_inprint.h ├── command.h ├── settings.h ├── render.h ├── log_overlay.h ├── input.h ├── config.h ├── gamepads.c ├── events.c ├── inprint2.c ├── fx_cube.c ├── main.c ├── ini.c ├── command.c └── log_overlay.c ├── .gitignore ├── shell.nix ├── Android.mk ├── .editorconfig ├── default.nix ├── Makefile ├── .github └── workflows │ ├── update-package-versions.yml │ ├── build-ubuntu.yml │ ├── build-windows.yml │ └── build-macos.yml ├── flake.lock ├── flake.nix ├── LICENSE ├── CMakeLists.txt ├── AUDIOGUIDE.md └── README.md /package/macos/m8c.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? 2 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | ColumnLimit: '100' 3 | IndentWidth: '2' 4 | UseTab: Never -------------------------------------------------------------------------------- /package/share/icons/hicolor/32x32/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/32x32/apps/m8c.png -------------------------------------------------------------------------------- /package/share/icons/hicolor/48x48/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/48x48/apps/m8c.png -------------------------------------------------------------------------------- /package/share/icons/hicolor/64x64/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/64x64/apps/m8c.png -------------------------------------------------------------------------------- /package/share/icons/hicolor/128x128/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/128x128/apps/m8c.png -------------------------------------------------------------------------------- /package/share/icons/hicolor/256x256/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/256x256/apps/m8c.png -------------------------------------------------------------------------------- /package/share/icons/hicolor/512x512/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/512x512/apps/m8c.png -------------------------------------------------------------------------------- /package/macos/m8c.app/Contents/Resources/m8c.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/macos/m8c.app/Contents/Resources/m8c.icns -------------------------------------------------------------------------------- /package/share/icons/hicolor/1024x1024/apps/m8c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laamaa/m8c/HEAD/package/share/icons/hicolor/1024x1024/apps/m8c.png -------------------------------------------------------------------------------- /package/share/applications/m8c.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=m8c 4 | Exec=m8c 5 | Icon=m8c 6 | Categories=Audio;AudioVideo 7 | Version=1.4 8 | -------------------------------------------------------------------------------- /src/events.h: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #ifndef EVENTS_H_ 5 | #define EVENTS_H_ 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /package/appimage/m8c.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=m8c 4 | Comment= 5 | Exec=m8c 6 | Icon=icon 7 | Terminal=false 8 | Categories=Audio 9 | X-AppImage-Version=1.0 10 | -------------------------------------------------------------------------------- /src/gamepads.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jonne on 8/19/24. 3 | // 4 | 5 | #ifndef GAMEPADS_H_ 6 | #define GAMEPADS_H_ 7 | 8 | int gamepads_initialize(void); 9 | void gamepads_close(void); 10 | 11 | #endif //GAMEPADS_H_ 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dylib 2 | *.a 3 | *.o 4 | *.exe 5 | .DS_Store 6 | m8c 7 | .vscode 8 | .cache/ 9 | font.c 10 | build/ 11 | compile_commands.json 12 | .clangd 13 | result* 14 | cmake-build-* 15 | .idea 16 | CMakeCache.txt 17 | CMakeFiles/ 18 | cmake_install.cmake 19 | -------------------------------------------------------------------------------- /package/macos/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.device.audio-input 6 | 7 | 8 | -------------------------------------------------------------------------------- /package/appimage/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).shellNix 14 | -------------------------------------------------------------------------------- /src/fx_cube.h: -------------------------------------------------------------------------------- 1 | #ifndef FX_CUBE_H_ 2 | #define FX_CUBE_H_ 3 | 4 | #include 5 | void fx_cube_init(SDL_Renderer *target_renderer, SDL_Color foreground_color, 6 | unsigned int texture_width, unsigned int texture_height, 7 | unsigned int font_glyph_width); 8 | void fx_cube_destroy(void); 9 | int fx_cube_update(void); 10 | #endif -------------------------------------------------------------------------------- /src/backends/audio.h: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | #ifndef AUDIO_H 4 | #define AUDIO_H 5 | 6 | int audio_initialize(const char *output_device_name, unsigned int audio_buffer_size); 7 | void audio_toggle(const char *output_device_name, unsigned int audio_buffer_size); 8 | void audio_process(void); 9 | void audio_close(void); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /Android.mk: -------------------------------------------------------------------------------- 1 | LOCAL_PATH := $(call my-dir) 2 | 3 | include $(CLEAR_VARS) 4 | 5 | LOCAL_MODULE := m8c 6 | 7 | LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/src/*.c) 8 | 9 | LOCAL_EXPORT_C_INCLUDES := $(wildcard $(LOCAL_PATH)/src/*.h) 10 | 11 | LOCAL_CFLAGS += -DUSE_LIBUSB 12 | 13 | LOCAL_SHARED_LIBRARIES := usb-1.0 SDL3 14 | 15 | LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid 16 | 17 | include $(BUILD_SHARED_LIBRARY) 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | [*.c] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.h] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # Tab indentation (no size specified) 21 | [Makefile] 22 | indent_style = tab -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = (import 3 | ( 4 | let 5 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 6 | in 7 | fetchTarball { 8 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) 12 | { 13 | src = ./.; 14 | }).defaultNix.packages.${builtins.currentSystem}; 15 | in 16 | { 17 | inherit (pkgs) m8c; 18 | } 19 | -------------------------------------------------------------------------------- /src/common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H_ 2 | #define COMMON_H_ 3 | #include "config.h" 4 | 5 | // On MacOS TARGET_OS_IOS is defined as 0, so make sure that it's consistent on other platforms as 6 | // well 7 | #ifndef TARGET_OS_IOS 8 | #define TARGET_OS_IOS 0 9 | #endif 10 | 11 | enum app_state { QUIT, INITIALIZE, WAIT_FOR_DEVICE, RUN }; 12 | 13 | struct app_context { 14 | config_params_s conf; 15 | enum app_state app_state; 16 | char *preferred_device; 17 | unsigned char device_connected; 18 | unsigned char app_suspended; 19 | }; 20 | 21 | #endif -------------------------------------------------------------------------------- /src/ini.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 rxi 3 | * 4 | * This library is free software; you can redistribute it and/or modify it 5 | * under the terms of the MIT license. See `ini.c` for details. 6 | */ 7 | 8 | #ifndef INI_H 9 | #define INI_H 10 | 11 | #define INI_VERSION "0.1.1" 12 | 13 | typedef struct ini_t ini_t; 14 | 15 | ini_t* ini_load(const char *filename); 16 | void ini_free(ini_t *ini); 17 | const char* ini_get(const ini_t *ini, const char *section, const char *key); 18 | int ini_sget(const ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/backends/ringbuffer.h: -------------------------------------------------------------------------------- 1 | #ifndef M8C_RINGBUFFER_H 2 | #define M8C_RINGBUFFER_H 3 | 4 | #include 5 | 6 | typedef struct { 7 | uint8_t *buffer; 8 | uint32_t head; 9 | uint32_t tail; 10 | uint32_t max_size; 11 | uint32_t size; 12 | } RingBuffer; 13 | 14 | RingBuffer *ring_buffer_create(uint32_t size); 15 | 16 | uint32_t ring_buffer_empty(RingBuffer *rb); 17 | 18 | uint32_t ring_buffer_pop(RingBuffer *rb, uint8_t *data, uint32_t length); 19 | 20 | uint32_t ring_buffer_push(RingBuffer *rb, const uint8_t *data, uint32_t length); 21 | 22 | void ring_buffer_free(RingBuffer *rb); 23 | 24 | #endif // M8C_RINGBUFFER_H 25 | -------------------------------------------------------------------------------- /package/appimage/m8c.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | m8c 4 | MIT 5 | MIT 6 | m8c 7 | A Dirtywave M8 remote client 8 | 9 |

m8c is a client for Dirtywave M8 tracker's remote (headless) mode and can mirror the display and route audio.

10 |
11 | m8c.desktop 12 | https://m8c.laamaa.fi 13 | 14 | 15 | 16 | 17 | 18 | 19 | m8c.desktop 20 | 21 |
-------------------------------------------------------------------------------- /src/backends/m8.h: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | #ifndef M8_H_ 4 | #define M8_H_ 5 | 6 | #include "../config.h" 7 | 8 | enum return_codes { 9 | DEVICE_DISCONNECTED = 0, 10 | DEVICE_PROCESSING = 1, 11 | DEVICE_FATAL_ERROR = -1 12 | }; 13 | 14 | int m8_initialize(int verbose, const char *preferred_device); 15 | int m8_list_devices(void); 16 | int m8_reset_display(void); 17 | int m8_enable_display(unsigned char reset_display); 18 | int m8_send_msg_controller(unsigned char input); 19 | int m8_send_msg_keyjazz(unsigned char note, unsigned char velocity); 20 | int m8_process_data(const config_params_s *conf); 21 | int m8_pause_processing(void); 22 | int m8_resume_processing(void); 23 | int m8_close(void); 24 | 25 | #endif -------------------------------------------------------------------------------- /src/fonts/fonts.h: -------------------------------------------------------------------------------- 1 | // fonts.h 2 | #ifndef FONTS_H 3 | #define FONTS_H 4 | 5 | #include 6 | 7 | struct inline_font { 8 | const int width; 9 | const int height; 10 | const int glyph_x; 11 | const int glyph_y; 12 | const int screen_offset_x; 13 | const int screen_offset_y; 14 | const int text_offset_y; 15 | const int waveform_max_height; 16 | const long image_size; 17 | const unsigned char image_data[]; 18 | }; 19 | 20 | // Number of available fonts 21 | size_t fonts_count(void); 22 | 23 | // Get a font by index (returns NULL if index is out of range) 24 | const struct inline_font *fonts_get(size_t index); 25 | 26 | // Get the whole font table (read-only). If count != NULL, it receives the length. 27 | const struct inline_font *const *fonts_all(size_t *count); 28 | 29 | #endif // FONTS_H -------------------------------------------------------------------------------- /src/SDL2_inprint.h: -------------------------------------------------------------------------------- 1 | // Based on bitmap font routine by driedfruit, https://github.com/driedfruit/SDL_inprint 2 | // Released into public domain. 3 | 4 | #ifndef SDL2_inprint_h 5 | #define SDL2_inprint_h 6 | 7 | #include "fonts/fonts.h" 8 | #include 9 | 10 | extern void inline_font_initialize(const struct inline_font *font); 11 | extern void inline_font_close(void); 12 | 13 | extern void inline_font_set_renderer(SDL_Renderer *renderer); 14 | extern void infont(SDL_Texture *font); 15 | extern void incolor1(const SDL_Color *color); 16 | extern void incolor(Uint32 color); /* Color must be in 0x00RRGGBB format ! */ 17 | extern void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y, Uint32 fgcolor, 18 | Uint32 bgcolor); 19 | 20 | const struct inline_font *inline_font_get_current(void); 21 | 22 | #endif /* SDL2_inprint_h */ 23 | -------------------------------------------------------------------------------- /src/fonts/fonts.c: -------------------------------------------------------------------------------- 1 | // fonts.c 2 | #include "fonts.h" 3 | #include "font1.h" 4 | #include "font2.h" 5 | #include "font3.h" 6 | #include "font4.h" 7 | #include "font5.h" 8 | 9 | #include 10 | 11 | static struct inline_font *fonts_storage[] = { 12 | &font_v1_small, &font_v1_large, &font_v2_small, &font_v2_large, &font_v2_huge 13 | }; 14 | 15 | #define FONT_COUNT (sizeof(fonts_storage) / sizeof(fonts_storage[0])) 16 | 17 | size_t fonts_count(void) { 18 | return FONT_COUNT; 19 | } 20 | 21 | const struct inline_font *fonts_get(const size_t index) { 22 | return (index < FONT_COUNT) ? (const struct inline_font *)fonts_storage[index] : NULL; 23 | } 24 | 25 | const struct inline_font *const *fonts_all(size_t *count) { 26 | if (count) *count = FONT_COUNT; 27 | // Cast to a read-only view so callers can’t mutate the array or elements 28 | return (const struct inline_font *const *)fonts_storage; 29 | } -------------------------------------------------------------------------------- /src/command.h: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #ifndef COMMAND_H_ 5 | #define COMMAND_H_ 6 | 7 | #include 8 | 9 | struct position { 10 | uint16_t x; 11 | uint16_t y; 12 | }; 13 | 14 | struct size { 15 | uint16_t width; 16 | uint16_t height; 17 | }; 18 | 19 | struct color { 20 | uint8_t r; 21 | uint8_t g; 22 | uint8_t b; 23 | }; 24 | 25 | struct draw_rectangle_command { 26 | struct position pos; 27 | struct size size; 28 | struct color color; 29 | }; 30 | 31 | struct draw_character_command { 32 | int c; 33 | struct position pos; 34 | struct color foreground; 35 | struct color background; 36 | }; 37 | 38 | struct draw_oscilloscope_waveform_command { 39 | struct color color; 40 | uint8_t waveform[480]; 41 | uint16_t waveform_size; 42 | }; 43 | 44 | int process_command(const uint8_t *recv_buf, uint32_t size); 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | // Simple in-app settings overlay for configuring input bindings and a few options 2 | 3 | #ifndef SETTINGS_H_ 4 | #define SETTINGS_H_ 5 | 6 | #include 7 | #include 8 | 9 | #include "config.h" 10 | 11 | // Forward declaration to avoid header coupling 12 | struct app_context; 13 | 14 | // Open/close and state query 15 | void settings_toggle_open(void); 16 | bool settings_is_open(void); 17 | 18 | // Event handling (consume SDL events when open) 19 | void settings_handle_event(struct app_context *ctx, const SDL_Event *e); 20 | 21 | // Render the settings overlay into a texture-sized canvas and composite to the window 22 | // texture_w/texture_h should be the logical render size (e.g. 320x240 or 480x320) 23 | void settings_render_overlay(SDL_Renderer *rend, const config_params_s *conf, int texture_w, int texture_h); 24 | 25 | // Notify settings overlay that logical render size changed; drops cached texture 26 | void settings_on_texture_size_change(SDL_Renderer *rend); 27 | 28 | #endif // SETTINGS_H_ 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/render.h: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #ifndef RENDER_H_ 5 | #define RENDER_H_ 6 | 7 | #include "command.h" 8 | #include "config.h" 9 | 10 | #include 11 | 12 | int renderer_initialize(config_params_s *conf); 13 | void renderer_close(void); 14 | void renderer_set_font_mode(int mode); 15 | void renderer_fix_texture_scaling_after_window_resize(config_params_s *conf); 16 | void renderer_clear_screen(void); 17 | 18 | void draw_waveform(struct draw_oscilloscope_waveform_command *command); 19 | void draw_rectangle(struct draw_rectangle_command *command); 20 | int draw_character(struct draw_character_command *command); 21 | 22 | void set_m8_model(unsigned int model); 23 | 24 | void render_screen(config_params_s *conf); 25 | int toggle_fullscreen(config_params_s *conf); 26 | void display_keyjazz_overlay(uint8_t show, uint8_t base_octave, uint8_t velocity); 27 | 28 | void show_error_message(const char *message); 29 | 30 | int screensaver_init(void); 31 | void screensaver_draw(void); 32 | void screensaver_destroy(void); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/log_overlay.h: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #ifndef LOG_OVERLAY_H_ 5 | #define LOG_OVERLAY_H_ 6 | 7 | #include 8 | 9 | // Initialize SDL log capture to mirror messages into the in-app overlay buffer 10 | void log_overlay_init(void); 11 | 12 | // Toggle overlay visibility 13 | void log_overlay_toggle(void); 14 | 15 | // Return non-zero if the overlay is currently visible 16 | int log_overlay_is_visible(void); 17 | 18 | // Invalidate any cached resources (e.g., after texture size change) 19 | void log_overlay_invalidate(void); 20 | 21 | // Destroy internal resources used by the overlay 22 | void log_overlay_destroy(void); 23 | 24 | // Ensure the overlay texture is up to date and composite it to the current render target 25 | // font_mode_current is used to restore the caller's font after drawing 26 | void log_overlay_render(SDL_Renderer *renderer, 27 | int logical_texture_width, 28 | int logical_texture_height, 29 | SDL_ScaleMode texture_scaling_mode, 30 | int font_mode_current); 31 | 32 | #endif // LOG_OVERLAY_H_ 33 | 34 | -------------------------------------------------------------------------------- /package/macos/m8c.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${MACOSX_BUNDLE_EXECUTABLE_NAME} 9 | CFBundleGetInfoString 10 | ${MACOSX_BUNDLE_INFO_STRING} 11 | CFBundleIconFile 12 | ${MACOSX_BUNDLE_ICON_FILE} 13 | CFBundleIdentifier 14 | ${MACOSX_BUNDLE_GUI_IDENTIFIER} 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleLongVersionString 18 | ${MACOSX_BUNDLE_LONG_VERSION_STRING} 19 | CFBundleName 20 | ${MACOSX_BUNDLE_BUNDLE_NAME} 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | ${MACOSX_BUNDLE_SHORT_VERSION_STRING} 25 | CFBundleSignature 26 | APPL 27 | CFBundleVersion 28 | ${MACOSX_BUNDLE_BUNDLE_VERSION} 29 | CSResourcesFileMapped 30 | 31 | NSHumanReadableCopyright 32 | ${MACOSX_BUNDLE_COPYRIGHT} 33 | NSMicrophoneUsageDescription 34 | Microphone access is required to enable audio routing. 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/input.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jonne Kokkonen on 15.4.2025. 3 | // 4 | 5 | #ifndef INPUT_H 6 | #define INPUT_H 7 | #include "common.h" 8 | #include "config.h" 9 | 10 | #include 11 | 12 | typedef enum input_buttons_t { 13 | INPUT_UP, 14 | INPUT_DOWN, 15 | INPUT_LEFT, 16 | INPUT_RIGHT, 17 | INPUT_OPT, 18 | INPUT_EDIT, 19 | INPUT_SELECT, 20 | INPUT_START, 21 | INPUT_MAX 22 | } input_buttons_t; 23 | 24 | // Bits for M8 input messages 25 | typedef enum keycodes_t { 26 | key_left = 1 << 7, 27 | key_up = 1 << 6, 28 | key_down = 1 << 5, 29 | key_select = 1 << 4, 30 | key_start = 1 << 3, 31 | key_right = 1 << 2, 32 | key_opt = 1 << 1, 33 | key_edit = 1 34 | } keycodes_t; 35 | 36 | typedef enum input_type_t { normal, keyjazz, special } input_type_t; 37 | 38 | typedef enum special_messages_t { 39 | msg_reset_display = 2 40 | } special_messages_t; 41 | 42 | typedef struct input_msg_s { 43 | input_type_t type; 44 | unsigned char value; 45 | unsigned char value2; 46 | } input_msg_s; 47 | 48 | input_msg_s input_get_msg(config_params_s *conf); 49 | int input_process_and_send(const struct app_context *ctx); 50 | void input_handle_key_down_event(struct app_context *ctx, const SDL_Event *event); 51 | void input_handle_key_up_event(const struct app_context *ctx, const SDL_Event *event); 52 | void input_handle_gamepad_button(struct app_context *ctx, SDL_GamepadButton button, bool pressed); 53 | void input_handle_gamepad_axis(const struct app_context *ctx, SDL_GamepadAxis axis, Sint16 value); 54 | void input_handle_finger_down(struct app_context *ctx, const SDL_Event *event); 55 | 56 | #endif // INPUT_H 57 | -------------------------------------------------------------------------------- /src/backends/ringbuffer.c: -------------------------------------------------------------------------------- 1 | #include "ringbuffer.h" 2 | #include 3 | 4 | RingBuffer *ring_buffer_create(uint32_t size) { 5 | RingBuffer *rb = SDL_malloc(sizeof(*rb)); 6 | rb->buffer = SDL_malloc(sizeof(*rb->buffer) * size); 7 | rb->head = 0; 8 | rb->tail = 0; 9 | rb->max_size = size; 10 | rb->size = 0; 11 | return rb; 12 | } 13 | 14 | void ring_buffer_free(RingBuffer *rb) { 15 | SDL_free(rb->buffer); 16 | SDL_free(rb); 17 | } 18 | 19 | uint32_t ring_buffer_empty(RingBuffer *rb) { return rb->size == 0; } 20 | 21 | uint32_t ring_buffer_full(RingBuffer *rb) { return rb->size == rb->max_size; } 22 | 23 | uint32_t ring_buffer_push(RingBuffer *rb, const uint8_t *data, uint32_t length) { 24 | if (ring_buffer_full(rb)) { 25 | return -1; // buffer full, push fails 26 | } 27 | uint32_t space1 = rb->max_size - rb->tail; 28 | uint32_t n = length <= rb->max_size - rb->size ? length : rb->max_size - rb->size; 29 | if (n <= space1) { 30 | SDL_memcpy(rb->buffer + rb->tail, data, n); 31 | } else { 32 | SDL_memcpy(rb->buffer + rb->tail, data, space1); 33 | SDL_memcpy(rb->buffer, data + space1, n - space1); 34 | } 35 | rb->tail = (rb->tail + n) % rb->max_size; 36 | rb->size += n; 37 | return n; // push successful, returns number of bytes pushed 38 | } 39 | 40 | uint32_t ring_buffer_pop(RingBuffer *rb, uint8_t *data, uint32_t length) { 41 | if (ring_buffer_empty(rb)) { 42 | return -1; // buffer empty, pop fails 43 | } 44 | uint32_t space1 = rb->max_size - rb->head; 45 | uint32_t n = length <= rb->size ? length : rb->size; 46 | if (n <= space1) { 47 | SDL_memcpy(data, rb->buffer + rb->head, n); 48 | } else { 49 | SDL_memcpy(data, rb->buffer + rb->head, space1); 50 | SDL_memcpy(data + space1, rb->buffer, n - space1); 51 | } 52 | rb->head = (rb->head + n) % rb->max_size; 53 | rb->size -= n; 54 | return n; // pop successful, returns number of bytes popped 55 | } 56 | -------------------------------------------------------------------------------- /src/backends/queue.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jonne on 3/17/25. 3 | // 4 | 5 | #ifndef QUEUE_H 6 | #define QUEUE_H 7 | 8 | #include 9 | 10 | #define MAX_QUEUE_SIZE 8192 11 | 12 | typedef struct { 13 | unsigned char *messages[MAX_QUEUE_SIZE]; 14 | size_t lengths[MAX_QUEUE_SIZE]; // Store lengths of each message 15 | int front; 16 | int rear; 17 | SDL_Mutex *mutex; 18 | SDL_Condition *cond; 19 | } message_queue_s; 20 | 21 | /** 22 | * Initializes the message queue structure. 23 | * 24 | * @param queue A pointer to the message queue structure to be initialized. 25 | */ 26 | void init_queue(message_queue_s *queue); 27 | 28 | /** 29 | * Destroys the message queue and releases associated resources. 30 | * 31 | * @param queue A pointer to the message queue structure to be destroyed. 32 | */ 33 | void destroy_queue(message_queue_s *queue); 34 | 35 | /** 36 | * Retrieves and removes a message from the front of the message queue. 37 | * If the queue is empty, the function returns NULL. 38 | * 39 | * @param queue A pointer to the message queue structure from which the message is to be retrieved. 40 | * @param length A pointer to a variable where the length of the retrieved message will be stored. 41 | * @return A pointer to the retrieved message, or NULL if the queue is empty. 42 | */ 43 | unsigned char *pop_message(message_queue_s *queue, size_t *length); 44 | 45 | /** 46 | * Adds a new message to the message queue. 47 | * If the queue is full, the message will not be added. 48 | * 49 | * @param queue A pointer to the message queue structure where the message is to be stored. 50 | * @param message A pointer to the message data to be added to the queue. 51 | * @param length The length of the message in bytes. 52 | */ 53 | void push_message(message_queue_s *queue, const unsigned char *message, size_t length); 54 | 55 | /** 56 | * Calculates the current size of the message queue. 57 | * 58 | * @param queue A pointer to the message queue structure whose size is to be determined. 59 | * @return The number of messages currently in the queue. 60 | */ 61 | unsigned int queue_size(const message_queue_s *queue); 62 | 63 | #endif // QUEUE_H 64 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #Set the compiler you are using 2 | CC ?= gcc 3 | 4 | #Set the filename extension of your C files 5 | EXTENSION = .c 6 | 7 | # Location of the source files 8 | SOURCE_DIR = src/ 9 | 10 | # Find all source files in the src directory and subdirectories 11 | SRC_FILES := $(shell find $(SOURCE_DIR) -type f -name "*$(EXTENSION)") 12 | 13 | # Convert to object files 14 | OBJ := $(SRC_FILES:.c=.o) 15 | 16 | # Find all header files for dependencies 17 | DEPS := $(shell find src -type f -name "*.h") 18 | 19 | 20 | #Any special libraries you are using in your project (e.g. -lbcm2835 -lrt `pkg-config --libs gtk+-3.0` ), or leave blank 21 | INCLUDES = $(shell pkg-config --libs sdl3 libserialport | sed 's/-mwindows//') 22 | 23 | #Set any compiler flags you want to use (e.g. -I/usr/include/somefolder `pkg-config --cflags gtk+-3.0` ), or leave blank 24 | local_CFLAGS = $(CFLAGS) $(shell pkg-config --cflags sdl3 libserialport) -DUSE_LIBSERIALPORT -Wall -Wextra -O2 -pipe -I. -DNDEBUG 25 | 26 | #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 27 | %.o: %$(EXTENSION) $(DEPS) 28 | $(CC) -c -o $@ $< $(local_CFLAGS) 29 | 30 | #Combine them into the output file 31 | #Set your desired exe output file name here 32 | m8c: $(OBJ) 33 | $(CC) -o $@ $^ $(local_CFLAGS) $(INCLUDES) 34 | 35 | libusb: INCLUDES = $(shell pkg-config --libs sdl3 libusb-1.0) 36 | libusb: local_CFLAGS = $(CFLAGS) $(shell pkg-config --cflags sdl3 libusb-1.0) -Wall -Wextra -O2 -pipe -I. -DUSE_LIBUSB=1 -DNDEBUG 37 | libusb: m8c 38 | 39 | rtmidi: INCLUDES = $(shell pkg-config --libs sdl3 rtmidi) 40 | rtmidi: local_CFLAGS = $(CFLAGS) $(shell pkg-config --cflags sdl3 rtmidi) -Wall -Wextra -O2 -pipe -I. -DUSE_RTMIDI -DNDEBUG 41 | rtmidi: m8c 42 | 43 | #Cleanup 44 | .PHONY: clean 45 | 46 | clean: 47 | rm -f src/*.o src/backends/*.o *~ m8c 48 | 49 | # PREFIX is environment variable, but if it is not set, then set default value 50 | ifeq ($(PREFIX),) 51 | PREFIX := /usr/local 52 | endif 53 | 54 | install: m8c 55 | install -d $(DESTDIR)$(PREFIX)/bin/ 56 | install -m 755 m8c $(DESTDIR)$(PREFIX)/bin/ 57 | -------------------------------------------------------------------------------- /package/appimage/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | # Ubuntu 20.04 5 | 6 | APP=m8c 7 | VERSION=2.0.0 8 | 9 | if [ "$1" == "build-sdl" ]; then 10 | 11 | ## Build SDL 12 | SDL_VERSION=3.2.6 13 | SDL_SHA256="096a0b843dd1124afda41c24bd05034af75af37e9a1b9d205cc0a70193b27e1a SDL3-3.2.6.tar.gz" 14 | 15 | sudo apt-get install build-essential git make \ 16 | pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \ 17 | libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \ 18 | libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev \ 19 | libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \ 20 | libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libserialport-dev python2 21 | 22 | if [ ! -d SDL3-$SDL_VERSION ]; then 23 | 24 | curl -O https://www.libsdl.org/release/SDL3-$SDL_VERSION.tar.gz 25 | 26 | if ! echo $SDL_SHA256 | sha256sum -c --status -; then 27 | echo "SDL archive checksum failed" >&2 28 | exit 1 29 | fi 30 | 31 | tar zxvf SDL3-$SDL_VERSION.tar.gz 32 | pushd SDL3-$SDL_VERSION 33 | mkdir build_x86_64 34 | cmake -S . -B build_x86_64 35 | cmake --build build_x86_64 36 | sudo cmake --install build_x86_64 37 | popd 38 | 39 | fi 40 | 41 | fi 42 | 43 | if [ "$2" == "build-m8c" ]; then 44 | make 45 | fi 46 | 47 | mkdir -p $APP.AppDir/usr/bin 48 | cp m8c $APP.AppDir/usr/bin/ 49 | cp gamecontrollerdb.txt $APP.AppDir 50 | 51 | mkdir -p $APP.AppDir/usr/share/applications/ $APP.AppDir/usr/share/metainfo/ 52 | cp package/appimage/m8c.desktop $APP.AppDir/usr/share/applications/ 53 | #appstreamcli seems to crash 54 | #cp package/appimage/m8c.appdata.xml $APP.AppDir/usr/share/metainfo/ 55 | cp package/appimage/icon.svg $APP.AppDir 56 | 57 | wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) 58 | chmod +x ./appimagetool-*.AppImage 59 | ./appimagetool-*.AppImage deploy ./$APP.AppDir/usr/share/applications/m8c.desktop 60 | ./appimagetool-*.AppImage ./$APP.AppDir 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #ifndef CONFIG_H_ 5 | #define CONFIG_H_ 6 | 7 | #include "ini.h" 8 | 9 | typedef struct config_params_s { 10 | char *filename; 11 | unsigned int init_fullscreen; 12 | unsigned int integer_scaling; 13 | unsigned int wait_packets; 14 | unsigned int audio_enabled; 15 | unsigned int audio_buffer_size; 16 | char *audio_device_name; 17 | 18 | unsigned int key_up; 19 | unsigned int key_left; 20 | unsigned int key_down; 21 | unsigned int key_right; 22 | unsigned int key_select; 23 | unsigned int key_select_alt; 24 | unsigned int key_start; 25 | unsigned int key_start_alt; 26 | unsigned int key_opt; 27 | unsigned int key_opt_alt; 28 | unsigned int key_edit; 29 | unsigned int key_edit_alt; 30 | unsigned int key_delete; 31 | unsigned int key_reset; 32 | unsigned int key_jazz_inc_octave; 33 | unsigned int key_jazz_dec_octave; 34 | unsigned int key_jazz_inc_velocity; 35 | unsigned int key_jazz_dec_velocity; 36 | unsigned int key_toggle_audio; 37 | unsigned int key_toggle_settings; 38 | unsigned int key_toggle_log; 39 | 40 | int gamepad_up; 41 | int gamepad_left; 42 | int gamepad_down; 43 | int gamepad_right; 44 | int gamepad_select; 45 | int gamepad_start; 46 | int gamepad_opt; 47 | int gamepad_edit; 48 | int gamepad_quit; 49 | int gamepad_reset; 50 | 51 | int gamepad_analog_threshold; 52 | int gamepad_analog_invert; 53 | int gamepad_analog_axis_updown; 54 | int gamepad_analog_axis_leftright; 55 | int gamepad_analog_axis_start; 56 | int gamepad_analog_axis_select; 57 | int gamepad_analog_axis_opt; 58 | int gamepad_analog_axis_edit; 59 | 60 | } config_params_s; 61 | 62 | config_params_s config_initialize(char *filename); 63 | void config_read(config_params_s *conf); 64 | void read_audio_config(const ini_t *ini, config_params_s *conf); 65 | void read_graphics_config(const ini_t *ini, config_params_s *conf); 66 | void read_key_config(const ini_t *ini, config_params_s *conf); 67 | void read_gamepad_config(const ini_t *ini, config_params_s *conf); 68 | 69 | // Expose write so settings UI can persist changes 70 | void write_config(const config_params_s *conf); 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /.github/workflows/update-package-versions.yml: -------------------------------------------------------------------------------- 1 | name: Update Version and Hash in flake.nix 2 | 3 | on: 4 | push: 5 | tags: 6 | - v2.* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update-version: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | ref: ${{ github.event.push.head.ref }} 19 | 20 | - name: Set up Nix 21 | uses: cachix/install-nix-action@v22 22 | with: 23 | nix_path: nixpkgs=channel:nixos-unstable 24 | 25 | - name: Find newest annotated tag 26 | id: newest-tag 27 | run: | 28 | latest_annotated_tag=$(git describe --tags $(git rev-list --tags --max-count=1)) 29 | echo "Newest annotated tag: $latest_annotated_tag" 30 | echo "latest_annotated_tag=$latest_annotated_tag" >> $GITHUB_OUTPUT 31 | 32 | - name: Check if tag is valid version 33 | id: valid-tag 34 | run: | 35 | latest_annotated_tag="${{ steps.newest-tag.outputs.latest_annotated_tag }}" 36 | echo "Passed value: $latest_annotated_tag" 37 | if [[ $latest_annotated_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 38 | echo "Valid tag format" 39 | echo "valid=true" >> $GITHUB_OUTPUT 40 | else 41 | echo "Invalid tag format" 42 | echo "valid=false" >> $GITHUB_OUTPUT 43 | fi 44 | 45 | - name: Update version in flake.nix 46 | if: steps.valid-tag.outputs.valid 47 | run: | 48 | latest_annotated_tag="${{ steps.newest-tag.outputs.latest_annotated_tag }}" 49 | latest_annotated_tag_without_v="${latest_annotated_tag#v}" # Remove 'v' prefix 50 | sed -i "s/version = \".*\";/version = \"$latest_annotated_tag_without_v\";/" flake.nix 51 | new_hash=$(nix-prefetch-url --unpack --type sha256 "https://github.com/laamaa/m8c/archive/v$latest_annotated_tag_without_v.tar.gz") # Use updated variable name 52 | sed -i "s/hash = \".*\";/hash = \"sha256:$new_hash\";/" flake.nix 53 | git config user.email "github-actions@github.com" 54 | git config user.name "GitHub Actions" 55 | git commit -am "Update version and hash in flake.nix to $latest_annotated_tag_without_v" 56 | git push origin HEAD:main 57 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "nixpkgs": { 20 | "locked": { 21 | "lastModified": 1756819007, 22 | "narHash": "sha256-12V64nKG/O/guxSYnr5/nq1EfqwJCdD2+cIGmhz3nrE=", 23 | "owner": "NixOS", 24 | "repo": "nixpkgs", 25 | "rev": "aaff8c16d7fc04991cac6245bee1baa31f72b1e1", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "NixOS", 30 | "ref": "nixpkgs-unstable", 31 | "repo": "nixpkgs", 32 | "type": "github" 33 | } 34 | }, 35 | "root": { 36 | "inputs": { 37 | "flake-compat": "flake-compat", 38 | "nixpkgs": "nixpkgs", 39 | "systems": "systems", 40 | "treefmt-nix": "treefmt-nix" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | }, 58 | "treefmt-nix": { 59 | "inputs": { 60 | "nixpkgs": [ 61 | "nixpkgs" 62 | ] 63 | }, 64 | "locked": { 65 | "lastModified": 1721769617, 66 | "narHash": "sha256-6Pqa0bi5nV74IZcENKYRToRNM5obo1EQ+3ihtunJ014=", 67 | "owner": "numtide", 68 | "repo": "treefmt-nix", 69 | "rev": "8db8970be1fb8be9c845af7ebec53b699fe7e009", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "numtide", 74 | "repo": "treefmt-nix", 75 | "type": "github" 76 | } 77 | } 78 | }, 79 | "root": "root", 80 | "version": 7 81 | } 82 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | systems.url = "github:nix-systems/default"; 5 | treefmt-nix = { 6 | url = "github:numtide/treefmt-nix"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | flake-compat = { 10 | url = "github:edolstra/flake-compat"; 11 | flake = false; 12 | }; 13 | }; 14 | 15 | outputs = { self, nixpkgs, systems, treefmt-nix, ... }: 16 | let 17 | pname = "m8c"; 18 | version = "2.2.0"; 19 | m8c-package = 20 | { stdenv 21 | , cmake 22 | , copyDesktopItems 23 | , pkg-config 24 | , sdl3 25 | , libserialport 26 | }: 27 | stdenv.mkDerivation { 28 | inherit pname version; 29 | src = ./.; 30 | 31 | nativeBuildInputs = [ cmake copyDesktopItems pkg-config ]; 32 | buildInputs = [ sdl3 libserialport ]; 33 | }; 34 | eachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f 35 | (import nixpkgs { inherit system; }) 36 | ); 37 | treefmtEval = eachSystem (pkgs: treefmt-nix.lib.evalModule pkgs (_: { 38 | projectRootFile = "flake.nix"; 39 | programs = { 40 | clang-format.enable = false; # TODO(pope): Enable and format C code 41 | deadnix.enable = true; 42 | nixpkgs-fmt.enable = true; 43 | statix.enable = true; 44 | }; 45 | })); 46 | in 47 | { 48 | packages = eachSystem (pkgs: rec { 49 | m8c = pkgs.callPackage m8c-package { }; 50 | default = m8c; 51 | }); 52 | 53 | overlays.default = final: _prev: { 54 | inherit (self.packages.${final.system}) m8c; 55 | }; 56 | 57 | apps = eachSystem (pkgs: rec { 58 | m8c = { 59 | type = "app"; 60 | program = "${self.packages.${pkgs.system}.m8c}/bin/m8c"; 61 | }; 62 | default = m8c; 63 | }); 64 | 65 | devShells = eachSystem (pkgs: with pkgs; { 66 | default = mkShell { 67 | packages = [ 68 | cmake 69 | gnumake 70 | nix-prefetch-github 71 | treefmtEval.${system}.config.build.wrapper 72 | ]; 73 | inputsFrom = [ self.packages.${system}.m8c ]; 74 | }; 75 | }); 76 | 77 | formatter = eachSystem (pkgs: treefmtEval.${pkgs.system}.config.build.wrapper); 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/backends/queue.c: -------------------------------------------------------------------------------- 1 | #include "queue.h" 2 | #include 3 | #include 4 | #include 5 | 6 | // Initialize the message queue 7 | void init_queue(message_queue_s *queue) { 8 | queue->front = 0; 9 | queue->rear = 0; 10 | queue->mutex = SDL_CreateMutex(); 11 | queue->cond = SDL_CreateCondition(); 12 | } 13 | 14 | // Free allocated memory and destroy mutex 15 | void destroy_queue(message_queue_s *queue) { 16 | SDL_LockMutex(queue->mutex); 17 | 18 | while (queue->front != queue->rear) { 19 | SDL_free(queue->messages[queue->front]); 20 | queue->front = (queue->front + 1) % MAX_QUEUE_SIZE; 21 | } 22 | 23 | SDL_UnlockMutex(queue->mutex); 24 | SDL_DestroyMutex(queue->mutex); 25 | SDL_DestroyCondition(queue->cond); 26 | } 27 | 28 | // Push a message to the queue 29 | void push_message(message_queue_s *queue, const unsigned char *message, size_t length) { 30 | SDL_LockMutex(queue->mutex); 31 | 32 | if ((queue->rear + 1) % MAX_QUEUE_SIZE == queue->front) { 33 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM,"Queue is full, cannot add message."); 34 | } else { 35 | // Allocate space for the message and store it 36 | queue->messages[queue->rear] = SDL_malloc(length); 37 | SDL_memcpy(queue->messages[queue->rear], message, length); 38 | queue->lengths[queue->rear] = length; 39 | queue->rear = (queue->rear + 1) % MAX_QUEUE_SIZE; 40 | SDL_SignalCondition(queue->cond); // Signal consumer thread 41 | } 42 | 43 | SDL_UnlockMutex(queue->mutex); 44 | } 45 | 46 | // Pop a message from the queue 47 | unsigned char *pop_message(message_queue_s *queue, size_t *length) { 48 | SDL_LockMutex(queue->mutex); 49 | 50 | // Check if the queue is empty 51 | if (queue->front == queue->rear) { 52 | SDL_UnlockMutex(queue->mutex); 53 | return NULL; // Return NULL if there are no messages 54 | } 55 | 56 | // Otherwise, retrieve the message and its length 57 | *length = queue->lengths[queue->front]; 58 | unsigned char *message = queue->messages[queue->front]; 59 | queue->front = (queue->front + 1) % MAX_QUEUE_SIZE; 60 | 61 | SDL_UnlockMutex(queue->mutex); 62 | return message; 63 | } 64 | 65 | unsigned int queue_size(const message_queue_s *queue) { 66 | SDL_LockMutex(queue->mutex); 67 | const unsigned int size = (queue->rear - queue->front + MAX_QUEUE_SIZE) % MAX_QUEUE_SIZE; 68 | SDL_UnlockMutex(queue->mutex); 69 | return size; 70 | } -------------------------------------------------------------------------------- /src/backends/slip.h: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2018 Marcin Borowicz 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | /* This code is originally by marcinbor85, https://github.com/marcinbor85/slip 26 | It has been simplified a bit as CRC checking etc. is not required in this 27 | program. */ 28 | 29 | #ifndef SLIP_H_ 30 | #define SLIP_H_ 31 | 32 | #include 33 | 34 | #define SLIP_SPECIAL_BYTE_END 0xC0 35 | #define SLIP_SPECIAL_BYTE_ESC 0xDB 36 | 37 | #define SLIP_ESCAPED_BYTE_END 0xDC 38 | #define SLIP_ESCAPED_BYTE_ESC 0xDD 39 | 40 | typedef enum { 41 | SLIP_STATE_NORMAL = 0x00, 42 | SLIP_STATE_ESCAPED 43 | } slip_state_t; 44 | 45 | typedef struct { 46 | uint8_t *buf; 47 | uint32_t buf_size; 48 | int (*recv_message)(uint8_t *data, uint32_t size); 49 | } slip_descriptor_s; 50 | 51 | typedef struct { 52 | slip_state_t state; 53 | uint32_t size; 54 | const slip_descriptor_s *descriptor; 55 | } slip_handler_s; 56 | 57 | typedef enum { 58 | SLIP_NO_ERROR = 0x00, 59 | SLIP_ERROR_BUFFER_OVERFLOW, 60 | SLIP_ERROR_UNKNOWN_ESCAPED_BYTE, 61 | SLIP_ERROR_INVALID_PACKET 62 | } slip_error_t; 63 | 64 | slip_error_t slip_init(slip_handler_s *slip, const slip_descriptor_s *descriptor); 65 | slip_error_t slip_read_byte(slip_handler_s *slip, uint8_t byte); 66 | 67 | #endif -------------------------------------------------------------------------------- /src/backends/slip.c: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2018 Marcin Borowicz 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | /* This code is originally by marcinbor85, https://github.com/marcinbor85/slip 26 | It has been simplified a bit as CRC checking etc. is not required in this 27 | program. */ 28 | 29 | #include "slip.h" 30 | 31 | #include 32 | #include 33 | 34 | static void reset_rx(slip_handler_s *slip) { 35 | assert(slip != NULL); 36 | 37 | slip->state = SLIP_STATE_NORMAL; 38 | slip->size = 0; 39 | } 40 | 41 | slip_error_t slip_init(slip_handler_s *slip, const slip_descriptor_s *descriptor) { 42 | assert(slip != NULL); 43 | assert(descriptor != NULL); 44 | assert(descriptor->buf != NULL); 45 | assert(descriptor->recv_message != NULL); 46 | 47 | slip->descriptor = descriptor; 48 | reset_rx(slip); 49 | 50 | return SLIP_NO_ERROR; 51 | } 52 | 53 | static slip_error_t put_byte_to_buffer(slip_handler_s *slip, const uint8_t byte) { 54 | slip_error_t error = SLIP_NO_ERROR; 55 | 56 | assert(slip != NULL); 57 | 58 | if (slip->size >= slip->descriptor->buf_size) { 59 | error = SLIP_ERROR_BUFFER_OVERFLOW; 60 | reset_rx(slip); 61 | } else { 62 | slip->descriptor->buf[slip->size++] = byte; 63 | slip->state = SLIP_STATE_NORMAL; 64 | } 65 | 66 | return error; 67 | } 68 | 69 | slip_error_t slip_read_byte(slip_handler_s *slip, uint8_t byte) { 70 | slip_error_t error = SLIP_NO_ERROR; 71 | 72 | assert(slip != NULL); 73 | 74 | switch (slip->state) { 75 | case SLIP_STATE_NORMAL: 76 | switch (byte) { 77 | case SLIP_SPECIAL_BYTE_END: 78 | if (!slip->descriptor->recv_message(slip->descriptor->buf, slip->size)) { 79 | error = SLIP_ERROR_INVALID_PACKET; 80 | } 81 | reset_rx(slip); 82 | break; 83 | case SLIP_SPECIAL_BYTE_ESC: 84 | slip->state = SLIP_STATE_ESCAPED; 85 | break; 86 | default: 87 | error = put_byte_to_buffer(slip, byte); 88 | break; 89 | } 90 | break; 91 | 92 | case SLIP_STATE_ESCAPED: 93 | switch (byte) { 94 | case SLIP_ESCAPED_BYTE_END: 95 | byte = SLIP_SPECIAL_BYTE_END; 96 | break; 97 | case SLIP_ESCAPED_BYTE_ESC: 98 | byte = SLIP_SPECIAL_BYTE_ESC; 99 | break; 100 | default: 101 | error = SLIP_ERROR_UNKNOWN_ESCAPED_BYTE; 102 | reset_rx(slip); 103 | break; 104 | } 105 | 106 | if (error != SLIP_NO_ERROR) 107 | break; 108 | 109 | error = put_byte_to_buffer(slip, byte); 110 | break; 111 | } 112 | 113 | return error; 114 | } 115 | -------------------------------------------------------------------------------- /src/gamepads.c: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jonne on 8/19/24. 3 | // 4 | 5 | #include "gamepads.h" 6 | 7 | #include 8 | #include 9 | 10 | // Maximum number of game controllers to support 11 | #define MAX_CONTROLLERS 4 12 | 13 | SDL_Gamepad *game_controllers[MAX_CONTROLLERS]; 14 | 15 | /** 16 | * Initializes available game controllers and loads game controller mappings. 17 | * 18 | * This function scans for connected joysticks and attempts to open those that are recognized 19 | * as game controllers. It also loads the game controller mapping database to improve compatibility 20 | * with various devices. Any errors during initialization are logged. 21 | * 22 | * @return The number of successfully initialized game controllers. Returns -1 if an error occurs 23 | * during the initialization process. 24 | */ 25 | int gamepads_initialize(void) { 26 | 27 | int num_joysticks = 0; 28 | SDL_JoystickID *joystick_ids = NULL; 29 | 30 | if (SDL_Init(SDL_INIT_GAMEPAD) == false) { 31 | SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Failed to initialize SDL_GAMEPAD: %s", SDL_GetError()); 32 | return -1; 33 | } 34 | 35 | int controller_index = 0; 36 | 37 | SDL_Log("Looking for game controllers"); 38 | SDL_Delay(10); // Some controllers like XBone wired need a little while to get ready 39 | 40 | // Try to load the game controller database file 41 | char db_filename[2048] = {0}; 42 | if (snprintf(db_filename, sizeof(db_filename), "%sgamecontrollerdb.txt", 43 | SDL_GetPrefPath("", "m8c")) >= (int)sizeof(db_filename)) { 44 | SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Path too long for buffer"); 45 | return -1; 46 | } 47 | SDL_Log("Trying to open game controller database from %s", db_filename); 48 | SDL_IOStream *db_rw = SDL_IOFromFile(db_filename, "rb"); 49 | if (db_rw == NULL) { 50 | snprintf(db_filename, sizeof(db_filename), "%sgamecontrollerdb.txt", SDL_GetBasePath()); 51 | SDL_Log("Trying to open game controller database from %s", db_filename); 52 | db_rw = SDL_IOFromFile(db_filename, "rb"); 53 | } 54 | 55 | if (db_rw != NULL) { 56 | const int mappings = SDL_AddGamepadMappingsFromIO(db_rw, true); 57 | if (mappings != -1) { 58 | SDL_Log("Found %d game controller mappings", mappings); 59 | } else { 60 | SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Error loading game controller mappings."); 61 | } 62 | } else { 63 | SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Unable to open game controller database file."); 64 | } 65 | 66 | joystick_ids = SDL_GetGamepads(&num_joysticks); 67 | if (joystick_ids == NULL) { 68 | SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Failed to get gamepad IDs: %s", SDL_GetError()); 69 | return -1; 70 | } 71 | 72 | // Open all available game controllers 73 | SDL_Log("Found %d gamepads", num_joysticks); 74 | for (int i = 0; i < num_joysticks; i++) { 75 | if (!SDL_IsGamepad(joystick_ids[i])) 76 | continue; 77 | if (controller_index >= MAX_CONTROLLERS) 78 | break; 79 | game_controllers[controller_index] = SDL_OpenGamepad(joystick_ids[i]); 80 | if (game_controllers[controller_index] == NULL) { 81 | SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Failed to open gamepad %d: %s", i, SDL_GetError()); 82 | continue; 83 | } 84 | SDL_Log("Controller %d: %s", controller_index + 1, 85 | SDL_GetGamepadName(game_controllers[controller_index])); 86 | controller_index++; 87 | } 88 | 89 | SDL_free(joystick_ids); 90 | 91 | return controller_index; 92 | } 93 | 94 | // Closes all open game controllers 95 | void gamepads_close(void) { 96 | 97 | for (int i = 0; i < MAX_CONTROLLERS; i++) { 98 | if (game_controllers[i]) 99 | SDL_CloseGamepad(game_controllers[i]); 100 | } 101 | 102 | SDL_QuitSubSystem(SDL_INIT_GAMEPAD); 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/build-ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: m8c linux x64 build 2 | 3 | env: 4 | SDL_VERSION: 3.2.20 5 | SDL_SHA256: "467600ae090dd28616fa37369faf4e3143198ff1da37729b552137e47f751a67 SDL3-3.2.20.tar.gz" 6 | 7 | on: 8 | push: 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | build-linux: 15 | runs-on: ubuntu-latest 16 | name: linux-x86_64 17 | steps: 18 | - name: 'Install dependencies' 19 | run: | 20 | sudo apt-get update -y 21 | sudo apt-get install -y --fix-missing build-essential libserialport-dev zip git make pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libpipewire-0.3-dev libwayland-dev libdecor-0-dev 22 | 23 | - name: 'Checkout' 24 | uses: actions/checkout@v4 25 | 26 | - name: Set current date as env variable 27 | run: echo "NOW=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 28 | 29 | - name: 'Cache SDL3 files' 30 | id: cache-x86_64-sdl3-files 31 | uses: actions/cache@v4 32 | with: 33 | path: 'SDL3-3.2.20' 34 | key: linux-x86_64-sdl3-files 35 | 36 | - name: 'Download SDL3 sources' 37 | if: steps.cache-x86_64-sdl3-files.outputs.cache-hit != 'true' 38 | run: | 39 | (curl -O https://www.libsdl.org/release/SDL3-$SDL_VERSION.tar.gz || curl -O -L https://github.com/libsdl-org/SDL/releases/download/release-$SDL_VERSION/SDL3-$SDL_VERSION.tar.gz) 40 | if ! echo $SDL_SHA256 | sha256sum -c --status -; then echo "SDL archive checksum failed"; exit 1; fi 41 | tar zxvf SDL3-$SDL_VERSION.tar.gz 42 | 43 | - name: 'Build SDL3' 44 | if: steps.cache-x86_64-sdl3-files.outputs.cache-hit != 'true' 45 | run: | 46 | pushd SDL3-$SDL_VERSION 47 | mkdir build_x86_64 48 | cmake -S . -B build_x86_64 -DCMAKE_BUILD_TYPE=Release 49 | cmake --build build_x86_64 50 | popd 51 | 52 | - name: 'Install SDL3' 53 | run: | 54 | pushd SDL3-$SDL_VERSION 55 | sudo cmake --install build_x86_64 56 | popd 57 | 58 | - name: 'Build binary' 59 | run: | 60 | make 61 | 62 | - name: 'Upload artifact' 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: m8c-${{ env.NOW }}-linux-x86_64 66 | path: | 67 | LICENSE 68 | README.md 69 | AUDIOGUIDE.md 70 | m8c 71 | gamecontrollerdb.txt 72 | 73 | - name: 'Build AppImage' 74 | run: | 75 | bash package/appimage/package.sh 76 | 77 | - name: 'Upload artifact' 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: m8c-${{ env.NOW }}-linux-x86_64.AppImage 81 | path: | 82 | m8c*.AppImage 83 | 84 | build-linux-with-nix: 85 | runs-on: ubuntu-latest 86 | name: with-nix-linux-x86_64 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | 91 | - name: Set current date as env variable 92 | run: echo "NOW=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 93 | 94 | - name: Set up Nix 95 | uses: cachix/install-nix-action@v31 96 | with: 97 | nix_path: nixpkgs=channel:nixos-unstable 98 | 99 | - name: 'Build binary' 100 | run: nix-build 101 | 102 | - name: 'Upload artifact' 103 | uses: actions/upload-artifact@v4 104 | with: 105 | name: m8c-${{ env.NOW }}-with-nix-linux-x86_64 106 | path: result 107 | -------------------------------------------------------------------------------- /src/fonts/font1.h: -------------------------------------------------------------------------------- 1 | #ifndef FONT1_H_ 2 | #define FONT1_H_ 3 | 4 | #include "fonts.h" 5 | 6 | struct inline_font font_v1_small = { 7 | 470, 8 | 7, 9 | 5, 10 | 7, 11 | 0, 12 | 0, 13 | 3, 14 | 24, 15 | 566, 16 | { 17 | 0x42, 0x4D, 0x36, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x7C, 18 | 0x00, 0x00, 0x00, 0xD6, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0xA4, 0x01, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 20 | 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 21 | 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x47, 0x52, 0x73, 0x00, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 25 | 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0x00, 0x40, 0x34, 27 | 0x01, 0x80, 0x00, 0x40, 0x12, 0x1F, 0xFF, 0xFE, 0x1F, 0xFC, 0x9E, 0x08, 0x10, 0x00, 0x00, 28 | 0x8E, 0x8F, 0x9D, 0xEF, 0xC1, 0xD1, 0xFB, 0xA3, 0xF8, 0xC5, 0xD0, 0x6C, 0x7C, 0x47, 0x12, 29 | 0x31, 0xF7, 0xC6, 0x1C, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0x93, 0xF1, 0x77, 0x22, 0xEA, 0xC7, 30 | 0xF0, 0x0C, 0x3E, 0x7F, 0xE3, 0xF1, 0xFF, 0xFE, 0x4F, 0x80, 0x00, 0x00, 0x15, 0xF9, 0xC8, 31 | 0x02, 0x40, 0x08, 0x40, 0x02, 0x11, 0x24, 0x02, 0x10, 0xC4, 0x91, 0x0A, 0x10, 0x20, 0x20, 32 | 0x10, 0x8C, 0x63, 0x18, 0x42, 0x31, 0x24, 0x65, 0x08, 0xC6, 0x30, 0x94, 0x42, 0x48, 0xAB, 33 | 0x71, 0x0C, 0x04, 0x14, 0x00, 0x00, 0x8C, 0x61, 0x18, 0x10, 0x31, 0x20, 0xB4, 0x4A, 0xC6, 34 | 0x3F, 0xFC, 0x02, 0x48, 0xD2, 0xAA, 0x0A, 0x22, 0x48, 0x80, 0x00, 0x80, 0x3E, 0x55, 0xD4, 35 | 0x02, 0x40, 0x08, 0x00, 0x01, 0x19, 0x24, 0x02, 0x10, 0xC4, 0x91, 0x08, 0x00, 0x6F, 0xB0, 36 | 0x97, 0x8C, 0x61, 0x18, 0x42, 0x71, 0x24, 0x69, 0x08, 0xC6, 0x30, 0xAC, 0x82, 0x48, 0xAA, 37 | 0xAA, 0x0A, 0x04, 0x24, 0x00, 0x00, 0xFC, 0x61, 0x1F, 0x93, 0xF1, 0x20, 0xA8, 0x4A, 0xC6, 38 | 0x31, 0x8C, 0x3E, 0x48, 0xCA, 0xA4, 0xF9, 0x22, 0x48, 0x80, 0x00, 0x80, 0x15, 0xF2, 0x24, 39 | 0x02, 0x42, 0xBE, 0x0F, 0x80, 0x95, 0x27, 0xFF, 0xFF, 0xFC, 0x5F, 0xF8, 0x00, 0xE0, 0x38, 40 | 0x55, 0xFF, 0xA1, 0x1F, 0x7A, 0x1F, 0x20, 0x71, 0x08, 0xCE, 0x3E, 0x8F, 0x9C, 0x48, 0xC6, 41 | 0x24, 0x79, 0x04, 0x44, 0x00, 0x00, 0x0C, 0x61, 0x18, 0x92, 0x31, 0x20, 0xA4, 0x4A, 0xC6, 42 | 0x31, 0x8C, 0x20, 0x48, 0xC6, 0xAA, 0x88, 0xA2, 0x4A, 0x80, 0x00, 0x80, 0x3F, 0x4D, 0x50, 43 | 0x02, 0x41, 0x08, 0x00, 0x00, 0x53, 0x20, 0x43, 0x18, 0x40, 0x31, 0x8A, 0x10, 0x6F, 0xB0, 44 | 0x37, 0x8C, 0x61, 0x18, 0x42, 0x11, 0x20, 0x69, 0x0A, 0xD6, 0x31, 0x8C, 0x60, 0x48, 0xC6, 45 | 0x2A, 0x88, 0x84, 0x84, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x60, 0xA2, 0x4F, 0xFF, 46 | 0xFF, 0xFF, 0xFF, 0xF8, 0xC6, 0xB1, 0x8F, 0xE2, 0x48, 0xD8, 0x00, 0x82, 0x95, 0xFC, 0xC8, 47 | 0x82, 0x42, 0x88, 0x00, 0x00, 0x31, 0xE0, 0x43, 0x18, 0x40, 0x31, 0x88, 0x00, 0x20, 0x22, 48 | 0x31, 0x8C, 0x63, 0x18, 0x42, 0x31, 0x20, 0x65, 0x0D, 0xE6, 0x31, 0x8C, 0x60, 0x48, 0xC6, 49 | 0x31, 0x88, 0x45, 0x04, 0x28, 0x04, 0x04, 0x00, 0x10, 0x10, 0x10, 0x00, 0x20, 0x40, 0x00, 50 | 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x22, 0x48, 0xA4, 0x00, 0x82, 0x80, 0x40, 0x30, 51 | 0x81, 0x80, 0x00, 0x00, 0x00, 0x3F, 0x27, 0xFF, 0x1F, 0xFF, 0xEF, 0xF8, 0x00, 0x00, 0x01, 52 | 0xCE, 0x77, 0x9D, 0xEF, 0xFD, 0xD1, 0xF8, 0x63, 0x08, 0xC5, 0xDE, 0x77, 0x9F, 0xF8, 0xC6, 53 | 0x31, 0x8F, 0xC7, 0x0C, 0x10, 0x08, 0x04, 0x00, 0x10, 0x1C, 0x10, 0x20, 0xA0, 0xC0, 0x00, 54 | 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3E, 0x4F, 0x80, 0x00, 55 | }}; 56 | #endif // FONT1_H_ -------------------------------------------------------------------------------- /src/events.c: -------------------------------------------------------------------------------- 1 | #include "events.h" 2 | #include "backends/m8.h" 3 | #include "common.h" 4 | #include "gamepads.h" 5 | #include "input.h" 6 | #include "render.h" 7 | #include "settings.h" 8 | #include 9 | #include 10 | 11 | SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { 12 | struct app_context *ctx = appstate; 13 | SDL_AppResult ret_val = SDL_APP_CONTINUE; 14 | 15 | switch (event->type) { 16 | 17 | // --- System events --- 18 | case SDL_EVENT_QUIT: 19 | case SDL_EVENT_TERMINATING: 20 | ret_val = SDL_APP_SUCCESS; 21 | break; 22 | case SDL_EVENT_WINDOW_RESIZED: 23 | case SDL_EVENT_WINDOW_MOVED: 24 | // If the window size is changed, some systems might need a little nudge to fix scaling 25 | renderer_fix_texture_scaling_after_window_resize(&ctx->conf); 26 | break; 27 | 28 | // --- iOS specific events --- 29 | case SDL_EVENT_DID_ENTER_BACKGROUND: 30 | // iOS: Application entered into the background on iOS. About 5 seconds to stop things. 31 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Received SDL_EVENT_DID_ENTER_BACKGROUND"); 32 | ctx->app_suspended = 1; 33 | if (ctx->device_connected) 34 | m8_pause_processing(); 35 | break; 36 | case SDL_EVENT_WILL_ENTER_BACKGROUND: 37 | // iOS: App about to enter into the background 38 | break; 39 | case SDL_EVENT_WILL_ENTER_FOREGROUND: 40 | // iOS: App returning to the foreground 41 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Received SDL_EVENT_WILL_ENTER_FOREGROUND"); 42 | break; 43 | case SDL_EVENT_DID_ENTER_FOREGROUND: 44 | // iOS: App becomes interactive again 45 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Received SDL_EVENT_DID_ENTER_FOREGROUND"); 46 | ctx->app_suspended = 0; 47 | if (ctx->device_connected) { 48 | renderer_clear_screen(); 49 | m8_resume_processing(); 50 | } 51 | break; 52 | 53 | // --- Input events --- 54 | case SDL_EVENT_GAMEPAD_ADDED: 55 | case SDL_EVENT_GAMEPAD_REMOVED: 56 | // Reinitialize game controllers on controller add/remove/remap 57 | gamepads_initialize(); 58 | break; 59 | 60 | case SDL_EVENT_KEY_DOWN: 61 | // Toggle settings with F1 62 | if (event->key.scancode == ctx->conf.key_toggle_settings && event->key.repeat == 0) { 63 | settings_toggle_open(); 64 | return ret_val; 65 | } 66 | // Route to settings if open 67 | if (settings_is_open()) { 68 | settings_handle_event(ctx, event); 69 | return ret_val; 70 | } 71 | input_handle_key_down_event(ctx, event); 72 | break; 73 | 74 | case SDL_EVENT_KEY_UP: 75 | if (settings_is_open()) { 76 | settings_handle_event(ctx, event); 77 | return ret_val; 78 | } 79 | input_handle_key_up_event(ctx, event); 80 | break; 81 | 82 | case SDL_EVENT_GAMEPAD_BUTTON_DOWN: 83 | if (settings_is_open()) { 84 | settings_handle_event(ctx, event); 85 | return ret_val; 86 | } 87 | 88 | // Allow toggling the settings view using a gamepad only when the device is disconnected to 89 | // avoid accidentally opening the screen while using the device 90 | if (event->gbutton.button == SDL_GAMEPAD_BUTTON_BACK) { 91 | if (ctx->app_state == WAIT_FOR_DEVICE && !settings_is_open()) { 92 | settings_toggle_open(); 93 | } 94 | } 95 | 96 | input_handle_gamepad_button(ctx, event->gbutton.button, true); 97 | break; 98 | 99 | case SDL_EVENT_GAMEPAD_BUTTON_UP: 100 | if (settings_is_open()) { 101 | settings_handle_event(ctx, event); 102 | return ret_val; 103 | } 104 | input_handle_gamepad_button(ctx, event->gbutton.button, false); 105 | break; 106 | 107 | case SDL_EVENT_GAMEPAD_AXIS_MOTION: 108 | if (settings_is_open()) { 109 | settings_handle_event(ctx, event); 110 | return ret_val; 111 | } 112 | input_handle_gamepad_axis(ctx, event->gaxis.axis, event->gaxis.value); 113 | break; 114 | 115 | default: 116 | break; 117 | } 118 | return ret_val; 119 | } 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonne Kokkonen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ----------- 24 | 25 | The software contains a SLIP command processing routine by Marcin Borowicz (slip.c, slip.h): 26 | 27 | MIT License 28 | 29 | Copyright (c) 2018 Marcin Borowicz 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. 48 | 49 | ----------- 50 | 51 | The software contains a bitmap font rendered from Trash80's original work, https://fontstruct.com/fontstructions/show/413734/stealth57. 52 | The font (embedded in inline_font.h) is originally licensed under CC-BY-SA 3.0, https://creativecommons.org/licenses/by-sa/3.0/. 53 | The font is used in this program with the permission of the original author. 54 | 55 | ----------- 56 | 57 | The software contains portions of public domain code: 58 | Bitmap font routine by driedfruit, https://github.com/driedfruit/SDL_inprint (SDL2_inprint.h, inprint2.c) 59 | Serial port code from libserialport's examples (serial.c, serial.h) 60 | 61 | ----------- 62 | 63 | The software contains the Tiny ANSI C ini libary under The MIT License: 64 | rxi: https://github.com/rxi/ini (ini.h, ini.c) 65 | 66 | ----------- 67 | 68 | The source distribution contains a copy of SDL Game Controller Database: 69 | Copyright (C) 1997-2022 Sam Lantinga 70 | 71 | This software is provided 'as-is', without any express or implied 72 | warranty. In no event will the authors be held liable for any damages 73 | arising from the use of this software. 74 | 75 | Permission is granted to anyone to use this software for any purpose, 76 | including commercial applications, and to alter it and redistribute it 77 | freely, subject to the following restrictions: 78 | 79 | 1. The origin of this software must not be misrepresented; you must not 80 | claim that you wrote the original software. If you use this software 81 | in a product, an acknowledgment in the product documentation would be 82 | appreciated but is not required. 83 | 2. Altered source versions must be plainly marked as such, and must not be 84 | misrepresented as being the original software. 85 | 3. This notice may not be removed or altered from any source distribution. 86 | -------------------------------------------------------------------------------- /src/inprint2.c: -------------------------------------------------------------------------------- 1 | // Bitmap font routine originally by driedfruit, 2 | // https://github.com/driedfruit/SDL_inprint Released into public domain. 3 | // Modified to support multiple fonts & adding a background to text. 4 | 5 | #include "fonts/fonts.h" 6 | #include 7 | 8 | #define CHARACTERS_PER_ROW 94 9 | #define CHARACTERS_PER_COLUMN 1 10 | 11 | // Offset for seeking from limited character sets 12 | static const int font_offset = 127 - CHARACTERS_PER_ROW * CHARACTERS_PER_COLUMN; 13 | 14 | static SDL_Renderer *selected_renderer = NULL; 15 | static SDL_Texture *inline_font = NULL; 16 | static SDL_Texture *selected_font = NULL; 17 | static const struct inline_font *selected_inline_font; 18 | static Uint16 selected_font_w, selected_font_h; 19 | 20 | void inline_font_initialize(const struct inline_font *font) { 21 | 22 | if (inline_font != NULL) { 23 | return; 24 | } 25 | 26 | selected_inline_font = font; 27 | selected_font_w = selected_inline_font->width; 28 | selected_font_h = selected_inline_font->height; 29 | 30 | SDL_IOStream *font_bmp = 31 | SDL_IOFromConstMem(selected_inline_font->image_data, selected_inline_font->image_size); 32 | 33 | SDL_Surface *surface = SDL_LoadBMP_IO(font_bmp, 1); 34 | 35 | // Black is transparent 36 | SDL_SetSurfaceColorKey(surface, true, SDL_MapSurfaceRGB(surface, 0, 0, 0)); 37 | 38 | inline_font = SDL_CreateTextureFromSurface(selected_renderer, surface); 39 | 40 | SDL_DestroySurface(surface); 41 | 42 | selected_font = inline_font; 43 | } 44 | 45 | void inline_font_close(void) { 46 | SDL_DestroyTexture(inline_font); 47 | inline_font = NULL; 48 | } 49 | 50 | void inline_font_set_renderer(SDL_Renderer *renderer) { selected_renderer = renderer; } 51 | 52 | void infont(SDL_Texture *font) { 53 | 54 | if (font == NULL) { 55 | return; 56 | } 57 | 58 | const int w = 59 | (int)SDL_GetNumberProperty(SDL_GetTextureProperties(font), SDL_PROP_TEXTURE_WIDTH_NUMBER, 0); 60 | const int h = 61 | (int)SDL_GetNumberProperty(SDL_GetTextureProperties(font), SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0); 62 | 63 | selected_font = font; 64 | selected_font_w = w; 65 | selected_font_h = h; 66 | } 67 | void incolor1(const SDL_Color *color) { 68 | SDL_SetTextureColorMod(selected_font, color->r, color->g, color->b); 69 | } 70 | void incolor(const Uint32 fore) /* Color must be in 0x00RRGGBB format ! */ 71 | { 72 | SDL_Color pal[1]; 73 | pal[0].r = (Uint8)((fore & 0x00FF0000) >> 16); 74 | pal[0].g = (Uint8)((fore & 0x0000FF00) >> 8); 75 | pal[0].b = (Uint8)(fore & 0x000000FF); 76 | SDL_SetTextureColorMod(selected_font, pal[0].r, pal[0].g, pal[0].b); 77 | } 78 | void inprint(SDL_Renderer *dst, const char *str, Uint32 x, Uint32 y, const Uint32 fgcolor, 79 | const Uint32 bgcolor) { 80 | SDL_FRect s_rect; 81 | SDL_FRect d_rect; 82 | SDL_FRect bg_rect; 83 | 84 | static uint32_t previous_fgcolor; 85 | 86 | d_rect.x = (float)x; 87 | d_rect.y = (float)y; 88 | s_rect.w = (float)selected_font_w / CHARACTERS_PER_ROW; 89 | s_rect.h = (float)selected_font_h / CHARACTERS_PER_COLUMN; 90 | d_rect.w = s_rect.w; 91 | d_rect.h = s_rect.h; 92 | 93 | if (dst == NULL) 94 | dst = selected_renderer; 95 | 96 | for (; *str; str++) { 97 | const int ascii_code = (int)*str; 98 | int id = ascii_code - font_offset; 99 | 100 | #if (CHARACTERS_PER_COLUMN != 1) 101 | int row = id / CHARACTERS_PER_ROW; 102 | int col = id % CHARACTERS_PER_ROW; 103 | s_rect.x = col * s_rect.w; 104 | s_rect.y = row * s_rect.h; 105 | #else 106 | s_rect.x = (float)id * s_rect.w; 107 | s_rect.y = 0; 108 | #endif 109 | if (id + font_offset == '\n') { 110 | d_rect.x = (float)x; 111 | d_rect.y += s_rect.h + 1; 112 | continue; 113 | } 114 | if (fgcolor != previous_fgcolor) { 115 | incolor(fgcolor); 116 | previous_fgcolor = fgcolor; 117 | } 118 | 119 | if (bgcolor != fgcolor) { 120 | SDL_SetRenderDrawColor(selected_renderer, (bgcolor & 0x00FF0000) >> 16, 121 | (bgcolor & 0x0000FF00) >> 8, bgcolor & 0x000000FF, 0xFF); 122 | bg_rect = d_rect; 123 | bg_rect.w = (float)selected_inline_font->glyph_x; 124 | bg_rect.h = (float)selected_inline_font->glyph_y; 125 | 126 | SDL_RenderFillRect(dst, &bg_rect); 127 | } 128 | // Do not try to render a whitespace character because the font doesn't have one 129 | if (ascii_code != 32) { 130 | SDL_RenderTexture(dst, selected_font, &s_rect, &d_rect); 131 | } 132 | d_rect.x += (float)selected_inline_font->glyph_x + 1; 133 | } 134 | } 135 | 136 | const struct inline_font *inline_font_get_current(void) { 137 | return selected_inline_font; 138 | } 139 | -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | name: m8c win32/win64 build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-win: 10 | runs-on: windows-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - { sys: mingw32, env: i686, win: win32 } 16 | - { sys: mingw64, env: x86_64, win: win64 } 17 | name: ${{ matrix.win }} 18 | defaults: 19 | run: 20 | shell: msys2 {0} 21 | env: 22 | MINGW_ARCH: ${{ matrix.sys }} 23 | SDL_VERSION: 3.2.20 24 | steps: 25 | 26 | - name: 'git config' 27 | run: git config --global core.autocrlf input 28 | shell: bash 29 | 30 | - name: 'Checkout' 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: 'Setup MSYS2' 36 | uses: msys2/setup-msys2@v2 37 | with: 38 | msystem: ${{ matrix.sys }} 39 | update: true 40 | install: mingw-w64-${{ matrix.env }}-toolchain make mingw-w64-${{ matrix.env }}-cmake zip dos2unix autoconf automake-wrapper libtool make unzip 41 | 42 | - name: 'Cache SDL3 files' 43 | id: cache-sdl3-files 44 | uses: actions/cache@v4 45 | with: 46 | path: 'SDL3-3.2.20' 47 | key: win-sdl3-files-${{ matrix.sys }} 48 | 49 | - name: 'Download SDL3 sources' 50 | if: steps.cache-sdl3-files.outputs.cache-hit != 'true' 51 | run: | 52 | (curl https://www.libsdl.org/release/SDL3-$SDL_VERSION.tar.gz || curl -L https://github.com/libsdl-org/SDL/releases/download/release-$SDL_VERSION/SDL3-$SDL_VERSION.tar.gz) | tar zxvf - 53 | 54 | - name: 'Build and install SDL3' 55 | if: steps.cache-sdl3-files.outputs.cache-hit != 'true' 56 | run: | 57 | pushd SDL3-$SDL_VERSION 58 | mkdir build_${{ matrix.env }} 59 | cmake -S . -B build_${{ matrix.env }} -DCMAKE_TOOLCHAIN_FILE=build-scripts/cmake-toolchain-mingw64-${{ matrix.env }}.cmake 60 | cmake --build build_${{ matrix.env }} --parallel 61 | popd 62 | 63 | - name: 'Install SDL3' 64 | run: | 65 | pushd SDL3-$SDL_VERSION 66 | cmake --install build_${{ matrix.env }} --prefix C:/Libraries 67 | popd 68 | 69 | - name: 'Cache libserialport files' 70 | id: cache-libserialport-files 71 | uses: actions/cache@v4 72 | with: 73 | path: 'libserialport-master' 74 | key: win-libserialport-files-${{ matrix.sys }} 75 | 76 | - name: 'Build libserialport manually' 77 | if: steps.cache-libserialport-files.outputs.cache-hit != 'true' 78 | run: | 79 | wget https://github.com/sigrokproject/libserialport/archive/refs/heads/master.zip 80 | unzip master.zip 81 | pushd libserialport-master 82 | ./autogen.sh 83 | ./configure 84 | make 85 | popd 86 | 87 | - name: 'Install libserialport' 88 | run: | 89 | pushd libserialport-master 90 | make install 91 | popd 92 | 93 | - name: Set current date as env variable 94 | run: echo "NOW=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 95 | 96 | - name: 'Build package' 97 | run: | 98 | cmake . -G Ninja -DCMAKE_PREFIX_PATH=C:/Libraries -DCMAKE_BUILD_TYPE=Release 99 | cmake --build . 100 | strip -g m8c.exe 101 | if [ ${{ matrix.win }} == "win32" ] 102 | then 103 | cp C:/Libraries/bin/SDL3.dll . 104 | cp /mingw32/bin/libgcc_s_dw2-1.dll . 105 | cp /mingw32/bin/libserialport-0.dll . 106 | cp /mingw32/bin/libwinpthread-1.dll . 107 | else 108 | cp C:/Libraries/bin/SDL3.dll . 109 | cp /mingw64/bin/libserialport-0.dll . 110 | fi 111 | unix2dos README.md LICENSE AUDIOGUIDE.md 112 | - name: 'Upload artifact (win32)' 113 | if: matrix.win == 'win32' 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: m8c-${{ env.NOW }}-${{ matrix.win }} 117 | path: | 118 | m8c.exe 119 | SDL3.dll 120 | libserialport-0.dll 121 | libgcc_s_dw2-1.dll 122 | libwinpthread-1.dll 123 | gamecontrollerdb.txt 124 | LICENSE 125 | README.md 126 | AUDIOGUIDE.md 127 | - name: 'Upload artifact (win64)' 128 | if: matrix.win == 'win64' 129 | uses: actions/upload-artifact@v4 130 | with: 131 | name: m8c-${{ env.NOW }}-${{ matrix.win }} 132 | path: | 133 | m8c.exe 134 | SDL3.dll 135 | libserialport-0.dll 136 | gamecontrollerdb.txt 137 | LICENSE 138 | README.md 139 | AUDIOGUIDE.md 140 | -------------------------------------------------------------------------------- /.github/workflows/build-macos.yml: -------------------------------------------------------------------------------- 1 | name: m8c macOS builds 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-macos: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - name: m8c MacOS build (Intel) 17 | runner: macos-13 18 | build_dir: build-intel 19 | artifact_suffix: intel 20 | cmake_arch: x86_64 21 | - name: m8c MacOS build (Apple Silicon) 22 | runner: macos-latest 23 | build_dir: build-arm64 24 | artifact_suffix: applesilicon 25 | cmake_arch: arm64 26 | 27 | name: ${{ matrix.name }} 28 | runs-on: ${{ matrix.runner }} 29 | env: 30 | BUILD_DIR: ${{ matrix.build_dir }} 31 | 32 | steps: 33 | - name: Environment info 34 | run: | 35 | uname -m 36 | 37 | - name: Install dependencies 38 | run: brew install sdl3 libserialport 39 | 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Set current date as env variable 44 | run: echo "NOW=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 45 | 46 | - name: Create Custom Keychain 47 | id: createCustomKeychain 48 | if: github.event_name == 'release' 49 | env: 50 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 51 | run: | 52 | security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain 53 | security list-keychains -s build.keychain 54 | security default-keychain -s build.keychain 55 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain 56 | security set-keychain-settings build.keychain 57 | 58 | - name: Import Apple Developer Certificate 59 | id: importAppleCertificate 60 | if: github.event_name == 'release' 61 | env: 62 | CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} 63 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 64 | run: | 65 | echo "${{ secrets.MACOS_CERTIFICATE }}" | base64 --decode > developer_cert.p12 66 | security import developer_cert.p12 -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign 67 | security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain 68 | 69 | - name: Configure m8c 70 | id: configureApplication 71 | env: 72 | CODESIGN_CERT_NAME: ${{ secrets.MACOS_CODE_SIGN_IDENTITY }} 73 | run: | 74 | mkdir -p "${{ env.BUILD_DIR }}" 75 | pushd "${{ env.BUILD_DIR }}" 76 | cmake .. -DCMAKE_BUILD_TYPE=Release -DCODESIGN_CERT_NAME="$CODESIGN_CERT_NAME" 77 | popd 78 | 79 | - name: Build and package m8c 80 | id: buildApplication 81 | continue-on-error: true 82 | env: 83 | CMAKE_INSTALL_PREFIX: build_output 84 | run: | 85 | pushd "${{ env.BUILD_DIR }}" 86 | cpack -V . 87 | popd 88 | 89 | - name: View debug log if compilation fails 90 | if: failure() && steps.buildApplication.outcome == 'failure' 91 | run: | 92 | cat "${{ github.workspace }}/${{ env.BUILD_DIR }}/_CPack_Packages/Darwin/DragNDrop/PreinstallOutput.log" || true 93 | 94 | - name: Notarize the App 95 | id: notarizeApp 96 | if: github.event_name == 'release' 97 | env: 98 | APPLE_ID: ${{ secrets.APPLE_ID }} 99 | APPLE_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }} 100 | TEAM_ID: ${{ secrets.TEAM_ID }} 101 | run: | 102 | pushd "${{ env.BUILD_DIR }}" 103 | APP_PATH=$(find package-output -maxdepth 1 -name "m8c*.dmg" | head -n 1) 104 | xcrun notarytool submit \ 105 | --apple-id "$APPLE_ID" \ 106 | --team-id "$TEAM_ID" \ 107 | --password "$APPLE_PASSWORD" \ 108 | --wait \ 109 | "$APP_PATH" 110 | xcrun stapler staple "$APP_PATH" 111 | popd 112 | 113 | - name: Verify Stapling 114 | id: verifyStapling 115 | if: github.event_name == 'release' 116 | run: | 117 | APP_PATH=$(find "${{ env.BUILD_DIR }}/package-output" -maxdepth 1 -name "m8c*.dmg" | head -n 1) 118 | echo "Verifying stapling on $APP_PATH" 119 | xcrun stapler validate "$APP_PATH" 120 | 121 | - name: Delete Custom Keychain 122 | id: deleteCustomKeychain 123 | if: github.event_name == 'release' && steps.createCustomKeychain.outcome == 'success' 124 | run: | 125 | security delete-keychain build.keychain 126 | 127 | - name: Copy package 128 | run: | 129 | APP_PATH=$(find "${{ env.BUILD_DIR }}/package-output" -maxdepth 1 -name "m8c*.dmg" | head -n 1) 130 | mv "$APP_PATH" "m8c-${{ env.NOW }}-macos-${{ matrix.artifact_suffix }}.dmg" 131 | 132 | - name: Upload DMG package 133 | uses: actions/upload-artifact@v4 134 | with: 135 | name: m8c-${{ env.NOW }}-macos-${{ matrix.artifact_suffix }} 136 | path: m8c-${{ env.NOW }}-macos-${{ matrix.artifact_suffix }}.dmg -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project(m8c LANGUAGES C VERSION 2.2.0) 4 | 5 | set(APP_NAME m8c) 6 | 7 | option(USE_LIBSERIALPORT "Use libserialport as a backend" OFF) 8 | option(USE_LIBUSB "Use libusb as a backend" OFF) 9 | option(USE_RTMIDI "Use RtMidi as a backend" OFF) 10 | 11 | # Enable USE_LIBSERIALPORT by default if neither USE_LIBUSB nor USE_RTMIDI are defined 12 | if (NOT USE_LIBUSB AND NOT USE_RTMIDI) 13 | message(STATUS "Neither USE_LIBUSB nor USE_RTMIDI are enabled. Enabling USE_LIBSERIALPORT by default.") 14 | set(USE_LIBSERIALPORT ON) 15 | endif () 16 | 17 | 18 | file(GLOB m8c_SRC "src/*.h" "src/*.c" "src/backends/*.h" "src/backends/*.c" "src/fonts/*.h" "src/fonts/*.c") 19 | 20 | set(MACOS_CONTENTS "${CMAKE_CURRENT_SOURCE_DIR}/package/macos/m8c.app/Contents") 21 | set(MACOS_ENTITLEMENTS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/package/macos/Entitlements.plist") 22 | 23 | set(APP_ICON ${MACOS_CONTENTS}/Resources/m8c.icns) 24 | set_source_files_properties(${APP_ICON} PROPERTIES 25 | MACOSX_PACKAGE_LOCATION "Resources") 26 | 27 | if (APPLE) 28 | add_executable(${APP_NAME} MACOSX_BUNDLE ${APP_ICON} ${m8c_SRC}) 29 | elseif (WIN32) 30 | add_executable(${APP_NAME} WIN32 ${m8c_SRC}) 31 | else () 32 | add_executable(${APP_NAME} ${m8c_SRC}) 33 | endif () 34 | 35 | find_package(PkgConfig REQUIRED) 36 | find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) 37 | 38 | pkg_check_modules(SDL3 REQUIRED sdl3) 39 | 40 | target_link_options(${APP_NAME} PRIVATE ${SDL3_LDFLAGS}) 41 | target_include_directories(${APP_NAME} PRIVATE ${SDL3_INCLUDE_DIRS}) 42 | target_compile_options(${APP_NAME} PRIVATE ${SDL3_CFLAGS_OTHER}) 43 | 44 | if (USE_LIBSERIALPORT) 45 | pkg_check_modules(LIBSERIALPORT REQUIRED libserialport) 46 | target_link_options(${APP_NAME} PRIVATE ${LIBSERIALPORT_LDFLAGS}) 47 | target_include_directories(${APP_NAME} PRIVATE ${LIBSERIALPORT_INCLUDE_DIRS}) 48 | target_compile_options(${APP_NAME} PRIVATE ${LIBSERIALPORT_CFLAGS_OTHER}) 49 | target_compile_definitions(${APP_NAME} PRIVATE USE_LIBSERIALPORT) 50 | endif () 51 | 52 | if (USE_LIBUSB) 53 | pkg_check_modules(LIBUSB REQUIRED libusb-1.0) 54 | target_link_options(${APP_NAME} PRIVATE ${LIBUSB_LDFLAGS}) 55 | target_include_directories(${APP_NAME} PRIVATE ${LIBUSB_INCLUDE_DIRS}) 56 | target_compile_options(${APP_NAME} PRIVATE ${LIBUSB_CFLAGS_OTHER}) 57 | target_compile_definitions(${APP_NAME} PRIVATE USE_LIBUSB) 58 | endif () 59 | 60 | if (USE_RTMIDI) 61 | pkg_check_modules(RTMIDI REQUIRED rtmidi) 62 | target_link_options(${APP_NAME} PRIVATE ${RTMIDI_LDFLAGS}) 63 | target_include_directories(${APP_NAME} PRIVATE ${RTMIDI_INCLUDE_DIRS}) 64 | target_compile_options(${APP_NAME} PRIVATE ${RTMIDI_CFLAGS_OTHER}) 65 | target_compile_definitions(${APP_NAME} PRIVATE USE_RTMIDI) 66 | endif () 67 | 68 | if (WIN32) 69 | target_link_libraries(${APP_NAME} ${SDL3_LIBRARIES} ${LIBSERIALPORT_LIBRARIES}) 70 | endif () 71 | 72 | if (APPLE) 73 | # Destination paths below are relative to ${CMAKE_INSTALL_PREFIX} 74 | install(TARGETS ${APP_NAME} 75 | BUNDLE DESTINATION . COMPONENT Runtime 76 | RUNTIME DESTINATION bin COMPONENT Runtime 77 | ) 78 | 79 | set(CODESIGN_CERT_NAME "" CACHE STRING "The developer ID cert name for codesign") 80 | 81 | set_target_properties(${APP_NAME} PROPERTIES 82 | MACOSX_BUNDLE TRUE 83 | MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/package/macos/m8c.app/Contents/Info.plist" 84 | MACOSX_BUNDLE_BUNDLE_NAME "m8c" 85 | MACOSX_BUNDLE_COPYRIGHT "Copyright © 2021 laamaa. All rights reserved." 86 | MACOSX_BUNDLE_GUI_IDENTIFIER "fi.laamaa.m8c" 87 | MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" 88 | MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" 89 | MACOSX_BUNDLE_ICON_FILE "m8c.icns" 90 | ) 91 | 92 | set(APPS "\${CMAKE_INSTALL_PREFIX}/${APP_NAME}.app") 93 | 94 | if(CODESIGN_CERT_NAME) 95 | # Use the specified certificate 96 | install(CODE "include(BundleUtilities) 97 | fixup_bundle(\"${APPS}\" \"\" \"\") 98 | execute_process(COMMAND codesign --force --options runtime --deep --entitlements ../package/macos/Entitlements.plist --sign \"${CODESIGN_CERT_NAME}\" --timestamp \${CMAKE_INSTALL_PREFIX}/${APP_NAME}.app)") 99 | else() 100 | # Use ad-hoc signing (self-signed) when no certificate is specified 101 | install(CODE "include(BundleUtilities) 102 | fixup_bundle(\"${APPS}\" \"\" \"\") 103 | execute_process(COMMAND codesign --force --options runtime --deep --entitlements ../package/macos/Entitlements.plist --sign - --timestamp \${CMAKE_INSTALL_PREFIX}/${APP_NAME}.app)") 104 | endif() 105 | 106 | set(CPACK_PACKAGE_NAME "m8c") 107 | set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) 108 | set(CPACK_PACKAGE_DIRECTORY "package-output") 109 | set(CPACK_GENERATOR "DragNDrop") 110 | include(CPack) 111 | elseif (UNIX) 112 | install(TARGETS ${APP_NAME}) 113 | install(DIRECTORY package/share/applications DESTINATION share/) 114 | install(DIRECTORY package/share/icons DESTINATION share/) 115 | install(FILES AUDIOGUIDE.md README.md LICENSE DESTINATION share/doc/${APP_NAME}) 116 | install(FILES gamecontrollerdb.txt DESTINATION bin/) 117 | endif () 118 | -------------------------------------------------------------------------------- /src/fx_cube.c: -------------------------------------------------------------------------------- 1 | #include "SDL2_inprint.h" 2 | 3 | #include 4 | #include 5 | 6 | // Handle screensaver cube effect 7 | 8 | static SDL_Texture *texture_cube; 9 | static SDL_Texture *texture_text; 10 | static SDL_Renderer *fx_renderer; 11 | static SDL_Color line_color; 12 | 13 | static unsigned int center_x = 320 / 2; 14 | static unsigned int center_y = 240 / 2; 15 | 16 | static const float default_nodes[8][3] = {{-1, -1, -1}, {-1, -1, 1}, {-1, 1, -1}, {-1, 1, 1}, 17 | {1, -1, -1}, {1, -1, 1}, {1, 1, -1}, {1, 1, 1}}; 18 | 19 | static int edges[12][2] = {{0, 1}, {1, 3}, {3, 2}, {2, 0}, {4, 5}, {5, 7}, 20 | {7, 6}, {6, 4}, {0, 4}, {1, 5}, {2, 6}, {3, 7}}; 21 | 22 | static float nodes[8][3]; 23 | 24 | static void scale(const float factor0, const float factor1, const float factor2) { 25 | for (int i = 0; i < 8; i++) { 26 | nodes[i][0] *= factor0; 27 | nodes[i][1] *= factor1; 28 | nodes[i][2] *= factor2; 29 | } 30 | } 31 | 32 | static void rotate_cube(const float angle_x, const float angle_y) { 33 | const float sin_x = SDL_sinf(angle_x); 34 | const float cos_x = SDL_cosf(angle_x); 35 | const float sin_y = SDL_sinf(angle_y); 36 | const float cos_y = SDL_cosf(angle_y); 37 | for (int i = 0; i < 8; i++) { 38 | const float x = nodes[i][0]; 39 | const float y = nodes[i][1]; 40 | float z = nodes[i][2]; 41 | 42 | nodes[i][0] = x * cos_x - z * sin_x; 43 | nodes[i][2] = z * cos_x + x * sin_x; 44 | 45 | z = nodes[i][2]; 46 | 47 | nodes[i][1] = y * cos_y - z * sin_y; 48 | nodes[i][2] = z * cos_y + y * sin_y; 49 | } 50 | } 51 | 52 | void fx_cube_init(SDL_Renderer *target_renderer, const SDL_Color foreground_color, 53 | const unsigned int texture_width, const unsigned int texture_height, 54 | const unsigned int font_glyph_width) { 55 | 56 | fx_renderer = target_renderer; 57 | line_color = foreground_color; 58 | SDL_Point texture_size; 59 | 60 | SDL_Texture *og_target = SDL_GetRenderTarget(fx_renderer); 61 | 62 | texture_size.x = (int)SDL_GetNumberProperty(SDL_GetTextureProperties(og_target), 63 | SDL_PROP_TEXTURE_WIDTH_NUMBER, 0); 64 | texture_size.y = (int)SDL_GetNumberProperty(SDL_GetTextureProperties(og_target), 65 | SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0); 66 | 67 | texture_cube = SDL_CreateTexture(fx_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 68 | texture_size.x, texture_size.y); 69 | texture_text = SDL_CreateTexture(fx_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 70 | texture_size.x, texture_size.y); 71 | 72 | SDL_SetRenderTarget(fx_renderer, texture_text); 73 | SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); 74 | SDL_RenderClear(fx_renderer); 75 | 76 | inprint(fx_renderer, "M8 DEVICE NOT DETECTED", texture_width - font_glyph_width * 22 - 23, 77 | texture_height - 12, 0xFFFFFF, 0x000000); 78 | inprint(fx_renderer, "M8C", 2, 2, 0xFFFFFF, 0x000000); 79 | 80 | SDL_SetRenderTarget(fx_renderer, og_target); 81 | 82 | // Initialize default nodes 83 | SDL_memcpy(nodes, default_nodes, sizeof(default_nodes)); 84 | 85 | scale(50, 50, 50); 86 | rotate_cube(M_PI / 6, SDL_atanf(SDL_sqrtf(2))); 87 | 88 | SDL_SetTextureBlendMode(texture_cube, SDL_BLENDMODE_BLEND); 89 | SDL_SetTextureBlendMode(texture_text, SDL_BLENDMODE_BLEND); 90 | 91 | center_x = (int)(texture_size.x / 2.0); 92 | center_y = (int)(texture_size.y / 2.0); 93 | } 94 | 95 | void fx_cube_destroy(void) { 96 | // Free resources 97 | SDL_DestroyTexture(texture_cube); 98 | SDL_DestroyTexture(texture_text); 99 | 100 | // Force clear renderer 101 | SDL_SetRenderTarget(fx_renderer, NULL); 102 | SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); 103 | SDL_RenderClear(fx_renderer); 104 | } 105 | 106 | // Update the cube texture every 16ms>. Returns 1 if cube was updated, 0 if no changes were made. 107 | int fx_cube_update(void) { 108 | static Uint64 ticks_last_update = 0; 109 | 110 | if (SDL_GetTicks() - ticks_last_update >= 16) { 111 | ticks_last_update = SDL_GetTicks(); 112 | SDL_FPoint points[24]; 113 | int points_counter = 0; 114 | SDL_Texture *og_texture = SDL_GetRenderTarget(fx_renderer); 115 | 116 | SDL_SetRenderTarget(fx_renderer, texture_cube); 117 | SDL_SetRenderDrawColor(fx_renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); 118 | SDL_RenderClear(fx_renderer); 119 | 120 | const Uint64 ms = SDL_GetTicks(); 121 | const float t = (float)ms / 1000.0f; 122 | const float pulse = 1.0f + 0.25f * SDL_sinf(t); 123 | 124 | rotate_cube(M_PI / 180, M_PI / 270); 125 | 126 | for (int i = 0; i < 12; i++) { 127 | const float *p1 = nodes[edges[i][0]]; 128 | const float *p2 = nodes[edges[i][1]]; 129 | points[points_counter++] = (SDL_FPoint){p1[0] * pulse + center_x, p1[1] * pulse + center_y}; 130 | points[points_counter++] = (SDL_FPoint){p2[0] * pulse + center_x, p2[1] * pulse + center_y}; 131 | } 132 | 133 | SDL_RenderTexture(fx_renderer, texture_text, NULL, NULL); 134 | SDL_SetRenderDrawColor(fx_renderer, line_color.r, line_color.g, line_color.b, line_color.a); 135 | SDL_RenderLines(fx_renderer, points, 24); 136 | 137 | SDL_SetRenderTarget(fx_renderer, og_texture); 138 | SDL_RenderTexture(fx_renderer, texture_cube, NULL, NULL); 139 | return 1; 140 | } 141 | return 0; 142 | } 143 | -------------------------------------------------------------------------------- /AUDIOGUIDE.md: -------------------------------------------------------------------------------- 1 | # Audio setup for M8 headless 2 | 3 | Please note that the program includes SDL based audio routing built-in nowadays. 4 | 5 | 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. 6 | 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. 7 | 8 | ## Windows 9 | 10 | * Right click Sound in Taskbar 11 | * Open Sound Settings 12 | * Select M8 (or whatever your Teensy Interface is named) as Input 13 | * Select Device properties 14 | * Select Additional Device Properties 15 | * Select Listen tab 16 | * Check Listen to this device and then select output of your choice (whatever source plays your speakers/headphone uses) 17 | 18 | ## Linux / Raspberry Pi 19 | 20 | ### Pulseaudio 21 | 22 | If you have a desktop installation, chances are you're using Pulseaudio which has a module for making an internal loopback. 23 | 24 | run the following command in terminal: 25 | 26 | ``` 27 | pacmd load-module module-loopback latency_msec=1 28 | ``` 29 | 30 | after that, you should hear audio through your default input device. If not, check Input devices in Pulseaudio Volume Control 31 | ``` 32 | pavucontrol 33 | ``` 34 | 35 | If the tools are missing, install pavucontrol (Debian based systems): 36 | ``` 37 | apt install pavucontrol 38 | ``` 39 | 40 | ### JACK 41 | 42 | It is possible to route the M8 USB audio to another audio interface in Linux without a DAW, using JACK Audio Connection kit and a few other command line tools. 43 | 44 | Please note that this is not an optimal solution for getting audio from the headless M8, but as far as I know the easiest way to use the USB audio feature on Linux. The best parameters for running the applications can vary a lot depending on your configuration, and however you do it, there will be some latency. 45 | 46 | It is possible to use the integrated audio of the Pi for this, but it has quite a poor quality and likely will have sound popping issues while running this. 47 | 48 | These instructions were written for Raspberry PI OS desktop using a Raspberry Pi 3 B+, but should work for other Debian/Ubuntu flavors as well. 49 | 50 | Open Terminal and run the commands below to setup the system. 51 | 52 | ----- 53 | 54 | #### First time setup 55 | 56 | #### Install JACK Audio Connection Kit 57 | 58 | The jackd2 package will provide the basic tools that make the audio patching possible. 59 | 60 | ``` 61 | sudo apt install jackd2 62 | ``` 63 | 64 | #### Add your user to the audio group 65 | 66 | This is required to get real time priority for the JACK process. 67 | 68 | ``` 69 | sudo usermod -a -G audio $USER 70 | ``` 71 | 72 | You need to log out completely or reboot for this change to take effect. 73 | 74 | ----- 75 | 76 | #### Find your audio interface ALSA device id 77 | 78 | Use the ```aplay -l``` command to list the audio devices present in the system. 79 | 80 | ``` 81 | pi@raspberrypi:~ $ aplay -l 82 | **** List of PLAYBACK Hardware Devices **** 83 | card 0: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones] 84 | Subdevices: 8/8 85 | Subdevice #0: subdevice #0 86 | Subdevice #1: subdevice #1 87 | Subdevice #2: subdevice #2 88 | Subdevice #3: subdevice #3 89 | Subdevice #4: subdevice #4 90 | Subdevice #5: subdevice #5 91 | Subdevice #6: subdevice #6 92 | Subdevice #7: subdevice #7 93 | card 1: M8 [M8], device 0: USB Audio [USB Audio] 94 | Subdevices: 1/1 95 | Subdevice #0: subdevice #0 96 | card 2: vc4hdmi [vc4-hdmi], device 0: MAI PCM vc4-hdmi-hifi-0 [MAI PCM vc4-hdmi-hifi-0] 97 | Subdevices: 1/1 98 | Subdevice #0: subdevice #0 99 | card 3: v10 [AudioQuest DragonFly Red v1.0], device 0: USB Audio [USB Audio] 100 | Subdevices: 1/1 101 | Subdevice #0: subdevice #0 102 | 103 | ``` 104 | 105 | Take note of the card number you wish to use. In my case, I want to use card 3: v10. 106 | 107 | After these steps have been taken care of, we can try to route some audio. 108 | 109 | #### Start JACK server and route audio 110 | ``` 111 | jackd -d alsa -d hw:M8 -r44100 -p512 & 112 | ``` 113 | 114 | This will start the JACK server using ALSA backend driver (-d alsa), open the M8 USB audio interface (-d hw:M8) with the sample rate of 44100hz (-r44100) and buffer period size of 512 frames (-p512). The program will run in background (the & character in the end specifies this). 115 | If you don't see your terminal prompt, just press enter and it should appear. 116 | 117 | Next, we will create a virtual port for routing audio to another interface. We need to specify the audio interface id where we want to output audio here. 118 | 119 | ```alsa_out -j m8out -d hw: