diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..5673ef8 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-Iinclude, -std=c99] diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/ci.yml similarity index 67% rename from .github/workflows/c-cpp.yml rename to .github/workflows/ci.yml index d3817c3..97010b7 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ - name: build-and-test +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: branches: [ "**" ] @@ -41,4 +44,18 @@ jobs: - name: Smoke test help run: ./keyvibe --help || true + - name: Extra smoke tests (non-fatal) + run: | + ./keyvibe --reload || true + ./keyvibe --stop || true + + - name: Upload binaries + uses: actions/upload-artifact@v4 + with: + name: keyvibe-binaries + path: | + keyvibe + keyvibe-audio + keyvibe-input + diff --git a/Makefile b/Makefile index 86e7ca0..ae22213 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ - CC = gcc -CFLAGS = -Wall -Wextra -std=c99 +CFLAGS = -Wall -Wextra -std=c99 -Iinclude PREFIX ?= /usr # Pass PACKAGE_PREFIX macro for config.h @@ -11,13 +10,13 @@ LDFLAGS_KEYBOARD = $(shell pkg-config --libs libevdev libinput libudev) -lpthrea # Targets KeyVibe_TARGET = keyvibe -SOUND_TARGET = keyboard_sound_player -KEYBOARD_TARGET = get_key_presses +SOUND_TARGET = keyvibe-audio +KEYBOARD_TARGET = keyvibe-input # Sources -KeyVibe_SOURCE = keyvibe.c -SOUND_SOURCE = keyboard_sound_player.c -KEYBOARD_SOURCE = get_key_presses.c +KeyVibe_SOURCE = src/keyvibe_main.c +SOUND_SOURCE = src/keyvibe_audio.c +KEYBOARD_SOURCE = src/keyvibe_input.c # Install paths BINDIR = $(PREFIX)/bin diff --git a/compile_flags.txt b/compile_flags.txt new file mode 100644 index 0000000..74e5bc5 --- /dev/null +++ b/compile_flags.txt @@ -0,0 +1,4 @@ +-Iinclude +-std=c99 +-Wall +-Wextra diff --git a/get_key_presses.c b/get_key_presses.c deleted file mode 100644 index ee89d52..0000000 --- a/get_key_presses.c +++ /dev/null @@ -1,262 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "config.h" - -#define MAX_BUFFER_LENGTH 512 - -enum error_code { - NO_ERROR, - UDEV_FAILED, - LIBINPUT_FAILED, - SEAT_FAILED, - PERMISSION_FAILED -}; - -struct input_handler_data { - struct udev *udev; - struct libinput *libinput; -}; - -static void *handle_input(void *user_data) -{ - struct input_handler_data *input_handler_data = user_data; - - char line[MAX_BUFFER_LENGTH]; - while (fgets(line, MAX_BUFFER_LENGTH, stdin) != NULL) { - if (strcmp(line, "stop\n") == 0) { - libinput_unref(input_handler_data->libinput); - udev_unref(input_handler_data->udev); - exit(EXIT_SUCCESS); - } - } - - return NULL; -} - -static int open_restricted(const char *path, int flags, void *user_data) -{ - (void)user_data; - int fd = open(path, flags); - if (fd < 0) - fprintf(stderr, "Failed to open %s because of %s.\n", path, - strerror(errno)); - return fd < 0 ? -errno : fd; -} - -static void close_restricted(int fd, void *user_data) -{ - (void)user_data; - close(fd); -} - -static const struct libinput_interface interface = { - .open_restricted = open_restricted, - .close_restricted = close_restricted, -}; - -static int print_key_event(struct libinput_event *event) -{ - struct libinput_event_keyboard *keyboard = - libinput_event_get_keyboard_event(event); - - enum libinput_event_type event_type = libinput_event_get_type(event); - uint32_t time_stamp = libinput_event_keyboard_get_time(keyboard); - uint32_t key_code = libinput_event_keyboard_get_key(keyboard); - const char *key_name = libevdev_event_code_get_name(EV_KEY, key_code); - key_name = key_name ? key_name : "null"; - enum libinput_key_state state_code = - libinput_event_keyboard_get_key_state(keyboard); - const char *state_name = state_code == LIBINPUT_KEY_STATE_PRESSED ? - "PRESSED" : - "RELEASED"; - - return printf("{" - "\"event_name\": \"KEYBOARD_KEY\", " - "\"event_type\": %d, " - "\"time_stamp\": %d, " - "\"key_name\": \"%s\", " - "\"key_code\": %d, " - "\"state_name\": \"%s\", " - "\"state_code\": %d" - "}\n", - event_type, time_stamp, key_name, key_code, state_name, - state_code); -} - -static int print_button_event(struct libinput_event *event) -{ - struct libinput_event_pointer *pointer = - libinput_event_get_pointer_event(event); - - enum libinput_event_type event_type = libinput_event_get_type(event); - uint32_t time_stamp = libinput_event_pointer_get_time(pointer); - uint32_t button_code = libinput_event_pointer_get_button(pointer); - const char *button_name = - libevdev_event_code_get_name(EV_KEY, button_code); - enum libinput_button_state state_code = - libinput_event_pointer_get_button_state(pointer); - const char *state_name = state_code == LIBINPUT_BUTTON_STATE_PRESSED ? - "PRESSED" : - "RELEASED"; - return printf("{" - "\"event_name\": \"POINTER_BUTTON\", " - "\"event_type\": %d, " - "\"time_stamp\": %d, " - "\"key_name\": \"%s\", " - "\"key_code\": %d, " - "\"state_name\": \"%s\", " - "\"state_code\": %d" - "}\n", - event_type, time_stamp, button_name, button_code, - state_name, state_code); -} - -static int handle_events(struct libinput *libinput) -{ - int result = -1; - struct libinput_event *event; - - if (libinput_dispatch(libinput) < 0) - return result; - - // Please keep printing a line per json. - while ((event = libinput_get_event(libinput)) != NULL) { - switch (libinput_event_get_type(event)) { - // This program only handle key event. - case LIBINPUT_EVENT_KEYBOARD_KEY: - print_key_event(event); - break; - // Sorry, mouse button is also a key. - case LIBINPUT_EVENT_POINTER_BUTTON: - print_button_event(event); - break; - default: - break; - } - // Do a `fflush(stdout)` here, so when we write to pipes, - // the other one can always get a latest result. - // If we don't have `fflush(stdout)` here, pipe will save - // some lines in buffer and pass them together. - fflush(stdout); - libinput_event_destroy(event); - result = 0; - } - - return result; -} - -static int run_mainloop(struct libinput *libinput) -{ - struct pollfd fd; - fd.fd = libinput_get_fd(libinput); - fd.events = POLLIN; - fd.revents = 0; - - if (handle_events(libinput) != 0) { - fprintf(stderr, - "Expected device added events on startup but " - "got none. Maybe you don't have the right permissions?" - "\n"); - return -1; - } - while (poll(&fd, 1, -1) > -1) - handle_events(libinput); - return 0; -} - -void print_help(char *program_name) -{ - printf("The backend of Show Me The Key.\n"); - printf("Version " PROJECT_VERSION ".\n"); - printf("Usage: %s [OPTION…]\n", program_name); - printf("Options:\n"); - printf("\t-h, --help\tDisplay help then exit.\n"); - printf("\t-v, --version\tDisplay version then exit.\n"); - printf("Warning: This is the backend and is not designed to run " - "by users. You should run the frontend of Show Me The Key, " - "and the frontend will run this.\n"); -} - -int main(int argc, char *argv[]) -{ - const struct option long_options[] = { { "version", no_argument, 0, - 'v' }, - { "help", no_argument, 0, 'h' }, - { NULL, 0, NULL, 0 } }; - - int option_index = 0; - int opt = 0; - while ((opt = getopt_long(argc, argv, "vh", long_options, - &option_index)) != -1) { - switch (opt) { - case 0: - // We don't use this. - break; - case 'v': - printf(PROJECT_VERSION "\n"); - return 0; - case 'h': - print_help(argv[0]); - return 0; - case '?': - // getopt_long already printed an error message. - break; - default: - fprintf(stderr, "%s: Invalid option `-%c`.\n", argv[0], - opt); - break; - } - } - - struct udev *udev = udev_new(); - if (udev == NULL) { - fprintf(stderr, "Failed to initialize udev.\n"); - return UDEV_FAILED; - } - - struct libinput *libinput = - libinput_udev_create_context(&interface, NULL, udev); - if (!libinput) { - fprintf(stderr, "Failed to initialize libinput from udev.\n"); - return LIBINPUT_FAILED; - } - - // TODO: Support custom seat. - if (libinput_udev_assign_seat(libinput, "seat0") != 0) { - fprintf(stderr, "Failed to set seat.\n"); - libinput_unref(libinput); - udev_unref(udev); - return SEAT_FAILED; - } - - // Typically this will be run with pkexec as a subprocess, - // and the parent cannot kill it because it is privileged, - // so we use another thread to see if it gets "stop\n" from stdin, - // it will exit by itself. - pthread_t input_handler; - struct input_handler_data input_handler_data = { udev, libinput }; - pthread_create(&input_handler, NULL, handle_input, &input_handler_data); - - if (run_mainloop(libinput) < 0) - return PERMISSION_FAILED; - - libinput_unref(libinput); - udev_unref(udev); - - return NO_ERROR; -} diff --git a/config.h b/include/config.h similarity index 99% rename from config.h rename to include/config.h index 1347bde..9c2c9ff 100644 --- a/config.h +++ b/include/config.h @@ -18,3 +18,5 @@ #define KeyVibe_BIN_DIR PACKAGE_PREFIX "/bin" #endif + + diff --git a/keyboard_sound_player.c b/keyboard_sound_player.c deleted file mode 100644 index b3fcbd3..0000000 --- a/keyboard_sound_player.c +++ /dev/null @@ -1,642 +0,0 @@ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include // For dirname - -#define MAX_LINE_LENGTH 1024 -#define MAX_CONCURRENT_SOUNDS 10 - -typedef struct { - int start_ms; - int duration_ms; -} SoundMapping; - -typedef struct { - char press_file[256]; // used in multi mode - char release_file[256]; // used in multi mode - char generic_press_files[5][256]; // max 5 files GENERIC_R0..R4 - int num_generic_press_files; - - char sound_file[256]; // used in single mode - SoundMapping key_mappings[256]; // only for 'single' mode - - struct { - char *press; - char *release; - } multi_key_mappings[256]; - - int is_multi; - SF_INFO sf_info; -} SoundPack; - -typedef struct { - int key_code; - SoundPack *sound_pack; - int thread_id; - int is_pressed; -} PlaybackData; - -// Global sound pack -SoundPack g_sound_pack = {0}; -float g_volume = 1.0f; -int g_verbose = 0; - -// Thread pool for sound playback -pthread_t sound_threads[MAX_CONCURRENT_SOUNDS]; -volatile int thread_active[MAX_CONCURRENT_SOUNDS] = {0}; -pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; - -// Function to construct a full path -static void get_full_path(char *buffer, size_t buffer_size, const char *base_dir, const char *filename) { - if (filename == NULL || base_dir == NULL) { - buffer[0] = '\0'; - return; - } - // Check if filename is already an absolute path - if (filename[0] == '/') { - strncpy(buffer, filename, buffer_size - 1); - buffer[buffer_size - 1] = '\0'; - } else { - snprintf(buffer, buffer_size, "%s/%s", base_dir, filename); - } -} - - -int load_sound_config(const char *config_path) { - FILE *file = fopen(config_path, "r"); - if (!file) { - fprintf(stderr, "Error: Cannot open config file: %s\n", config_path); - perror("fopen"); - return -1; - } - - // Extract the directory of the config file - char config_path_copy[MAX_LINE_LENGTH]; // Use a copy because dirname can modify its argument - strncpy(config_path_copy, config_path, sizeof(config_path_copy) - 1); - config_path_copy[sizeof(config_path_copy) - 1] = '\0'; - char *config_dir = dirname(config_path_copy); - - fseek(file, 0, SEEK_END); - long size = ftell(file); - rewind(file); - - char *json_string = malloc(size + 1); - if (!json_string) { - fclose(file); - fprintf(stderr, "Error: Memory allocation failed\n"); - return -1; - } - - fread(json_string, 1, size, file); - json_string[size] = '\0'; - fclose(file); - - json_object *root = json_tokener_parse(json_string); - free(json_string); - if (!root) { - fprintf(stderr, "Error: Invalid JSON in config file\n"); - return -1; - } - - const char *key_type = "single"; - json_object *obj; - if (json_object_object_get_ex(root, "key_define_type", &obj)) - key_type = json_object_get_string(obj); - - g_sound_pack.is_multi = strcmp(key_type, "multi") == 0; - printf("Config loaded: Using %s mode\n", g_sound_pack.is_multi ? "multi" : "single"); - - if (g_sound_pack.is_multi) { - // Reset counter - g_sound_pack.num_generic_press_files = 0; - - if (json_object_object_get_ex(root, "sound", &obj)) { - const char *pattern = json_object_get_string(obj); - printf("Sound pattern: %s\n", pattern); - - // Check if pattern contains format specifier - if (strstr(pattern, "%d") || strstr(pattern, "{")) { - // Handle patterns like "GENERIC_R%d.mp3" or "GENERIC_R{0-4}.mp3" - for (int i = 0; i <= 4; i++) { - char temp_filename[256]; - if (strstr(pattern, "{")) { - // Replace {0-4} with actual number - char temp_pattern[256]; - strncpy(temp_pattern, pattern, sizeof(temp_pattern) - 1); - temp_pattern[sizeof(temp_pattern) - 1] = '\0'; - - // Simple replacement of {0-4} with %d - char *brace_start = strstr(temp_pattern, "{"); - char *brace_end = strstr(temp_pattern, "}"); - if (brace_start && brace_end) { - *brace_start = '%'; - *(brace_start + 1) = 'd'; - memmove(brace_start + 2, brace_end + 1, strlen(brace_end + 1) + 1); - } - snprintf(temp_filename, sizeof(temp_filename), temp_pattern, i); - } else { - snprintf(temp_filename, sizeof(temp_filename), pattern, i); - } - - // Construct the full path using the config directory - get_full_path(g_sound_pack.generic_press_files[i], sizeof(g_sound_pack.generic_press_files[i]), config_dir, temp_filename); - - // Check if file exists before adding to count - if (access(g_sound_pack.generic_press_files[i], R_OK) == 0) { - g_sound_pack.num_generic_press_files = i + 1; // Keep track of highest valid index + 1 - // printf("Found generic sound file: %s\n", g_sound_pack.generic_press_files[i]); - } else { - printf("Generic sound file not found: %s\n", g_sound_pack.generic_press_files[i]); - break; // Stop at first missing file - } - } - } else { - // Direct filename, no pattern - get_full_path(g_sound_pack.generic_press_files[0], sizeof(g_sound_pack.generic_press_files[0]), config_dir, pattern); - if (access(g_sound_pack.generic_press_files[0], R_OK) == 0) { - g_sound_pack.num_generic_press_files = 1; - printf("Found single generic sound file: %s\n", g_sound_pack.generic_press_files[0]); - } - } - - printf("Total generic press sound files: %d\n", g_sound_pack.num_generic_press_files); - } - - if (json_object_object_get_ex(root, "soundup", &obj)) { - char temp_release_file[256]; - strncpy(temp_release_file, json_object_get_string(obj), sizeof(temp_release_file) - 1); - temp_release_file[sizeof(temp_release_file) - 1] = '\0'; - get_full_path(g_sound_pack.release_file, sizeof(g_sound_pack.release_file), config_dir, temp_release_file); - printf("Release sound file: %s\n", g_sound_pack.release_file); - } - - if (json_object_object_get_ex(root, "defines", &obj)) { - // printf("Processing key definitions...\n"); - json_object_object_foreach(obj, key, val) { - int key_code; - int is_release = 0; - char *dash = strstr(key, "-up"); - if (dash) { - char key_copy[16]; - strncpy(key_copy, key, dash - key); - key_copy[dash - key] = '\0'; - key_code = atoi(key_copy); - is_release = 1; - } else { - key_code = atoi(key); - } - - if (key_code >= 0 && key_code < 256) { - const char *filename_relative = json_object_get_string(val); - char full_filename[MAX_LINE_LENGTH]; - get_full_path(full_filename, sizeof(full_filename), config_dir, filename_relative); - // printf("Key %d (%s): %s (full path: %s)\n", key_code, is_release ? "release" : "press", filename_relative, full_filename); - - if (is_release) { - if (g_sound_pack.multi_key_mappings[key_code].release) { - free(g_sound_pack.multi_key_mappings[key_code].release); - } - g_sound_pack.multi_key_mappings[key_code].release = strdup(full_filename); - } else { - if (g_sound_pack.multi_key_mappings[key_code].press) { - free(g_sound_pack.multi_key_mappings[key_code].press); - } - g_sound_pack.multi_key_mappings[key_code].press = strdup(full_filename); - } - } - } - } - } else { // Single mode - if (json_object_object_get_ex(root, "sound", &obj)) { - char temp_sound_file[256]; - strncpy(temp_sound_file, json_object_get_string(obj), sizeof(temp_sound_file) - 1); - temp_sound_file[sizeof(temp_sound_file) - 1] = '\0'; - get_full_path(g_sound_pack.sound_file, sizeof(g_sound_pack.sound_file), config_dir, temp_sound_file); - printf("Single mode sound file: %s\n", g_sound_pack.sound_file); - } - - if (json_object_object_get_ex(root, "defines", &obj)) { - json_object_object_foreach(obj, key, val) { - int key_code = atoi(key); - if (key_code >= 0 && key_code < 256 && - json_object_is_type(val, json_type_array) && - json_object_array_length(val) >= 2) { - g_sound_pack.key_mappings[key_code].start_ms = json_object_get_int(json_object_array_get_idx(val, 0)); - g_sound_pack.key_mappings[key_code].duration_ms = json_object_get_int(json_object_array_get_idx(val, 1)); - } - } - } - } - - json_object_put(root); - return 0; -} - -int init_audio() { - // For multi mode, we don't need to check a single sound file - if (g_sound_pack.is_multi) { - // printf("Multi mode: Audio will be initialized per sound file\n"); - return 0; - } - - // For single mode, check the main sound file - if (strlen(g_sound_pack.sound_file) == 0) { - fprintf(stderr, "Error: No sound file specified in config\n"); - return -1; - } - - if (access(g_sound_pack.sound_file, R_OK) != 0) { - fprintf(stderr, "Sound file not accessible: %s\n", g_sound_pack.sound_file); - perror("access"); - return -1; - } - - // Test open sound file to get info - SNDFILE *test_sf = sf_open(g_sound_pack.sound_file, SFM_READ, &g_sound_pack.sf_info); - if (!test_sf) { - fprintf(stderr, "Could not open sound file: %s\n", g_sound_pack.sound_file); - fprintf(stderr, "libsndfile error: %s\n", sf_strerror(NULL)); - return -1; - } - sf_close(test_sf); - - printf("Sound file info: %ld frames, %d channels, %d Hz\n", - g_sound_pack.sf_info.frames, g_sound_pack.sf_info.channels, g_sound_pack.sf_info.samplerate); - - return 0; -} - -void* play_sound_thread(void* arg) { - PlaybackData *data = (PlaybackData*)arg; - int key_code = data->key_code; - int thread_id = data->thread_id; - int is_pressed = data->is_pressed; - - const char *file_to_play = NULL; - - if (g_verbose) { - printf("Thread %d: Playing sound for key %d (%s)\n", - thread_id, key_code, is_pressed ? "press" : "release"); - } - - if (g_sound_pack.is_multi) { - // First try exact match - if (is_pressed && g_sound_pack.multi_key_mappings[key_code].press) { - file_to_play = g_sound_pack.multi_key_mappings[key_code].press; - } else if (!is_pressed && g_sound_pack.multi_key_mappings[key_code].release) { - file_to_play = g_sound_pack.multi_key_mappings[key_code].release; - } else if (is_pressed && g_sound_pack.num_generic_press_files > 0) { - // Fallback: random generic press - int idx = rand() % g_sound_pack.num_generic_press_files; - file_to_play = g_sound_pack.generic_press_files[idx]; - } else if (!is_pressed && strlen(g_sound_pack.release_file) > 0) { - file_to_play = g_sound_pack.release_file; - } - - if (!file_to_play) { - if (g_verbose) { - printf("Thread %d: No sound file found for key %d (%s)\n", - thread_id, key_code, is_pressed ? "press" : "release"); - } - goto exit_cleanup; - } - - if (g_verbose) { - printf("Thread %d: Using sound file: %s\n", thread_id, file_to_play); - } - - SF_INFO sf_info = {0}; - SNDFILE *sf = sf_open(file_to_play, SFM_READ, &sf_info); - if (!sf) { - fprintf(stderr, "Could not open sound file: %s (Error: %s)\n", - file_to_play, sf_strerror(NULL)); - goto exit_cleanup; - } - - pa_sample_spec ss = { - .format = PA_SAMPLE_S16LE, - .rate = sf_info.samplerate, - .channels = sf_info.channels - }; - - int pa_error; - pa_simple *pa_handle = pa_simple_new(NULL, "KeyboardSounds", PA_STREAM_PLAYBACK, - NULL, "playback", &ss, NULL, NULL, &pa_error); - if (!pa_handle) { - fprintf(stderr, "Could not initialize PulseAudio: %s\n", pa_strerror(pa_error)); - sf_close(sf); - goto exit_cleanup; - } - - int frames = 2048; - short *buffer = malloc(frames * sf_info.channels * sizeof(short)); - if (!buffer) { - pa_simple_free(pa_handle); - sf_close(sf); - goto exit_cleanup; - } - - sf_count_t read; - while ((read = sf_readf_short(sf, buffer, frames)) > 0) { - for (sf_count_t i = 0; i < read * sf_info.channels; i++) { - buffer[i] = (short)(buffer[i] * g_volume); - } - - int pa_write_error; - if (pa_simple_write(pa_handle, buffer, read * sf_info.channels * sizeof(short), &pa_write_error) < 0) { - fprintf(stderr, "PulseAudio write error: %s\n", pa_strerror(pa_write_error)); - break; - } - } - - int pa_drain_error; - pa_simple_drain(pa_handle, &pa_drain_error); - pa_simple_free(pa_handle); - sf_close(sf); - free(buffer); - } else { - if (key_code >= 256 || g_sound_pack.key_mappings[key_code].duration_ms == 0) { - if (g_verbose) { - printf("Thread %d: No mapping for key %d\n", thread_id, key_code); - } - goto exit_cleanup; - } - - SoundMapping *mapping = &g_sound_pack.key_mappings[key_code]; - - SF_INFO sf_info = g_sound_pack.sf_info; - SNDFILE *sf = sf_open(g_sound_pack.sound_file, SFM_READ, &sf_info); - if (!sf) { - fprintf(stderr, "Thread: Could not open sound file\n"); - goto exit_cleanup; - } - - pa_sample_spec ss = { - .format = PA_SAMPLE_S16LE, - .rate = sf_info.samplerate, - .channels = sf_info.channels - }; - - int pa_error; - pa_simple *pa_handle = pa_simple_new(NULL, "KeyboardSounds", PA_STREAM_PLAYBACK, - NULL, "playback", &ss, NULL, NULL, &pa_error); - if (!pa_handle) { - sf_close(sf); - fprintf(stderr, "Thread: Could not initialize PulseAudio: %s\n", pa_strerror(pa_error)); - goto exit_cleanup; - } - - sf_count_t start_frame = (mapping->start_ms * sf_info.samplerate) / 1000; - sf_count_t duration_frames = (mapping->duration_ms * sf_info.samplerate) / 1000; - - sf_seek(sf, start_frame, SEEK_SET); - - short *buffer = malloc(duration_frames * sf_info.channels * sizeof(short)); - if (!buffer) { - pa_simple_free(pa_handle); - sf_close(sf); - goto exit_cleanup; - } - - sf_count_t frames_read = sf_readf_short(sf, buffer, duration_frames); - - for (sf_count_t i = 0; i < frames_read * sf_info.channels; i++) { - buffer[i] = (short)(buffer[i] * g_volume); - } - - int pa_write_error, pa_drain_error; - pa_simple_write(pa_handle, buffer, frames_read * sf_info.channels * sizeof(short), &pa_write_error); - pa_simple_drain(pa_handle, &pa_drain_error); - - free(buffer); - pa_simple_free(pa_handle); - sf_close(sf); - } - -exit_cleanup: - pthread_mutex_lock(&thread_mutex); - thread_active[thread_id] = 0; - pthread_mutex_unlock(&thread_mutex); - free(data); - return NULL; -} - -int find_available_thread_slot() { - pthread_mutex_lock(&thread_mutex); - - // Find available slot - for (int i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { - if (!thread_active[i]) { - pthread_mutex_unlock(&thread_mutex); - return i; - } - } - - pthread_mutex_unlock(&thread_mutex); - return -1; // No available slots -} - -void play_sound_segment(int key_code, int is_pressed) { - // Only play sound on key press in single mode - if (!g_sound_pack.is_multi && !is_pressed) { - if (g_verbose) { - printf("Single mode: Ignoring key release for key %d\n", key_code); - } - return; - } - - int slot = find_available_thread_slot(); - if (slot == -1) { - if (g_verbose) { - printf("Warning: No available thread slots\n"); - } - return; - } - - PlaybackData *data = malloc(sizeof(PlaybackData)); - if (!data) return; - - data->key_code = key_code; - data->sound_pack = &g_sound_pack; - data->thread_id = slot; - data->is_pressed = is_pressed; - - pthread_mutex_lock(&thread_mutex); - thread_active[slot] = 1; - - if (pthread_create(&sound_threads[slot], NULL, play_sound_thread, data) != 0) { - thread_active[slot] = 0; - free(data); - fprintf(stderr, "Failed to create sound thread\n"); - } else { - pthread_detach(sound_threads[slot]); - } - - pthread_mutex_unlock(&thread_mutex); -} - -int parse_keyboard_event(const char *json_line, int *key_code, int *is_pressed) { - // Strip newline if present - char *line_copy = strdup(json_line); - if (!line_copy) return -1; - - char *newline = strchr(line_copy, '\n'); - if (newline) *newline = '\0'; - - if (g_verbose) { - printf("Parsing JSON: %s\n", line_copy); - } - - json_object *root = json_tokener_parse(line_copy); - free(line_copy); - - if (!root) { - fprintf(stderr, "Failed to parse JSON: %s\n", json_line); - return -1; - } - - json_object *key_code_obj, *state_code_obj; - - if (json_object_object_get_ex(root, "key_code", &key_code_obj) && - json_object_object_get_ex(root, "state_code", &state_code_obj)) { - - *key_code = json_object_get_int(key_code_obj); - *is_pressed = json_object_get_int(state_code_obj); - - if (g_verbose) { - printf("Parsed key event: key_code=%d, is_pressed=%d\n", *key_code, *is_pressed); - } - - json_object_put(root); - return 0; - } - - json_object_put(root); - return -1; -} - -void cleanup() { - printf("Cleaning up...\n"); - - // Give threads a moment to finish naturally - usleep(500000); // 500ms - - // Free dynamically allocated filenames in multi config - for (int i = 0; i < 256; i++) { - if (g_sound_pack.multi_key_mappings[i].press) { - free(g_sound_pack.multi_key_mappings[i].press); - g_sound_pack.multi_key_mappings[i].press = NULL; - } - if (g_sound_pack.multi_key_mappings[i].release) { - free(g_sound_pack.multi_key_mappings[i].release); - g_sound_pack.multi_key_mappings[i].release = NULL; - } - } - - // Reset thread state - pthread_mutex_lock(&thread_mutex); - for (int i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { - thread_active[i] = 0; - } - pthread_mutex_unlock(&thread_mutex); -} - -int main(int argc, char *argv[]) { - if (argc < 2 || argc > 4) { - fprintf(stderr, "Usage: %s [volume] [verbose]\n", argv[0]); - fprintf(stderr, " volume: 0-100 (default: 50)\n"); - fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n"); - return 1; - } - - // Set volume - if (argc >= 3) { - int volume_percent = atoi(argv[2]); - if (volume_percent < 0) volume_percent = 0; - if (volume_percent > 100) volume_percent = 100; - g_volume = volume_percent / 100.0f; - printf("Volume set to: %d%%\n", volume_percent); - } else { - printf("Volume set to: 50%% (default)\n"); - } - - // Set verbose mode - if (argc >= 4) { - g_verbose = atoi(argv[3]); - if (g_verbose) { - printf("Verbose mode enabled\n"); - } - } - - // Load sound configuration - if (load_sound_config(argv[1]) != 0) { - fprintf(stderr, "Failed to load sound configuration\n"); - return 1; - } - - // Initialize audio - if (init_audio() != 0) { - fprintf(stderr, "Failed to initialize audio\n"); - return 1; - } - - // printf("Keyboard sound player initialized. Listening for key events...\n"); - // printf("Max concurrent sounds: %d\n", MAX_CONCURRENT_SOUNDS); - // printf("Waiting for input on stdin...\n"); - // fflush(stdout); - - // Add timeout for debugging - fd_set readfds; - struct timeval timeout; - - // Read JSON lines from stdin with timeout - char line[MAX_LINE_LENGTH]; - while (1) { - FD_ZERO(&readfds); - FD_SET(STDIN_FILENO, &readfds); - - timeout.tv_sec = 1; // 1 second timeout - timeout.tv_usec = 0; - - int ready = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout); - - if (ready == -1) { - perror("select"); - break; - } else if (ready == 0) { - // Timeout - continue waiting - if (g_verbose) { - printf("Waiting for input...\n"); - } - continue; - } - - if (FD_ISSET(STDIN_FILENO, &readfds)) { - if (fgets(line, sizeof(line), stdin) == NULL) { - if (feof(stdin)) { - printf("EOF reached on stdin\n"); - } else { - perror("fgets"); - } - break; - } - - int key_code, is_pressed; - if (parse_keyboard_event(line, &key_code, &is_pressed) == 0) { - play_sound_segment(key_code, is_pressed); - } - } - } - - cleanup(); - return 0; -} diff --git a/src/keyvibe_audio.c b/src/keyvibe_audio.c new file mode 100644 index 0000000..25d19e3 --- /dev/null +++ b/src/keyvibe_audio.c @@ -0,0 +1,546 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_LINE_LENGTH 1024 +#define MAX_CONCURRENT_SOUNDS 10 + +typedef struct { + int start_ms; + int duration_ms; +} SoundMapping; +typedef struct { + char press_file[256]; + char release_file[256]; + char generic_press_files[5][256]; + int num_generic_press_files; + char sound_file[256]; + SoundMapping key_mappings[256]; + struct { + char *press; + char *release; + } multi_key_mappings[256]; + int is_multi; + SF_INFO sf_info; +} SoundPack; +typedef struct { + int key_code; + SoundPack *sound_pack; + int thread_id; + int is_pressed; +} PlaybackData; + +SoundPack g_sound_pack = {0}; +float g_volume = 1.0f; +int g_verbose = 0; +pthread_t sound_threads[MAX_CONCURRENT_SOUNDS]; +volatile int thread_active[MAX_CONCURRENT_SOUNDS] = {0}; +pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void get_full_path(char *buffer, size_t buffer_size, + const char *base_dir, const char *filename) { + if (!filename || !base_dir) { + buffer[0] = '\0'; + return; + } + if (filename[0] == '/') { + strncpy(buffer, filename, buffer_size - 1); + buffer[buffer_size - 1] = '\0'; + } else { + snprintf(buffer, buffer_size, "%s/%s", base_dir, filename); + } +} + +int load_sound_config(const char *config_path) { + FILE *file = fopen(config_path, "r"); + if (!file) { + fprintf(stderr, "Error: Cannot open config file: %s\n", config_path); + perror("fopen"); + return -1; + } + char config_path_copy[MAX_LINE_LENGTH]; + strncpy(config_path_copy, config_path, sizeof(config_path_copy) - 1); + config_path_copy[sizeof(config_path_copy) - 1] = '\0'; + char *config_dir = dirname(config_path_copy); + fseek(file, 0, SEEK_END); + long size = ftell(file); + rewind(file); + char *json_string = malloc(size + 1); + if (!json_string) { + fclose(file); + fprintf(stderr, "Error: Memory allocation failed\n"); + return -1; + } + fread(json_string, 1, size, file); + json_string[size] = '\0'; + fclose(file); + json_object *root = json_tokener_parse(json_string); + free(json_string); + if (!root) { + fprintf(stderr, "Error: Invalid JSON in config file\n"); + return -1; + } + const char *key_type = "single"; + json_object *obj; + if (json_object_object_get_ex(root, "key_define_type", &obj)) + key_type = json_object_get_string(obj); + g_sound_pack.is_multi = strcmp(key_type, "multi") == 0; + printf("Config loaded: Using %s mode\n", + g_sound_pack.is_multi ? "multi" : "single"); + if (g_sound_pack.is_multi) { + g_sound_pack.num_generic_press_files = 0; + if (json_object_object_get_ex(root, "sound", &obj)) { + const char *pattern = json_object_get_string(obj); + printf("Sound pattern: %s\n", pattern); + if (strstr(pattern, "%d") || strstr(pattern, "{")) { + for (int i = 0; i <= 4; i++) { + char temp_filename[256]; + if (strstr(pattern, "{")) { + char temp_pattern[256]; + strncpy(temp_pattern, pattern, sizeof(temp_pattern) - 1); + temp_pattern[sizeof(temp_pattern) - 1] = '\0'; + char *brace_start = strstr(temp_pattern, "{"); + char *brace_end = strstr(temp_pattern, "}"); + if (brace_start && brace_end) { + *brace_start = '%'; + *(brace_start + 1) = 'd'; + memmove(brace_start + 2, brace_end + 1, + strlen(brace_end + 1) + 1); + } + snprintf(temp_filename, sizeof(temp_filename), temp_pattern, i); + } else { + snprintf(temp_filename, sizeof(temp_filename), pattern, i); + } + get_full_path(g_sound_pack.generic_press_files[i], + sizeof(g_sound_pack.generic_press_files[i]), config_dir, + temp_filename); + if (access(g_sound_pack.generic_press_files[i], R_OK) == 0) { + g_sound_pack.num_generic_press_files = i + 1; + } else { + printf("Generic sound file not found: %s\n", + g_sound_pack.generic_press_files[i]); + break; + } + } + } else { + get_full_path(g_sound_pack.generic_press_files[0], + sizeof(g_sound_pack.generic_press_files[0]), config_dir, + pattern); + if (access(g_sound_pack.generic_press_files[0], R_OK) == 0) { + g_sound_pack.num_generic_press_files = 1; + printf("Found single generic sound file: %s\n", + g_sound_pack.generic_press_files[0]); + } + } + printf("Total generic press sound files: %d\n", + g_sound_pack.num_generic_press_files); + } + if (json_object_object_get_ex(root, "soundup", &obj)) { + char temp_release_file[256]; + strncpy(temp_release_file, json_object_get_string(obj), + sizeof(temp_release_file) - 1); + temp_release_file[sizeof(temp_release_file) - 1] = '\0'; + get_full_path(g_sound_pack.release_file, + sizeof(g_sound_pack.release_file), config_dir, + temp_release_file); + printf("Release sound file: %s\n", g_sound_pack.release_file); + } + if (json_object_object_get_ex(root, "defines", &obj)) { + json_object_object_foreach(obj, key, val) { + int key_code; + int is_release = 0; + char *dash = strstr(key, "-up"); + if (dash) { + char key_copy[16]; + strncpy(key_copy, key, dash - key); + key_copy[dash - key] = '\0'; + key_code = atoi(key_copy); + is_release = 1; + } else { + key_code = atoi(key); + } + if (key_code >= 0 && key_code < 256) { + const char *filename_relative = json_object_get_string(val); + char full_filename[MAX_LINE_LENGTH]; + get_full_path(full_filename, sizeof(full_filename), config_dir, + filename_relative); + if (is_release) { + if (g_sound_pack.multi_key_mappings[key_code].release) + free(g_sound_pack.multi_key_mappings[key_code].release); + g_sound_pack.multi_key_mappings[key_code].release = + strdup(full_filename); + } else { + if (g_sound_pack.multi_key_mappings[key_code].press) + free(g_sound_pack.multi_key_mappings[key_code].press); + g_sound_pack.multi_key_mappings[key_code].press = + strdup(full_filename); + } + } + } + } + } else { + if (json_object_object_get_ex(root, "sound", &obj)) { + char temp_sound_file[256]; + strncpy(temp_sound_file, json_object_get_string(obj), + sizeof(temp_sound_file) - 1); + temp_sound_file[sizeof(temp_sound_file) - 1] = '\0'; + get_full_path(g_sound_pack.sound_file, sizeof(g_sound_pack.sound_file), + config_dir, temp_sound_file); + printf("Single mode sound file: %s\n", g_sound_pack.sound_file); + } + if (json_object_object_get_ex(root, "defines", &obj)) { + json_object_object_foreach(obj, key, val) { + int key_code = atoi(key); + if (key_code >= 0 && key_code < 256 && + json_object_is_type(val, json_type_array) && + json_object_array_length(val) >= 2) { + g_sound_pack.key_mappings[key_code].start_ms = + json_object_get_int(json_object_array_get_idx(val, 0)); + g_sound_pack.key_mappings[key_code].duration_ms = + json_object_get_int(json_object_array_get_idx(val, 1)); + } + } + } + } + json_object_put(root); + return 0; +} + +int init_audio() { + if (g_sound_pack.is_multi) + return 0; + if (strlen(g_sound_pack.sound_file) == 0) { + fprintf(stderr, "Error: No sound file specified in config\n"); + return -1; + } + if (access(g_sound_pack.sound_file, R_OK) != 0) { + fprintf(stderr, "Sound file not accessible: %s\n", g_sound_pack.sound_file); + perror("access"); + return -1; + } + SNDFILE *test_sf = + sf_open(g_sound_pack.sound_file, SFM_READ, &g_sound_pack.sf_info); + if (!test_sf) { + fprintf(stderr, "Could not open sound file: %s\n", g_sound_pack.sound_file); + fprintf(stderr, "libsndfile error: %s\n", sf_strerror(NULL)); + return -1; + } + sf_close(test_sf); + printf("Sound file info: %ld frames, %d channels, %d Hz\n", + g_sound_pack.sf_info.frames, g_sound_pack.sf_info.channels, + g_sound_pack.sf_info.samplerate); + return 0; +} + +void *play_sound_thread(void *arg) { + PlaybackData *data = (PlaybackData *)arg; + int key_code = data->key_code; + int thread_id = data->thread_id; + int is_pressed = data->is_pressed; + const char *file_to_play = NULL; + if (g_verbose) { + printf("Thread %d: Playing sound for key %d (%s)\n", thread_id, key_code, + is_pressed ? "press" : "release"); + } + if (g_sound_pack.is_multi) { + if (is_pressed && g_sound_pack.multi_key_mappings[key_code].press) + file_to_play = g_sound_pack.multi_key_mappings[key_code].press; + else if (!is_pressed && g_sound_pack.multi_key_mappings[key_code].release) + file_to_play = g_sound_pack.multi_key_mappings[key_code].release; + else if (is_pressed && g_sound_pack.num_generic_press_files > 0) { + int idx = rand() % g_sound_pack.num_generic_press_files; + file_to_play = g_sound_pack.generic_press_files[idx]; + } else if (!is_pressed && strlen(g_sound_pack.release_file) > 0) + file_to_play = g_sound_pack.release_file; + if (!file_to_play) { + if (g_verbose) { + printf("Thread %d: No sound file found for key %d (%s)\n", thread_id, + key_code, is_pressed ? "press" : "release"); + } + goto exit_cleanup; + } + if (g_verbose) { + printf("Thread %d: Using sound file: %s\n", thread_id, file_to_play); + } + SF_INFO sf_info = {0}; + SNDFILE *sf = sf_open(file_to_play, SFM_READ, &sf_info); + if (!sf) { + fprintf(stderr, "Could not open sound file: %s (Error: %s)\n", + file_to_play, sf_strerror(NULL)); + goto exit_cleanup; + } + pa_sample_spec ss = {.format = PA_SAMPLE_S16LE, + .rate = sf_info.samplerate, + .channels = sf_info.channels}; + int pa_error; + pa_simple *pa_handle = + pa_simple_new(NULL, "KeyboardSounds", PA_STREAM_PLAYBACK, NULL, + "playback", &ss, NULL, NULL, &pa_error); + if (!pa_handle) { + fprintf(stderr, "Could not initialize PulseAudio: %s\n", + pa_strerror(pa_error)); + sf_close(sf); + goto exit_cleanup; + } + int frames = 2048; + short *buffer = malloc(frames * sf_info.channels * sizeof(short)); + if (!buffer) { + pa_simple_free(pa_handle); + sf_close(sf); + goto exit_cleanup; + } + sf_count_t read; + while ((read = sf_readf_short(sf, buffer, frames)) > 0) { + for (sf_count_t i = 0; i < read * sf_info.channels; i++) { + buffer[i] = (short)(buffer[i] * g_volume); + } + int pa_write_error; + if (pa_simple_write(pa_handle, buffer, + read * sf_info.channels * sizeof(short), + &pa_write_error) < 0) { + fprintf(stderr, "PulseAudio write error: %s\n", + pa_strerror(pa_write_error)); + break; + } + } + int pa_drain_error; + pa_simple_drain(pa_handle, &pa_drain_error); + pa_simple_free(pa_handle); + sf_close(sf); + free(buffer); + } else { + if (key_code >= 256 || + g_sound_pack.key_mappings[key_code].duration_ms == 0) { + if (g_verbose) { + printf("Thread %d: No mapping for key %d\n", thread_id, key_code); + } + goto exit_cleanup; + } + SoundMapping *mapping = &g_sound_pack.key_mappings[key_code]; + SF_INFO sf_info = g_sound_pack.sf_info; + SNDFILE *sf = sf_open(g_sound_pack.sound_file, SFM_READ, &sf_info); + if (!sf) { + fprintf(stderr, "Thread: Could not open sound file\n"); + goto exit_cleanup; + } + pa_sample_spec ss = {.format = PA_SAMPLE_S16LE, + .rate = sf_info.samplerate, + .channels = sf_info.channels}; + int pa_error; + pa_simple *pa_handle = + pa_simple_new(NULL, "KeyboardSounds", PA_STREAM_PLAYBACK, NULL, + "playback", &ss, NULL, NULL, &pa_error); + if (!pa_handle) { + sf_close(sf); + fprintf(stderr, "Thread: Could not initialize PulseAudio: %s\n", + pa_strerror(pa_error)); + goto exit_cleanup; + } + sf_count_t start_frame = (mapping->start_ms * sf_info.samplerate) / 1000; + sf_count_t duration_frames = + (mapping->duration_ms * sf_info.samplerate) / 1000; + sf_seek(sf, start_frame, SEEK_SET); + short *buffer = malloc(duration_frames * sf_info.channels * sizeof(short)); + if (!buffer) { + pa_simple_free(pa_handle); + sf_close(sf); + goto exit_cleanup; + } + sf_count_t frames_read = sf_readf_short(sf, buffer, duration_frames); + for (sf_count_t i = 0; i < frames_read * sf_info.channels; i++) { + buffer[i] = (short)(buffer[i] * g_volume); + } + int pa_write_error, pa_drain_error; + pa_simple_write(pa_handle, buffer, + frames_read * sf_info.channels * sizeof(short), + &pa_write_error); + pa_simple_drain(pa_handle, &pa_drain_error); + free(buffer); + pa_simple_free(pa_handle); + sf_close(sf); + } +exit_cleanup: + pthread_mutex_lock(&thread_mutex); + thread_active[thread_id] = 0; + pthread_mutex_unlock(&thread_mutex); + free(data); + return NULL; +} + +int find_available_thread_slot() { + pthread_mutex_lock(&thread_mutex); + for (int i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { + if (!thread_active[i]) { + pthread_mutex_unlock(&thread_mutex); + return i; + } + } + pthread_mutex_unlock(&thread_mutex); + return -1; +} + +void play_sound_segment(int key_code, int is_pressed) { + if (!g_sound_pack.is_multi && !is_pressed) { + if (g_verbose) { + printf("Single mode: Ignoring key release for key %d\n", key_code); + } + return; + } + int slot = find_available_thread_slot(); + if (slot == -1) { + if (g_verbose) { + printf("Warning: No available thread slots\n"); + } + return; + } + PlaybackData *data = malloc(sizeof(PlaybackData)); + if (!data) + return; + data->key_code = key_code; + data->sound_pack = &g_sound_pack; + data->thread_id = slot; + data->is_pressed = is_pressed; + pthread_mutex_lock(&thread_mutex); + thread_active[slot] = 1; + if (pthread_create(&sound_threads[slot], NULL, play_sound_thread, data) != + 0) { + thread_active[slot] = 0; + free(data); + fprintf(stderr, "Failed to create sound thread\n"); + } else { + pthread_detach(sound_threads[slot]); + } + pthread_mutex_unlock(&thread_mutex); +} + +int parse_keyboard_event(const char *json_line, int *key_code, + int *is_pressed) { + char *line_copy = strdup(json_line); + if (!line_copy) + return -1; + char *newline = strchr(line_copy, '\n'); + if (newline) + *newline = '\0'; + if (g_verbose) { + printf("Parsing JSON: %s\n", line_copy); + } + json_object *root = json_tokener_parse(line_copy); + free(line_copy); + if (!root) { + fprintf(stderr, "Failed to parse JSON: %s\n", json_line); + return -1; + } + json_object *key_code_obj, *state_code_obj; + if (json_object_object_get_ex(root, "key_code", &key_code_obj) && + json_object_object_get_ex(root, "state_code", &state_code_obj)) { + *key_code = json_object_get_int(key_code_obj); + *is_pressed = json_object_get_int(state_code_obj); + if (g_verbose) { + printf("Parsed key event: key_code=%d, is_pressed=%d\n", *key_code, + *is_pressed); + } + json_object_put(root); + return 0; + } + json_object_put(root); + return -1; +} + +void cleanup() { + printf("Cleaning up...\n"); + usleep(500000); + for (int i = 0; i < 256; i++) { + if (g_sound_pack.multi_key_mappings[i].press) { + free(g_sound_pack.multi_key_mappings[i].press); + g_sound_pack.multi_key_mappings[i].press = NULL; + } + if (g_sound_pack.multi_key_mappings[i].release) { + free(g_sound_pack.multi_key_mappings[i].release); + g_sound_pack.multi_key_mappings[i].release = NULL; + } + } + pthread_mutex_lock(&thread_mutex); + for (int i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { + thread_active[i] = 0; + } + pthread_mutex_unlock(&thread_mutex); +} + +int main(int argc, char *argv[]) { + if (argc < 2 || argc > 4) { + fprintf(stderr, "Usage: %s [volume] [verbose]\n", argv[0]); + fprintf(stderr, " volume: 0-100 (default: 50)\n"); + fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n"); + return 1; + } + if (argc >= 3) { + int volume_percent = atoi(argv[2]); + if (volume_percent < 0) + volume_percent = 0; + if (volume_percent > 100) + volume_percent = 100; + g_volume = volume_percent / 100.0f; + printf("Volume set to: %d%%\n", volume_percent); + } else { + printf("Volume set to: 50%% (default)\n"); + } + if (argc >= 4) { + g_verbose = atoi(argv[3]); + if (g_verbose) { + printf("Verbose mode enabled\n"); + } + } + if (load_sound_config(argv[1]) != 0) { + fprintf(stderr, "Failed to load sound configuration\n"); + return 1; + } + if (init_audio() != 0) { + fprintf(stderr, "Failed to initialize audio\n"); + return 1; + } + fd_set readfds; + struct timeval timeout; + char line[MAX_LINE_LENGTH]; + while (1) { + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + timeout.tv_sec = 1; + timeout.tv_usec = 0; + int ready = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout); + if (ready == -1) { + perror("select"); + break; + } else if (ready == 0) { + if (g_verbose) { + printf("Waiting for input...\n"); + } + continue; + } + if (FD_ISSET(STDIN_FILENO, &readfds)) { + if (fgets(line, sizeof(line), stdin) == NULL) { + if (feof(stdin)) { + printf("EOF reached on stdin\n"); + } else { + perror("fgets"); + } + break; + } + int key_code, is_pressed; + if (parse_keyboard_event(line, &key_code, &is_pressed) == 0) { + play_sound_segment(key_code, is_pressed); + } + } + } + cleanup(); + return 0; +} diff --git a/src/keyvibe_input.c b/src/keyvibe_input.c new file mode 100644 index 0000000..c6cb073 --- /dev/null +++ b/src/keyvibe_input.c @@ -0,0 +1,202 @@ +// Renamed from get_key_presses.c to keyvibe_input.c (inlined) +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "config.h" + +#define MAX_BUFFER_LENGTH 512 + +enum error_code { + NO_ERROR, + UDEV_FAILED, + LIBINPUT_FAILED, + SEAT_FAILED, + PERMISSION_FAILED +}; + +struct input_handler_data { + struct udev *udev; + struct libinput *libinput; +}; + +static void *handle_input(void *user_data) { + struct input_handler_data *input_handler_data = user_data; + char line[MAX_BUFFER_LENGTH]; + while (fgets(line, MAX_BUFFER_LENGTH, stdin) != NULL) { + if (strcmp(line, "stop\n") == 0) { + libinput_unref(input_handler_data->libinput); + udev_unref(input_handler_data->udev); + exit(EXIT_SUCCESS); + } + } + return NULL; +} + +static int open_restricted(const char *path, int flags, void *user_data) { + (void)user_data; + int fd = open(path, flags); + if (fd < 0) + fprintf(stderr, "Failed to open %s because of %s.\n", path, + strerror(errno)); + return fd < 0 ? -errno : fd; +} + +static void close_restricted(int fd, void *user_data) { + (void)user_data; + close(fd); +} + +static const struct libinput_interface interface = { + .open_restricted = open_restricted, .close_restricted = close_restricted}; + +static int print_key_event(struct libinput_event *event) { + struct libinput_event_keyboard *keyboard = + libinput_event_get_keyboard_event(event); + enum libinput_event_type event_type = libinput_event_get_type(event); + uint32_t time_stamp = libinput_event_keyboard_get_time(keyboard); + uint32_t key_code = libinput_event_keyboard_get_key(keyboard); + const char *key_name = libevdev_event_code_get_name(EV_KEY, key_code); + key_name = key_name ? key_name : "null"; + enum libinput_key_state state_code = + libinput_event_keyboard_get_key_state(keyboard); + const char *state_name = + state_code == LIBINPUT_KEY_STATE_PRESSED ? "PRESSED" : "RELEASED"; + return printf("{\"event_name\": \"KEYBOARD_KEY\", \"event_type\": %d, " + "\"time_stamp\": %d, \"key_name\": \"%s\", \"key_code\": %d, " + "\"state_name\": \"%s\", \"state_code\": %d}\n", + event_type, time_stamp, key_name, key_code, state_name, + state_code); +} + +static int print_button_event(struct libinput_event *event) { + struct libinput_event_pointer *pointer = + libinput_event_get_pointer_event(event); + enum libinput_event_type event_type = libinput_event_get_type(event); + uint32_t time_stamp = libinput_event_pointer_get_time(pointer); + uint32_t button_code = libinput_event_pointer_get_button(pointer); + const char *button_name = libevdev_event_code_get_name(EV_KEY, button_code); + enum libinput_button_state state_code = + libinput_event_pointer_get_button_state(pointer); + const char *state_name = + state_code == LIBINPUT_BUTTON_STATE_PRESSED ? "PRESSED" : "RELEASED"; + return printf("{\"event_name\": \"POINTER_BUTTON\", \"event_type\": %d, " + "\"time_stamp\": %d, \"key_name\": \"%s\", \"key_code\": %d, " + "\"state_name\": \"%s\", \"state_code\": %d}\n", + event_type, time_stamp, button_name, button_code, state_name, + state_code); +} + +static int handle_events(struct libinput *libinput) { + int result = -1; + struct libinput_event *event; + if (libinput_dispatch(libinput) < 0) + return result; + while ((event = libinput_get_event(libinput)) != NULL) { + switch (libinput_event_get_type(event)) { + case LIBINPUT_EVENT_KEYBOARD_KEY: + print_key_event(event); + break; + case LIBINPUT_EVENT_POINTER_BUTTON: + print_button_event(event); + break; + default: + break; + } + fflush(stdout); + libinput_event_destroy(event); + result = 0; + } + return result; +} + +static int run_mainloop(struct libinput *libinput) { + struct pollfd fd; + fd.fd = libinput_get_fd(libinput); + fd.events = POLLIN; + fd.revents = 0; + if (handle_events(libinput) != 0) { + fprintf(stderr, "Expected device added events on startup but got none. " + "Maybe you don't have the right permissions?\n"); + return -1; + } + while (poll(&fd, 1, -1) > -1) + handle_events(libinput); + return 0; +} + +void print_help(char *program_name) { + printf("The backend of Show Me The Key.\n"); + printf("Version " PROJECT_VERSION ".\n"); + printf("Usage: %s [OPTION…]\n", program_name); + printf("Options:\n"); + printf("\t-h, --help\tDisplay help then exit.\n"); + printf("\t-v, --version\tDisplay version then exit.\n"); + printf("Warning: This is the backend and is not designed to run by users. " + "You should run the frontend of Show Me The Key, and the frontend " + "will run this.\n"); +} + +int main(int argc, char *argv[]) { + const struct option long_options[] = {{"version", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {NULL, 0, NULL, 0}}; + int option_index = 0; + int opt = 0; + while ((opt = getopt_long(argc, argv, "vh", long_options, &option_index)) != + -1) { + switch (opt) { + case 0: + break; + case 'v': + printf(PROJECT_VERSION "\n"); + return 0; + case 'h': + print_help(argv[0]); + return 0; + case '?': + break; + default: + fprintf(stderr, "%s: Invalid option `-%c`.\n", argv[0], opt); + break; + } + } + struct udev *udev = udev_new(); + if (udev == NULL) { + fprintf(stderr, "Failed to initialize udev.\n"); + return UDEV_FAILED; + } + struct libinput *libinput = + libinput_udev_create_context(&interface, NULL, udev); + if (!libinput) { + fprintf(stderr, "Failed to initialize libinput from udev.\n"); + return LIBINPUT_FAILED; + } + if (libinput_udev_assign_seat(libinput, "seat0") != 0) { + fprintf(stderr, "Failed to set seat.\n"); + libinput_unref(libinput); + udev_unref(udev); + return SEAT_FAILED; + } + pthread_t input_handler; + struct input_handler_data input_handler_data = {udev, libinput}; + pthread_create(&input_handler, NULL, handle_input, &input_handler_data); + if (run_mainloop(libinput) < 0) + return PERMISSION_FAILED; + libinput_unref(libinput); + udev_unref(udev); + return NO_ERROR; +} diff --git a/keyvibe.c b/src/keyvibe_main.c similarity index 83% rename from keyvibe.c rename to src/keyvibe_main.c index 649d34c..b589523 100644 --- a/keyvibe.c +++ b/src/keyvibe_main.c @@ -1,4 +1,3 @@ - #define _POSIX_C_SOURCE 200809L #include "config.h" @@ -81,14 +80,9 @@ int list_sound_packs() { printf("======================\n"); while ((entry = readdir(dir)) != NULL) { - // Skip . and .. directories if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; - - // Build full path snprintf(path, sizeof(path), "%s/%s", AUDIO_BASE_DIR, entry->d_name); - - // Use stat to check if it's a directory if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { printf(" %s\n", entry->d_name); } @@ -106,68 +100,54 @@ int validate_sound_pack(const char *sound_name) { AUDIO_BASE_DIR, sound_name); snprintf(sound_path, sizeof(sound_path), "%s/%s", AUDIO_BASE_DIR, sound_name); - // Check if directory exists if (access(sound_path, F_OK) != 0) { fprintf(stderr, "Error: Sound pack '%s' not found in %s/\n", sound_name, AUDIO_BASE_DIR); fprintf(stderr, "Use --list to see available sound packs.\n"); return 0; } - - // Check if config.json exists if (access(config_path, R_OK) != 0) { fprintf(stderr, "Error: Config file not found: %s\n", config_path); return 0; } - return 1; } void cleanup_processes(int sig) { - (void)sig; // Suppress unused parameter warning - + (void)sig; if (!is_daemon) { printf("\nShutting down KeyVibe...\n"); } - if (sound_pid > 0) { kill(sound_pid, SIGTERM); waitpid(sound_pid, NULL, 0); } - if (keyboard_pid > 0) { kill(keyboard_pid, SIGTERM); waitpid(keyboard_pid, NULL, 0); } - if (pidfile_path[0] != '\0') { unlink(pidfile_path); } - exit(0); } -// Function to check if sudo credentials are cached int check_sudo_cached() { int status = system("sudo -n true 2>/dev/null"); return WEXITSTATUS(status) == 0; } -// Function to prompt for sudo password if needed int ensure_sudo_access() { if (check_sudo_cached()) { - return 1; // Already have sudo access + return 1; } - printf("This program requires sudo access to monitor keyboard events.\n"); printf("Please enter your password when prompted:\n"); - int status = system("sudo -v"); if (WEXITSTATUS(status) != 0) { fprintf(stderr, "Error: Failed to obtain sudo access\n"); return 0; } - return 1; } @@ -178,9 +158,8 @@ static char *get_user_config_path(char *buffer, size_t buflen) { if (pw && pw->pw_dir) home = pw->pw_dir; } - if (!home) { + if (!home) return NULL; - } snprintf(buffer, buflen, "%s/%s", home, USER_CONFIG_FILENAME); return buffer; } @@ -199,12 +178,10 @@ static int build_paths_for_sound(const char *sound_name, char *out_config_path, size_t name_len = strlen(sound_name); const char *cfg_suffix = "/config.json"; size_t cfg_suffix_len = strlen(cfg_suffix); - // Need: base + '/' + name + cfg_suffix + null if (base_len + 1 + name_len + cfg_suffix_len + 1 > out_config_sz) { fprintf(stderr, "Error: sound pack name too long for path buffer.\n"); return 0; } - // Need: base + '/' + name + null if (base_len + 1 + name_len + 1 > out_sound_sz) { fprintf(stderr, "Error: sound pack name too long for directory buffer.\n"); return 0; @@ -256,20 +233,16 @@ static void daemonize_self() { if (pid < 0) exit(1); if (pid > 0) - exit(0); // Parent exits - + exit(0); if (setsid() < 0) exit(1); - pid = fork(); if (pid < 0) exit(1); if (pid > 0) exit(0); - umask(0); chdir("/"); - int fd = open("/dev/null", O_RDWR); if (fd >= 0) { dup2(fd, STDIN_FILENO); @@ -301,8 +274,6 @@ static int start_children(const char *sound_dir, const char *config_path, perror("pipe"); return 0; } - - // Fork sound player first sound_pid = fork(); if (sound_pid == -1) { perror("fork"); @@ -310,11 +281,9 @@ static int start_children(const char *sound_dir, const char *config_path, close(pipefd[1]); return 0; } - char sound_player_path[MAX_PATH_LENGTH]; - snprintf(sound_player_path, sizeof(sound_player_path), - "%s/keyboard_sound_player", KeyVibe_BIN_DIR); - + snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio", + KeyVibe_BIN_DIR); if (sound_pid == 0) { close(pipefd[1]); dup2(pipefd[0], STDIN_FILENO); @@ -327,13 +296,11 @@ static int start_children(const char *sound_dir, const char *config_path, snprintf(volume_str, sizeof(volume_str), "%d", volume); char verbose_str[8]; snprintf(verbose_str, sizeof(verbose_str), "%d", verbose); - execl(sound_player_path, "keyboard_sound_player", "config.json", volume_str, + execl(sound_player_path, "keyvibe-audio", "config.json", volume_str, verbose_str, (char *)NULL); - perror("execl keyboard_sound_player"); + perror("execl keyvibe-audio"); exit(1); } - - // Keyboard listener keyboard_pid = fork(); if (keyboard_pid == -1) { perror("fork"); @@ -342,20 +309,17 @@ static int start_children(const char *sound_dir, const char *config_path, close(pipefd[1]); return 0; } - char get_key_presses_path[MAX_PATH_LENGTH]; snprintf(get_key_presses_path, sizeof(get_key_presses_path), - "%s/get_key_presses", KeyVibe_BIN_DIR); - + "%s/keyvibe-input", KeyVibe_BIN_DIR); if (keyboard_pid == 0) { close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); close(pipefd[1]); - execl(get_key_presses_path, "get_key_presses", (char *)NULL); - perror("execl get_key_presses"); + execl(get_key_presses_path, "keyvibe-input", (char *)NULL); + perror("execl keyvibe-input"); exit(1); } - close(pipefd[0]); close(pipefd[1]); return 1; @@ -451,13 +415,11 @@ static int read_user_config(const char *path, char **out_sound, } int main(int argc, char *argv[]) { - char *sound_name = "eg-oreo"; // Default sound pack + char *sound_name = "eg-oreo"; int verbose = 0; int list_sounds = 0; int flag_daemon = 0; int flag_stop = 0; - - // Parse command line arguments static struct option long_options[] = { {"sound", required_argument, 0, 's'}, {"volume", required_argument, 0, 'V'}, @@ -469,13 +431,10 @@ int main(int argc, char *argv[]) { {"help", no_argument, 0, 'h'}, {"verbose", no_argument, 0, 'v'}, {0, 0, 0, 0}}; - int volume = 50; int override_config = 0; char *cli_sound = NULL; int cli_volume = -1; - - // Load user config if present (pre-parse so CLI can override) char user_cfg_path[MAX_PATH_LENGTH]; if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { if (access(user_cfg_path, R_OK) == 0) { @@ -489,7 +448,6 @@ int main(int argc, char *argv[]) { } } } - int opt; while ((opt = getopt_long(argc, argv, "s:V:clhv", long_options, NULL)) != -1) { @@ -516,23 +474,21 @@ int main(int argc, char *argv[]) { case 1001: flag_stop = 1; break; - case 1002: - // --reload acts like sending SIGHUP to daemon - { - build_pidfile_path(pidfile_path, sizeof(pidfile_path)); - pid_t running_pid = 0; - if (!read_pidfile(pidfile_path, &running_pid) || - !process_is_running(running_pid)) { - fprintf(stderr, "KeyVibe: not running.\n"); - return 1; - } - if (kill(running_pid, SIGHUP) != 0) { - perror("kill"); - return 1; - } - printf("KeyVibe reload signal sent.\n"); - return 0; + case 1002: { + build_pidfile_path(pidfile_path, sizeof(pidfile_path)); + pid_t running_pid = 0; + if (!read_pidfile(pidfile_path, &running_pid) || + !process_is_running(running_pid)) { + fprintf(stderr, "KeyVibe: not running.\n"); + return 1; } + if (kill(running_pid, SIGHUP) != 0) { + perror("kill"); + return 1; + } + printf("KeyVibe reload signal sent.\n"); + return 0; + } case 'h': print_usage(argv[0]); return 0; @@ -544,16 +500,10 @@ int main(int argc, char *argv[]) { return 1; } } - - // Handle list command if (list_sounds) { return list_sound_packs(); } - - // PID file path build_pidfile_path(pidfile_path, sizeof(pidfile_path)); - - // Handle stop command early if (flag_stop) { pid_t running_pid = 0; if (!read_pidfile(pidfile_path, &running_pid)) { @@ -571,7 +521,6 @@ int main(int argc, char *argv[]) { perror("kill"); return 1; } - // Wait up to ~3s for it to exit for (int i = 0; i < 30; i++) { if (!process_is_running(running_pid)) { unlink(pidfile_path); @@ -580,25 +529,20 @@ int main(int argc, char *argv[]) { } struct timespec ts; ts.tv_sec = 0; - ts.tv_nsec = 100000000L; // 100ms + ts.tv_nsec = 100000000L; nanosleep(&ts, NULL); } fprintf(stderr, "KeyVibe: process did not stop in time\n"); return 1; } - - // Apply CLI overrides only if -c/--override-config was provided if (override_config) { if (cli_sound != NULL) sound_name = cli_sound; if (cli_volume >= 0) volume = cli_volume; } - - // If no config file existed, create one with defaults if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { if (access(user_cfg_path, F_OK) != 0) { - // Use current values (defaults or CLI overrides) for initial file if (!write_user_config(user_cfg_path, sound_name, volume)) { fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path); } else if (verbose) { @@ -606,38 +550,25 @@ int main(int argc, char *argv[]) { } } } - - // Validate sound pack if (!validate_sound_pack(sound_name)) { return 1; } - - // No sudo pre-check; we'll run without sudo by default - - // Check if required executables exist char get_key_presses_path[MAX_PATH_LENGTH]; char sound_player_path[MAX_PATH_LENGTH]; - snprintf(get_key_presses_path, sizeof(get_key_presses_path), - "%s/get_key_presses", KeyVibe_BIN_DIR); - snprintf(sound_player_path, sizeof(sound_player_path), - "%s/keyboard_sound_player", KeyVibe_BIN_DIR); - + "%s/keyvibe-input", KeyVibe_BIN_DIR); + snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio", + KeyVibe_BIN_DIR); if (access(get_key_presses_path, X_OK) != 0) { fprintf(stderr, "Error: Cannot find or execute %s\n", get_key_presses_path); return 1; } - if (access(sound_player_path, X_OK) != 0) { fprintf(stderr, "Error: Cannot find or execute %s\n", sound_player_path); return 1; } - - // Setup signal handler for cleanup signal(SIGINT, cleanup_processes); signal(SIGTERM, cleanup_processes); - - // If daemon mode requested, ensure not already running, then daemonize if (flag_daemon) { pid_t existing = 0; if (read_pidfile(pidfile_path, &existing) && process_is_running(existing)) { @@ -648,24 +579,14 @@ int main(int argc, char *argv[]) { } daemonize_self(); is_daemon = 1; - // Write our pidfile (this is the supervising process) - if (!write_pidfile(pidfile_path, getpid())) { - // If we cannot write pidfile, still continue but warn and run in - // foreground to avoid orphaning However we're already daemonized; best we - // can do is proceed. - } + write_pidfile(pidfile_path, getpid()); } - - // Build paths char config_path[MAX_PATH_LENGTH]; char sound_dir[MAX_PATH_LENGTH]; - if (!build_paths_for_sound(sound_name, config_path, sizeof(config_path), sound_dir, sizeof(sound_dir))) { return 1; } - - // Store current settings for reloads current_volume = volume; current_verbose = verbose; strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1); @@ -674,7 +595,6 @@ int main(int argc, char *argv[]) { current_config_path[sizeof(current_config_path) - 1] = '\0'; strncpy(current_sound_dir, sound_dir, sizeof(current_sound_dir) - 1); current_sound_dir[sizeof(current_sound_dir) - 1] = '\0'; - if (verbose && !is_daemon) { printf("KeyVibe starting...\n"); printf("Sound pack: %s\n", sound_name); @@ -687,41 +607,31 @@ int main(int argc, char *argv[]) { printf("Press Ctrl+C to exit.\n"); } } - - // Start children (sound + keyboard) if (!start_children(sound_dir, config_path, volume, verbose)) { return 1; } - int status; pid_t finished_pid; - signal(SIGHUP, handle_sighup); - pthread_t inotify_thread; struct inotify_thread_args in_args; if (is_daemon) { - // Monitor user config for changes - char user_cfg_path[MAX_PATH_LENGTH]; - if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { - strncpy(in_args.path, user_cfg_path, sizeof(in_args.path) - 1); + char user_cfg_path2[MAX_PATH_LENGTH]; + if (get_user_config_path(user_cfg_path2, sizeof(user_cfg_path2))) { + strncpy(in_args.path, user_cfg_path2, sizeof(in_args.path) - 1); in_args.path[sizeof(in_args.path) - 1] = '\0'; pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args); pthread_detach(inotify_thread); } } - - // Main supervise loop: wait for child exits or reload requests while (1) { if (reload_requested) { reload_requested = 0; - // Re-read user config and optionally apply overrides only if - // override_config was set initially - char user_cfg_path[MAX_PATH_LENGTH]; - if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { + char user_cfg_path3[MAX_PATH_LENGTH]; + if (get_user_config_path(user_cfg_path3, sizeof(user_cfg_path3))) { char *cfg_sound = NULL; int cfg_volume = current_volume; - if (read_user_config(user_cfg_path, &cfg_sound, &cfg_volume)) { + if (read_user_config(user_cfg_path3, &cfg_sound, &cfg_volume)) { if (cfg_sound) { strncpy(current_sound_name, cfg_sound, sizeof(current_sound_name) - 1); @@ -730,25 +640,19 @@ int main(int argc, char *argv[]) { current_volume = cfg_volume; } } - - // Validate new sound pack and paths if (validate_sound_pack(current_sound_name)) { if (!build_paths_for_sound(current_sound_name, current_config_path, sizeof(current_config_path), current_sound_dir, sizeof(current_sound_dir))) { - // Skip reload if paths invalid continue; } - - // Restart children with new settings stop_children(); start_children(current_sound_dir, current_config_path, current_volume, current_verbose); } continue; } - finished_pid = waitpid(-1, &status, WNOHANG); if (finished_pid == 0) { struct timespec ts; @@ -777,7 +681,6 @@ int main(int argc, char *argv[]) { break; } } - if (!is_daemon) { printf("KeyVibe exited.\n"); }