#define _GNU_SOURCE #include "utils.h" #include #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[512]; struct { char *press; char *release; } multi_key_mappings[512]; int is_multi; SF_INFO sf_info; } SoundPack; typedef struct { int key_code; int thread_id; int is_pressed; } PlaybackData; SoundPack g_sound_pack = {0}; SoundPack g_mouse_sound_pack = {0}; float g_volume = 1.0f; float g_mouse_volume = 1.0f; int g_verbose = 0; int g_mute = 0; int g_keyboard_mute = 0; int g_mouse_mute = 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 { if (!safe_snprintf(buffer, buffer_size, "%s/%s", base_dir, filename)) { buffer[0] = '\0'; } } } 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); if (size < 0) { fclose(file); fprintf(stderr, "Error: ftell failed while reading %s\n", config_path); return -1; } errno = 0; rewind(file); if (errno != 0) { fclose(file); fprintf(stderr, "Error: rewind failed while reading %s\n", config_path); return -1; } char *json_string = malloc((size_t)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; if (g_verbose) { 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); if (g_verbose) 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 { if (g_verbose) 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; if (g_verbose) printf("Found single generic sound file: %s\n", g_sound_pack.generic_press_files[0]); } } if (g_verbose) 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); if (g_verbose) 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; // Handle mouse key names first if (strcmp(key, "MouseLeft") == 0) { key_code = 272; } else if (strcmp(key, "MouseRight") == 0) { key_code = 273; } else if (strcmp(key, "MouseMiddle") == 0) { key_code = 274; } else { // Handle regular key codes with -up suffix 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 < 512) { 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 { // Try "sound" first, then "audio_file" for mouse configs if (json_object_object_get_ex(root, "sound", &obj) || json_object_object_get_ex(root, "audio_file", &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); if (g_verbose) printf("Single mode sound file: %s\n", g_sound_pack.sound_file); } // Try "defines" first, then "definitions" for mouse configs json_object *defines_obj = NULL; if (json_object_object_get_ex(root, "defines", &defines_obj) || json_object_object_get_ex(root, "definitions", &defines_obj)) { json_object_object_foreach(defines_obj, key, val) { int key_code; // Handle mouse key names if (strcmp(key, "MouseLeft") == 0) { key_code = 272; } else if (strcmp(key, "MouseRight") == 0) { key_code = 273; } else if (strcmp(key, "MouseMiddle") == 0) { key_code = 274; } else { key_code = atoi(key); } if (key_code >= 0 && key_code < 512) { // Handle different config formats if (json_object_is_type(val, json_type_array)) { // Standard format: [start_ms, duration_ms] if (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)); } } else if (json_object_is_type(val, json_type_object)) { // Mouse format: {"timing": [[start1, duration1], [start2, // duration2]]} json_object *timing_array; if (json_object_object_get_ex(val, "timing", &timing_array) && json_object_is_type(timing_array, json_type_array) && json_object_array_length(timing_array) > 0) { // Use the first timing entry json_object *first_timing = json_object_array_get_idx(timing_array, 0); if (json_object_is_type(first_timing, json_type_array) && json_object_array_length(first_timing) >= 2) { g_sound_pack.key_mappings[key_code].start_ms = json_object_get_int( json_object_array_get_idx(first_timing, 0)); g_sound_pack.key_mappings[key_code].duration_ms = json_object_get_int( json_object_array_get_idx(first_timing, 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); if (g_verbose) { 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; // Determine if this is a mouse event (key codes 272, 273, 274) int is_mouse_event = (key_code == 272 || key_code == 273 || key_code == 274); SoundPack *sound_pack = is_mouse_event ? &g_mouse_sound_pack : &g_sound_pack; float volume = is_mouse_event ? g_mouse_volume : g_volume; int mute_state = is_mouse_event ? g_mouse_mute : g_keyboard_mute; if (g_verbose) { printf("Thread %d: Playing %s sound for key %d (%s)\n", thread_id, is_mouse_event ? "mouse" : "keyboard", key_code, is_pressed ? "press" : "release"); } // Check if this type of sound is muted if (g_mute || mute_state) { if (g_verbose) { printf("Thread %d: %s sound is muted\n", thread_id, is_mouse_event ? "Mouse" : "Keyboard"); } goto exit_cleanup; } if (sound_pack->is_multi) { if (is_pressed && sound_pack->multi_key_mappings[key_code].press) file_to_play = sound_pack->multi_key_mappings[key_code].press; else if (!is_pressed && sound_pack->multi_key_mappings[key_code].release) file_to_play = sound_pack->multi_key_mappings[key_code].release; else if (is_pressed && sound_pack->num_generic_press_files > 0) { int idx = rand() % sound_pack->num_generic_press_files; file_to_play = sound_pack->generic_press_files[idx]; } else if (!is_pressed && strlen(sound_pack->release_file) > 0) file_to_play = 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] * 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 >= 512 || 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 = &sound_pack->key_mappings[key_code]; SF_INFO sf_info = sound_pack->sf_info; SNDFILE *sf = sf_open(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] * 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_mute) { if (g_verbose) { printf("Sound muted - ignoring key %d (%s)\n", key_code, is_pressed ? "press" : "release"); } return; } 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->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() { if (g_verbose) printf("Cleaning up...\n"); usleep(500000); for (int i = 0; i < 512; 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); } static int read_mute_state() { char mute_file[MAX_LINE_LENGTH]; const char *rd = getenv("XDG_RUNTIME_DIR"); if (!rd || strlen(rd) == 0) { rd = "/tmp"; } if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd, (int)getuid())) { return 0; } FILE *f = fopen(mute_file, "r"); if (!f) { return 0; // Default to unmuted if file doesn't exist } int mute = 0; if (fscanf(f, "%d", &mute) == 1) { fclose(f); return mute; } fclose(f); return 0; } static int read_keyboard_mute_state() { char mute_file[MAX_LINE_LENGTH]; const char *rd = getenv("XDG_RUNTIME_DIR"); if (!rd || strlen(rd) == 0) { rd = "/tmp"; } if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-kbd-mute-%d", rd, (int)getuid())) { return 0; } FILE *f = fopen(mute_file, "r"); if (!f) { return 0; } int mute = 0; if (fscanf(f, "%d", &mute) == 1) { fclose(f); return mute; } fclose(f); return 0; } static int read_mouse_mute_state() { char mute_file[MAX_LINE_LENGTH]; const char *rd = getenv("XDG_RUNTIME_DIR"); if (!rd || strlen(rd) == 0) { rd = "/tmp"; } if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-mouse-mute-%d", rd, (int)getuid())) { return 0; } FILE *f = fopen(mute_file, "r"); if (!f) { return 0; } int mute = 0; if (fscanf(f, "%d", &mute) == 1) { fclose(f); return mute; } fclose(f); return 0; } int main(int argc, char *argv[]) { if (argc < 2 || argc > 9) { fprintf(stderr, "Usage: %s [volume] [verbose] [mute] [mouse_config] " "[mouse_volume] [keyboard_mute] [mouse_mute]\n", argv[0]); fprintf(stderr, " volume: 0-100 (default: 50)\n"); fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n"); fprintf(stderr, " mute: 1 to mute all sound (default: 0)\n"); fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n"); fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n"); fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\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; if (g_verbose) printf("Volume set to: %d%%\n", volume_percent); } else { if (g_verbose) printf("Volume set to: 50%% (default)\n"); } if (argc >= 4) { g_verbose = atoi(argv[3]); if (g_verbose) { printf("Verbose mode enabled\n"); } } if (argc >= 5) { g_mute = atoi(argv[4]); if (g_mute) { printf("Sound muted\n"); } } // Parse mouse config and volume if provided if (argc >= 6) { if (load_sound_config(argv[5]) != 0) { fprintf(stderr, "Failed to load mouse sound configuration\n"); return 1; } // Copy the loaded config to mouse sound pack g_mouse_sound_pack = g_sound_pack; if (g_verbose) { printf("Mouse sound pack loaded from: %s\n", argv[5]); } } if (argc >= 7) { int mouse_volume_percent = atoi(argv[6]); if (mouse_volume_percent < 0) mouse_volume_percent = 0; if (mouse_volume_percent > 100) mouse_volume_percent = 100; g_mouse_volume = mouse_volume_percent / 100.0f; if (g_verbose) printf("Mouse volume set to: %d%%\n", mouse_volume_percent); } if (argc >= 8) { g_keyboard_mute = atoi(argv[7]); if (g_verbose) printf("Keyboard mute: %s\n", g_keyboard_mute ? "enabled" : "disabled"); } if (argc >= 9) { g_mouse_mute = atoi(argv[8]); if (g_verbose) printf("Mouse mute: %s\n", g_mouse_mute ? "enabled" : "disabled"); } if (load_sound_config(argv[1]) != 0) { fprintf(stderr, "Failed to load keyboard sound configuration\n"); return 1; } if (init_audio() != 0) { fprintf(stderr, "Failed to initialize audio\n"); return 1; } // Remove signal handlers - we'll use file-based approach instead fd_set readfds; struct timeval timeout; char line[MAX_LINE_LENGTH]; while (1) { // Check mute state from files periodically g_mute = read_mute_state(); g_keyboard_mute = read_keyboard_mute_state(); g_mouse_mute = read_mouse_mute_state(); 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) { if (errno == EINTR) { // Signal interrupted select, continue continue; } 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)) { if (g_verbose) 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; }