feat: implement keyvibe daemon with sound selection and configuration reloading
This commit is contained in:
10 files changed
+804
-1033
No files matched your search
@@ -1,6 +1,9 @@
|
|||||||
|
|
||||||
name: build-and-test
|
name: build-and-test
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "**" ]
|
branches: [ "**" ]
|
||||||
@@ -41,4 +44,18 @@ jobs:
|
|||||||
- name: Smoke test help
|
- name: Smoke test help
|
||||||
run: ./keyvibe --help || true
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -Wall -Wextra -std=c99
|
CFLAGS = -Wall -Wextra -std=c99 -Iinclude
|
||||||
PREFIX ?= /usr
|
PREFIX ?= /usr
|
||||||
|
|
||||||
# Pass PACKAGE_PREFIX macro for config.h
|
# Pass PACKAGE_PREFIX macro for config.h
|
||||||
@@ -11,13 +10,13 @@ LDFLAGS_KEYBOARD = $(shell pkg-config --libs libevdev libinput libudev) -lpthrea
|
|||||||
|
|
||||||
# Targets
|
# Targets
|
||||||
KeyVibe_TARGET = keyvibe
|
KeyVibe_TARGET = keyvibe
|
||||||
SOUND_TARGET = keyboard_sound_player
|
SOUND_TARGET = keyvibe-audio
|
||||||
KEYBOARD_TARGET = get_key_presses
|
KEYBOARD_TARGET = keyvibe-input
|
||||||
|
|
||||||
# Sources
|
# Sources
|
||||||
KeyVibe_SOURCE = keyvibe.c
|
KeyVibe_SOURCE = src/keyvibe_main.c
|
||||||
SOUND_SOURCE = keyboard_sound_player.c
|
SOUND_SOURCE = src/keyvibe_audio.c
|
||||||
KEYBOARD_SOURCE = get_key_presses.c
|
KEYBOARD_SOURCE = src/keyvibe_input.c
|
||||||
|
|
||||||
# Install paths
|
# Install paths
|
||||||
BINDIR = $(PREFIX)/bin
|
BINDIR = $(PREFIX)/bin
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-Iinclude
|
||||||
|
-std=c99
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
#include <poll.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <getopt.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
|
|
||||||
#include <libudev.h>
|
|
||||||
#include <libinput.h>
|
|
||||||
#include <libevdev/libevdev.h>
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -18,3 +18,5 @@
|
|||||||
#define KeyVibe_BIN_DIR PACKAGE_PREFIX "/bin"
|
#define KeyVibe_BIN_DIR PACKAGE_PREFIX "/bin"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@@ -1,642 +0,0 @@
|
|||||||
#define _GNU_SOURCE
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <sys/select.h>
|
|
||||||
#include <sys/time.h>
|
|
||||||
#include <json-c/json.h>
|
|
||||||
#include <pulse/simple.h>
|
|
||||||
#include <sndfile.h>
|
|
||||||
#include <pulse/error.h>
|
|
||||||
#include <libgen.h> // 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 <config.json> [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;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <json-c/json.h>
|
||||||
|
#include <libgen.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <pulse/error.h>
|
||||||
|
#include <pulse/simple.h>
|
||||||
|
#include <sndfile.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#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 <config.json> [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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
// Renamed from get_key_presses.c to keyvibe_input.c (inlined)
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <libevdev/libevdev.h>
|
||||||
|
#include <libinput.h>
|
||||||
|
#include <libudev.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
+24
-121
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
#define _POSIX_C_SOURCE 200809L
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -81,14 +80,9 @@ int list_sound_packs() {
|
|||||||
printf("======================\n");
|
printf("======================\n");
|
||||||
|
|
||||||
while ((entry = readdir(dir)) != NULL) {
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
// Skip . and .. directories
|
|
||||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Build full path
|
|
||||||
snprintf(path, sizeof(path), "%s/%s", AUDIO_BASE_DIR, entry->d_name);
|
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)) {
|
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||||
printf(" %s\n", entry->d_name);
|
printf(" %s\n", entry->d_name);
|
||||||
}
|
}
|
||||||
@@ -106,68 +100,54 @@ int validate_sound_pack(const char *sound_name) {
|
|||||||
AUDIO_BASE_DIR, sound_name);
|
AUDIO_BASE_DIR, sound_name);
|
||||||
snprintf(sound_path, sizeof(sound_path), "%s/%s", 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) {
|
if (access(sound_path, F_OK) != 0) {
|
||||||
fprintf(stderr, "Error: Sound pack '%s' not found in %s/\n", sound_name,
|
fprintf(stderr, "Error: Sound pack '%s' not found in %s/\n", sound_name,
|
||||||
AUDIO_BASE_DIR);
|
AUDIO_BASE_DIR);
|
||||||
fprintf(stderr, "Use --list to see available sound packs.\n");
|
fprintf(stderr, "Use --list to see available sound packs.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if config.json exists
|
|
||||||
if (access(config_path, R_OK) != 0) {
|
if (access(config_path, R_OK) != 0) {
|
||||||
fprintf(stderr, "Error: Config file not found: %s\n", config_path);
|
fprintf(stderr, "Error: Config file not found: %s\n", config_path);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanup_processes(int sig) {
|
void cleanup_processes(int sig) {
|
||||||
(void)sig; // Suppress unused parameter warning
|
(void)sig;
|
||||||
|
|
||||||
if (!is_daemon) {
|
if (!is_daemon) {
|
||||||
printf("\nShutting down KeyVibe...\n");
|
printf("\nShutting down KeyVibe...\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sound_pid > 0) {
|
if (sound_pid > 0) {
|
||||||
kill(sound_pid, SIGTERM);
|
kill(sound_pid, SIGTERM);
|
||||||
waitpid(sound_pid, NULL, 0);
|
waitpid(sound_pid, NULL, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyboard_pid > 0) {
|
if (keyboard_pid > 0) {
|
||||||
kill(keyboard_pid, SIGTERM);
|
kill(keyboard_pid, SIGTERM);
|
||||||
waitpid(keyboard_pid, NULL, 0);
|
waitpid(keyboard_pid, NULL, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pidfile_path[0] != '\0') {
|
if (pidfile_path[0] != '\0') {
|
||||||
unlink(pidfile_path);
|
unlink(pidfile_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if sudo credentials are cached
|
|
||||||
int check_sudo_cached() {
|
int check_sudo_cached() {
|
||||||
int status = system("sudo -n true 2>/dev/null");
|
int status = system("sudo -n true 2>/dev/null");
|
||||||
return WEXITSTATUS(status) == 0;
|
return WEXITSTATUS(status) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to prompt for sudo password if needed
|
|
||||||
int ensure_sudo_access() {
|
int ensure_sudo_access() {
|
||||||
if (check_sudo_cached()) {
|
if (check_sudo_cached()) {
|
||||||
return 1; // Already have sudo access
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("This program requires sudo access to monitor keyboard events.\n");
|
printf("This program requires sudo access to monitor keyboard events.\n");
|
||||||
printf("Please enter your password when prompted:\n");
|
printf("Please enter your password when prompted:\n");
|
||||||
|
|
||||||
int status = system("sudo -v");
|
int status = system("sudo -v");
|
||||||
if (WEXITSTATUS(status) != 0) {
|
if (WEXITSTATUS(status) != 0) {
|
||||||
fprintf(stderr, "Error: Failed to obtain sudo access\n");
|
fprintf(stderr, "Error: Failed to obtain sudo access\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,9 +158,8 @@ static char *get_user_config_path(char *buffer, size_t buflen) {
|
|||||||
if (pw && pw->pw_dir)
|
if (pw && pw->pw_dir)
|
||||||
home = pw->pw_dir;
|
home = pw->pw_dir;
|
||||||
}
|
}
|
||||||
if (!home) {
|
if (!home)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
|
||||||
snprintf(buffer, buflen, "%s/%s", home, USER_CONFIG_FILENAME);
|
snprintf(buffer, buflen, "%s/%s", home, USER_CONFIG_FILENAME);
|
||||||
return buffer;
|
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);
|
size_t name_len = strlen(sound_name);
|
||||||
const char *cfg_suffix = "/config.json";
|
const char *cfg_suffix = "/config.json";
|
||||||
size_t cfg_suffix_len = strlen(cfg_suffix);
|
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) {
|
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");
|
fprintf(stderr, "Error: sound pack name too long for path buffer.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// Need: base + '/' + name + null
|
|
||||||
if (base_len + 1 + name_len + 1 > out_sound_sz) {
|
if (base_len + 1 + name_len + 1 > out_sound_sz) {
|
||||||
fprintf(stderr, "Error: sound pack name too long for directory buffer.\n");
|
fprintf(stderr, "Error: sound pack name too long for directory buffer.\n");
|
||||||
return 0;
|
return 0;
|
||||||
@@ -256,20 +233,16 @@ static void daemonize_self() {
|
|||||||
if (pid < 0)
|
if (pid < 0)
|
||||||
exit(1);
|
exit(1);
|
||||||
if (pid > 0)
|
if (pid > 0)
|
||||||
exit(0); // Parent exits
|
exit(0);
|
||||||
|
|
||||||
if (setsid() < 0)
|
if (setsid() < 0)
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|
||||||
pid = fork();
|
pid = fork();
|
||||||
if (pid < 0)
|
if (pid < 0)
|
||||||
exit(1);
|
exit(1);
|
||||||
if (pid > 0)
|
if (pid > 0)
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|
||||||
umask(0);
|
umask(0);
|
||||||
chdir("/");
|
chdir("/");
|
||||||
|
|
||||||
int fd = open("/dev/null", O_RDWR);
|
int fd = open("/dev/null", O_RDWR);
|
||||||
if (fd >= 0) {
|
if (fd >= 0) {
|
||||||
dup2(fd, STDIN_FILENO);
|
dup2(fd, STDIN_FILENO);
|
||||||
@@ -301,8 +274,6 @@ static int start_children(const char *sound_dir, const char *config_path,
|
|||||||
perror("pipe");
|
perror("pipe");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fork sound player first
|
|
||||||
sound_pid = fork();
|
sound_pid = fork();
|
||||||
if (sound_pid == -1) {
|
if (sound_pid == -1) {
|
||||||
perror("fork");
|
perror("fork");
|
||||||
@@ -310,11 +281,9 @@ static int start_children(const char *sound_dir, const char *config_path,
|
|||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
char sound_player_path[MAX_PATH_LENGTH];
|
char sound_player_path[MAX_PATH_LENGTH];
|
||||||
snprintf(sound_player_path, sizeof(sound_player_path),
|
snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio",
|
||||||
"%s/keyboard_sound_player", KeyVibe_BIN_DIR);
|
KeyVibe_BIN_DIR);
|
||||||
|
|
||||||
if (sound_pid == 0) {
|
if (sound_pid == 0) {
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
dup2(pipefd[0], STDIN_FILENO);
|
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);
|
snprintf(volume_str, sizeof(volume_str), "%d", volume);
|
||||||
char verbose_str[8];
|
char verbose_str[8];
|
||||||
snprintf(verbose_str, sizeof(verbose_str), "%d", verbose);
|
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);
|
verbose_str, (char *)NULL);
|
||||||
perror("execl keyboard_sound_player");
|
perror("execl keyvibe-audio");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard listener
|
|
||||||
keyboard_pid = fork();
|
keyboard_pid = fork();
|
||||||
if (keyboard_pid == -1) {
|
if (keyboard_pid == -1) {
|
||||||
perror("fork");
|
perror("fork");
|
||||||
@@ -342,20 +309,17 @@ static int start_children(const char *sound_dir, const char *config_path,
|
|||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
char get_key_presses_path[MAX_PATH_LENGTH];
|
char get_key_presses_path[MAX_PATH_LENGTH];
|
||||||
snprintf(get_key_presses_path, sizeof(get_key_presses_path),
|
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) {
|
if (keyboard_pid == 0) {
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
dup2(pipefd[1], STDOUT_FILENO);
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
execl(get_key_presses_path, "get_key_presses", (char *)NULL);
|
execl(get_key_presses_path, "keyvibe-input", (char *)NULL);
|
||||||
perror("execl get_key_presses");
|
perror("execl keyvibe-input");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -451,13 +415,11 @@ static int read_user_config(const char *path, char **out_sound,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
char *sound_name = "eg-oreo"; // Default sound pack
|
char *sound_name = "eg-oreo";
|
||||||
int verbose = 0;
|
int verbose = 0;
|
||||||
int list_sounds = 0;
|
int list_sounds = 0;
|
||||||
int flag_daemon = 0;
|
int flag_daemon = 0;
|
||||||
int flag_stop = 0;
|
int flag_stop = 0;
|
||||||
|
|
||||||
// Parse command line arguments
|
|
||||||
static struct option long_options[] = {
|
static struct option long_options[] = {
|
||||||
{"sound", required_argument, 0, 's'},
|
{"sound", required_argument, 0, 's'},
|
||||||
{"volume", required_argument, 0, 'V'},
|
{"volume", required_argument, 0, 'V'},
|
||||||
@@ -469,13 +431,10 @@ int main(int argc, char *argv[]) {
|
|||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{"verbose", no_argument, 0, 'v'},
|
{"verbose", no_argument, 0, 'v'},
|
||||||
{0, 0, 0, 0}};
|
{0, 0, 0, 0}};
|
||||||
|
|
||||||
int volume = 50;
|
int volume = 50;
|
||||||
int override_config = 0;
|
int override_config = 0;
|
||||||
char *cli_sound = NULL;
|
char *cli_sound = NULL;
|
||||||
int cli_volume = -1;
|
int cli_volume = -1;
|
||||||
|
|
||||||
// Load user config if present (pre-parse so CLI can override)
|
|
||||||
char user_cfg_path[MAX_PATH_LENGTH];
|
char user_cfg_path[MAX_PATH_LENGTH];
|
||||||
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
|
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
|
||||||
if (access(user_cfg_path, R_OK) == 0) {
|
if (access(user_cfg_path, R_OK) == 0) {
|
||||||
@@ -489,7 +448,6 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int opt;
|
int opt;
|
||||||
while ((opt = getopt_long(argc, argv, "s:V:clhv", long_options, NULL)) !=
|
while ((opt = getopt_long(argc, argv, "s:V:clhv", long_options, NULL)) !=
|
||||||
-1) {
|
-1) {
|
||||||
@@ -516,9 +474,7 @@ int main(int argc, char *argv[]) {
|
|||||||
case 1001:
|
case 1001:
|
||||||
flag_stop = 1;
|
flag_stop = 1;
|
||||||
break;
|
break;
|
||||||
case 1002:
|
case 1002: {
|
||||||
// --reload acts like sending SIGHUP to daemon
|
|
||||||
{
|
|
||||||
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
|
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
|
||||||
pid_t running_pid = 0;
|
pid_t running_pid = 0;
|
||||||
if (!read_pidfile(pidfile_path, &running_pid) ||
|
if (!read_pidfile(pidfile_path, &running_pid) ||
|
||||||
@@ -544,16 +500,10 @@ int main(int argc, char *argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle list command
|
|
||||||
if (list_sounds) {
|
if (list_sounds) {
|
||||||
return list_sound_packs();
|
return list_sound_packs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PID file path
|
|
||||||
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
|
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
|
||||||
|
|
||||||
// Handle stop command early
|
|
||||||
if (flag_stop) {
|
if (flag_stop) {
|
||||||
pid_t running_pid = 0;
|
pid_t running_pid = 0;
|
||||||
if (!read_pidfile(pidfile_path, &running_pid)) {
|
if (!read_pidfile(pidfile_path, &running_pid)) {
|
||||||
@@ -571,7 +521,6 @@ int main(int argc, char *argv[]) {
|
|||||||
perror("kill");
|
perror("kill");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
// Wait up to ~3s for it to exit
|
|
||||||
for (int i = 0; i < 30; i++) {
|
for (int i = 0; i < 30; i++) {
|
||||||
if (!process_is_running(running_pid)) {
|
if (!process_is_running(running_pid)) {
|
||||||
unlink(pidfile_path);
|
unlink(pidfile_path);
|
||||||
@@ -580,25 +529,20 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
struct timespec ts;
|
struct timespec ts;
|
||||||
ts.tv_sec = 0;
|
ts.tv_sec = 0;
|
||||||
ts.tv_nsec = 100000000L; // 100ms
|
ts.tv_nsec = 100000000L;
|
||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
}
|
}
|
||||||
fprintf(stderr, "KeyVibe: process did not stop in time\n");
|
fprintf(stderr, "KeyVibe: process did not stop in time\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply CLI overrides only if -c/--override-config was provided
|
|
||||||
if (override_config) {
|
if (override_config) {
|
||||||
if (cli_sound != NULL)
|
if (cli_sound != NULL)
|
||||||
sound_name = cli_sound;
|
sound_name = cli_sound;
|
||||||
if (cli_volume >= 0)
|
if (cli_volume >= 0)
|
||||||
volume = cli_volume;
|
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 (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
|
||||||
if (access(user_cfg_path, F_OK) != 0) {
|
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)) {
|
if (!write_user_config(user_cfg_path, sound_name, volume)) {
|
||||||
fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path);
|
fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path);
|
||||||
} else if (verbose) {
|
} else if (verbose) {
|
||||||
@@ -606,38 +550,25 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate sound pack
|
|
||||||
if (!validate_sound_pack(sound_name)) {
|
if (!validate_sound_pack(sound_name)) {
|
||||||
return 1;
|
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 get_key_presses_path[MAX_PATH_LENGTH];
|
||||||
char sound_player_path[MAX_PATH_LENGTH];
|
char sound_player_path[MAX_PATH_LENGTH];
|
||||||
|
|
||||||
snprintf(get_key_presses_path, sizeof(get_key_presses_path),
|
snprintf(get_key_presses_path, sizeof(get_key_presses_path),
|
||||||
"%s/get_key_presses", KeyVibe_BIN_DIR);
|
"%s/keyvibe-input", KeyVibe_BIN_DIR);
|
||||||
snprintf(sound_player_path, sizeof(sound_player_path),
|
snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio",
|
||||||
"%s/keyboard_sound_player", KeyVibe_BIN_DIR);
|
KeyVibe_BIN_DIR);
|
||||||
|
|
||||||
if (access(get_key_presses_path, X_OK) != 0) {
|
if (access(get_key_presses_path, X_OK) != 0) {
|
||||||
fprintf(stderr, "Error: Cannot find or execute %s\n", get_key_presses_path);
|
fprintf(stderr, "Error: Cannot find or execute %s\n", get_key_presses_path);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (access(sound_player_path, X_OK) != 0) {
|
if (access(sound_player_path, X_OK) != 0) {
|
||||||
fprintf(stderr, "Error: Cannot find or execute %s\n", sound_player_path);
|
fprintf(stderr, "Error: Cannot find or execute %s\n", sound_player_path);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup signal handler for cleanup
|
|
||||||
signal(SIGINT, cleanup_processes);
|
signal(SIGINT, cleanup_processes);
|
||||||
signal(SIGTERM, cleanup_processes);
|
signal(SIGTERM, cleanup_processes);
|
||||||
|
|
||||||
// If daemon mode requested, ensure not already running, then daemonize
|
|
||||||
if (flag_daemon) {
|
if (flag_daemon) {
|
||||||
pid_t existing = 0;
|
pid_t existing = 0;
|
||||||
if (read_pidfile(pidfile_path, &existing) && process_is_running(existing)) {
|
if (read_pidfile(pidfile_path, &existing) && process_is_running(existing)) {
|
||||||
@@ -648,24 +579,14 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
daemonize_self();
|
daemonize_self();
|
||||||
is_daemon = 1;
|
is_daemon = 1;
|
||||||
// Write our pidfile (this is the supervising process)
|
write_pidfile(pidfile_path, getpid());
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build paths
|
|
||||||
char config_path[MAX_PATH_LENGTH];
|
char config_path[MAX_PATH_LENGTH];
|
||||||
char sound_dir[MAX_PATH_LENGTH];
|
char sound_dir[MAX_PATH_LENGTH];
|
||||||
|
|
||||||
if (!build_paths_for_sound(sound_name, config_path, sizeof(config_path),
|
if (!build_paths_for_sound(sound_name, config_path, sizeof(config_path),
|
||||||
sound_dir, sizeof(sound_dir))) {
|
sound_dir, sizeof(sound_dir))) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store current settings for reloads
|
|
||||||
current_volume = volume;
|
current_volume = volume;
|
||||||
current_verbose = verbose;
|
current_verbose = verbose;
|
||||||
strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1);
|
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';
|
current_config_path[sizeof(current_config_path) - 1] = '\0';
|
||||||
strncpy(current_sound_dir, sound_dir, sizeof(current_sound_dir) - 1);
|
strncpy(current_sound_dir, sound_dir, sizeof(current_sound_dir) - 1);
|
||||||
current_sound_dir[sizeof(current_sound_dir) - 1] = '\0';
|
current_sound_dir[sizeof(current_sound_dir) - 1] = '\0';
|
||||||
|
|
||||||
if (verbose && !is_daemon) {
|
if (verbose && !is_daemon) {
|
||||||
printf("KeyVibe starting...\n");
|
printf("KeyVibe starting...\n");
|
||||||
printf("Sound pack: %s\n", sound_name);
|
printf("Sound pack: %s\n", sound_name);
|
||||||
@@ -687,41 +607,31 @@ int main(int argc, char *argv[]) {
|
|||||||
printf("Press Ctrl+C to exit.\n");
|
printf("Press Ctrl+C to exit.\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start children (sound + keyboard)
|
|
||||||
if (!start_children(sound_dir, config_path, volume, verbose)) {
|
if (!start_children(sound_dir, config_path, volume, verbose)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int status;
|
int status;
|
||||||
pid_t finished_pid;
|
pid_t finished_pid;
|
||||||
|
|
||||||
signal(SIGHUP, handle_sighup);
|
signal(SIGHUP, handle_sighup);
|
||||||
|
|
||||||
pthread_t inotify_thread;
|
pthread_t inotify_thread;
|
||||||
struct inotify_thread_args in_args;
|
struct inotify_thread_args in_args;
|
||||||
if (is_daemon) {
|
if (is_daemon) {
|
||||||
// Monitor user config for changes
|
char user_cfg_path2[MAX_PATH_LENGTH];
|
||||||
char user_cfg_path[MAX_PATH_LENGTH];
|
if (get_user_config_path(user_cfg_path2, sizeof(user_cfg_path2))) {
|
||||||
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
|
strncpy(in_args.path, user_cfg_path2, sizeof(in_args.path) - 1);
|
||||||
strncpy(in_args.path, user_cfg_path, sizeof(in_args.path) - 1);
|
|
||||||
in_args.path[sizeof(in_args.path) - 1] = '\0';
|
in_args.path[sizeof(in_args.path) - 1] = '\0';
|
||||||
pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args);
|
pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args);
|
||||||
pthread_detach(inotify_thread);
|
pthread_detach(inotify_thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main supervise loop: wait for child exits or reload requests
|
|
||||||
while (1) {
|
while (1) {
|
||||||
if (reload_requested) {
|
if (reload_requested) {
|
||||||
reload_requested = 0;
|
reload_requested = 0;
|
||||||
// Re-read user config and optionally apply overrides only if
|
char user_cfg_path3[MAX_PATH_LENGTH];
|
||||||
// override_config was set initially
|
if (get_user_config_path(user_cfg_path3, sizeof(user_cfg_path3))) {
|
||||||
char user_cfg_path[MAX_PATH_LENGTH];
|
|
||||||
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
|
|
||||||
char *cfg_sound = NULL;
|
char *cfg_sound = NULL;
|
||||||
int cfg_volume = current_volume;
|
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) {
|
if (cfg_sound) {
|
||||||
strncpy(current_sound_name, cfg_sound,
|
strncpy(current_sound_name, cfg_sound,
|
||||||
sizeof(current_sound_name) - 1);
|
sizeof(current_sound_name) - 1);
|
||||||
@@ -730,25 +640,19 @@ int main(int argc, char *argv[]) {
|
|||||||
current_volume = cfg_volume;
|
current_volume = cfg_volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate new sound pack and paths
|
|
||||||
if (validate_sound_pack(current_sound_name)) {
|
if (validate_sound_pack(current_sound_name)) {
|
||||||
if (!build_paths_for_sound(current_sound_name, current_config_path,
|
if (!build_paths_for_sound(current_sound_name, current_config_path,
|
||||||
sizeof(current_config_path),
|
sizeof(current_config_path),
|
||||||
current_sound_dir,
|
current_sound_dir,
|
||||||
sizeof(current_sound_dir))) {
|
sizeof(current_sound_dir))) {
|
||||||
// Skip reload if paths invalid
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart children with new settings
|
|
||||||
stop_children();
|
stop_children();
|
||||||
start_children(current_sound_dir, current_config_path, current_volume,
|
start_children(current_sound_dir, current_config_path, current_volume,
|
||||||
current_verbose);
|
current_verbose);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
finished_pid = waitpid(-1, &status, WNOHANG);
|
finished_pid = waitpid(-1, &status, WNOHANG);
|
||||||
if (finished_pid == 0) {
|
if (finished_pid == 0) {
|
||||||
struct timespec ts;
|
struct timespec ts;
|
||||||
@@ -777,7 +681,6 @@ int main(int argc, char *argv[]) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_daemon) {
|
if (!is_daemon) {
|
||||||
printf("KeyVibe exited.\n");
|
printf("KeyVibe exited.\n");
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user