├── 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: -r 44100 &``` 120 | 121 | example: 122 | 123 | ```alsa_out -j m8out -d hw:3 -r 44100 &``` 124 | 125 | This creates a JACK client called "m8out" (-j m8out) that performs output to ALSA audio hardware interface number 3 (-d hw:3) with a samplerate of 44100 (-r 44100). The ampersand at the end runs the program in the background. 126 | 127 | If you wish to use the Pi integrated sound board, check the number of the "bcm2835 Headphones" device. Usually it is 0. 128 | 129 | After the virtual port has been created, we need to connect it to the M8 USB Audio capture device. 130 | 131 | ``` 132 | jack_connect system:capture_1 m8out:playback_1 133 | jack_connect system:capture_2 m8out:playback_2 134 | ``` 135 | 136 | This will connect the audio ports. You should be able to hear sounds from the M8 now. 137 | 138 | If you wish to shut down the applications, you can do that by sending a interrupt signal to the processes. 139 | 140 | ```killall -s SIGINT jackd alsa_out``` 141 | 142 | 143 | #### TL;DR 144 | Run these in Terminal: 145 | 146 | ``` 147 | sudo apt install jackd2 148 | sudo usermod -a -G audio $USER 149 | sudo reboot 150 | 151 | ``` 152 | ``` 153 | jackd -d alsa -d hw:M8 -r44100 -p512 & 154 | alsa_out -j m8out -d hw:0 -r 44100 & 155 | jack_connect system:capture_1 m8out:playback_1 156 | jack_connect system:capture_2 m8out:playback_2 157 | ``` 158 | -------------------------------------------------------------------------------- /src/fonts/font2.h: -------------------------------------------------------------------------------- 1 | #ifndef FONT2_H_ 2 | #define FONT2_H_ 3 | 4 | #include "fonts.h" 5 | 6 | struct inline_font font_v1_large = { 7 | 752, 8 | 9, 9 | 8, 10 | 9, 11 | 0, 12 | -40, 13 | 4, 14 | 22, 15 | 1010, 16 | { 17 | 0x42, 0x4D, 0xF2, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x7C, 18 | 0x00, 0x00, 0x00, 0xF0, 0x02, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0x60, 0x03, 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, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCF, 0xFF, 0xFF, 0xE7, 27 | 0x7D, 0x84, 0xFF, 0xFC, 0x9F, 0xFF, 0xFF, 0x9F, 0xFF, 0x9F, 0xFF, 0x00, 0x00, 0x00, 0x00, 28 | 0xFC, 0x00, 0x00, 0xE7, 0x01, 0xFC, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xE7, 0x81, 0x3C, 0x01, 29 | 0x81, 0x03, 0x00, 0x3F, 0x81, 0x3C, 0x00, 0x81, 0x3C, 0x00, 0x3C, 0x3C, 0x81, 0x3F, 0x84, 30 | 0x3C, 0x81, 0xE7, 0x81, 0xE7, 0x3C, 0x3C, 0xE7, 0x00, 0xF0, 0xFF, 0x0F, 0xFF, 0x80, 0xFF, 31 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x00, 0x3C, 0x00, 0x00, 0x3C, 0x00, 0x24, 0x3C, 0x00, 32 | 0x3F, 0xFC, 0x3F, 0x00, 0x80, 0x00, 0x3F, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xE7, 0x00, 0xFF, 33 | 0x00, 0x00, 0xFF, 0xFF, 0xDB, 0x00, 0x38, 0x39, 0xFF, 0xF9, 0xCF, 0xFF, 0xFF, 0xCF, 0xFF, 34 | 0xFF, 0x3F, 0x3C, 0xE7, 0x3F, 0xFC, 0xFC, 0xFC, 0x3C, 0xE7, 0x3C, 0xFC, 0xFF, 0x9F, 0xFC, 35 | 0xFF, 0x9F, 0xFF, 0x3F, 0x3C, 0x3C, 0x3C, 0x39, 0x3F, 0x3F, 0x3C, 0x3C, 0xE7, 0x3C, 0x39, 36 | 0x3F, 0x3C, 0x3C, 0x3C, 0x3F, 0x39, 0x39, 0x3C, 0xE7, 0x3C, 0xC3, 0x18, 0x3C, 0xE7, 0x3F, 37 | 0xF3, 0xFC, 0xCF, 0xFF, 0xFF, 0xFF, 0x3C, 0x3C, 0x3F, 0x3C, 0x3F, 0x9F, 0xFC, 0x3C, 0xE7, 38 | 0xFC, 0x39, 0xCF, 0x24, 0x3C, 0x3C, 0x3F, 0xFC, 0x3F, 0xFC, 0x9F, 0x3C, 0x1F, 0x24, 0x99, 39 | 0xFC, 0x9F, 0x3C, 0xE7, 0x3C, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x81, 0xE4, 0x9D, 0x30, 0xFF, 40 | 0xF3, 0xE7, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0x9F, 0x3C, 0xE7, 0x3F, 0xFC, 0xFC, 0xFC, 0x3C, 41 | 0xE7, 0x3C, 0xFC, 0xE7, 0x9F, 0xF8, 0x81, 0x8F, 0xE7, 0x21, 0x3C, 0x3C, 0x3F, 0x3C, 0x3F, 42 | 0x3F, 0x3C, 0x3C, 0xE7, 0x3C, 0x33, 0x3F, 0x3C, 0x3C, 0x3C, 0x3F, 0x32, 0x33, 0xFC, 0xE7, 43 | 0x3C, 0x99, 0x00, 0x99, 0xE7, 0x9F, 0xF3, 0xF9, 0xCF, 0xFF, 0xFF, 0xFF, 0x3C, 0x3C, 0x3F, 44 | 0x3C, 0x3F, 0x9F, 0xFC, 0x3C, 0xE7, 0xFC, 0x33, 0xCF, 0x24, 0x3C, 0x3C, 0x3F, 0xFC, 0x3F, 45 | 0xFC, 0x9F, 0x3C, 0x0F, 0x24, 0xC3, 0xFC, 0xCF, 0x3C, 0xE7, 0x3C, 0xFF, 0x00, 0x00, 0xCF, 46 | 0xFF, 0xDB, 0xE4, 0xCF, 0x87, 0xFF, 0xF3, 0xE7, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xCF, 0x3C, 47 | 0xE7, 0x3F, 0xFC, 0xFC, 0xFC, 0x3C, 0xE7, 0x3C, 0xFC, 0xFF, 0xFF, 0xF0, 0xFF, 0x87, 0xE7, 48 | 0x24, 0x3C, 0x3C, 0x3F, 0x3C, 0x3F, 0x3F, 0x3C, 0x3C, 0xE7, 0xFC, 0x27, 0x3F, 0x3C, 0x38, 49 | 0x3C, 0x3F, 0x24, 0x27, 0xFC, 0xE7, 0x3C, 0x3C, 0x24, 0xC3, 0xE7, 0xCF, 0xF3, 0xF3, 0xCF, 50 | 0xFF, 0xFF, 0xFF, 0x00, 0x3C, 0x3F, 0x3C, 0x00, 0x9F, 0x00, 0x3C, 0xE7, 0xFC, 0x07, 0xCF, 51 | 0x24, 0x3C, 0x3C, 0x00, 0x00, 0x3F, 0x00, 0x9F, 0x3C, 0x27, 0x24, 0xE7, 0x00, 0xE7, 0x3C, 52 | 0xE7, 0x3C, 0xFF, 0x00, 0x00, 0xCF, 0xFF, 0xDB, 0x00, 0xE7, 0xC7, 0xFF, 0xF3, 0xE7, 0x33, 53 | 0x81, 0xFF, 0x81, 0xFF, 0xE7, 0x24, 0xE7, 0x00, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x81, 0x00, 54 | 0xFF, 0xFF, 0xE0, 0xFF, 0x83, 0xE1, 0x24, 0x00, 0x01, 0x3F, 0x3C, 0x03, 0x03, 0x30, 0x00, 55 | 0xE7, 0xFC, 0x0F, 0x3F, 0x3C, 0x30, 0x3C, 0x01, 0x3C, 0x01, 0x81, 0xE7, 0x3C, 0x3C, 0x3C, 56 | 0xE7, 0x81, 0xE7, 0xF3, 0xE7, 0xCF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3C, 0x3F, 0x3C, 0x3C, 0x9F, 57 | 0x3C, 0x3C, 0xE7, 0xFC, 0x33, 0xCF, 0x24, 0x3C, 0x3C, 0x3C, 0x3C, 0x3F, 0x3F, 0x9F, 0x3C, 58 | 0x33, 0x24, 0xC3, 0x3C, 0xF3, 0x3C, 0xE7, 0x3C, 0x23, 0x00, 0x00, 0xCF, 0xFF, 0x81, 0x27, 59 | 0xF3, 0x93, 0xFF, 0xF3, 0xE7, 0x87, 0xE7, 0xFF, 0xFF, 0xFF, 0xF3, 0x3C, 0xE7, 0xFC, 0xFC, 60 | 0x3C, 0x3F, 0x3F, 0xF9, 0x3C, 0x3C, 0xE7, 0x9F, 0xF0, 0xFF, 0x87, 0xFC, 0x20, 0x3C, 0x3B, 61 | 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3C, 0xE7, 0xFC, 0x27, 0x3F, 0x24, 0x24, 0x3C, 0x3C, 0x3C, 62 | 0x3C, 0x3F, 0xE7, 0x3C, 0x3C, 0x3C, 0xC3, 0x3C, 0xF3, 0xF3, 0xCF, 0xCF, 0xFF, 0xFF, 0xFF, 63 | 0xFC, 0x3C, 0x3F, 0x3C, 0x3C, 0x00, 0x3C, 0x3C, 0xE7, 0xFC, 0x39, 0xCF, 0x24, 0x3C, 0x3C, 64 | 0x3C, 0x3C, 0x3F, 0x3F, 0x9F, 0x3C, 0x39, 0x24, 0x99, 0x3C, 0xF9, 0x3C, 0xE7, 0x3C, 0x89, 65 | 0x00, 0x00, 0xCF, 0xC9, 0xDB, 0x27, 0xB9, 0x93, 0xE7, 0xF3, 0xE7, 0xCF, 0xE7, 0xFF, 0xFF, 66 | 0xFF, 0xF9, 0x3C, 0xE7, 0xFC, 0xFC, 0x3C, 0x3F, 0x3F, 0xFC, 0x3C, 0x3C, 0xFF, 0xFF, 0xF8, 67 | 0x81, 0x8F, 0xFC, 0x3C, 0x3C, 0x39, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3C, 0xE7, 0xFC, 0x33, 68 | 0x3F, 0x00, 0x0C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3F, 0xE7, 0x3C, 0x3C, 0x3C, 0x99, 0x3C, 0xF9, 69 | 0xF3, 0x9F, 0xCF, 0x27, 0xFF, 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x00, 0x00, 0x87, 70 | 0x80, 0x3C, 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x24, 0x3C, 71 | 0x3C, 0x00, 0x3C, 0xE7, 0x3C, 0xFF, 0x00, 0x00, 0xCF, 0xC9, 0xFF, 0x00, 0x1C, 0xC7, 0xE7, 72 | 0xF9, 0xCF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3C, 0xE7, 0xFC, 0xFC, 0x3C, 0x3F, 0x3F, 73 | 0xFC, 0x9C, 0x3C, 0xFF, 0xFF, 0xFC, 0xFF, 0x9F, 0x3C, 0x3C, 0x99, 0x39, 0x3C, 0x39, 0x3F, 74 | 0x3F, 0x3C, 0x3C, 0xE7, 0xFC, 0x39, 0x3F, 0x18, 0x1C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0xE7, 75 | 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0xFC, 0xF3, 0x3F, 0xCF, 0x8F, 0xFF, 0x9F, 0xFF, 0x3F, 0xFF, 76 | 0xFC, 0xFF, 0x9F, 0xFF, 0x3F, 0xFF, 0xFF, 0x3F, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 77 | 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3C, 0xE7, 0x3C, 0xFF, 0x00, 0x00, 0xCF, 78 | 0xC9, 0xFF, 0xE7, 0xBE, 0xFF, 0xE7, 0xFC, 0x9F, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 79 | 0x87, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 80 | 0x81, 0xC3, 0x03, 0x81, 0x03, 0x00, 0x00, 0x81, 0x3C, 0x00, 0x00, 0x3C, 0x3F, 0x3C, 0x3C, 81 | 0x81, 0x01, 0x81, 0x01, 0x81, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0xF0, 0xFF, 0x0F, 82 | 0xDF, 0xFF, 0x3F, 0xFF, 0x3F, 0xFF, 0xFC, 0xFF, 0x80, 0xFF, 0x3F, 0xE7, 0xFC, 0x3F, 0x0F, 83 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 84 | 0xE7, 0x00, 0xFF, 0x00, 0x00, 85 | }}; 86 | #endif // FONT2_H_ -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | /* Uncomment this line to enable debug messages or call make with `make 5 | CFLAGS=-DDEBUG_MSG` */ 6 | // #define DEBUG_MSG 7 | 8 | #define APP_VERSION "v2.2.0" 9 | 10 | #include 11 | #define SDL_MAIN_USE_CALLBACKS 12 | #include 13 | #include 14 | 15 | #include "SDL2_inprint.h" 16 | #include "backends/audio.h" 17 | #include "backends/m8.h" 18 | #include "common.h" 19 | #include "config.h" 20 | #include "gamepads.h" 21 | #include "render.h" 22 | #include "log_overlay.h" 23 | 24 | static void do_wait_for_device(struct app_context *ctx) { 25 | static Uint64 ticks_poll_device = 0; 26 | static int screensaver_initialized = 0; 27 | 28 | // Handle app suspension 29 | if (ctx->app_suspended) { 30 | return; 31 | } 32 | 33 | if (!screensaver_initialized) { 34 | screensaver_initialized = screensaver_init(); 35 | } 36 | screensaver_draw(); 37 | render_screen(&ctx->conf); 38 | 39 | // Poll for M8 device every second 40 | if (ctx->device_connected == 0 && SDL_GetTicks() - ticks_poll_device > 1000) { 41 | ticks_poll_device = SDL_GetTicks(); 42 | if (m8_initialize(0, ctx->preferred_device)) { 43 | 44 | if (ctx->conf.audio_enabled) { 45 | if (!audio_initialize(ctx->conf.audio_device_name, ctx->conf.audio_buffer_size)) { 46 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "Cannot initialize audio"); 47 | ctx->conf.audio_enabled = 0; 48 | } 49 | } 50 | 51 | const int m8_enabled = m8_enable_display(1); 52 | // Device was found; enable display and proceed to the main loop 53 | if (m8_enabled == 1) { 54 | ctx->app_state = RUN; 55 | ctx->device_connected = 1; 56 | SDL_Delay(100); // Give the display time to initialize 57 | screensaver_destroy(); 58 | screensaver_initialized = 0; 59 | m8_reset_display(); // Avoid display glitches. 60 | } else { 61 | SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Device not detected."); 62 | ctx->app_state = QUIT; 63 | screensaver_destroy(); 64 | screensaver_initialized = 0; 65 | #ifdef USE_RTMIDI 66 | show_error_message( 67 | "Cannot initialize M8 remote display. Make sure you're running " 68 | "firmware 6.0.0 or newer. Please close and restart the application to try again."); 69 | #endif 70 | } 71 | } 72 | } 73 | } 74 | 75 | static config_params_s initialize_config(int argc, char *argv[], char **preferred_device, 76 | char **config_filename) { 77 | for (int i = 1; i < argc; i++) { 78 | if (SDL_strcmp(argv[i], "--list") == 0) { 79 | exit(m8_list_devices()); 80 | } 81 | if (SDL_strcmp(argv[i], "--dev") == 0 && i + 1 < argc) { 82 | *preferred_device = argv[i + 1]; 83 | SDL_Log("Using preferred device: %s", *preferred_device); 84 | i++; 85 | } else if (SDL_strcmp(argv[i], "--config") == 0 && i + 1 < argc) { 86 | *config_filename = argv[i + 1]; 87 | SDL_Log("Using config file: %s", *config_filename); 88 | i++; 89 | } 90 | } 91 | 92 | config_params_s conf = config_initialize(*config_filename); 93 | 94 | if (TARGET_OS_IOS == 1) { 95 | // Predefined settings for iOS 96 | conf.init_fullscreen = 1; 97 | } 98 | config_read(&conf); 99 | 100 | return conf; 101 | } 102 | 103 | // Main callback loop - read inputs, process data from the device, render screen 104 | SDL_AppResult SDL_AppIterate(void *appstate) { 105 | if (appstate == NULL) { 106 | return SDL_APP_FAILURE; 107 | } 108 | 109 | struct app_context *ctx = appstate; 110 | SDL_AppResult app_result = SDL_APP_CONTINUE; 111 | 112 | switch (ctx->app_state) { 113 | case INITIALIZE: 114 | break; 115 | 116 | case WAIT_FOR_DEVICE: 117 | do_wait_for_device(ctx); 118 | break; 119 | 120 | case RUN: { 121 | const int result = m8_process_data(&ctx->conf); 122 | if (result == DEVICE_DISCONNECTED) { 123 | ctx->device_connected = 0; 124 | ctx->app_state = WAIT_FOR_DEVICE; 125 | audio_close(); 126 | } else if (result == DEVICE_FATAL_ERROR) { 127 | return SDL_APP_FAILURE; 128 | } 129 | render_screen(&ctx->conf); 130 | break; 131 | } 132 | 133 | case QUIT: 134 | app_result = SDL_APP_SUCCESS; 135 | break; 136 | } 137 | 138 | return app_result; 139 | } 140 | 141 | // Initialize the app: initialize context, configs, renderer controllers and attempt to find M8 142 | SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { 143 | SDL_SetAppMetadata("M8C",APP_VERSION,"fi.laamaa.m8c"); 144 | 145 | char *config_filename = NULL; 146 | 147 | // Initialize in-app log capture/overlay 148 | log_overlay_init(); 149 | 150 | #ifndef NDEBUG 151 | // Show debug messages in the application log 152 | SDL_SetLogPriorities(SDL_LOG_PRIORITY_DEBUG); 153 | SDL_LogDebug(SDL_LOG_CATEGORY_TEST, "Running a Debug build"); 154 | #else 155 | // Show debug messages in the application log 156 | SDL_SetLogPriorities(SDL_LOG_PRIORITY_INFO); 157 | #endif 158 | 159 | // Process the application's main callback roughly at 120 Hz 160 | SDL_SetHint(SDL_HINT_MAIN_CALLBACK_RATE, "120"); 161 | 162 | struct app_context *ctx = SDL_calloc(1, sizeof(struct app_context)); 163 | if (ctx == NULL) { 164 | SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "SDL_calloc failed: %s", SDL_GetError()); 165 | return SDL_APP_FAILURE; 166 | } 167 | 168 | *appstate = ctx; 169 | ctx->app_state = INITIALIZE; 170 | ctx->conf = initialize_config(argc, argv, &ctx->preferred_device, &config_filename); 171 | 172 | if (!renderer_initialize(&ctx->conf)) { 173 | SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Failed to initialize renderer."); 174 | return SDL_APP_FAILURE; 175 | } 176 | 177 | ctx->device_connected = 178 | m8_initialize(1, ctx->preferred_device); 179 | 180 | if (gamepads_initialize() < 0) { 181 | SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Failed to initialize game controllers."); 182 | return SDL_APP_FAILURE; 183 | } 184 | 185 | if (ctx->device_connected && m8_enable_display(1)) { 186 | if (ctx->conf.audio_enabled) { 187 | audio_initialize(ctx->conf.audio_device_name, ctx->conf.audio_buffer_size); 188 | } 189 | ctx->app_state = RUN; 190 | render_screen(&ctx->conf); 191 | } else { 192 | SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Device not detected."); 193 | ctx->device_connected = 0; 194 | ctx->app_state = WAIT_FOR_DEVICE; 195 | } 196 | 197 | return SDL_APP_CONTINUE; 198 | } 199 | 200 | void SDL_AppQuit(void *appstate, SDL_AppResult result) { 201 | (void)result; // Suppress compiler warning 202 | 203 | struct app_context *app = appstate; 204 | 205 | if (app) { 206 | if (app->app_state == WAIT_FOR_DEVICE) { 207 | screensaver_destroy(); 208 | } 209 | if (app->conf.audio_enabled) { 210 | audio_close(); 211 | } 212 | gamepads_close(); 213 | renderer_close(); 214 | inline_font_close(); 215 | if (app->device_connected) { 216 | m8_close(); 217 | } 218 | SDL_free(app); 219 | 220 | SDL_Log("Shutting down."); 221 | SDL_Quit(); 222 | } 223 | } -------------------------------------------------------------------------------- /src/ini.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 rxi 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "ini.h" 29 | 30 | struct ini_t { 31 | char *data; 32 | char *end; 33 | }; 34 | 35 | /* Case insensitive string compare */ 36 | static int strcmpci(const char *a, const char *b) { 37 | for (;;) { 38 | const int d = tolower(*a) - tolower(*b); 39 | if (d != 0 || !*a) { 40 | return d; 41 | } 42 | a++, b++; 43 | } 44 | } 45 | 46 | /* Returns the next string in the split data */ 47 | static char *next(const ini_t *ini, char *p) { 48 | p += strlen(p); 49 | while (p < ini->end && *p == '\0') { 50 | p++; 51 | } 52 | return p; 53 | } 54 | 55 | static void trim_back(const ini_t *ini, char *p) { 56 | while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) { 57 | *p-- = '\0'; 58 | } 59 | } 60 | 61 | static char *discard_line(const ini_t *ini, char *p) { 62 | while (p < ini->end && *p != '\n') { 63 | *p++ = '\0'; 64 | } 65 | return p; 66 | } 67 | 68 | static char *unescape_quoted_value(const ini_t *ini, char *p) { 69 | /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q` 70 | * as escape sequences are always larger than their resultant data */ 71 | char *q = p; 72 | p++; 73 | while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') { 74 | if (*p == '\\') { 75 | /* Handle escaped char */ 76 | p++; 77 | switch (*p) { 78 | default: 79 | *q = *p; 80 | break; 81 | case 'r': 82 | *q = '\r'; 83 | break; 84 | case 'n': 85 | *q = '\n'; 86 | break; 87 | case 't': 88 | *q = '\t'; 89 | break; 90 | case '\r': 91 | case '\n': 92 | case '\0': 93 | goto end; 94 | } 95 | 96 | } else { 97 | /* Handle normal char */ 98 | *q = *p; 99 | } 100 | q++, p++; 101 | } 102 | end: 103 | return q; 104 | } 105 | 106 | /* Splits data in place into strings containing section-headers, keys and 107 | * values using one or more '\0' as a delimiter. Unescapes quoted values */ 108 | static void split_data(const ini_t *ini) { 109 | char *value_start, *line_start; 110 | char *p = ini->data; 111 | 112 | while (p < ini->end) { 113 | switch (*p) { 114 | case '\r': 115 | case '\n': 116 | case '\t': 117 | case ' ': 118 | *p = '\0'; 119 | /* Fall through */ 120 | 121 | case '\0': 122 | p++; 123 | break; 124 | 125 | case '[': 126 | p += strcspn(p, "]\n"); 127 | *p = '\0'; 128 | break; 129 | 130 | case ';': 131 | p = discard_line(ini, p); 132 | break; 133 | 134 | default: 135 | line_start = p; 136 | p += strcspn(p, "=\n"); 137 | 138 | /* Is line missing a '='? */ 139 | if (*p != '=') { 140 | p = discard_line(ini, line_start); 141 | break; 142 | } 143 | trim_back(ini, p - 1); 144 | 145 | /* Replace '=' and whitespace after it with '\0' */ 146 | do { 147 | *p++ = '\0'; 148 | } while (*p == ' ' || *p == '\r' || *p == '\t'); 149 | 150 | /* Is a value after '=' missing? */ 151 | if (*p == '\n' || *p == '\0') { 152 | p = discard_line(ini, line_start); 153 | break; 154 | } 155 | 156 | if (*p == '"') { 157 | /* Handle quoted string value */ 158 | value_start = p; 159 | p = unescape_quoted_value(ini, p); 160 | 161 | /* Was the string empty? */ 162 | if (p == value_start) { 163 | p = discard_line(ini, line_start); 164 | break; 165 | } 166 | 167 | /* Discard the rest of the line after the string value */ 168 | p = discard_line(ini, p); 169 | 170 | } else { 171 | /* Handle normal value */ 172 | p += strcspn(p, "\n"); 173 | trim_back(ini, p - 1); 174 | } 175 | break; 176 | } 177 | } 178 | } 179 | 180 | ini_t *ini_load(const char *filename) { 181 | ini_t *ini = NULL; 182 | FILE *fp = NULL; 183 | int n, sz; 184 | 185 | /* Init ini struct */ 186 | ini = malloc(sizeof(*ini)); 187 | if (!ini) { 188 | goto fail; 189 | } 190 | memset(ini, 0, sizeof(*ini)); 191 | 192 | /* Open file */ 193 | fp = fopen(filename, "rb"); 194 | if (!fp) { 195 | goto fail; 196 | } 197 | 198 | /* Get file size */ 199 | fseek(fp, 0, SEEK_END); 200 | sz = ftell(fp); 201 | if (sz == 0) { 202 | goto fail; 203 | } 204 | rewind(fp); 205 | 206 | /* Load file content into memory, null terminate, init end var */ 207 | ini->data = malloc(sz + 1); 208 | ini->data[sz] = '\0'; 209 | ini->end = ini->data + sz; 210 | n = fread(ini->data, 1, sz, fp); 211 | if (n != sz) { 212 | goto fail; 213 | } 214 | 215 | /* Prepare data */ 216 | split_data(ini); 217 | 218 | /* Clean up and return */ 219 | fclose(fp); 220 | return ini; 221 | 222 | fail: 223 | if (fp) 224 | fclose(fp); 225 | if (ini) 226 | ini_free(ini); 227 | return NULL; 228 | } 229 | 230 | void ini_free(ini_t *ini) { 231 | free(ini->data); 232 | free(ini); 233 | } 234 | 235 | const char *ini_get(const ini_t *ini, const char *section, const char *key) { 236 | const char *current_section = ""; 237 | char *p = ini->data; 238 | 239 | if (*p == '\0') { 240 | p = next(ini, p); 241 | } 242 | 243 | while (p < ini->end) { 244 | if (*p == '[') { 245 | /* Handle section */ 246 | current_section = p + 1; 247 | 248 | } else { 249 | /* Handle key */ 250 | char *val = next(ini, p); 251 | if (!section || !strcmpci(section, current_section)) { 252 | if (!strcmpci(p, key)) { 253 | return val; 254 | } 255 | } 256 | p = val; 257 | } 258 | 259 | p = next(ini, p); 260 | } 261 | 262 | return NULL; 263 | } 264 | 265 | int ini_sget(const ini_t *ini, const char *section, const char *key, const char *scanfmt, 266 | void *dst) { 267 | const char *val = ini_get(ini, section, key); 268 | if (!val) { 269 | return 0; 270 | } 271 | if (scanfmt) { 272 | sscanf(val, scanfmt, dst); 273 | } else { 274 | *(const char **)dst = val; 275 | } 276 | return 1; 277 | } 278 | -------------------------------------------------------------------------------- /src/command.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #include 5 | 6 | #include "command.h" 7 | #include "render.h" 8 | #include 9 | 10 | #define ArrayCount(x) sizeof(x) / sizeof((x)[1]) 11 | 12 | // Convert 2 little-endian 8bit bytes to a 16bit integer 13 | static uint16_t decodeInt16(const uint8_t *data, const uint8_t start) { 14 | return data[start] | (((uint16_t)data[start + 1] << 8) & UINT16_MAX); 15 | } 16 | 17 | enum m8_command_bytes { 18 | draw_rectangle_command = 0xFE, 19 | draw_rectangle_command_pos_datalength = 5, 20 | draw_rectangle_command_pos_color_datalength = 8, 21 | draw_rectangle_command_pos_size_datalength = 9, 22 | draw_rectangle_command_pos_size_color_datalength = 12, 23 | draw_character_command = 0xFD, 24 | draw_character_command_datalength = 12, 25 | draw_oscilloscope_waveform_command = 0xFC, 26 | draw_oscilloscope_waveform_command_mindatalength = 1 + 3, 27 | draw_oscilloscope_waveform_command_maxdatalength = 1 + 3 + 480, 28 | joypad_keypressedstate_command = 0xFB, 29 | joypad_keypressedstate_command_datalength = 3, 30 | system_info_command = 0xFF, 31 | system_info_command_datalength = 6 32 | }; 33 | 34 | static void dump_packet(const uint32_t size, const uint8_t *recv_buf) { 35 | for (uint32_t a = 0; a < size; a++) { 36 | SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "0x%02X ", recv_buf[a]); 37 | } 38 | SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "\n"); 39 | } 40 | 41 | int process_command(const uint8_t *recv_buf, uint32_t size) { 42 | 43 | switch (recv_buf[0]) { 44 | 45 | case draw_rectangle_command: { 46 | if (size != draw_rectangle_command_pos_datalength && 47 | size != draw_rectangle_command_pos_color_datalength && 48 | size != draw_rectangle_command_pos_size_datalength && 49 | size != draw_rectangle_command_pos_size_color_datalength) { 50 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, 51 | "Invalid draw rectangle packet: expected length of %d, %d, %d or %d, got %d", 52 | draw_rectangle_command_pos_datalength, 53 | draw_rectangle_command_pos_color_datalength, 54 | draw_rectangle_command_pos_size_datalength, 55 | draw_rectangle_command_pos_size_color_datalength, size); 56 | dump_packet(size, recv_buf); 57 | return 0; 58 | } 59 | /* Support variable sized rectangle commands 60 | If colors are omitted, the last drawn color should be used 61 | If size is omitted, the size should be 1x1 pixels 62 | So basically the command can be 5, 8, 9 or 12 bytes long */ 63 | 64 | static struct draw_rectangle_command rectcmd; 65 | 66 | rectcmd.pos.x = decodeInt16(recv_buf, 1); 67 | rectcmd.pos.y = decodeInt16(recv_buf, 3); 68 | 69 | switch (size) { 70 | case draw_rectangle_command_pos_datalength: 71 | rectcmd.size.width = 1; 72 | rectcmd.size.height = 1; 73 | break; 74 | case draw_rectangle_command_pos_color_datalength: 75 | rectcmd.size.width = 1; 76 | rectcmd.size.height = 1; 77 | rectcmd.color.r = recv_buf[5]; 78 | rectcmd.color.g = recv_buf[6]; 79 | rectcmd.color.b = recv_buf[7]; 80 | break; 81 | case draw_rectangle_command_pos_size_datalength: 82 | rectcmd.size.width = decodeInt16(recv_buf, 5); 83 | rectcmd.size.height = decodeInt16(recv_buf, 7); 84 | break; 85 | case draw_rectangle_command_pos_size_color_datalength: 86 | rectcmd.size.width = decodeInt16(recv_buf, 5); 87 | rectcmd.size.height = decodeInt16(recv_buf, 7); 88 | rectcmd.color.r = recv_buf[9]; 89 | rectcmd.color.g = recv_buf[10]; 90 | rectcmd.color.b = recv_buf[11]; 91 | break; 92 | default: 93 | assert(0 && "Unreachable"); 94 | return 0; 95 | } 96 | 97 | draw_rectangle(&rectcmd); 98 | return 1; 99 | } 100 | 101 | case draw_character_command: { 102 | if (size != draw_character_command_datalength) { 103 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, 104 | "Invalid draw character packet: expected length %d, got %d", 105 | draw_character_command_datalength, size); 106 | dump_packet(size, recv_buf); 107 | return 0; 108 | } 109 | struct draw_character_command charcmd = { 110 | recv_buf[1], // char 111 | {decodeInt16(recv_buf, 2), decodeInt16(recv_buf, 4)}, // position x/y 112 | {recv_buf[6], recv_buf[7], recv_buf[8]}, // foreground r/g/b 113 | {recv_buf[9], recv_buf[10], recv_buf[11]}}; // background r/g/b 114 | draw_character(&charcmd); 115 | return 1; 116 | } 117 | 118 | case draw_oscilloscope_waveform_command: { 119 | if (size < draw_oscilloscope_waveform_command_mindatalength || 120 | size > draw_oscilloscope_waveform_command_maxdatalength) { 121 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, 122 | "Invalid draw oscilloscope packet: expected length between %d and %d, got %d", 123 | draw_oscilloscope_waveform_command_mindatalength, 124 | draw_oscilloscope_waveform_command_maxdatalength, size); 125 | dump_packet(size, recv_buf); 126 | return 0; 127 | } 128 | struct draw_oscilloscope_waveform_command osccmd = {0}; 129 | 130 | osccmd.color = (struct color){recv_buf[1], recv_buf[2], recv_buf[3]}; // color r/g/b 131 | memcpy(osccmd.waveform, &recv_buf[4], size - 4); 132 | 133 | osccmd.waveform_size = (size & UINT16_MAX) - 4; 134 | 135 | draw_waveform(&osccmd); 136 | return 1; 137 | } 138 | 139 | case joypad_keypressedstate_command: { 140 | if (size != joypad_keypressedstate_command_datalength) { 141 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, 142 | "Invalid joypad keypressed state packet: expected length %d, " 143 | "got %d\n", 144 | joypad_keypressedstate_command_datalength, size); 145 | dump_packet(size, recv_buf); 146 | return 0; 147 | } 148 | 149 | // nothing is done with joypad key pressed packets for now 150 | return 1; 151 | } 152 | 153 | case system_info_command: { 154 | if (size != system_info_command_datalength) { 155 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, 156 | "Invalid system info packet: expected length %d, got %d\n", 157 | system_info_command_datalength, size); 158 | dump_packet(size, recv_buf); 159 | break; 160 | } 161 | 162 | char *hwtype[4] = {"Headless", "Beta M8", "Production M8", "Production M8 Model:02"}; 163 | 164 | static int system_info_printed = 0; 165 | 166 | if (system_info_printed == 0) { 167 | const char *hwname = recv_buf[1] < ArrayCount(hwtype) ? hwtype[recv_buf[1]] : "Unknown"; 168 | SDL_Log("** Hardware info ** Device type: %s, Firmware ver %d.%d.%d", hwname, recv_buf[2], 169 | recv_buf[3], recv_buf[4]); 170 | system_info_printed = 1; 171 | } 172 | 173 | if (recv_buf[1] == 0x03) { 174 | set_m8_model(1); 175 | } else { 176 | set_m8_model(0); 177 | } 178 | 179 | renderer_set_font_mode(recv_buf[5]); 180 | 181 | return 1; 182 | } 183 | 184 | default: 185 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Invalid packet"); 186 | dump_packet(size, recv_buf); 187 | return 0; 188 | } 189 | return 1; 190 | } 191 | -------------------------------------------------------------------------------- /src/fonts/font3.h: -------------------------------------------------------------------------------- 1 | #ifndef FONT3_H_ 2 | #define FONT3_H_ 3 | 4 | #include "fonts.h" 5 | 6 | struct inline_font font_v2_small = { 7 | 846, 8 | 9, 9 | 9, 10 | 9, 11 | 0, 12 | -2, 13 | 5, 14 | 38, 15 | 1118, 16 | { 17 | 0x42, 0x4D, 0x5E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x7C, 18 | 0x00, 0x00, 0x00, 0x4E, 0x03, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0xCC, 0x03, 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, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF7, 0x7E, 27 | 0xF7, 0xC4, 0x3B, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xEF, 0xFF, 0xFB, 0xFD, 0xFE, 0x00, 0x00, 28 | 0x00, 0x00, 0x1F, 0xE0, 0x00, 0x03, 0xFC, 0x00, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 29 | 0xDE, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x30, 0x03, 0xFC, 0x00, 0xFE, 0x00, 0x00, 0x1F, 0xC0, 30 | 0x07, 0xF3, 0xF8, 0x00, 0xFF, 0x01, 0x3F, 0x80, 0x1E, 0xF0, 0x07, 0xBD, 0xFC, 0xFE, 0x00, 31 | 0x00, 0x3C, 0x7F, 0xEE, 0x3F, 0xFC, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 32 | 0x00, 0xFE, 0x00, 0x00, 0x1F, 0xC0, 0x07, 0x73, 0xF8, 0x00, 0xFF, 0xFF, 0x3F, 0xC0, 0x00, 33 | 0x00, 0x00, 0xFC, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x07, 0xFC, 0x00, 0x00, 0xFF, 34 | 0xFF, 0xF7, 0x60, 0x0B, 0xD3, 0xD7, 0xFF, 0xFD, 0xBF, 0xFF, 0xFD, 0xEF, 0xFF, 0xFF, 0xFE, 35 | 0xFE, 0xFE, 0xF7, 0xBF, 0xFF, 0xDF, 0xEF, 0xF3, 0xFB, 0xFC, 0xFE, 0xFF, 0x7F, 0xDF, 0xFF, 36 | 0x3F, 0xFF, 0x3F, 0xFE, 0xFF, 0x7F, 0x3F, 0x9F, 0xEF, 0xD7, 0xFB, 0xFD, 0xFC, 0xFE, 0xF7, 37 | 0xFF, 0x9F, 0xAF, 0xF7, 0xF3, 0xF1, 0xFC, 0xFF, 0x7E, 0xBF, 0x7F, 0xDE, 0xF7, 0xF7, 0x5C, 38 | 0xF9, 0x7D, 0xFF, 0x5F, 0xFD, 0xFF, 0xDF, 0xBF, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0x9F, 0xEF, 39 | 0xE7, 0xFB, 0xFF, 0xFC, 0xFE, 0xF7, 0xFF, 0x9F, 0xAF, 0xF7, 0x73, 0xF9, 0xFC, 0xFF, 0xFF, 40 | 0x3F, 0xFF, 0xCF, 0xF7, 0xF3, 0x7D, 0xDD, 0x7D, 0xFF, 0x3F, 0xDF, 0xCF, 0xF7, 0xF7, 0xFC, 41 | 0x00, 0x00, 0x7F, 0xFF, 0xC0, 0x1E, 0xED, 0xC3, 0xEF, 0xFF, 0xFB, 0xDF, 0xFF, 0xFD, 0xFF, 42 | 0xFF, 0xFF, 0xFF, 0x7E, 0xFE, 0xF7, 0xBF, 0xFF, 0xDF, 0xEF, 0xF3, 0xFB, 0xFC, 0xFE, 0xFF, 43 | 0x3F, 0xDF, 0xFE, 0x38, 0x0F, 0x1F, 0xDE, 0x80, 0x7F, 0x3F, 0x9F, 0xEF, 0xE7, 0xFB, 0xFD, 44 | 0xFC, 0xFE, 0xF7, 0xFF, 0x9F, 0x6F, 0xF7, 0xF3, 0xE9, 0xFC, 0xFF, 0x7D, 0x3E, 0xFF, 0xDE, 45 | 0xF7, 0xF6, 0xED, 0x75, 0xBB, 0xFF, 0x6F, 0xFD, 0xFF, 0xBF, 0xBF, 0xFF, 0xFF, 0xFF, 0x7F, 46 | 0x3F, 0x9F, 0xEF, 0xE7, 0xFB, 0xFF, 0xFC, 0xFE, 0xF7, 0xFF, 0x9F, 0x6F, 0xF7, 0x73, 0xF9, 47 | 0xFC, 0x00, 0x00, 0x3F, 0xFF, 0xCF, 0xF7, 0xF3, 0xBD, 0xDD, 0xBB, 0xFF, 0x4F, 0xDF, 0xCF, 48 | 0xF7, 0xF7, 0xFC, 0x00, 0x00, 0x7F, 0xFF, 0xF7, 0x7E, 0xEE, 0xFB, 0xD7, 0xFF, 0xFB, 0xDF, 49 | 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xBE, 0xFE, 0xF7, 0xBF, 0xFF, 0xDF, 0xEF, 0xF3, 0xFB, 50 | 0xFC, 0xFE, 0xFF, 0x7F, 0xFF, 0xFC, 0x3F, 0xFF, 0x0F, 0xDE, 0xBA, 0x7F, 0x3F, 0x9F, 0xEF, 51 | 0xE7, 0xFB, 0xFD, 0xFC, 0xFE, 0xF7, 0xFF, 0x9E, 0xEF, 0xF7, 0xF3, 0xD9, 0xFC, 0xFF, 0x7B, 52 | 0x3D, 0xFF, 0xDE, 0xF7, 0xF5, 0xF5, 0xAD, 0xD7, 0xFF, 0x77, 0xFD, 0xFF, 0x7F, 0xBF, 0xFF, 53 | 0xFF, 0xFF, 0x00, 0x3F, 0x9F, 0xEF, 0xE0, 0x00, 0x0C, 0x00, 0xFE, 0xF7, 0xFF, 0x80, 0xEF, 54 | 0xF7, 0x73, 0xF9, 0xFC, 0xFE, 0x7F, 0x3F, 0xC0, 0x0F, 0xF7, 0xF3, 0xDD, 0xDD, 0xC7, 0x00, 55 | 0x71, 0xDF, 0xCF, 0xF7, 0xF7, 0xFC, 0x00, 0x00, 0x7F, 0xFF, 0xF7, 0x60, 0x0F, 0x7D, 0xBB, 56 | 0xFF, 0xFB, 0xDF, 0xDD, 0xE0, 0x3F, 0xF8, 0x0F, 0xFF, 0xDE, 0xEE, 0xF7, 0x80, 0x00, 0x00, 57 | 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x7F, 0xFF, 0xF8, 0x3F, 0xFF, 0x07, 0xC0, 0x82, 0x00, 58 | 0x00, 0x1F, 0xEF, 0xE0, 0x00, 0x01, 0xC0, 0x00, 0xF7, 0xFF, 0x81, 0xEF, 0xF7, 0x73, 0xB9, 59 | 0xFC, 0x00, 0x7F, 0x00, 0x00, 0x1E, 0xF7, 0xF3, 0xF9, 0xDD, 0xEF, 0x00, 0x7B, 0xFD, 0xFE, 60 | 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x9F, 0xEF, 0xE7, 0xF3, 0xFD, 0xFC, 0xFE, 0xF7, 61 | 0xFF, 0x9F, 0x6F, 0xF7, 0x73, 0xF9, 0xFC, 0xFE, 0x7F, 0x3F, 0xDF, 0xEF, 0xF7, 0xF3, 0xED, 62 | 0xDD, 0xBB, 0x7F, 0x7E, 0x5F, 0xCF, 0xF7, 0xF3, 0x3C, 0x00, 0x00, 0x7F, 0xFF, 0xF7, 0x6E, 63 | 0xFF, 0xBE, 0x7F, 0xFF, 0xFB, 0xDF, 0xEB, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE, 0xFE, 0xF7, 64 | 0xFF, 0xBF, 0xCF, 0xE7, 0xFB, 0xFF, 0xFC, 0xFE, 0x7F, 0x7F, 0xFF, 0xFC, 0x3F, 0xFF, 0x0F, 65 | 0xFC, 0xFA, 0x7F, 0x3E, 0xDF, 0xEF, 0xE7, 0xFB, 0xFD, 0xFE, 0xFE, 0xF7, 0xFF, 0x9E, 0xEF, 66 | 0xF6, 0xB3, 0x79, 0xFC, 0xFE, 0x7F, 0x3F, 0x9F, 0xFE, 0xF7, 0xF3, 0xF9, 0xFD, 0xD7, 0x7F, 67 | 0x7D, 0xFD, 0xFD, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x9F, 0xEF, 0xE7, 0xF3, 0xFD, 68 | 0xFC, 0xFE, 0xF7, 0xFF, 0x9F, 0xAF, 0xF7, 0x73, 0xF9, 0xFC, 0xFE, 0x7F, 0x3F, 0xDF, 0xEF, 69 | 0xF7, 0xF3, 0xF5, 0xDD, 0x7D, 0x7F, 0x7F, 0x9F, 0xCF, 0xF7, 0xF4, 0xDC, 0x00, 0x00, 0x7F, 70 | 0xB7, 0xC0, 0x0E, 0xF1, 0xDD, 0xBD, 0xFF, 0xFB, 0xDF, 0xF7, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 71 | 0xF6, 0xFE, 0xF7, 0xFF, 0xBF, 0xCF, 0xE7, 0xFB, 0xFF, 0xFC, 0xFE, 0x7F, 0x3F, 0xDF, 0xFE, 72 | 0x38, 0x0F, 0x1F, 0xFC, 0x82, 0x7F, 0x3E, 0xDF, 0xEF, 0xE7, 0xFB, 0xFD, 0xFE, 0xFE, 0xF7, 73 | 0xFF, 0x9F, 0x6F, 0xF5, 0xD2, 0xF9, 0xFC, 0xFE, 0x7F, 0x3F, 0x9F, 0xFE, 0xF7, 0xF3, 0xF9, 74 | 0xFD, 0xBB, 0x7F, 0x7E, 0xFD, 0xFB, 0xFF, 0xBB, 0xBF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 75 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x1F, 0xCF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x00, 0x00, 0x07, 0xF3, 0xF9, 0xDC, 0xFE, 0x7F, 0x00, 0x1F, 0xCF, 0xF7, 0xF7, 0xFC, 77 | 0x00, 0x00, 0x7F, 0xB7, 0xF7, 0x60, 0x05, 0xED, 0xBD, 0xFF, 0xFD, 0xBF, 0xEB, 0xFD, 0xFF, 78 | 0xFF, 0xFF, 0xFF, 0xFA, 0xFE, 0xF7, 0xFF, 0xBF, 0xCF, 0xE7, 0xFB, 0xFF, 0xFC, 0xFE, 0x7F, 79 | 0x7F, 0xFF, 0xFF, 0x3F, 0xFF, 0x3D, 0xFC, 0xFE, 0x7F, 0x3E, 0xDF, 0xEF, 0xD7, 0xFB, 0xFD, 80 | 0xFE, 0xFE, 0xF7, 0xFF, 0x9F, 0xAF, 0xF3, 0xE1, 0xF9, 0xFC, 0xFE, 0x7F, 0x3F, 0x9F, 0xFE, 81 | 0xF7, 0xF3, 0xF9, 0xFD, 0x7D, 0x7F, 0x7F, 0x7D, 0xF7, 0xFF, 0xBD, 0x7F, 0xFF, 0xBF, 0xFF, 82 | 0xBF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 83 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xCF, 84 | 0xF7, 0xF7, 0xFC, 0x00, 0x00, 0x7F, 0xB7, 0xF7, 0x7E, 0xF1, 0xF6, 0x7D, 0xFF, 0xFE, 0x7F, 85 | 0xDD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x07, 0x80, 0x00, 0x0F, 0xE0, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 87 | 0x30, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x7F, 0x9F, 0xCF, 0xF7, 0xF3, 0xF8, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x07, 0xF3, 0xF9, 0xFC, 0xFE, 0x7F, 0x00, 0x3C, 0x6F, 0xFE, 0x3E, 0xFF, 89 | 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 90 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 91 | 0xFF, 0xC0, 0x0F, 0xF0, 0x07, 0xFC, 0x00, 0x00, 92 | }}; 93 | #endif // FONT3_H_ -------------------------------------------------------------------------------- /src/backends/audio_sdl.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | #ifndef USE_LIBUSB 4 | #include "audio.h" 5 | #include 6 | 7 | SDL_AudioStream *audio_stream_in, *audio_stream_out; 8 | 9 | static unsigned int audio_paused = 0; 10 | static unsigned int audio_initialized = 0; 11 | static SDL_AudioSpec audio_spec_in = {SDL_AUDIO_S16LE, 2, 44100}; 12 | 13 | static void SDLCALL audio_cb_out(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) { 14 | // suppress compiler warnings 15 | (void)userdata; 16 | 17 | if (additional_amount <= 0) { 18 | return; 19 | } 20 | 21 | const int bytes_available = SDL_GetAudioStreamAvailable(audio_stream_in); 22 | if (bytes_available == -1) { 23 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, 24 | "Error getting available audio stream bytes: %s, destroying audio", 25 | SDL_GetError()); 26 | audio_close(); 27 | return; 28 | } 29 | 30 | // Decide how much to feed this time. 31 | int to_write_goal = additional_amount; 32 | if (total_amount > 0) { 33 | const int prefill_cap = additional_amount * 2; 34 | if (to_write_goal < total_amount) { 35 | to_write_goal = SDL_min(total_amount, prefill_cap); 36 | } 37 | } 38 | 39 | int to_write = to_write_goal; 40 | Uint8 temp[4096]; 41 | 42 | while (to_write > 0) { 43 | int still_avail = SDL_GetAudioStreamAvailable(audio_stream_in); 44 | if (still_avail <= 0) { 45 | break; // nothing more to pull now 46 | } 47 | 48 | int chunk = still_avail; 49 | if (chunk > (int)sizeof(temp)) chunk = (int)sizeof(temp); 50 | if (chunk > to_write) chunk = to_write; 51 | 52 | const int got = SDL_GetAudioStreamData(audio_stream_in, temp, chunk); 53 | if (got == -1) { 54 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, 55 | "Error reading audio stream data: %s, destroying audio", 56 | SDL_GetError()); 57 | audio_close(); 58 | return; 59 | } 60 | if (got == 0) { 61 | break; // no data currently available 62 | } 63 | 64 | if (!SDL_PutAudioStreamData(stream, temp, got)) { 65 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, 66 | "Error putting audio stream data: %s, destroying audio", 67 | SDL_GetError()); 68 | audio_close(); 69 | return; 70 | } 71 | 72 | to_write -= got; 73 | } 74 | } 75 | 76 | void audio_toggle(const char *output_device_name, unsigned int audio_buffer_size) { 77 | if (!audio_initialized) { 78 | audio_initialize(output_device_name, audio_buffer_size); 79 | return; 80 | } 81 | if (audio_paused) { 82 | SDL_ResumeAudioStreamDevice(audio_stream_out); 83 | SDL_ResumeAudioStreamDevice(audio_stream_in); 84 | } else { 85 | SDL_PauseAudioStreamDevice(audio_stream_in); 86 | SDL_PauseAudioStreamDevice(audio_stream_out); 87 | } 88 | audio_paused = !audio_paused; 89 | SDL_Log(audio_paused ? "Audio paused" : "Audio resumed"); 90 | } 91 | 92 | int audio_initialize(const char *output_device_name, const unsigned int audio_buffer_size) { 93 | 94 | int num_devices_in, num_devices_out; 95 | SDL_AudioDeviceID m8_device_id = 0; 96 | SDL_AudioDeviceID output_device_id = 0; 97 | 98 | if (SDL_Init(SDL_INIT_AUDIO) == false) { 99 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "SDL Audio init failed, SDL Error: %s", SDL_GetError()); 100 | return 0; 101 | } 102 | 103 | SDL_AudioDeviceID *devices_in = SDL_GetAudioRecordingDevices(&num_devices_in); 104 | if (!devices_in) { 105 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "No audio capture devices, SDL Error: %s", SDL_GetError()); 106 | return 0; 107 | } 108 | 109 | SDL_AudioDeviceID *devices_out = SDL_GetAudioPlaybackDevices(&num_devices_out); 110 | if (!devices_out) { 111 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "No audio playback devices, SDL Error: %s", 112 | SDL_GetError()); 113 | return 0; 114 | } 115 | 116 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "Audio input devices:"); 117 | for (int i = 0; i < num_devices_in; i++) { 118 | const SDL_AudioDeviceID instance_id = devices_in[i]; 119 | const char *device_name = SDL_GetAudioDeviceName(instance_id); 120 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "%s", device_name); 121 | if (SDL_strstr(device_name, "M8") != NULL) { 122 | SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "M8 Audio Input device found: %s", device_name); 123 | m8_device_id = instance_id; 124 | } 125 | } 126 | 127 | if (output_device_name != NULL) { 128 | for (int i = 0; i < num_devices_out; i++) { 129 | const SDL_AudioDeviceID instance_id = devices_out[i]; 130 | const char *device_name = SDL_GetAudioDeviceName(instance_id); 131 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "%s", device_name); 132 | if (SDL_strcasestr(device_name, output_device_name) != NULL) { 133 | SDL_Log("Requested output device found: %s", device_name); 134 | output_device_id = instance_id; 135 | } 136 | } 137 | } 138 | 139 | SDL_free(devices_in); 140 | SDL_free(devices_out); 141 | 142 | if (!output_device_id) { 143 | output_device_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; 144 | } 145 | 146 | if (!m8_device_id) { 147 | // forget about it 148 | SDL_Log("Cannot find M8 audio input device"); 149 | return 0; 150 | } 151 | 152 | char audio_buffer_size_str[256]; 153 | SDL_snprintf(audio_buffer_size_str, sizeof(audio_buffer_size_str), "%d", audio_buffer_size); 154 | if (audio_buffer_size > 0) { 155 | SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, "Setting requested audio device sample frames to %d", 156 | audio_buffer_size); 157 | SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, audio_buffer_size_str); 158 | } 159 | 160 | audio_stream_out = SDL_OpenAudioDeviceStream(output_device_id, NULL, audio_cb_out, NULL); 161 | 162 | SDL_AudioSpec audio_spec_out; 163 | int audio_out_buffer_size_real, audio_in_buffer_size_real = 0; 164 | 165 | SDL_GetAudioDeviceFormat(output_device_id, &audio_spec_out, &audio_out_buffer_size_real); 166 | 167 | if (!audio_stream_out) { 168 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "Error opening audio output device: %s", SDL_GetError()); 169 | return 0; 170 | } 171 | SDL_LogInfo(SDL_LOG_CATEGORY_AUDIO, 172 | "Opening audio output: rate %dhz, buffer size: %d frames", audio_spec_out.freq, 173 | audio_out_buffer_size_real); 174 | 175 | audio_stream_in = SDL_OpenAudioDeviceStream(m8_device_id, &audio_spec_in, NULL, NULL); 176 | if (!audio_stream_in) { 177 | SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "Error opening audio input device: %s", SDL_GetError()); 178 | SDL_DestroyAudioStream(audio_stream_out); 179 | return 0; 180 | } 181 | 182 | SDL_SetAudioStreamFormat(audio_stream_in, &audio_spec_in, &audio_spec_out); 183 | SDL_GetAudioDeviceFormat(m8_device_id, &audio_spec_in, &audio_in_buffer_size_real); 184 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "Audiospec In: format %d, channels %d, rate %d, buffer size %d frames", 185 | audio_spec_in.format, audio_spec_in.channels, audio_spec_in.freq, audio_in_buffer_size_real); 186 | 187 | 188 | 189 | SDL_ResumeAudioStreamDevice(audio_stream_out); 190 | SDL_ResumeAudioStreamDevice(audio_stream_in); 191 | 192 | audio_paused = 0; 193 | audio_initialized = 1; 194 | 195 | return 1; 196 | } 197 | 198 | void audio_close(void) { 199 | if (!audio_initialized) 200 | return; 201 | SDL_Log("Closing audio devices"); 202 | SDL_DestroyAudioStream(audio_stream_in); 203 | SDL_DestroyAudioStream(audio_stream_out); 204 | SDL_QuitSubSystem(SDL_INIT_AUDIO); 205 | audio_initialized = 0; 206 | } 207 | 208 | #endif 209 | -------------------------------------------------------------------------------- /src/fonts/font4.h: -------------------------------------------------------------------------------- 1 | #ifndef FONT4_H_ 2 | #define FONT4_H_ 3 | 4 | #include "fonts.h" 5 | 6 | struct inline_font font_v2_large = { 7 | 940, 8 | 10, 9 | 10, 10 | 10, 11 | 0, 12 | -2, 13 | 4, 14 | 38, 15 | 1346, 16 | { 17 | 0x42, 0x4D, 0x42, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x7C, 18 | 0x00, 0x00, 0x00, 0xAC, 0x03, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0xB0, 0x04, 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, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0xCF, 27 | 0xCF, 0x3E, 0x60, 0xCF, 0xFF, 0xF9, 0x3F, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xE7, 0xF3, 0xFC, 28 | 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0xFF, 0x3F, 0xFF, 0xFF, 29 | 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0x03, 0x3F, 0x00, 0x18, 0x00, 0x01, 0x80, 0x0F, 0xF8, 0x04, 30 | 0xFC, 0x00, 0x00, 0x13, 0xF2, 0x00, 0x3F, 0x0F, 0xC8, 0x04, 0xFF, 0x83, 0x0F, 0xC0, 0x07, 31 | 0xCF, 0x80, 0x7C, 0xF3, 0xF0, 0xFC, 0x00, 0x40, 0x0F, 0x0F, 0xFC, 0xE1, 0xFF, 0xF0, 0x03, 32 | 0xFF, 0x80, 0x00, 0x18, 0x02, 0x00, 0x80, 0x33, 0xF0, 0x04, 0xFC, 0x00, 0x00, 0x13, 0xE0, 33 | 0x00, 0x33, 0x0F, 0xC8, 0x04, 0xFF, 0xFF, 0x0F, 0xF0, 0x07, 0x80, 0x80, 0x43, 0xF8, 0x00, 34 | 0xFC, 0x00, 0x40, 0x00, 0x00, 0xFF, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0xCC, 35 | 0x01, 0x1E, 0x40, 0x0F, 0xFF, 0xF1, 0x1F, 0xFF, 0xFF, 0x3C, 0xFF, 0xFF, 0xE7, 0xF1, 0xFC, 36 | 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0xFF, 0x3F, 0xFC, 0xFF, 37 | 0xFF, 0xFF, 0xFF, 0xFF, 0x3C, 0x03, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x00, 38 | 0xFC, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x3F, 0x0F, 0x80, 0x00, 0xFF, 0x06, 0x0F, 0x80, 0x03, 39 | 0xCF, 0x00, 0x38, 0x71, 0xE0, 0x78, 0x00, 0x00, 0x0F, 0x0F, 0xF8, 0xE1, 0xFF, 0xF0, 0x03, 40 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0xF0, 0x00, 0xFC, 0x00, 0x00, 0x03, 0xC4, 41 | 0x00, 0x33, 0x0F, 0xC0, 0x00, 0xFF, 0xFF, 0x0F, 0xF0, 0x03, 0x00, 0x00, 0x01, 0xF0, 0x00, 42 | 0x78, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 43 | 0x00, 0x8F, 0xCE, 0x1F, 0xFF, 0xE3, 0x8F, 0xFF, 0xFF, 0x3C, 0xFF, 0xFF, 0xFF, 0xF8, 0xFC, 44 | 0xFC, 0xF3, 0xCF, 0xFF, 0xF3, 0xFC, 0xFF, 0x0F, 0xCF, 0xF0, 0xFC, 0xFF, 0x33, 0xFC, 0xFF, 45 | 0xE7, 0x80, 0x79, 0xFF, 0xFC, 0xFF, 0x3F, 0x0F, 0xC3, 0xFC, 0xFC, 0x3F, 0xCF, 0xF3, 0xF0, 46 | 0xFC, 0xF3, 0xFF, 0xC3, 0xC4, 0xFF, 0x3F, 0x0F, 0x03, 0xF0, 0xFF, 0x3C, 0x0F, 0x1F, 0xF3, 47 | 0xCF, 0x3F, 0x30, 0x30, 0xC2, 0x31, 0xFF, 0x23, 0xFF, 0x3F, 0xF1, 0xF9, 0xFF, 0xFF, 0xFF, 48 | 0xFF, 0x3F, 0x0F, 0xC3, 0xFC, 0xFC, 0x3F, 0xF3, 0xFF, 0xF0, 0xFC, 0xF3, 0xFF, 0xC3, 0x8F, 49 | 0xCF, 0x33, 0x0F, 0xC3, 0xF0, 0x01, 0x80, 0x0F, 0xFF, 0xF3, 0x3F, 0x3F, 0x08, 0xF3, 0x32, 50 | 0x31, 0xFF, 0x03, 0xF3, 0xF0, 0xFF, 0x3F, 0x3F, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x03, 51 | 0xCC, 0xC7, 0xCE, 0x1F, 0xFF, 0xE7, 0xCF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFC, 0x7C, 52 | 0xFC, 0xF3, 0xCF, 0xFF, 0xF3, 0xFC, 0xFF, 0x0F, 0xCF, 0xF0, 0xFC, 0xFF, 0x33, 0xFC, 0xFF, 53 | 0xC7, 0x80, 0x78, 0xFF, 0x3C, 0xC0, 0x3F, 0x0F, 0xC3, 0xFC, 0xFC, 0x3F, 0xCF, 0xF3, 0xF0, 54 | 0xFC, 0xF3, 0xFF, 0xC3, 0x8C, 0xFF, 0x3F, 0x0E, 0x03, 0xF0, 0xFF, 0x38, 0x0E, 0x3F, 0xF3, 55 | 0xCF, 0x3F, 0x23, 0x10, 0x03, 0x03, 0xFF, 0x31, 0xFF, 0x3F, 0xE3, 0xF9, 0xFF, 0xFF, 0xFF, 56 | 0xFF, 0x00, 0x0F, 0xC3, 0xFC, 0xFC, 0x00, 0x33, 0xF8, 0x00, 0xFC, 0xF3, 0xFF, 0xC0, 0x1F, 57 | 0xCF, 0x33, 0x0F, 0xC3, 0xF0, 0x00, 0x00, 0x0F, 0xF8, 0x03, 0x3F, 0x3F, 0x0C, 0x73, 0x33, 58 | 0x03, 0x80, 0x20, 0x73, 0xF0, 0xFF, 0x3F, 0x3F, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0xCE, 59 | 0x00, 0xE3, 0xC0, 0x0F, 0xFF, 0xE7, 0xCF, 0xCE, 0x78, 0x07, 0xFF, 0x80, 0x7F, 0xFE, 0x3C, 60 | 0xCC, 0xF3, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 61 | 0x87, 0xFF, 0xF8, 0x7F, 0x04, 0xC0, 0x00, 0x00, 0x03, 0xFC, 0xFC, 0x00, 0x00, 0x03, 0x00, 62 | 0x00, 0xF3, 0xFF, 0xC0, 0x1C, 0xFF, 0x33, 0x0C, 0x43, 0xF0, 0x01, 0x31, 0x00, 0x18, 0x03, 63 | 0xCF, 0x3F, 0x07, 0x82, 0x13, 0x87, 0x80, 0x38, 0xFF, 0x3F, 0xC7, 0xF9, 0xFF, 0xFF, 0xFF, 64 | 0xFF, 0x00, 0x0F, 0xC3, 0xFC, 0xFC, 0x00, 0x33, 0xF0, 0x00, 0xFC, 0xF3, 0xFF, 0xC0, 0x3F, 65 | 0xCF, 0x33, 0x0F, 0xC3, 0xF0, 0xFC, 0x3F, 0x0F, 0xF0, 0x07, 0x3F, 0x3F, 0x0E, 0x33, 0x33, 66 | 0x03, 0x00, 0x38, 0x13, 0xF0, 0xFF, 0x3F, 0x08, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0xCC, 67 | 0x01, 0xF1, 0xE0, 0xCF, 0xFF, 0xE7, 0xCF, 0xC4, 0x78, 0x07, 0xFF, 0x80, 0x7F, 0xFF, 0x1C, 68 | 0xCC, 0xF3, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 69 | 0x07, 0xFF, 0xF8, 0x3F, 0x00, 0xCC, 0x00, 0x00, 0x33, 0xFC, 0xFC, 0x00, 0x00, 0x03, 0x00, 70 | 0x00, 0xF3, 0xFF, 0xC0, 0x1C, 0xFF, 0x21, 0x08, 0xC3, 0xF0, 0x00, 0x33, 0x00, 0x00, 0x07, 71 | 0xCF, 0x3F, 0x0F, 0xC3, 0x33, 0x87, 0x00, 0x3C, 0x7F, 0x3F, 0x8F, 0xF9, 0xFF, 0xFF, 0xFF, 72 | 0xFF, 0xFF, 0x0F, 0xC3, 0xFC, 0xFC, 0x3F, 0x00, 0x33, 0xF0, 0xFC, 0xF3, 0xFF, 0xC3, 0x1F, 73 | 0xCF, 0x33, 0x0F, 0xC3, 0xF0, 0xFC, 0x3F, 0x0F, 0xF3, 0xFF, 0x3F, 0x3F, 0x0F, 0x13, 0x32, 74 | 0x31, 0x3F, 0x3F, 0x03, 0xF0, 0xFF, 0x3F, 0x00, 0x70, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 75 | 0xCF, 0xF8, 0xE6, 0x7F, 0xFF, 0xE7, 0xCF, 0xF1, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x8C, 76 | 0xFC, 0xF3, 0xFF, 0xCF, 0xF0, 0xFC, 0x3F, 0xCF, 0xFF, 0xF0, 0xFC, 0x3F, 0x33, 0xFC, 0xFF, 77 | 0x87, 0x80, 0x78, 0x7F, 0xF0, 0xCC, 0x3F, 0x0F, 0x93, 0xFC, 0xFC, 0x3F, 0xCF, 0xF3, 0xFC, 78 | 0xFC, 0xF3, 0xFF, 0xC3, 0x8C, 0xFF, 0x00, 0x01, 0xC3, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xFF, 79 | 0xCF, 0x3F, 0x0F, 0xC3, 0xF3, 0x03, 0x3F, 0x3E, 0x3F, 0x3F, 0x1F, 0xF9, 0xE6, 0x7F, 0xFF, 80 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xC3, 0xFF, 0x03, 0x8F, 81 | 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x0F, 0x83, 0x30, 82 | 0x78, 0x3F, 0x00, 0x03, 0xF0, 0xFF, 0x3F, 0x22, 0x70, 0x00, 0x00, 0x3F, 0xC9, 0xF0, 0x00, 83 | 0x00, 0xFC, 0x66, 0x73, 0xFF, 0xE3, 0x8F, 0xF1, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC4, 84 | 0xFC, 0xF3, 0xFF, 0xCF, 0xF0, 0xFC, 0x3F, 0xCF, 0xFF, 0xF0, 0xFC, 0x3F, 0x33, 0xFC, 0xFF, 85 | 0xC7, 0x80, 0x78, 0xF3, 0xF0, 0xFC, 0x3F, 0x0F, 0x93, 0xFC, 0xF8, 0x3F, 0xCF, 0xF3, 0xFC, 86 | 0xFC, 0xF3, 0xFF, 0xC3, 0xC4, 0xFF, 0x0C, 0x03, 0xC3, 0xF0, 0xFC, 0x3F, 0x0F, 0xC3, 0xFF, 87 | 0xCF, 0x3F, 0x0F, 0xC3, 0xF2, 0x31, 0x3F, 0x3F, 0x1F, 0x3E, 0x3F, 0xF9, 0xE0, 0x7F, 0xFF, 88 | 0x3F, 0x00, 0x40, 0x18, 0x02, 0x00, 0x80, 0x73, 0xF8, 0x00, 0x01, 0xC3, 0xFF, 0x03, 0xC7, 89 | 0xCF, 0x00, 0x40, 0x18, 0x04, 0x01, 0x80, 0x20, 0x08, 0x00, 0x00, 0x3F, 0x0F, 0xC3, 0x30, 90 | 0xFC, 0x3F, 0x00, 0x03, 0xF0, 0xFF, 0x3F, 0x3F, 0xF0, 0x00, 0x00, 0x3F, 0xC9, 0xFC, 0xCE, 91 | 0x00, 0x9E, 0x20, 0x73, 0xFF, 0xF1, 0x1F, 0xC4, 0x7F, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 92 | 0x00, 0x03, 0xC0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 93 | 0xE7, 0xFF, 0xF9, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 94 | 0xFC, 0x00, 0x3F, 0xC3, 0xE0, 0xFF, 0x1E, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 95 | 0x00, 0x3F, 0x0F, 0xC3, 0xF0, 0x78, 0x3F, 0x00, 0x0F, 0x0C, 0x7F, 0xE1, 0xF0, 0xFF, 0xFE, 96 | 0x3F, 0xFF, 0xCF, 0xFF, 0xFF, 0xFC, 0xFF, 0xF0, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 97 | 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 98 | 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0x3F, 0xC9, 0xFC, 0xCF, 99 | 0xCF, 0x9F, 0x30, 0xF3, 0xFF, 0xF9, 0x3F, 0xCE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 100 | 0x00, 0x03, 0xC0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 101 | 0xFF, 0xFF, 0xFF, 0xF8, 0x06, 0x01, 0x80, 0x40, 0x38, 0x00, 0x03, 0x80, 0x20, 0x08, 0x00, 102 | 0xFC, 0x00, 0x3F, 0xC3, 0xF0, 0xFF, 0x3F, 0x0F, 0xC8, 0x04, 0x01, 0x80, 0x40, 0x18, 0x00, 103 | 0x00, 0x3F, 0x0F, 0xC3, 0xF0, 0xFC, 0x3F, 0x00, 0x0F, 0x0C, 0xFF, 0xE1, 0xF9, 0xFF, 0xFE, 104 | 0x7F, 0xFF, 0xCF, 0xFF, 0xFF, 0xFC, 0xFF, 0xF8, 0x0F, 0xFC, 0xFF, 0xF3, 0xFF, 0xC3, 0xFF, 105 | 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 106 | 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0x00, 0x3F, 0xF0, 0x00, 0x00, 107 | }}; 108 | #endif // FONT4_H_ -------------------------------------------------------------------------------- /src/log_overlay.c: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | #include "log_overlay.h" 5 | 6 | #include 7 | 8 | #include "SDL2_inprint.h" 9 | #include "fonts/fonts.h" 10 | 11 | #define LOG_BUFFER_MAX_LINES 512 12 | #define LOG_LINE_MAX_CHARS 256 13 | 14 | static SDL_Texture *overlay_texture = NULL; 15 | static int overlay_visible = 0; 16 | static int overlay_needs_redraw = 0; 17 | 18 | static char log_lines[LOG_BUFFER_MAX_LINES][LOG_LINE_MAX_CHARS]; 19 | static int log_line_start = 0; 20 | static int log_line_count = 0; 21 | 22 | static SDL_LogOutputFunction prev_log_output_fn = NULL; 23 | static void *prev_log_output_userdata = NULL; 24 | static SDL_Mutex *log_mutex = NULL; // Mutex for protecting log buffer 25 | 26 | static void log_buffer_append_line(const char *line) { 27 | if (line[0] == '\0') { 28 | return; 29 | } 30 | // Protect buffer updates (can be called from non-main threads) 31 | if (log_mutex) 32 | SDL_LockMutex(log_mutex); 33 | const int index = (log_line_start + log_line_count) % LOG_BUFFER_MAX_LINES; 34 | SDL_strlcpy(log_lines[index], line, LOG_LINE_MAX_CHARS); 35 | if (log_line_count < LOG_BUFFER_MAX_LINES) { 36 | log_line_count++; 37 | } else { 38 | log_line_start = (log_line_start + 1) % LOG_BUFFER_MAX_LINES; 39 | } 40 | overlay_needs_redraw = 1; 41 | if (log_mutex) 42 | SDL_UnlockMutex(log_mutex); 43 | } 44 | 45 | static void sdl_log_capture(void *userdata, int category, SDL_LogPriority priority, 46 | const char *message) { 47 | // Suppress unused variable warnings 48 | (void)userdata; 49 | (void)category; 50 | (void)priority; 51 | 52 | char formatted[LOG_LINE_MAX_CHARS]; 53 | SDL_snprintf(formatted, sizeof(formatted), ">%s", message ? message : ""); 54 | log_buffer_append_line(formatted); 55 | 56 | if (prev_log_output_fn != NULL) { 57 | prev_log_output_fn(prev_log_output_userdata, category, priority, message); 58 | } 59 | } 60 | 61 | void log_overlay_init(void) { 62 | // Create synchronization primitive before hooking log output 63 | if (!log_mutex) { 64 | log_mutex = SDL_CreateMutex(); 65 | if (!log_mutex) { 66 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create log mutex: %s", SDL_GetError()); 67 | } 68 | } 69 | SDL_GetLogOutputFunction(&prev_log_output_fn, &prev_log_output_userdata); 70 | SDL_SetLogOutputFunction(sdl_log_capture, NULL); 71 | } 72 | 73 | void log_overlay_toggle(void) { 74 | overlay_visible = !overlay_visible; 75 | overlay_needs_redraw = 1; 76 | } 77 | 78 | int log_overlay_is_visible(void) { return overlay_visible; } 79 | 80 | void log_overlay_invalidate(void) { 81 | if (overlay_texture != NULL) { 82 | SDL_DestroyTexture(overlay_texture); 83 | overlay_texture = NULL; 84 | } 85 | overlay_needs_redraw = 1; 86 | } 87 | 88 | void log_overlay_destroy(void) { 89 | // Restore previous log output function 90 | if (prev_log_output_fn) { 91 | SDL_SetLogOutputFunction(prev_log_output_fn, prev_log_output_userdata); 92 | prev_log_output_fn = NULL; 93 | prev_log_output_userdata = NULL; 94 | } 95 | if (overlay_texture != NULL) { 96 | SDL_DestroyTexture(overlay_texture); 97 | overlay_texture = NULL; 98 | } 99 | // Destroy synchronization primitive 100 | if (log_mutex) { 101 | SDL_DestroyMutex(log_mutex); 102 | log_mutex = NULL; 103 | } 104 | overlay_needs_redraw = 1; 105 | } 106 | 107 | void log_overlay_render(SDL_Renderer *renderer, int logical_texture_width, 108 | int logical_texture_height, SDL_ScaleMode scale_mode, 109 | int font_mode_current) { 110 | if (!overlay_visible) { 111 | return; 112 | } 113 | if (overlay_texture == NULL) { 114 | overlay_texture = 115 | SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 116 | logical_texture_width, logical_texture_height); 117 | if (overlay_texture == NULL) { 118 | SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Couldn't create log texture: %s", SDL_GetError()); 119 | return; 120 | } 121 | SDL_SetTextureBlendMode(overlay_texture, SDL_BLENDMODE_BLEND); 122 | SDL_SetTextureScaleMode(overlay_texture, scale_mode); 123 | } 124 | 125 | // Only update the overlay texture when its contents changed. 126 | if (overlay_needs_redraw) { 127 | overlay_needs_redraw = 0; 128 | 129 | // Take a snapshot of the log ring buffer so we can render without holding the mutex. 130 | // This prevents data races and avoids deadlocks if rendering logs internally. 131 | int local_count = 0; 132 | int local_start = 0; 133 | if (log_mutex) 134 | SDL_LockMutex(log_mutex); 135 | local_count = log_line_count; 136 | local_start = log_line_start; 137 | 138 | // Snapshot holds copies of each visible line in chronological order. 139 | char(*snapshot)[LOG_LINE_MAX_CHARS] = NULL; 140 | if (local_count > 0) { 141 | snapshot = (char(*)[LOG_LINE_MAX_CHARS])SDL_calloc((size_t)local_count, sizeof(log_lines[0])); 142 | } 143 | if (snapshot) { 144 | for (int i = 0; i < local_count; i++) { 145 | const int idx = (local_start + i) % LOG_BUFFER_MAX_LINES; 146 | SDL_strlcpy(snapshot[i], log_lines[idx], LOG_LINE_MAX_CHARS); 147 | } 148 | } 149 | if (log_mutex) 150 | SDL_UnlockMutex(log_mutex); 151 | 152 | // Bind the overlay texture as the render target and clear it with a translucent background. 153 | SDL_Texture *prev_target = SDL_GetRenderTarget(renderer); 154 | if (!SDL_SetRenderTarget(renderer, overlay_texture)) { 155 | SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to set render target: %s", SDL_GetError()); 156 | } 157 | 158 | SDL_SetRenderDrawColor(renderer, 0, 0, 0, 220); 159 | SDL_RenderClear(renderer); 160 | 161 | // Switch to a small font for the overlay; remember previous mode to restore later. 162 | const int prev_font_mode = font_mode_current; 163 | inline_font_close(); 164 | const struct inline_font *font_small = fonts_get(0); 165 | if (font_small) { 166 | inline_font_initialize(font_small); 167 | 168 | // Layout calculations: 169 | // - glyph_x/y = character cell size in pixels. 170 | // - margin_x/y = inner padding around the overlay. 171 | // - cols = how many characters fit per line (accounting for a 1px inter-glyph gap). 172 | const int line_height = font_small->glyph_y + 1; 173 | const int margin_x = 2; 174 | const int margin_y = 1; 175 | const int usable_width = logical_texture_width - (margin_x * 2); 176 | const int cols = SDL_max(1, usable_width / (font_small->glyph_x + 1)); 177 | 178 | // Determine how many text rows fit on the screen vertically. 179 | const int max_rows = (logical_texture_height - margin_y * 2) / line_height; 180 | 181 | // We want to show the newest content; walk backwards over the snapshot to find 182 | // which line (and intra-line character offset) should be the first visible row, 183 | // so that the last max_rows rows are visible. 184 | int rows_needed = max_rows; 185 | int start_idx = 0; // index in snapshot[] to start drawing from 186 | size_t start_char_offset = 0; // per-line character offset (for wrapped lines) 187 | 188 | if (local_count > 0) { 189 | for (int n = local_count - 1; n >= 0 && rows_needed > 0; n--) { 190 | if (!snapshot) break; // nullptr safety 191 | const size_t len = SDL_strlen(snapshot[n]); 192 | // How many wrapped rows this line consumes 193 | const int rows_for_line = SDL_max(1, (int)((len + cols - 1) / cols)); 194 | if (rows_for_line >= rows_needed) { 195 | // This line provides the first visible portion. 196 | // Compute which character to start from so we only draw the last rows_needed rows. 197 | const int offset = SDL_max(0, (int)len - rows_needed * cols); 198 | start_idx = n; 199 | start_char_offset = (size_t)offset; 200 | break; 201 | } 202 | // Not enough rows on this line, include it fully and continue upwards. 203 | rows_needed -= rows_for_line; 204 | start_idx = n; 205 | start_char_offset = 0; 206 | } 207 | } 208 | 209 | // Render loop: 210 | // - Iterate from start_idx to the newest item (end of snapshot). 211 | // - For each line, draw it in chunks of `cols` characters (word-wrap by fixed width). 212 | // - Stop when we run out of vertical space. 213 | if (local_count > 0) { 214 | int y = margin_y; 215 | size_t offset = start_char_offset; 216 | 217 | for (int cur = start_idx; cur < local_count && y < logical_texture_height; cur++) { 218 | if (!snapshot || cur < 0 || cur >= local_count) { 219 | break; 220 | } 221 | 222 | const char *s = snapshot[cur]; 223 | const size_t len = SDL_strlen(s); 224 | 225 | for (size_t pos = offset; pos < len && y < logical_texture_height;) { 226 | const Uint32 fg = 0xFFFFFF; // draw text in white 227 | const size_t remaining = len - pos; 228 | 229 | // Take up to `cols` characters for this visual row 230 | size_t take = (size_t)cols < remaining ? (size_t)cols : remaining; 231 | 232 | // Copy the slice into a temporary buffer for printing 233 | char buf[LOG_LINE_MAX_CHARS]; 234 | if (take >= sizeof(buf)) { 235 | take = sizeof(buf) - 1; 236 | } 237 | SDL_memcpy(buf, s + pos, take); 238 | buf[take] = '\0'; 239 | 240 | // Draw the row and advance one line vertically 241 | inprint(renderer, buf, margin_x, y, fg, fg); 242 | y += line_height; 243 | pos += take; 244 | } 245 | 246 | // After the first (possibly partial) slice of this line, subsequent lines start at 0 247 | offset = 0; 248 | } 249 | } 250 | } else { 251 | SDL_LogError(SDL_LOG_CATEGORY_RENDER, "fonts_get(0) returned NULL"); 252 | } 253 | 254 | // Restore previous font mode and previous render target. 255 | inline_font_close(); 256 | inline_font_initialize(fonts_get(prev_font_mode)); 257 | SDL_SetRenderTarget(renderer, prev_target); 258 | 259 | // Free the snapshot after rendering. 260 | if (snapshot) { 261 | SDL_free(snapshot); 262 | } 263 | } 264 | 265 | // Composite the overlay texture to the current render target every frame while visible. 266 | if (overlay_texture) { 267 | if (!SDL_RenderTexture(renderer, overlay_texture, NULL, NULL)) { 268 | SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Couldn't render log overlay texture: %s", 269 | SDL_GetError()); 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/backends/audio_libusb.c: -------------------------------------------------------------------------------- 1 | #include "SDL3/SDL_audio.h" 2 | #include "SDL3/SDL_error.h" 3 | #ifdef USE_LIBUSB 4 | 5 | #include "m8.h" 6 | #include "ringbuffer.h" 7 | #include 8 | #include 9 | #include 10 | 11 | #define EP_ISO_IN 0x85 12 | #define IFACE_NUM 4 13 | 14 | #define NUM_TRANSFERS 64 15 | #define PACKET_SIZE 180 16 | #define NUM_PACKETS 2 17 | 18 | extern libusb_device_handle *devh; 19 | 20 | SDL_AudioStream *sdl_audio_stream = NULL; 21 | int audio_initialized = 0; 22 | RingBuffer *audio_buffer = NULL; 23 | static uint8_t *audio_callback_buffer = NULL; 24 | static size_t audio_callback_buffer_size = 0; 25 | static int audio_prebuffer_filled = 0; 26 | #define PREBUFFER_SIZE (8 * 1024) // Wait for 8KB before starting playback 27 | 28 | static void audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) { 29 | (void)userdata; // Suppress unused parameter warning 30 | (void)additional_amount; // Suppress unused parameter warning 31 | 32 | // Reallocate callback buffer if needed 33 | if (audio_callback_buffer_size < (size_t)total_amount) { 34 | audio_callback_buffer = SDL_realloc(audio_callback_buffer, total_amount); 35 | if (audio_callback_buffer == NULL) { 36 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to allocate audio buffer"); 37 | return; 38 | } 39 | audio_callback_buffer_size = (size_t)total_amount; 40 | } 41 | 42 | // Try to get audio data from ring buffer 43 | uint32_t available_bytes = audio_buffer->size; 44 | 45 | // Check if we have enough data for initial buffering 46 | if (!audio_prebuffer_filled && available_bytes < PREBUFFER_SIZE) { 47 | // Not enough data yet, output silence and wait 48 | SDL_memset(audio_callback_buffer, 0, total_amount); 49 | if(!SDL_PutAudioStreamData(stream, audio_callback_buffer, total_amount)) { 50 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to put audio stream data: %s", SDL_GetError()); 51 | } 52 | return; 53 | } 54 | 55 | // Mark prebuffer as filled once we have enough data 56 | if (!audio_prebuffer_filled) { 57 | audio_prebuffer_filled = 1; 58 | SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Audio prebuffer filled, starting playback"); 59 | } 60 | 61 | if (available_bytes >= (uint32_t)total_amount) { 62 | // We have enough data, read it 63 | uint32_t read_len = ring_buffer_pop(audio_buffer, audio_callback_buffer, total_amount); 64 | if (read_len > 0) { 65 | if(!SDL_PutAudioStreamData(stream, audio_callback_buffer, read_len)) { 66 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to put audio stream data: %s", SDL_GetError()); 67 | } 68 | } 69 | } else if (available_bytes > 0) { 70 | // We have some data but not enough - read what we can and pad with silence 71 | uint32_t read_len = ring_buffer_pop(audio_buffer, audio_callback_buffer, available_bytes); 72 | SDL_memset(audio_callback_buffer + read_len, 0, total_amount - read_len); 73 | if(!SDL_PutAudioStreamData(stream, audio_callback_buffer, total_amount)) { 74 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to put audio stream data: %s", SDL_GetError()); 75 | } 76 | SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Partial buffer: %d/%d bytes", available_bytes, total_amount); 77 | } else { 78 | // No data available - put silence and reset prebuffer flag 79 | SDL_memset(audio_callback_buffer, 0, total_amount); 80 | if(!SDL_PutAudioStreamData(stream, audio_callback_buffer, total_amount)) { 81 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to put audio stream data: %s", SDL_GetError()); 82 | } 83 | audio_prebuffer_filled = 0; // Reset prebuffer to avoid continuous dropouts 84 | SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Buffer underflow! Resetting prebuffer"); 85 | } 86 | } 87 | 88 | static void cb_xfr(struct libusb_transfer *xfr) { 89 | unsigned int i; 90 | static int error_count = 0; 91 | 92 | for (i = 0; i < (unsigned int)xfr->num_iso_packets; i++) { 93 | struct libusb_iso_packet_descriptor *pack = &xfr->iso_packet_desc[i]; 94 | 95 | if (pack->status != LIBUSB_TRANSFER_COMPLETED) { 96 | error_count++; 97 | if (error_count % 100 == 1) { // Log only every 100th error to avoid spam 98 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "XFR callback error (status %d: %s)", pack->status, 99 | libusb_error_name(pack->status)); 100 | } 101 | continue; // Skip this packet but continue processing others 102 | } 103 | 104 | if (pack->actual_length > 0) { 105 | const uint8_t *data = libusb_get_iso_packet_buffer_simple(xfr, i); 106 | if (sdl_audio_stream != 0 && audio_buffer != NULL) { 107 | uint32_t actual = ring_buffer_push(audio_buffer, data, pack->actual_length); 108 | if (actual == (uint32_t)-1) { 109 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Buffer overflow!"); 110 | } 111 | } 112 | } 113 | } 114 | 115 | // Reset error count on successful transfer 116 | if (xfr->status == LIBUSB_TRANSFER_COMPLETED) { 117 | error_count = 0; 118 | } 119 | 120 | int submit_result = libusb_submit_transfer(xfr); 121 | if (submit_result < 0) { 122 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "error re-submitting URB: %s", libusb_error_name(submit_result)); 123 | SDL_free(xfr->buffer); 124 | } 125 | } 126 | 127 | static struct libusb_transfer *xfr[NUM_TRANSFERS]; 128 | 129 | static int benchmark_in() { 130 | int i; 131 | 132 | for (i = 0; i < NUM_TRANSFERS; i++) { 133 | xfr[i] = libusb_alloc_transfer(NUM_PACKETS); 134 | if (!xfr[i]) { 135 | SDL_Log("Could not allocate transfer"); 136 | return -ENOMEM; 137 | } 138 | 139 | Uint8 *buffer = SDL_malloc(PACKET_SIZE * NUM_PACKETS); 140 | 141 | libusb_fill_iso_transfer(xfr[i], devh, EP_ISO_IN, buffer, PACKET_SIZE * NUM_PACKETS, 142 | NUM_PACKETS, cb_xfr, NULL, 0); 143 | libusb_set_iso_packet_lengths(xfr[i], PACKET_SIZE); 144 | 145 | libusb_submit_transfer(xfr[i]); 146 | } 147 | 148 | return 1; 149 | } 150 | 151 | int audio_initialize(const char *output_device_name, unsigned int audio_buffer_size) { 152 | (void)audio_buffer_size; // Suppress unused parameter warning 153 | 154 | SDL_Log("USB audio setup"); 155 | 156 | if (devh == NULL) { 157 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Device handle is NULL - cannot initialize audio"); 158 | return -1; 159 | } 160 | 161 | int rc; 162 | 163 | rc = libusb_kernel_driver_active(devh, IFACE_NUM); 164 | if (rc < 0) { 165 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error checking kernel driver status: %s", libusb_error_name(rc)); 166 | return rc; 167 | } 168 | if (rc == 1) { 169 | SDL_Log("Detaching kernel driver"); 170 | rc = libusb_detach_kernel_driver(devh, IFACE_NUM); 171 | if (rc < 0) { 172 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Could not detach kernel driver: %s", libusb_error_name(rc)); 173 | return rc; 174 | } 175 | } 176 | 177 | rc = libusb_claim_interface(devh, IFACE_NUM); 178 | if (rc < 0) { 179 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error claiming interface: %s\n", libusb_error_name(rc)); 180 | return rc; 181 | } 182 | 183 | rc = libusb_set_interface_alt_setting(devh, IFACE_NUM, 1); 184 | if (rc < 0) { 185 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error setting alt setting: %s\n", libusb_error_name(rc)); 186 | return rc; 187 | } 188 | 189 | if (!SDL_WasInit(SDL_INIT_AUDIO)) { 190 | if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { 191 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Init audio failed %s", SDL_GetError()); 192 | return -1; 193 | } 194 | } else { 195 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Audio was already initialised"); 196 | } 197 | 198 | static SDL_AudioSpec audio_spec; 199 | audio_spec.format = SDL_AUDIO_S16; 200 | audio_spec.channels = 2; 201 | audio_spec.freq = 44100; 202 | 203 | SDL_Log("Current audio driver is %s and device %s", SDL_GetCurrentAudioDriver(), 204 | output_device_name); 205 | 206 | // Create larger ring buffer for stable audio - about 1.5 seconds at 44.1kHz stereo 16-bit 207 | audio_buffer = ring_buffer_create(256 * 1024); 208 | 209 | if (SDL_strcasecmp(SDL_GetCurrentAudioDriver(), "openslES") == 0 || output_device_name == NULL) { 210 | SDL_Log("Using default audio device"); 211 | sdl_audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec, &audio_callback, &audio_buffer); 212 | } else { 213 | // TODO: Implement audio device selection 214 | sdl_audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec, &audio_callback, &audio_buffer); 215 | } 216 | 217 | if (sdl_audio_stream == NULL) { 218 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to open audio stream: %s", SDL_GetError()); 219 | ring_buffer_free(audio_buffer); 220 | return -1; 221 | } 222 | 223 | SDL_ResumeAudioStreamDevice(sdl_audio_stream); 224 | 225 | // Good to go 226 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Starting capture"); 227 | if ((rc = benchmark_in()) < 0) { 228 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Capture failed to start: %d", rc); 229 | return rc; 230 | } 231 | 232 | audio_initialized = 1; 233 | audio_prebuffer_filled = 0; // Reset prebuffer state 234 | SDL_Log("Successful init"); 235 | return 1; 236 | 237 | } 238 | 239 | void audio_close() { 240 | if (devh == NULL) { 241 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "Device handle is NULL - audio already closed or not initialized"); 242 | return; 243 | } 244 | if (!audio_initialized) { 245 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "Audio not initialized - nothing to close"); 246 | return; 247 | } 248 | 249 | SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "Closing audio"); 250 | 251 | int rc; 252 | 253 | for (int i = 0; i < NUM_TRANSFERS; i++) { 254 | rc = libusb_cancel_transfer(xfr[i]); 255 | if (rc < 0) { 256 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error cancelling transfer: %s\n", 257 | libusb_error_name(rc)); 258 | } 259 | } 260 | 261 | SDL_Log("Freeing interface %d", IFACE_NUM); 262 | 263 | rc = libusb_release_interface(devh, IFACE_NUM); 264 | if (rc < 0) { 265 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error releasing interface: %s\n", libusb_error_name(rc)); 266 | return; 267 | } 268 | 269 | if (sdl_audio_stream != NULL) { 270 | SDL_Log("Closing audio device"); 271 | SDL_DestroyAudioStream(sdl_audio_stream); 272 | sdl_audio_stream = 0; 273 | } 274 | 275 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Audio closed"); 276 | 277 | ring_buffer_free(audio_buffer); 278 | 279 | // Free callback buffer 280 | if (audio_callback_buffer) { 281 | SDL_free(audio_callback_buffer); 282 | audio_callback_buffer = NULL; 283 | audio_callback_buffer_size = 0; 284 | } 285 | 286 | audio_initialized = 0; 287 | audio_prebuffer_filled = 0; 288 | } 289 | 290 | void audio_toggle(const char *output_device_name, unsigned int audio_buffer_size) { 291 | (void)output_device_name; // Suppress unused parameter warning 292 | (void)audio_buffer_size; // Suppress unused parameter warning 293 | SDL_Log("Libusb audio toggling not implemented yet"); 294 | } 295 | 296 | #endif 297 | -------------------------------------------------------------------------------- /src/fonts/font5.h: -------------------------------------------------------------------------------- 1 | #ifndef FONT5_H_ 2 | #define FONT5_H_ 3 | 4 | #include "fonts.h" 5 | 6 | struct inline_font font_v2_huge = { 7 | 1128, 8 | 12, 9 | 12, 10 | 12, 11 | 0, 12 | -54, 13 | 4, 14 | 24, 15 | 1874, 16 | { 17 | 0x42, 0x4D, 0x52, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x7C, 18 | 0x00, 0x00, 0x00, 0x68, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0xC0, 0x06, 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, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 27 | 0xFF, 0x9F, 0xFF, 0xFC, 0x1C, 0xFF, 0xFF, 0xF3, 0xE7, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 28 | 0xF9, 0xFF, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0xFF, 29 | 0xC0, 0x00, 0xFF, 0xCF, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xF8, 0x03, 0x3F, 30 | 0xC0, 0x01, 0x80, 0x00, 0x01, 0x80, 0x03, 0xFF, 0x80, 0x13, 0xFC, 0x00, 0x00, 0x01, 0x3F, 31 | 0xC8, 0x00, 0x3F, 0xC3, 0xFC, 0x80, 0x13, 0xFF, 0x80, 0xC3, 0xFC, 0x00, 0x1F, 0x9F, 0x80, 32 | 0x1F, 0x9F, 0x3F, 0xC3, 0xFC, 0x00, 0x10, 0x00, 0xC0, 0xFF, 0xFC, 0xE0, 0x7F, 0xFF, 0x00, 33 | 0x0F, 0xFF, 0x80, 0x00, 0x01, 0x80, 0x08, 0x00, 0x80, 0x0C, 0xFF, 0x00, 0x13, 0xFC, 0x00, 34 | 0x00, 0x01, 0x3F, 0x80, 0x00, 0x39, 0xC3, 0xFC, 0x80, 0x13, 0xFF, 0xFF, 0xC3, 0xFF, 0x00, 35 | 0x1E, 0x00, 0x80, 0x10, 0xFF, 0x80, 0x03, 0xFC, 0x00, 0x10, 0x00, 0x00, 0x03, 0xFF, 0x00, 36 | 0x0F, 0xFF, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xE6, 0x70, 0x01, 0x9F, 0x18, 0x08, 0xFF, 37 | 0xFF, 0xE3, 0xE3, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xF9, 0xFF, 0x1F, 0xF0, 0x00, 0x00, 38 | 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0xFF, 0xCF, 0xFF, 0x8F, 39 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xF0, 0x03, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 40 | 0x03, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x3F, 0xC3, 0xF8, 0x00, 41 | 0x03, 0xFF, 0x01, 0x83, 0xF8, 0x00, 0x0F, 0x9F, 0x00, 0x0F, 0x0F, 0x1F, 0x81, 0xF8, 0x00, 42 | 0x00, 0x00, 0xC0, 0xFF, 0xF8, 0xE0, 0x7F, 0xFF, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x00, 0x00, 43 | 0x00, 0x00, 0x00, 0x0C, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x3F, 0x10, 0x00, 0x39, 44 | 0xC3, 0xFC, 0x00, 0x03, 0xFF, 0xFF, 0xC3, 0xFF, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x7F, 0x00, 45 | 0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x00, 0xFF, 46 | 0xFF, 0xFF, 0xE6, 0x70, 0x00, 0x8F, 0x11, 0xC1, 0xFF, 0xFF, 0xC7, 0xF1, 0xFF, 0xFF, 0xF9, 47 | 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF3, 0xFC, 0xF9, 0xF3, 0xFF, 0xFF, 0xCF, 0xFC, 0xFF, 48 | 0xC3, 0xFC, 0xFF, 0xC3, 0xFC, 0xFF, 0xCF, 0x9F, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 49 | 0xF3, 0xFF, 0x3F, 0xC3, 0xFC, 0x3F, 0xF3, 0xFC, 0x3F, 0xF3, 0xFF, 0x3F, 0xC3, 0xFC, 0xF9, 50 | 0xFF, 0xFC, 0x3F, 0x13, 0xFF, 0x3F, 0xC3, 0xF0, 0x3F, 0xC3, 0xFF, 0x3F, 0x03, 0xF1, 0xFF, 51 | 0xCF, 0x9F, 0x3F, 0xCE, 0x07, 0x0F, 0x08, 0xF1, 0xFF, 0xC8, 0xFF, 0xCF, 0xFF, 0xF1, 0xFE, 52 | 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC3, 0xFC, 0x3F, 0xF3, 0xFC, 0x3F, 0xFC, 0xFF, 0xFF, 53 | 0xC3, 0xFC, 0xF9, 0xFF, 0xFC, 0x3E, 0x3F, 0x9F, 0x39, 0xC3, 0xFC, 0x3F, 0xC3, 0xFF, 0xFF, 54 | 0xC3, 0xFF, 0xFF, 0xCC, 0xFF, 0x3F, 0xC2, 0x3F, 0x39, 0xC8, 0xF1, 0xFF, 0xCC, 0x7F, 0x3F, 55 | 0xC3, 0xFF, 0x3F, 0xCF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x80, 0x1F, 0x9C, 0xC7, 56 | 0x13, 0xE3, 0xFF, 0xFF, 0xCF, 0xF9, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 57 | 0xF3, 0xFC, 0xF9, 0xF3, 0xFF, 0xFF, 0xCF, 0xFC, 0xFF, 0xC3, 0xFC, 0xFF, 0xC3, 0xFC, 0xFF, 58 | 0xCF, 0x9F, 0xCF, 0xFF, 0xCF, 0xC0, 0x3F, 0x3F, 0xFF, 0xF3, 0xFF, 0x3F, 0xC3, 0xFC, 0x3F, 59 | 0xF3, 0xFC, 0x3F, 0xF3, 0xFF, 0x3F, 0xC3, 0xFC, 0xF9, 0xFF, 0xFC, 0x3E, 0x33, 0xFF, 0x3F, 60 | 0xC3, 0xE0, 0x3F, 0xC3, 0xFF, 0x3E, 0x03, 0xE3, 0xFF, 0xCF, 0x9F, 0x3F, 0xCC, 0x63, 0x06, 61 | 0x0C, 0x63, 0xFF, 0xCC, 0x7F, 0xCF, 0xFF, 0xE3, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 62 | 0xC3, 0xFC, 0x3F, 0xF3, 0xFC, 0x3F, 0xFC, 0xFF, 0xFF, 0xC3, 0xFC, 0xF9, 0xFF, 0xFC, 0x3C, 63 | 0x7F, 0x9F, 0x39, 0xC3, 0xFC, 0x3F, 0xC0, 0x01, 0x80, 0x03, 0xFF, 0xFF, 0xCC, 0xFF, 0x3F, 64 | 0xC3, 0x1F, 0x39, 0xCC, 0x63, 0xFF, 0xCE, 0x3F, 0x3F, 0xC3, 0xFF, 0x3F, 0xCF, 0xFF, 0x00, 65 | 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0x80, 0x1F, 0x9C, 0xE3, 0xF3, 0xC1, 0xFF, 0xFF, 0xCF, 0xF9, 66 | 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xF3, 0xFC, 0xF9, 0xF3, 0xFF, 0xFF, 67 | 0xCF, 0xFC, 0xFF, 0xC3, 0xFC, 0xFF, 0xC3, 0xFC, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0x8F, 0xC0, 68 | 0x3F, 0x1F, 0xF9, 0xF3, 0x80, 0x3F, 0xC3, 0xFC, 0x3F, 0xF3, 0xFC, 0x3F, 0xF3, 0xFF, 0x3F, 69 | 0xC3, 0xFC, 0xF9, 0xFF, 0xFC, 0x3C, 0x73, 0xFF, 0x3F, 0xC3, 0xC4, 0x3F, 0xC3, 0xFF, 0x3C, 70 | 0x43, 0xC7, 0xFF, 0xCF, 0x9F, 0x3F, 0xC8, 0xF1, 0x20, 0x4E, 0x07, 0xFF, 0xCE, 0x3F, 0xCF, 71 | 0xFF, 0xC7, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0xFC, 0x3F, 0xF3, 0xFC, 0x00, 72 | 0x00, 0x03, 0x80, 0x03, 0xFC, 0xF9, 0xFF, 0xFC, 0x00, 0xFF, 0x9F, 0x39, 0xC3, 0xFC, 0x3F, 73 | 0xC0, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x0C, 0xFF, 0x3F, 0xC3, 0x8F, 0x39, 0xCE, 0x07, 0x80, 74 | 0x0F, 0x1F, 0x3F, 0xC3, 0xFF, 0x3F, 0xC2, 0x3F, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xE6, 75 | 0x78, 0x00, 0xF1, 0xF1, 0x88, 0xFF, 0xFF, 0xCF, 0xF9, 0xFF, 0xFF, 0xC0, 0x3F, 0xFF, 0xC0, 76 | 0x3F, 0xFF, 0xF1, 0xF3, 0x9C, 0xF9, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 77 | 0xC0, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0x0F, 0xF8, 0x13, 0x80, 0x00, 78 | 0x00, 0x00, 0x3F, 0xF3, 0xFC, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0xF9, 0xFF, 0xFC, 0x00, 79 | 0xF3, 0xFF, 0x39, 0xC3, 0x8C, 0x3F, 0xC0, 0x01, 0x3C, 0xC0, 0x01, 0x80, 0x0F, 0x9F, 0x3F, 80 | 0xC1, 0xF8, 0x30, 0xCF, 0x0F, 0x80, 0x0F, 0x1F, 0xCF, 0xFF, 0x8F, 0xFE, 0x7F, 0xFF, 0xFF, 81 | 0xFF, 0xFF, 0x00, 0x03, 0xFC, 0x3F, 0xF3, 0xFC, 0x00, 0x00, 0x03, 0x00, 0x03, 0xFC, 0xF9, 82 | 0xFF, 0xFC, 0x01, 0xFF, 0x9F, 0x39, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFF, 0x00, 83 | 0x1C, 0xFF, 0x3F, 0xC3, 0xC7, 0x39, 0xCE, 0x07, 0x00, 0x0F, 0x8F, 0x3F, 0xC3, 0xFF, 0x3F, 84 | 0xC0, 0x1F, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xE6, 0x70, 0x01, 0xF8, 0xF8, 0x1C, 0xFF, 85 | 0xFF, 0xCF, 0xF9, 0xF3, 0x9F, 0xC0, 0x3F, 0xFF, 0xC0, 0x3F, 0xFF, 0xF8, 0xF3, 0x9C, 0xF9, 86 | 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00, 0x0F, 0xFF, 0xCF, 87 | 0xFE, 0x0F, 0xFF, 0xFF, 0x07, 0xFC, 0x03, 0x9C, 0x00, 0x00, 0x03, 0x3F, 0xF3, 0xFC, 0x00, 88 | 0x00, 0x00, 0x30, 0x00, 0x00, 0xF9, 0xFF, 0xFC, 0x00, 0xF3, 0xFF, 0x30, 0xC3, 0x1C, 0x3F, 89 | 0xC0, 0x00, 0x3F, 0xC0, 0x00, 0x00, 0x1F, 0x9F, 0x3F, 0xC3, 0xFC, 0x39, 0xCF, 0x0F, 0x00, 90 | 0x0F, 0x8F, 0xCF, 0xFF, 0x1F, 0xFE, 0x79, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFC, 0x3F, 91 | 0xF3, 0xFC, 0x3F, 0xCC, 0xFF, 0x3F, 0xC3, 0xFC, 0xF9, 0xFF, 0xFC, 0x38, 0xFF, 0x9F, 0x39, 92 | 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFF, 0x3F, 0xFC, 0xFF, 0x3F, 0xC3, 0xE3, 0x39, 93 | 0xCC, 0x63, 0x3F, 0xCF, 0xC7, 0x3F, 0xC3, 0xFF, 0x3F, 0xC8, 0x9F, 0x00, 0x00, 0x00, 0x3F, 94 | 0xFF, 0xFF, 0x80, 0x13, 0x9F, 0xFC, 0x7E, 0x0F, 0xFF, 0xFF, 0xCF, 0xF9, 0xF1, 0x1F, 0xF9, 95 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x73, 0xFC, 0xF9, 0xFF, 0xFC, 0xFF, 0xC3, 0xFC, 0x3F, 96 | 0xF3, 0xFF, 0xFF, 0xC3, 0xFC, 0x3F, 0xCF, 0xFF, 0xCF, 0xFF, 0x0F, 0xC0, 0x3F, 0x0F, 0xFF, 97 | 0xC3, 0x9C, 0x3F, 0xC3, 0xF9, 0x3F, 0xF3, 0xFC, 0x3F, 0xF3, 0xFF, 0x3F, 0xF3, 0xFC, 0xF9, 98 | 0xFF, 0xFC, 0x3C, 0x73, 0xFF, 0x20, 0x42, 0x3C, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 99 | 0xFF, 0x9F, 0x3F, 0xC3, 0xFC, 0x3F, 0xCE, 0x07, 0x3F, 0xCF, 0xC7, 0xCF, 0xFE, 0x3F, 0xFE, 100 | 0x78, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFC, 0x3F, 0xF3, 0xFC, 0x3F, 0xCC, 0xFF, 0x3F, 101 | 0xC0, 0x00, 0xF9, 0xFF, 0xFC, 0x3C, 0x7F, 0x9F, 0x39, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 102 | 0xC3, 0xFF, 0x3F, 0xFC, 0xFF, 0x3F, 0xC3, 0xF1, 0x39, 0xC8, 0xF1, 0x3F, 0xCF, 0xE3, 0x3F, 103 | 0xC3, 0xFF, 0x3F, 0xCF, 0xFF, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0x80, 0x13, 0x9F, 0x8E, 104 | 0x3C, 0xE7, 0xFF, 0xFF, 0xCF, 0xF9, 0xF8, 0x3F, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 105 | 0x33, 0xFC, 0xF9, 0xFF, 0xFC, 0xFF, 0xC3, 0xFC, 0x3F, 0xF3, 0xFF, 0xFF, 0xC3, 0xFC, 0x3F, 106 | 0xCF, 0x9F, 0xFF, 0xFF, 0x8F, 0xC0, 0x3F, 0x1F, 0xFF, 0xC3, 0x9C, 0x3F, 0xC3, 0xF9, 0x3F, 107 | 0xF3, 0xFC, 0x3F, 0xF3, 0xFF, 0x3F, 0xF3, 0xFC, 0xF9, 0xFF, 0xFC, 0x3E, 0x33, 0xFF, 0x06, 108 | 0x00, 0x7C, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 0xFF, 0x9F, 0x3F, 0xC3, 0xFC, 0x3F, 109 | 0xCC, 0x63, 0x3F, 0xCF, 0xE3, 0xCF, 0xFC, 0x7F, 0xFE, 0x7C, 0x63, 0xFF, 0xFF, 0xFF, 0x00, 110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0x00, 0x00, 0x01, 0xE1, 0xFF, 0xF0, 0x3E, 111 | 0x3F, 0x9F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 112 | 0xC3, 0xF8, 0x39, 0xC1, 0xF8, 0x3F, 0xC0, 0x00, 0x3F, 0xC3, 0xFF, 0x3F, 0xCF, 0xFF, 0x00, 113 | 0x00, 0x00, 0x3F, 0xF3, 0x3F, 0xE6, 0x70, 0x00, 0x8F, 0x1C, 0xE7, 0x3F, 0xFF, 0xC7, 0xF1, 114 | 0xF8, 0x3F, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x13, 0xFC, 0xF9, 0xFF, 0xFC, 0xFF, 115 | 0xC3, 0xFC, 0x3F, 0xF3, 0xFF, 0xFF, 0xC3, 0xFC, 0x3F, 0xCF, 0x9F, 0xFF, 0xFF, 0xCF, 0xFF, 116 | 0xFF, 0x3F, 0x3F, 0xC3, 0x9C, 0x3F, 0xC3, 0xF9, 0x3F, 0xF3, 0xF8, 0x3F, 0xF3, 0xFF, 0x3F, 117 | 0xF3, 0xFC, 0xF9, 0xFF, 0xFC, 0x3F, 0x13, 0xFF, 0x0F, 0x00, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 118 | 0xC3, 0xFC, 0x3F, 0xFF, 0x9F, 0x3F, 0xC3, 0xFC, 0x3F, 0xC8, 0xF1, 0x3F, 0xCF, 0xF1, 0xCF, 119 | 0xF8, 0xFF, 0xFE, 0x7E, 0x07, 0xFF, 0xFC, 0xFF, 0x00, 0x10, 0x01, 0x80, 0x08, 0x00, 0x80, 120 | 0x1C, 0xFF, 0x80, 0x03, 0xFF, 0xE1, 0xFF, 0xF0, 0x3F, 0x1F, 0x9F, 0x00, 0x10, 0x01, 0x80, 121 | 0x10, 0x01, 0x80, 0x08, 0x00, 0x80, 0x00, 0x00, 0x3F, 0xC3, 0xFC, 0x39, 0xC3, 0xFC, 0x3F, 122 | 0xC0, 0x00, 0x3F, 0xC3, 0xFF, 0x3F, 0xCF, 0xFF, 0x00, 0x00, 0x00, 0x3F, 0xF3, 0x3F, 0xE6, 123 | 0x78, 0x00, 0x8F, 0x9C, 0x07, 0x3F, 0xFF, 0xE3, 0xE3, 0xF1, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 124 | 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x00, 125 | 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 126 | 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x0F, 0xFC, 0x3F, 127 | 0x83, 0xFF, 0x1F, 0x81, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 128 | 0xC3, 0xFC, 0x3F, 0xC1, 0xF8, 0x3F, 0xC0, 0x00, 0xC0, 0xF1, 0xFF, 0xE0, 0x7F, 0x0F, 0xFF, 129 | 0xF8, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFC, 0x00, 0xFF, 0xF3, 0xFF, 0xFF, 130 | 0xFF, 0xFF, 0x3F, 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 131 | 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0x00, 132 | 0x0F, 0xFF, 0x00, 0x00, 0x00, 0x3F, 0xF3, 0x3F, 0xFF, 0xFF, 0x9F, 0xFF, 0xFE, 0x0F, 0x3F, 133 | 0xFF, 0xF3, 0xE7, 0xF3, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x01, 134 | 0xF0, 0x00, 0x00, 0x03, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 135 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x18, 0x01, 0x80, 0x10, 0x03, 0x80, 0x00, 0x03, 0x80, 136 | 0x08, 0x00, 0x80, 0x03, 0xFC, 0x00, 0x0F, 0xFC, 0x3F, 0xC3, 0xFF, 0x3F, 0xC3, 0xFC, 0x80, 137 | 0x10, 0x01, 0x80, 0x10, 0x01, 0x80, 0x00, 0x00, 0x3F, 0xC3, 0xFC, 0x3F, 0xC3, 0xFC, 0x3F, 138 | 0xC0, 0x00, 0xC0, 0xF3, 0xFF, 0xE0, 0x7F, 0x9F, 0xFF, 0xF9, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 139 | 0xFF, 0xFC, 0xFF, 0xFE, 0x00, 0xFF, 0xF3, 0xFF, 0xF9, 0xFF, 0xFC, 0x3F, 0xFE, 0x1F, 0xFF, 140 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 141 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0x00, 0x0F, 0xFF, 0x00, 0x00, 0x00, 142 | }}; 143 | #endif // FONT5_H_ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # m8c 2 | 3 | ## Introduction 4 | 5 | *m8c* is a remote display client for the Dirtywave M8 Tracker. It mirrors the M8’s display, enables 6 | keyboard/gamepad control, and can route audio to your computer—useful for recording, streaming, larger screens, 7 | or alternative input methods. The application is cross‑platform and can be built on Linux, Windows (MSYS2/MINGW64), and 8 | macOS. 9 | 10 | The Dirtywave M8 Tracker is a portable sequencer and synthesizer featuring eight tracks of assignable instruments 11 | (using engines like FM, waveform synthesis, virtual analog, sample playback, and MIDI output). 12 | It is inspired by the Game Boy tracker [Little Sound DJ](https://www.littlesounddj.com/lsd/index.php). 13 | 14 | m8c works with the M8 hardware over USB. It also supports 15 | the [M8 Headless](https://github.com/Dirtywave/M8HeadlessFirmware) 16 | firmware running on a [Teensy](https://www.pjrc.com/teensy/) microcontroller. If you enjoy the M8 and its 17 | tracker workflow, please support [Dirtywave](https://dirtywave.com/) by purchasing the hardware. You can check 18 | availability [here](https://dirtywave.com/products/m8-tracker-model-02). You can also 19 | support the creator, Trash80 via [Patreon](https://www.patreon.com/trash80). 20 | 21 | Many thanks to: 22 | 23 | * Trash80: For the great M8 hardware and the original fonts that were converted to a bitmap for use in the program. 24 | * driedfruit: For a wonderful little routine to blit inline bitmap 25 | fonts, [SDL_inprint](https://github.com/driedfruit/SDL_inprint/) 26 | * marcinbor85: For the slip handling routine, https://github.com/marcinbor85/slip 27 | * turbolent: For the great Golang-based g0m8 application, which I used as a reference on how the M8 serial protocol 28 | works. 29 | * *Everyone who's contributed to m8c!* 30 | 31 | ------- 32 | 33 | ## Installation 34 | 35 | ### Quick Start 36 | 37 | 1. Download the prebuilt binary for your platform from the [releases section](https://github.com/laamaa/m8c/releases/) 38 | 2. Connect your M8 or Teensy (with headless firmware) to your computer 39 | 3. Run the program—it should automatically detect your device 40 | 41 | ### Windows 42 | 43 | There are prebuilt binaries available in the [releases section](https://github.com/laamaa/m8c/releases/) for Windows. 44 | 45 | When running the program for the first time on Windows, Windows Defender may show a warning about an unrecognized app. 46 | Click "More info" and then "Run anyway" to proceed. 47 | 48 | ### macOS 49 | 50 | There are prebuilt binaries available in the [releases section](https://github.com/laamaa/m8c/releases/) for recent 51 | versions of macOS. 52 | 53 | ### Linux 54 | 55 | There are packages available for NixOS, an AppImage for easy installation, or you can build the program from source. 56 | 57 | #### AppImage 58 | 59 | An AppImage is available for Linux in the [releases section](https://github.com/laamaa/m8c/releases/). To use it: 60 | 61 | 1. Download the `.AppImage` file from the releases 62 | 2. Make it executable: `chmod +x m8c-*.AppImage` 63 | 3. Run it: `./m8c-*.AppImage` 64 | 65 | The AppImage is portable and doesn't require installation—it can be run directly from the file. 66 | 67 | #### NixOS 68 | 69 | ```sh 70 | nix-env -iA m8c -f https://github.com/laamaa/m8c/archive/refs/heads/main.tar.gz 71 | ``` 72 | 73 | Or if you're using flakes and the nix command, you can run the app directly with: 74 | 75 | ```sh 76 | nix run github:laamaa/m8c 77 | ``` 78 | 79 | ### Building from source code 80 | 81 | #### Install dependencies 82 | 83 | You will need git, gcc, pkg-config, make and the development headers for SDL3 and libserialport. 84 | 85 | ##### Linux (Apt/Ubuntu) 86 | 87 | For Ubuntu 25.04 and later, SDL3 packages are available in the official repositories: 88 | 89 | ```sh 90 | sudo apt update && sudo apt install -y git gcc pkg-config make libserialport-dev libsdl3-dev 91 | ``` 92 | 93 | For older Ubuntu versions, there is no official SDL3 package yet in the Ubuntu repositories. 94 | You'll likely need to build the library 95 | yourself. https://github.com/libsdl-org/SDL/blob/main/docs/README-cmake.md#building-sdl-on-unix 96 | 97 | ##### Windows (MSYS2/MINGW64) 98 | 99 | This assumes you have [MSYS2](https://www.msys2.org/) installed: 100 | 101 | ```sh 102 | pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkg-config mingw-w64-x86_64-make mingw-w64-x86_64-SDL3 mingw-w64-x86_64-libserialport 103 | ``` 104 | 105 | ##### macOS 106 | 107 | This assumes you have [installed brew](https://docs.brew.sh/Installation) 108 | 109 | ```sh 110 | brew update && brew install git gcc make sdl3 libserialport pkg-config 111 | ``` 112 | 113 | #### Download source code 114 | 115 | ```sh 116 | mkdir code && cd code 117 | git clone https://github.com/laamaa/m8c.git 118 | ``` 119 | 120 | #### Build the program 121 | 122 | ```sh 123 | cd m8c 124 | make 125 | ``` 126 | 127 | #### Start the program 128 | 129 | Connect the M8 or Teensy (with headless firmware) to your computer and start the program. It should automatically detect 130 | your device. 131 | 132 | ```sh 133 | ./m8c 134 | ``` 135 | 136 | If the stars are aligned correctly, you should see the M8 screen. 137 | 138 | #### Choosing a preferred device 139 | 140 | When you have multiple M8 devices connected, and you want to choose a specific one or launch m8c multiple times, you can 141 | get the list of devices by running 142 | 143 | ```sh 144 | ./m8c --list 145 | ``` 146 | 147 | Example output: 148 | 149 | ``` 150 | 2024-02-25 18:39:27.806 m8c[99838:4295527] INFO: Found M8 device: /dev/cu.usbmodem124709801 151 | 2024-02-25 18:39:27.807 m8c[99838:4295527] INFO: Found M8 device: /dev/cu.usbmodem121136001 152 | ``` 153 | 154 | You can specify the preferred device by using the `--dev` option: 155 | 156 | ```sh 157 | ./m8c --dev /dev/cu.usbmodem124709801 158 | ``` 159 | 160 | **Note:** The `--dev` option can force detection of any serial device by name. This is useful if libserialport cannot 161 | get the correct USB identifiers, like on some Windows 11 setups, for example. You may need to look up the correct device 162 | name from Device Manager, if `--list` does not give you any results, for example. 163 | 164 | ----------- 165 | 166 | ## Keyboard mappings 167 | 168 | Default keys for controlling the program: 169 | 170 | * ↑ = up 171 | * ↓ = down 172 | * ← = left 173 | * → = right 174 | * z / left shift = shift 175 | * x / space = play 176 | * a / left alt = opt 177 | * s / left ctrl = edit 178 | 179 | Additional controls: 180 | 181 | * Alt+Enter = toggle full screen / windowed 182 | * Alt+F4 = quit program 183 | * Delete = opt+edit (deletes a row) 184 | * Esc = toggle keyjazz on/off 185 | * r / select+start+opt+edit = reset display (if glitches appear on the screen, use this) 186 | * F1 = open config editor 187 | * F2 = toggle in-app log overlay 188 | * F12 = toggle audio routing on / off 189 | 190 | ### Keyjazz 191 | 192 | Keyjazz allows entering notes with a keyboard, old school tracker-style. The layout is two octaves, starting from keys Z 193 | and Q. 194 | When keyjazz is active, regular a/s/z/x keys are disabled. The base octave can be adjusted with numpad star/divide keys 195 | and the velocity can be set 196 | 197 | * Numpad asterisk (*): increase base octave 198 | * Numpad divide (/): decrease base octave 199 | * Numpad plus (+): increase velocity by 10 200 | * Numpad minus (-): decrease velocity by 10 201 | * Holding the ALT key while changing velocity increases/decreases the value in steps of 1 instead of the default. 202 | 203 | ## Gamepads 204 | 205 | The program uses SDL's game controller system, which should make it work automatically with most gamepads. On startup, 206 | the program tries to load a SDL game controller database named `gamecontrollerdb.txt` from the same directory as the 207 | config file. If your joypad doesn't work out of the box, you might need to create custom bindings to this file, 208 | with [SDL2 Gamepad Tool](https://generalarcade.com/gamepadtool/), for example. 209 | 210 | ### Gamepad Configuration 211 | 212 | To configure your gamepad: 213 | 214 | 1. Download the [SDL2 Gamepad Tool](https://generalarcade.com/gamepadtool/) 215 | 2. Connect your gamepad and open the tool 216 | 3. Create custom bindings and save them to `gamecontrollerdb.txt` 217 | 4. Place the file in the same directory as your `config.ini` file 218 | 219 | ## Audio 220 | 221 | m8c supports audio routing from the M8 device to your computer's audio output. 222 | 223 | ### Audio Controls 224 | 225 | - **Toggle audio routing:** F12 (default) or configure `key_toggle_audio` in config 226 | - **Audio buffer size:** Configure `audio_buffer_size` in config (0 = SDL default) 227 | - **Audio device:** Configure `audio_device_name` in config for specific device selection 228 | 229 | ### Platform-specific Notes 230 | 231 | - **macOS:** Grant microphone permission for audio routing to work 232 | - **Linux:** May require additional audio permissions or configuration 233 | - **Windows:** Should work with standard audio drivers 234 | 235 | ## Config 236 | 237 | ### Settings menu (in-app) 238 | 239 | You can change the most common options without editing `config.ini` using the in-app settings overlay. 240 | 241 | - **How to open:** 242 | - Keyboard: press F1. 243 | - Gamepad: hold the Back/Select button for about 2 seconds (works only when the M8 is disconnected to avoid opening 244 | the menu accidentally). 245 | - **How to navigate:** 246 | - Move: Up/Down arrows or D‑pad. 247 | - Activate/enter: Enter/Space or South/A. 248 | - Adjust values (sliders/integers): Left/Right arrows or D‑pad left/right. 249 | - Back/close: Esc or F1; on gamepad use East/B or Back. 250 | - While remapping inputs, the menu will prompt you; press the desired key/button or move an axis. Use Esc/East/Back 251 | to 252 | cancel a capture. 253 | 254 | Changes take effect immediately; use Save if you want them persisted to disk. 255 | 256 | ### Settings file (config.ini) 257 | 258 | Application settings and keyboard/game controller bindings can be configured via `config.ini`. 259 | 260 | The keyboard configuration uses SDL Scancodes, a reference list can be found 261 | at https://wiki.libsdl.org/SDL2/SDLScancodeLookup 262 | 263 | If the file does not exist, it will be created in one of these locations: 264 | 265 | * Windows: `C:\Users\\AppData\Roaming\m8c\config.ini` 266 | * Linux: `/home//.local/share/m8c/config.ini` 267 | * macOS: `/Users//Library/Application Support/m8c/config.ini` 268 | 269 | You can choose to load an alternate configuration with the `--config` command line option. Example: 270 | 271 | ```sh 272 | m8c --config alternate_config.ini 273 | ``` 274 | 275 | This looks for a config file with the given name in the same directory as the default config. If you specify a config 276 | file that does not exist, a new default config file with the specified name will be created, which you can then edit. 277 | 278 | ### Log overlay 279 | 280 | An in-app log overlay is available for platforms where reading console output is inconvenient. 281 | 282 | - Default toggle key: F2. You can change it in the config editor, or `config.ini` under `[keyboard]` using 283 | `key_toggle_log=`. 284 | - The overlay shows recent `SDL_Log*` messages. 285 | - Long lines are wrapped to fit; the view tails the most recent output. 286 | 287 | Enjoy making some nice music! 288 | 289 | ----------- 290 | 291 | ## FAQ 292 | 293 | ### Permission Issues 294 | 295 | * When starting the program, something like the following appears, and the program does not start: 296 | 297 | ```sh 298 | $ ./m8c 299 | INFO: Looking for USB serial devices. 300 | INFO: Found M8 in /dev/ttyACM1. 301 | INFO: Opening port. 302 | ERROR: Error: Failed: Permission denied 303 | ``` 304 | 305 | This is likely caused because the user running m8c does not have permission to use the serial port. The easiest way to 306 | fix this is to add the current user to a group with permission to use the serial port. 307 | 308 | On Linux systems, look at the permissions on the serial port shown on the line that says "Found M8 in": 309 | 310 | ```sh 311 | $ ls -la /dev/ttyACM1 312 | crw-rw---- 1 root dialout 166, 0 Jan 8 14:51 /dev/ttyACM0 313 | ``` 314 | 315 | In this case the serial port is owned by the user 'root' and the group 'dialout'. Both the user and the group have 316 | read/write permissions. To add a user to the group, run this command, replacing 'dialout' with the group shown on your 317 | own system: 318 | 319 | ```sh 320 | sudo adduser $USER dialout 321 | ``` 322 | 323 | You may need to log out and back in or even fully reboot the system for this change to take effect, but this will 324 | hopefully fix the problem. 325 | 326 | Some distributions have also had conflicts with the kernel version and libserialport packaging, please see [this issue for more details](https://github.com/laamaa/m8c/issues/20). 327 | 328 | ### Device Not Found 329 | 330 | * The program starts but shows "No M8 device found": 331 | - Ensure your M8 or Teensy is connected via USB 332 | - If using a Teensy, check that the headless firmware is properly installed 333 | - Try running with `--list` to see detected devices 334 | - On Linux, verify USB permissions (see permission issues above) 335 | 336 | ### Audio Issues 337 | 338 | * No audio output: 339 | - Check that audio routing is enabled (F12) 340 | - Verify audio device selection in config 341 | - On macOS, ensure microphone permission is granted 342 | -------------------------------------------------------------------------------- /src/backends/m8_rtmidi.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | #ifdef USE_RTMIDI 4 | 5 | #ifndef NDEBUG 6 | #define RTMIDI_DEBUG 7 | #endif 8 | 9 | #include "../command.h" 10 | #include "../config.h" 11 | #include "m8.h" 12 | #include "queue.h" 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | RtMidiInPtr midi_in; 19 | RtMidiOutPtr midi_out; 20 | message_queue_s queue; 21 | 22 | const unsigned char m8_sysex_header[5] = {0xF0, 0x00, 0x02, 0x61, 0x00}; 23 | const unsigned int m8_sysex_header_size = sizeof(m8_sysex_header); 24 | const unsigned char sysex_message_end = 0xF7; 25 | 26 | bool midi_processing_suspended = false; 27 | bool midi_sysex_received = false; 28 | 29 | static bool message_is_m8_sysex(const unsigned char *message) { 30 | if (memcmp(m8_sysex_header, message, m8_sysex_header_size) == 0) { 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | static void midi_decode(const uint8_t *encoded_data, size_t length, uint8_t **decoded_data, 37 | size_t *decoded_length) { 38 | if (length < m8_sysex_header_size) { 39 | // Invalid data 40 | *decoded_data = NULL; 41 | *decoded_length = 0; 42 | return; 43 | } 44 | 45 | // Skip header "F0 00 02 61" and the first MSB byte 46 | size_t pos = m8_sysex_header_size + 1; 47 | 48 | // Calculate expected output size (ignoring EOT if present) 49 | const size_t expected_output_size = 50 | (length - m8_sysex_header_size) - ((length - m8_sysex_header_size) / 8); 51 | if (encoded_data[length - 1] == sysex_message_end) { 52 | length--; // Ignore the EOT byte 53 | } 54 | 55 | // Allocate memory for decoded output 56 | *decoded_data = (uint8_t *)SDL_malloc(expected_output_size); 57 | if (*decoded_data == NULL) { 58 | *decoded_length = 0; 59 | return; 60 | } 61 | 62 | uint8_t bit_counter = 0; 63 | uint8_t bit_byte_counter = 0; 64 | uint8_t *out = *decoded_data; 65 | *decoded_length = 0; 66 | 67 | while (pos < length) { 68 | // Extract MSB from the "bit field" position 69 | uint8_t msb = (encoded_data[bit_byte_counter * 8 + m8_sysex_header_size] >> bit_counter) & 0x01; 70 | 71 | // Extract LSB from data byte 72 | uint8_t lsb = encoded_data[pos] & 0x7F; 73 | 74 | // Reconstruct original byte, skipping the MSB bytes in output 75 | *out = (msb << 7) | lsb; 76 | out++; 77 | (*decoded_length)++; 78 | 79 | bit_counter++; 80 | pos++; 81 | 82 | if (bit_counter == 7) { 83 | bit_counter = 0; 84 | bit_byte_counter++; 85 | pos++; // Skip the MSB byte 86 | } 87 | } 88 | } 89 | 90 | static void midi_callback(double delta_time, const unsigned char *message, size_t message_size, 91 | void *user_data) { 92 | // Unused variables 93 | (void)delta_time; 94 | (void)user_data; 95 | 96 | if (midi_processing_suspended || message_size < 5 || !message_is_m8_sysex(message)) 97 | return; 98 | 99 | if (!midi_sysex_received) { 100 | midi_sysex_received = true; 101 | } 102 | 103 | unsigned char *decoded_data; 104 | size_t decoded_length; 105 | midi_decode(message, message_size, &decoded_data, &decoded_length); 106 | 107 | // If you need to debug incoming MIDI packets, you can uncomment the lines below: 108 | 109 | /* printf("Original data: "); 110 | for (size_t i = 0; i < message_size; i++) { 111 | printf("%02X ", message[i]); 112 | } */ 113 | 114 | if (decoded_data) { 115 | /* printf("\nDecoded MIDI Data: "); 116 | for (size_t i = 0; i < decoded_length; i++) { 117 | printf("%02X ", decoded_data[i]); 118 | } 119 | printf("\n"); */ 120 | push_message(&queue, decoded_data, decoded_length); 121 | SDL_free(decoded_data); 122 | } else { 123 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Decoding failed.\n"); 124 | } 125 | } 126 | 127 | static void close_and_free_midi_ports(void) { 128 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Freeing MIDI ports"); 129 | if (midi_in != NULL) { 130 | rtmidi_in_cancel_callback(midi_in); 131 | rtmidi_close_port(midi_in); 132 | rtmidi_in_free(midi_in); 133 | } 134 | if (midi_out != NULL) { 135 | rtmidi_close_port(midi_out); 136 | rtmidi_out_free(midi_out); 137 | } 138 | midi_in = NULL; 139 | midi_out = NULL; 140 | midi_sysex_received = false; 141 | } 142 | 143 | static int initialize_rtmidi(void) { 144 | SDL_Log("Initializing rtmidi"); 145 | midi_in = rtmidi_in_create(RTMIDI_API_UNSPECIFIED, "m8c_in", 2048); 146 | midi_out = rtmidi_out_create(RTMIDI_API_UNSPECIFIED, "m8c_out"); 147 | if (midi_in == NULL || midi_out == NULL) { 148 | return 0; 149 | } 150 | return 1; 151 | } 152 | 153 | static int detect_m8_midi_device(const int verbose, const char *preferred_device) { 154 | if (midi_in->ptr == NULL) { 155 | midi_in = NULL; 156 | midi_out = NULL; 157 | return -1; 158 | } 159 | int m8_midi_port_number = -1; 160 | const unsigned int ports_total_in = rtmidi_get_port_count(midi_in); 161 | if (verbose) 162 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Number of MIDI in ports: %d", ports_total_in); 163 | for (unsigned int port_number = 0; port_number < ports_total_in; port_number++) { 164 | int port_name_length_in; 165 | rtmidi_get_port_name(midi_in, port_number, NULL, &port_name_length_in); 166 | char port_name[port_name_length_in]; 167 | rtmidi_get_port_name(midi_in, port_number, &port_name[0], &port_name_length_in); 168 | if (verbose) 169 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "MIDI IN port %d, name: %s", port_number, port_name); 170 | if (SDL_strncmp("M8", port_name, 2) == 0) { 171 | m8_midi_port_number = port_number; 172 | if (verbose) 173 | SDL_Log("Found M8 Input in MIDI port %d", port_number); 174 | if (preferred_device != NULL && SDL_strcmp(preferred_device, port_name) == 0) { 175 | SDL_Log("Found preferred device, breaking"); 176 | break; 177 | } 178 | } 179 | } 180 | return m8_midi_port_number; 181 | } 182 | 183 | static int device_still_exists(void) { 184 | if (midi_in->ptr == NULL || midi_out->ptr == NULL) { 185 | return 0; 186 | }; 187 | 188 | int m8_midi_port_number = 0; 189 | 190 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Checking if opened MIDI port still exists"); 191 | 192 | m8_midi_port_number = detect_m8_midi_device(0, NULL); 193 | if (m8_midi_port_number >= 0) { 194 | return 1; 195 | } 196 | return 0; 197 | } 198 | 199 | static int disconnect(void) { 200 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending disconnect message to M8"); 201 | const unsigned char disconnect_sysex[8] = {0xF0, 0x00, 0x02, 0x61, 0x00, 0x00, 'D', 0xF7}; 202 | const int result = 203 | rtmidi_out_send_message(midi_out, &disconnect_sysex[0], sizeof(disconnect_sysex)); 204 | if (result != 0) { 205 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to send disconnect"); 206 | } 207 | close_and_free_midi_ports(); 208 | return !result; 209 | } 210 | 211 | int m8_initialize(const int verbose, const char *preferred_device) { 212 | int m8_midi_port_number = 0; 213 | 214 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Initialize M8 via RTMIDI called"); 215 | if (midi_in == NULL || midi_out == NULL) { 216 | initialize_rtmidi(); 217 | }; 218 | m8_midi_port_number = detect_m8_midi_device(verbose, preferred_device); 219 | if (m8_midi_port_number >= 0) { 220 | rtmidi_in_ignore_types(midi_in, false, true, true); // Allow sysex 221 | rtmidi_open_port(midi_in, m8_midi_port_number, "M8"); 222 | rtmidi_open_port(midi_out, m8_midi_port_number, "M8"); 223 | init_queue(&queue); 224 | return 1; 225 | } 226 | return 0; 227 | } 228 | 229 | int m8_reset_display(void) { 230 | SDL_Log("Reset display"); 231 | const unsigned char reset_sysex[8] = {0xF0, 0x00, 0x02, 0x61, 0x00, 0x00, 'R', 0xF7}; 232 | const int result = rtmidi_out_send_message(midi_out, &reset_sysex[0], sizeof(reset_sysex)); 233 | if (result != 0) { 234 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error resetting M8 display, error %s", midi_out->msg); 235 | return 0; 236 | } 237 | return 1; 238 | } 239 | 240 | int m8_enable_display(const unsigned char reset_display) { 241 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending enable command sysex"); 242 | rtmidi_in_set_callback(midi_in, midi_callback, NULL); 243 | const unsigned char disconnect_sysex[8] = {0xF0, 0x00, 0x02, 0x61, 0x00, 0x00, 'D', 0xF7}; 244 | int result = 245 | rtmidi_out_send_message(midi_out, &disconnect_sysex[0], sizeof(disconnect_sysex)); 246 | if (result != 0) { 247 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to send disconnect"); 248 | } 249 | const unsigned char enable_sysex[8] = {0xF0, 0x00, 0x02, 0x61, 0x00, 0x00, 'E', 0xF7}; 250 | result = rtmidi_out_send_message(midi_out, &enable_sysex[0], sizeof(enable_sysex)); 251 | if (result != 0) { 252 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to send remote display enable command"); 253 | return 0; 254 | } 255 | // Wait a second to get a reply from the device 256 | const Uint64 timer_wait_for_response = SDL_GetTicks(); 257 | while (!midi_sysex_received) { 258 | if (SDL_GetTicks() - timer_wait_for_response > 1000) { 259 | SDL_LogCritical( 260 | SDL_LOG_CATEGORY_SYSTEM, 261 | "No response from device. Please make sure you're using M8 firmware 6.0.0 or newer."); 262 | close_and_free_midi_ports(); 263 | return 0; 264 | } 265 | SDL_Delay(5); 266 | } 267 | if (reset_display) { 268 | return m8_reset_display(); 269 | } 270 | return 1; 271 | } 272 | 273 | int m8_send_msg_controller(const unsigned char input) { 274 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending controller input 0x%02X", input); 275 | 276 | // Get MSB from input 277 | const uint8_t msb = 0 | (input & 0x80 ? 1u << 2 : 0); 278 | const unsigned char controller_sysex[10] = { 279 | 0xF0, 0x00, 0x02, 0x61, 0x00, msb, 0x00, (uint8_t)('C' & 0x7F), (uint8_t)(input & 0x7F), 280 | 0xF7}; 281 | 282 | const int result = rtmidi_out_send_message(midi_out, controller_sysex, sizeof(controller_sysex)); 283 | if (result != 0) { 284 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to send key input message"); 285 | return 0; 286 | } 287 | return 1; 288 | } 289 | 290 | int m8_send_msg_keyjazz(const unsigned char note, unsigned char velocity) { 291 | if (velocity > 0x7F) { 292 | velocity = 0x7F; 293 | } 294 | 295 | // Special case for note off 296 | if (note == 0xFF && velocity == 0x00) { 297 | 298 | const uint8_t msb = 0 | 1u << 2; 299 | const unsigned char keyjazz_sysex[10] = { 300 | 0xF0, 0x00, 0x02, 0x61, 0x00, 301 | msb, 0x00, (uint8_t)('K' & 0x7F), (uint8_t)(0xFF & 0x7F), 302 | 0xF7 303 | }; 304 | 305 | const int result = rtmidi_out_send_message(midi_out, &keyjazz_sysex[0], sizeof(keyjazz_sysex)); 306 | if (result != 0) { 307 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to send all notes off"); 308 | return 0; 309 | } 310 | return 1; 311 | } 312 | 313 | // Get MSB from input 314 | const uint8_t msb = 0 | (note & 0x80 ? 1u << 2 : 0) | (velocity & 0x80 ? 1u << 3 : 0); 315 | const unsigned char keyjazz_sysex[11] = { 316 | 0xF0, 0x00, 0x02, 0x61, 0x00, 317 | msb, 0x00, (uint8_t)('K' & 0x7F), (uint8_t)(note & 0x7F), (uint8_t)(velocity & 0x7F), 318 | 0xF7 319 | }; 320 | 321 | const int result = rtmidi_out_send_message(midi_out, &keyjazz_sysex[0], sizeof(keyjazz_sysex)); 322 | if (result != 0) { 323 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to send keyjazz input message"); 324 | return 0; 325 | } 326 | return 1; 327 | } 328 | 329 | int m8_process_data(const config_params_s *conf) { 330 | static unsigned int empty_cycles = 0; 331 | 332 | if (queue_size(&queue) > 0) { 333 | unsigned char *command; 334 | empty_cycles = 0; 335 | size_t length = 0; 336 | while ((command = pop_message(&queue, &length)) != NULL) { 337 | process_command(command, length); 338 | SDL_free(command); 339 | } 340 | } else { 341 | empty_cycles++; 342 | if (empty_cycles >= conf->wait_packets) { 343 | if (device_still_exists()) { 344 | empty_cycles = 0; 345 | return DEVICE_PROCESSING; 346 | } 347 | SDL_Log("No messages received for %d cycles, assuming device disconnected", empty_cycles); 348 | close_and_free_midi_ports(); 349 | destroy_queue(&queue); 350 | empty_cycles = 0; 351 | return DEVICE_DISCONNECTED; 352 | } 353 | } 354 | return DEVICE_PROCESSING; 355 | } 356 | 357 | int m8_close(void) { 358 | const int result = disconnect(); 359 | destroy_queue(&queue); 360 | return result; 361 | } 362 | 363 | int m8_list_devices(void) { 364 | if (midi_in == NULL || midi_out == NULL) { 365 | initialize_rtmidi(); 366 | }; 367 | const unsigned int ports_total_in = rtmidi_get_port_count(midi_in); 368 | SDL_Log("Number of MIDI in ports: %d", ports_total_in); 369 | for (unsigned int port_number = 0; port_number < ports_total_in; port_number++) { 370 | int port_name_length_in; 371 | rtmidi_get_port_name(midi_in, port_number, NULL, &port_name_length_in); 372 | char port_name[port_name_length_in]; 373 | rtmidi_get_port_name(midi_in, port_number, &port_name[0], &port_name_length_in); 374 | SDL_Log("MIDI IN port %d, name: %s", port_number, port_name); 375 | } 376 | close_and_free_midi_ports(); 377 | return 1; 378 | } 379 | 380 | int m8_pause_processing(void) { 381 | midi_processing_suspended = true; 382 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Pausing MIDI processing"); 383 | return 1; 384 | } 385 | int m8_resume_processing(void) { 386 | midi_processing_suspended = false; 387 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Resuming MIDI processing"); 388 | m8_reset_display(); 389 | return 1; 390 | } 391 | 392 | #endif 393 | -------------------------------------------------------------------------------- /src/backends/m8_libserialport.c: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Jonne Kokkonen 2 | // Released under the MIT licence, https://opensource.org/licenses/MIT 3 | 4 | // Contains portions of code from libserialport's examples released to the 5 | // public domain 6 | 7 | #ifdef USE_LIBSERIALPORT 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "../command.h" 14 | #include "../config.h" 15 | #include "m8.h" 16 | #include "queue.h" 17 | #include "slip.h" 18 | 19 | #define SERIAL_READ_SIZE 1024 // maximum amount of bytes to read from the serial in one pass 20 | #define SERIAL_READ_DELAY_MS 4 // delay between serial reads in milliseconds 21 | 22 | struct sp_port *m8_port = NULL; 23 | // allocate memory for serial buffers 24 | static uint8_t serial_buffer[SERIAL_READ_SIZE] = {0}; 25 | static uint8_t slip_buffer[SERIAL_READ_SIZE] = {0}; 26 | static slip_handler_s slip; 27 | message_queue_s queue; 28 | 29 | SDL_Thread *serial_thread = NULL; 30 | 31 | // Structure to pass data to the thread 32 | typedef struct { 33 | int should_stop; // Shared stop flag 34 | } thread_params_s; 35 | 36 | thread_params_s thread_params; 37 | 38 | // Helper function for error handling 39 | static int check(enum sp_return result); 40 | 41 | static int send_message_to_queue(uint8_t *data, const uint32_t size) { 42 | push_message(&queue, data, size); 43 | return 1; 44 | } 45 | 46 | static int disconnect() { 47 | SDL_Log("Disconnecting M8"); 48 | 49 | // wait for the serial processing thread to finish 50 | thread_params.should_stop = 1; 51 | SDL_WaitThread(serial_thread, NULL); 52 | destroy_queue(&queue); 53 | 54 | const unsigned char buf[1] = {'D'}; 55 | 56 | int result = sp_blocking_write(m8_port, buf, 1, 5); 57 | if (result != 1) { 58 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending disconnect, code %d", result); 59 | result = 0; 60 | } 61 | 62 | sp_close(m8_port); 63 | sp_free_port(m8_port); 64 | m8_port = NULL; 65 | return result; 66 | } 67 | 68 | static int detect_m8_serial_device(const struct sp_port *m8_port, const char *preferred_device) { 69 | // Check the connection method - we want USB serial devices 70 | const enum sp_transport transport = sp_get_port_transport(m8_port); 71 | 72 | if (transport == SP_TRANSPORT_USB) { 73 | // If a preferred device is specified, check if this port matches it 74 | if (preferred_device != NULL) { 75 | const char *port_name = sp_get_port_name(m8_port); 76 | if (strcmp(preferred_device, port_name) == 0) { 77 | return 1; // Force return 1 for preferred device 78 | } 79 | } 80 | 81 | // Get the USB vendor and product IDs. 82 | int usb_vid, usb_pid; 83 | sp_get_port_usb_vid_pid(m8_port, &usb_vid, &usb_pid); 84 | 85 | if (usb_vid == 0x16C0 && usb_pid == 0x048A) 86 | return 1; 87 | } 88 | 89 | return 0; 90 | } 91 | 92 | // Checks for connected devices and whether the specified device still exists 93 | static int serial_port_connected() { 94 | 95 | int device_found = 0; 96 | 97 | struct sp_port **port_list; 98 | 99 | const enum sp_return result = sp_list_ports(&port_list); 100 | 101 | if (result != SP_OK) { 102 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!\n"); 103 | abort(); 104 | } 105 | 106 | for (int i = 0; port_list[i] != NULL; i++) { 107 | const struct sp_port *port = port_list[i]; 108 | 109 | if (detect_m8_serial_device(port, NULL)) { 110 | if (strcmp(sp_get_port_name(port), sp_get_port_name(m8_port)) == 0) 111 | device_found = 1; 112 | } 113 | } 114 | 115 | sp_free_port_list(port_list); 116 | return device_found; 117 | } 118 | 119 | static void process_received_bytes(const uint8_t *buffer, int bytes_read, slip_handler_s *slip) { 120 | const uint8_t *cur = buffer; 121 | const uint8_t *end = buffer + bytes_read; 122 | while (cur < end) { 123 | const int slip_result = slip_read_byte(slip, *cur++); 124 | if (slip_result != SLIP_NO_ERROR) { 125 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SLIP error %d", slip_result); 126 | } 127 | } 128 | } 129 | 130 | static int thread_process_serial_data(void *data) { 131 | const thread_params_s *thread_params = data; 132 | 133 | while (!thread_params->should_stop) { 134 | // attempt to read from serial port 135 | const int bytes_read = sp_nonblocking_read(m8_port, serial_buffer, SERIAL_READ_SIZE); 136 | 137 | if (bytes_read < 0) { 138 | SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "Error %d reading serial.", bytes_read); 139 | disconnect(); 140 | return 0; 141 | } 142 | 143 | if (bytes_read > 0) { 144 | process_received_bytes(serial_buffer, bytes_read, &slip); 145 | } 146 | 147 | SDL_Delay(SERIAL_READ_DELAY_MS); 148 | } 149 | return 1; 150 | } 151 | 152 | static int configure_serial_port(struct sp_port *port) { 153 | if (check(sp_open(port, SP_MODE_READ_WRITE)) != SP_OK) 154 | return 0; 155 | if (check(sp_set_baudrate(port, 115200)) != SP_OK) 156 | return 0; 157 | if (check(sp_set_bits(port, 8)) != SP_OK) 158 | return 0; 159 | if (check(sp_set_parity(port, SP_PARITY_NONE)) != SP_OK) 160 | return 0; 161 | if (check(sp_set_stopbits(port, 1)) != SP_OK) 162 | return 0; 163 | if (check(sp_set_flowcontrol(port, SP_FLOWCONTROL_NONE)) != SP_OK) 164 | return 0; 165 | return 1; 166 | } 167 | 168 | // Helper function for error handling. 169 | static int check(const enum sp_return result) { 170 | char *error_message; 171 | 172 | switch (result) { 173 | case SP_ERR_ARG: 174 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid argument"); 175 | break; 176 | case SP_ERR_FAIL: 177 | error_message = sp_last_error_message(); 178 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Failed: %s", error_message); 179 | sp_free_error_message(error_message); 180 | break; 181 | case SP_ERR_SUPP: 182 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Not supported"); 183 | break; 184 | case SP_ERR_MEM: 185 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Couldn't allocate memory"); 186 | break; 187 | case SP_OK: 188 | default: 189 | break; 190 | } 191 | return result; 192 | } 193 | 194 | // Extracted function for initializing threads and message queue 195 | static int initialize_serial_thread() { 196 | 197 | init_queue(&queue); 198 | serial_thread = SDL_CreateThread(thread_process_serial_data, "SerialThread", &thread_params); 199 | 200 | if (!serial_thread) { 201 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "SDL_CreateThread Error: %s", SDL_GetError()); 202 | SDL_Quit(); 203 | return 0; 204 | } 205 | 206 | thread_params.should_stop = 0; 207 | 208 | return 1; 209 | } 210 | 211 | // Extracted function for detecting and selecting the M8 device 212 | static int find_and_select_device(const char *preferred_device) { 213 | struct sp_port **port_list; 214 | const enum sp_return port_result = sp_list_ports(&port_list); 215 | 216 | if (port_result != SP_OK) { 217 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!"); 218 | return 0; 219 | } 220 | 221 | for (int i = 0; port_list[i] != NULL; i++) { 222 | const struct sp_port *port = port_list[i]; 223 | 224 | if (detect_m8_serial_device(port, preferred_device)) { 225 | char *port_name = sp_get_port_name(port); 226 | SDL_Log("Found M8 in %s", port_name); 227 | sp_copy_port(port, &m8_port); 228 | 229 | // Break if preferred device is found 230 | if (preferred_device != NULL && strcmp(preferred_device, port_name) == 0) { 231 | SDL_Log("Found preferred device, breaking"); 232 | break; 233 | } 234 | } 235 | } 236 | 237 | sp_free_port_list(port_list); 238 | return (m8_port != NULL); 239 | } 240 | 241 | int m8_initialize(const int verbose, const char *preferred_device) { 242 | if (m8_port != NULL) { 243 | // Port is already initialized 244 | return 1; 245 | } 246 | 247 | // Initialize slip descriptor 248 | static const slip_descriptor_s slip_descriptor = { 249 | .buf = slip_buffer, 250 | .buf_size = sizeof(slip_buffer), 251 | .recv_message = send_message_to_queue, 252 | }; 253 | slip_init(&slip, &slip_descriptor); 254 | 255 | if (verbose) { 256 | SDL_Log("Looking for USB serial devices"); 257 | } 258 | 259 | // Detect and select M8 device 260 | if (!find_and_select_device(preferred_device)) { 261 | if (verbose) { 262 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Cannot find a M8"); 263 | } 264 | return 0; 265 | } 266 | 267 | // Configure serial port 268 | if (!configure_serial_port(m8_port)) { 269 | return 0; 270 | } 271 | 272 | // Initialize message queue and threads 273 | return initialize_serial_thread(); 274 | } 275 | 276 | static int send_ping() { 277 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending ping"); 278 | const unsigned char buf[1] = {'X'}; 279 | const size_t nbytes = 1; 280 | const int result = sp_blocking_write(m8_port, buf, nbytes, 5); 281 | if (result != nbytes) { 282 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending ping, code %d", result); 283 | return 0; 284 | } 285 | return 1; 286 | } 287 | 288 | int m8_send_msg_controller(const uint8_t input) { 289 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending controller input %d", input); 290 | const unsigned char buf[2] = {'C', input}; 291 | const size_t nbytes = 2; 292 | const int result = sp_blocking_write(m8_port, buf, nbytes, 5); 293 | if (result != nbytes) { 294 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending input, code %d", result); 295 | return -1; 296 | } 297 | return 1; 298 | } 299 | 300 | int m8_send_msg_keyjazz(const uint8_t note, uint8_t velocity) { 301 | 302 | // Cap velocity to 7bits 303 | if (velocity > 0x7F) 304 | velocity = 0x7F; 305 | 306 | // Special case for note off 307 | if (note == 0xFF && velocity == 0x00) { 308 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending keyjazz note off"); 309 | const unsigned char buf[2] = {'K', 0xFF}; 310 | const size_t nbytes = 2; 311 | const int result = sp_blocking_write(m8_port, buf, nbytes, 5); 312 | if (result != nbytes) { 313 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending keyjazz, code %d", result); 314 | return -1; 315 | } 316 | return 1; 317 | } 318 | 319 | // Regular note on message 320 | SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "Sending keyjazz note %d, velocity %d", note, velocity); 321 | const unsigned char buf[3] = {'K', note, velocity}; 322 | const size_t nbytes = 3; 323 | const int result = sp_blocking_write(m8_port, buf, nbytes, 5); 324 | if (result != nbytes) { 325 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error sending keyjazz, code %d", result); 326 | return -1; 327 | } 328 | 329 | return 1; 330 | } 331 | 332 | int m8_list_devices() { 333 | struct sp_port **port_list; 334 | const enum sp_return result = sp_list_ports(&port_list); 335 | 336 | if (result != SP_OK) { 337 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "sp_list_ports() failed!\n"); 338 | return 1; 339 | } 340 | 341 | int devices_found = 0; 342 | for (int i = 0; port_list[i] != NULL; i++) { 343 | const struct sp_port *port = port_list[i]; 344 | 345 | if (detect_m8_serial_device(port, NULL)) { 346 | SDL_Log("Found M8 device: %s", sp_get_port_name(port)); 347 | devices_found++; 348 | } 349 | } 350 | if (devices_found == 0) { 351 | SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "No M8 devices found"); 352 | return 0; 353 | } 354 | 355 | sp_free_port_list(port_list); 356 | return 0; 357 | } 358 | 359 | int m8_reset_display() { 360 | SDL_Log("Reset display"); 361 | 362 | const unsigned char buf[1] = {'R'}; 363 | const int result = sp_blocking_write(m8_port, buf, 1, 5); 364 | if (result != 1) { 365 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error resetting M8 display, code %d", result); 366 | return 0; 367 | } 368 | return 1; 369 | } 370 | 371 | int m8_enable_display(const unsigned char reset_display) { 372 | SDL_Log("Enabling and resetting M8 display"); 373 | 374 | const char buf_enable[1] = {'E'}; 375 | int result = sp_blocking_write(m8_port, buf_enable, 1, 5); 376 | if (result != 1) { 377 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Error enabling M8 display, code %d", result); 378 | return 0; 379 | } 380 | 381 | // Wait for things to warm up 382 | SDL_Delay(500); 383 | 384 | if (reset_display) { 385 | result = m8_reset_display(); 386 | } 387 | 388 | return result; 389 | } 390 | 391 | int m8_process_data(const config_params_s *conf) { 392 | static unsigned int empty_cycles = 0; 393 | 394 | // Device likely has been disconnected 395 | if (m8_port == NULL) { 396 | return DEVICE_DISCONNECTED; 397 | } 398 | 399 | if (queue_size(&queue) > 0) { 400 | unsigned char *command; 401 | empty_cycles = 0; 402 | size_t length = 0; 403 | while ((command = pop_message(&queue, &length)) != NULL) { 404 | if (length > 0) { 405 | process_command(command, length); 406 | } 407 | SDL_free(command); 408 | } 409 | } else { 410 | empty_cycles++; 411 | if (empty_cycles >= conf->wait_packets) { 412 | // try opening the serial port to check if it's alive 413 | if (serial_port_connected()) { 414 | // check if the device responds to display reset 415 | if (!send_ping()) { 416 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Failed to ping device on reconnect"); 417 | disconnect(); 418 | return DEVICE_DISCONNECTED; 419 | } 420 | // the device is still there, carry on 421 | empty_cycles = 0; 422 | return DEVICE_PROCESSING; 423 | } 424 | SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, 425 | "No messages received for %d cycles, assuming device disconnected", 426 | empty_cycles); 427 | empty_cycles = 0; 428 | disconnect(); 429 | return DEVICE_DISCONNECTED; 430 | } 431 | } 432 | return DEVICE_PROCESSING; 433 | } 434 | 435 | int m8_close() { return disconnect(); } 436 | 437 | // These shouldn't be needed with serial 438 | int m8_pause_processing(void) { return 1; } 439 | int m8_resume_processing(void) { return 1; } 440 | #endif 441 | --------------------------------------------------------------------------------
m8c is a client for Dirtywave M8 tracker's remote (headless) mode and can mirror the display and route audio.