From 9694ec75ab1464b3699ef367f3800e60bef0cc7a Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 15 Aug 2014 20:49:23 +0200 Subject: initial import --- Makefile | 25 +++++ audio.c | 225 ++++++++++++++++++++++++++++++++++++++++++++ audio.h | 21 +++++ coveraudiod.config | 1 + coveraudiod.creator | 1 + coveraudiod.files | 8 ++ coveraudiod.includes | 2 + key.c | 19 ++++ key.h | 13 +++ key1.wav | Bin 0 -> 6000 bytes key2.wav | Bin 0 -> 5908 bytes key3.wav | Bin 0 -> 5868 bytes key4.wav | Bin 0 -> 5828 bytes key5.wav | Bin 0 -> 5704 bytes key6.wav | Bin 0 -> 5584 bytes main.c | 29 ++++++ mod1.wav | Bin 0 -> 4676 bytes mod2.wav | Bin 0 -> 4676 bytes x11.c | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++ x11.h | 9 ++ 20 files changed, 610 insertions(+) create mode 100644 Makefile create mode 100644 audio.c create mode 100644 audio.h create mode 100644 coveraudiod.config create mode 100644 coveraudiod.creator create mode 100644 coveraudiod.files create mode 100644 coveraudiod.includes create mode 100644 key.c create mode 100644 key.h create mode 100644 key1.wav create mode 100644 key2.wav create mode 100644 key3.wav create mode 100644 key4.wav create mode 100644 key5.wav create mode 100644 key6.wav create mode 100644 main.c create mode 100644 mod1.wav create mode 100644 mod2.wav create mode 100644 x11.c create mode 100644 x11.h diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f186a15 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CFLAGS:=-Wall -O0 -ggdb + +pkgmodules:=glib-2.0 libpulse-mainloop-glib sndfile x11 xi +includes:=$(shell pkg-config --cflags $(pkgmodules)) +libs:=$(shell pkg-config --libs $(pkgmodules)) +objs:=main.o x11.o audio.o key.o + +all: coveraudiod + +coveraudiod: $(objs) + $(CC) -o $@ $+ $(libs) + +$(objs): %.o: %.c + $(CC) -std=c99 $(includes) $(CFLAGS) -o $@ -c $< + +clean: + rm -f *.o coveraudiod + +install: coveraudiod + install coveraudiod $(DESTDIR)/usr/bin + install -d $(DESTDIR)/usr/share/coveraudiod + install *.wav $(DESTDIR)/usr/share/coveraudiod/ + +.PHONY: all clean install + diff --git a/audio.c b/audio.c new file mode 100644 index 0000000..6d38783 --- /dev/null +++ b/audio.c @@ -0,0 +1,225 @@ +#include + +#include +#include +#include + +#include "audio.h" + +static pa_context *context; + +#define PA_SAMPLE_NAME_PREFIX "coveraudiod-" +#define SAMPLE_FILES_PATH "/usr/share/coveraudiod/" + +struct sample_info { + const char *name; +} samples[NUM_SAMPLES] = { + {"key1"}, + {"key2"}, + {"key3"}, + {"key4"}, + {"key5"}, + {"key6"}, + {"mod1"}, + {"mod2"} +}; + +struct sample_loading_status { + const struct sample_info *sample; + SNDFILE *sndfile; + pa_stream *stream; + size_t nbytes; +}; + +static const char *build_file_name(const struct sample_info *sample) +{ + static char buffer[sizeof(SAMPLE_FILES_PATH) * 2]; + + snprintf(buffer, sizeof(buffer), "%s%s.%s", SAMPLE_FILES_PATH, sample->name, "wav"); + + return buffer; +} + +static const char *build_sample_name(const struct sample_info *sample) +{ + static char buffer[sizeof(PA_SAMPLE_NAME_PREFIX) * 2]; + + snprintf(buffer, sizeof(buffer), "%s%s", PA_SAMPLE_NAME_PREFIX, sample->name); + + return buffer; +} + +static void handle_stream_request(pa_stream *s, size_t nbytes, void *userdata) +{ + struct sample_loading_status *ldstatus = (struct sample_loading_status*) userdata; + + void *buffer; + + int err = pa_stream_begin_write(s, &buffer, &nbytes); + g_return_if_fail(err == 0); + + sf_count_t read = sf_read_raw(ldstatus->sndfile, buffer, nbytes); + + err = pa_stream_write(s, buffer, read, NULL, 0, PA_SEEK_RELATIVE); + if (err) { + g_warning("Failed to upload data to PulseAudio"); + pa_stream_disconnect(s); + } + + ldstatus->nbytes -= read; + + if (read < nbytes || ldstatus->nbytes <= 0) { + // EOF + pa_stream_finish_upload(s); + } +} + +static void handle_stream_notify(pa_stream *p, void *userdata) +{ + struct sample_loading_status *ldstatus = (struct sample_loading_status*) userdata; + + pa_stream_state_t status = pa_stream_get_state(p); + switch (status) { + case PA_STREAM_READY: + // Nothing to do, write request will be shortly called by PA. + break; + case PA_STREAM_FAILED: + g_warning("PulseAudio stream upload failed, closing"); + // Fallthrough + case PA_STREAM_TERMINATED: + pa_stream_unref(ldstatus->stream); + sf_close(ldstatus->sndfile); + free(ldstatus); + break; + default: + break; + } +} + +static bool load_sample(const struct sample_info *sample) +{ + struct sample_loading_status *ldstatus = calloc(1, sizeof(struct sample_loading_status)); + ldstatus->sample = sample; + + const char *file_name = build_file_name(sample); + SF_INFO info; + + ldstatus->sndfile = sf_open(file_name, SFM_READ, &info); + if (!ldstatus->sndfile) { + g_warning("Could not open sample: %s", file_name); + return false; + } + + pa_sample_spec spec; + spec.rate = info.samplerate; + spec.channels = info.channels; + + switch (info.format & SF_FORMAT_SUBMASK) { + case SF_FORMAT_PCM_16: + spec.format = PA_SAMPLE_S16NE; + break; + case SF_FORMAT_PCM_24: + spec.format = PA_SAMPLE_S24NE; + break; + case SF_FORMAT_PCM_32: + spec.format = PA_SAMPLE_S32NE; + break; + case SF_FORMAT_ULAW: + spec.format = PA_SAMPLE_ULAW; + break; + case SF_FORMAT_FLOAT: + case SF_FORMAT_DOUBLE: + spec.format = PA_SAMPLE_FLOAT32NE; + break; + default: + g_warning("Unknown sample format: %s", file_name); + sf_close(ldstatus->sndfile); + free(ldstatus); + return false; + } + + g_return_val_if_fail(pa_sample_spec_valid(&spec), false); + + ldstatus->nbytes = pa_frame_size(&spec) * info.frames; + + pa_proplist *prop = pa_proplist_new(); + pa_proplist_sets(prop, PA_PROP_MEDIA_ROLE, "event"); + pa_proplist_sets(prop, PA_PROP_MEDIA_FILENAME, file_name); + + ldstatus->stream = pa_stream_new_with_proplist(context, build_sample_name(sample), + &spec, NULL, prop); + + pa_stream_set_state_callback(ldstatus->stream, handle_stream_notify, ldstatus); + pa_stream_set_write_callback(ldstatus->stream, handle_stream_request, ldstatus); + + int err = pa_stream_connect_upload(ldstatus->stream, ldstatus->nbytes); + if (err) { + g_warning("pa_stream_connect_upload failed"); + pa_stream_unref(ldstatus->stream); + sf_close(ldstatus->sndfile); + free(ldstatus); + return false; + } + + return true; +} + +static void load_samples() +{ + for (int i = 0; i < NUM_SAMPLES; i++) { + if (!load_sample(&samples[i])) { + return; + } + } +} + +static void handle_context_notify(pa_context *c, void *userdata) +{ + pa_context_state_t state = pa_context_get_state(c); + switch (state) { + case PA_CONTEXT_READY: + load_samples(); + break; + case PA_CONTEXT_FAILED: + g_warning("Cannot connect to PulseAudio"); + break; + default: + break; + } +} + +bool audio_init() +{ + pa_glib_mainloop *paloop = pa_glib_mainloop_new(NULL); + context = pa_context_new(pa_glib_mainloop_get_api(paloop), "coveraudiod"); + + if (!context) { + g_warning("Cannot create PulseAudio context"); + return false; + } + + pa_context_set_state_callback(context, handle_context_notify, NULL); + + int err = pa_context_connect(context, NULL, 0, NULL); + if (err) { + g_warning("Cannot connect to PulseAudio"); + return false; + } + + return true; +} + +void audio_close() +{ + pa_context_unref(context); +} + +void audio_play_sample(int num_sample) +{ + const char *sample_name = build_sample_name(&samples[num_sample]); + + pa_operation *op = pa_context_play_sample(context, sample_name, NULL, + PA_VOLUME_NORM, NULL, NULL); + + pa_operation_unref(op); +} diff --git a/audio.h b/audio.h new file mode 100644 index 0000000..c180ed0 --- /dev/null +++ b/audio.h @@ -0,0 +1,21 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include + +enum samples { + FIRST_KEY_SAMPLE = 0, + LAST_KEY_SAMPLE = 5, + NUM_KEY_SAMPLES = 6, + FIRST_MOD_SAMPLE = 6, + LAST_MOD_SAMPLE = 7, + NUM_MOD_SAMPLES = 2, + NUM_SAMPLES = 8 +}; + +bool audio_init(); +void audio_close(); + +void audio_play_sample(int num_sample); + +#endif // AUDIO_H diff --git a/coveraudiod.config b/coveraudiod.config new file mode 100644 index 0000000..8cec188 --- /dev/null +++ b/coveraudiod.config @@ -0,0 +1 @@ +// ADD PREDEFINED MACROS HERE! diff --git a/coveraudiod.creator b/coveraudiod.creator new file mode 100644 index 0000000..e94cbbd --- /dev/null +++ b/coveraudiod.creator @@ -0,0 +1 @@ +[General] diff --git a/coveraudiod.files b/coveraudiod.files new file mode 100644 index 0000000..76f544c --- /dev/null +++ b/coveraudiod.files @@ -0,0 +1,8 @@ +Makefile +main.c +x11.h +x11.c +audio.h +audio.c +key.h +key.c diff --git a/coveraudiod.includes b/coveraudiod.includes new file mode 100644 index 0000000..eb319c9 --- /dev/null +++ b/coveraudiod.includes @@ -0,0 +1,2 @@ +/usr/include/glib-2.0 +/usr/lib64/glib-2.0/include diff --git a/key.c b/key.c new file mode 100644 index 0000000..6fe76a2 --- /dev/null +++ b/key.c @@ -0,0 +1,19 @@ +#include "audio.h" +#include "key.h" + +static unsigned int key_press_count = 0; + +void handle_keypress(bool dead_key) +{ + int num_sample = 0; + + if (dead_key) { + num_sample = (key_press_count % NUM_MOD_SAMPLES) + FIRST_MOD_SAMPLE; + } else { + num_sample = (key_press_count % NUM_KEY_SAMPLES) + FIRST_KEY_SAMPLE; + } + + audio_play_sample(num_sample); + + key_press_count++; +} diff --git a/key.h b/key.h new file mode 100644 index 0000000..c70dfd1 --- /dev/null +++ b/key.h @@ -0,0 +1,13 @@ +#ifndef KEY_H +#define KEY_H + +#include + +#define MICROSOFT_VENDOR_ID 0x045e + +#define MICROSOFT_TOUCH_COVER_1_PRODUCT_ID 0x079a +#define MICROSOFT_TOUCH_COVER_2_PRODUCT_ID 0x07a7 + +void handle_keypress(bool dead_key); + +#endif // KEY_H diff --git a/key1.wav b/key1.wav new file mode 100644 index 0000000..873f216 Binary files /dev/null and b/key1.wav differ diff --git a/key2.wav b/key2.wav new file mode 100644 index 0000000..15da16f Binary files /dev/null and b/key2.wav differ diff --git a/key3.wav b/key3.wav new file mode 100644 index 0000000..15f64bf Binary files /dev/null and b/key3.wav differ diff --git a/key4.wav b/key4.wav new file mode 100644 index 0000000..a464895 Binary files /dev/null and b/key4.wav differ diff --git a/key5.wav b/key5.wav new file mode 100644 index 0000000..387bef1 Binary files /dev/null and b/key5.wav differ diff --git a/key6.wav b/key6.wav new file mode 100644 index 0000000..2ccbde2 Binary files /dev/null and b/key6.wav differ diff --git a/main.c b/main.c new file mode 100644 index 0000000..bfec675 --- /dev/null +++ b/main.c @@ -0,0 +1,29 @@ +#include +#include + +#include + +#include "x11.h" +#include "audio.h" + +int main(int argc, char *argv[]) +{ + GMainLoop *loop = g_main_loop_new(NULL, TRUE); + + if (!x11_init()) { + return EXIT_FAILURE; + } + + if (!audio_init()) { + return EXIT_FAILURE; + } + + g_debug("coveraudiod running"); + + g_main_loop_run(loop); + + audio_close(); + x11_close(); + + return EXIT_SUCCESS; +} diff --git a/mod1.wav b/mod1.wav new file mode 100644 index 0000000..c535f8a Binary files /dev/null and b/mod1.wav differ diff --git a/mod2.wav b/mod2.wav new file mode 100644 index 0000000..c535f8a Binary files /dev/null and b/mod2.wav differ diff --git a/x11.c b/x11.c new file mode 100644 index 0000000..3b8bcdc --- /dev/null +++ b/x11.c @@ -0,0 +1,257 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "x11.h" +#include "key.h" + +#define ONLY_SURFACE_COVER 1 + +static Display *display; + +static int xi_opcode; +static int xi_event; +static int xi_error; + +static Atom product_id; +static Atom button_horiz_wheel_left; + +static bool is_keyboard(const XIDeviceInfo *info) +{ + if (!info->enabled) + return false; + + // Surface keyboard appears as a SlavePointer sometimes + if (info->use != XISlaveKeyboard && info->use != XISlavePointer) + return false; + + for (int i = 0; i < info->num_classes; i++) { + if (info->classes[i]->type != KeyClass) + continue; + + const XIKeyClassInfo *key_info = (XIKeyClassInfo *)info->classes[i]; + if (key_info->num_keycodes > 40) { + // If it has more than 40 keys, it is a keyboard + return true; + } + } + + return false; +} + +static bool is_surface_touch_cover(const XIDeviceInfo *info) +{ + Atom act_type; + int act_format; + unsigned long nitems, bytes_after; + unsigned char *data; + Status status; + + // First test: product_id + status = XIGetProperty(display, info->deviceid, product_id, 0, 2, False, XA_INTEGER, + &act_type, &act_format, &nitems, &bytes_after, &data); + + if (status != Success) { + return false; + } + + if (act_type != XA_INTEGER || act_format != 32) { + XFree(data); + return false; + } + + int32_t *int_data = (int32_t*)data; + int vendor_id = int_data[0]; + int product_id = int_data[1]; + XFree(data); + + if (vendor_id == MICROSOFT_VENDOR_ID) { + if (product_id != MICROSOFT_TOUCH_COVER_1_PRODUCT_ID && + product_id != MICROSOFT_TOUCH_COVER_2_PRODUCT_ID) { + return false; + } + // TODO: Touch cover 2 + } else { + return false; + } + + // Second test: check if it has a horizontal wheel (all Surface covers 'have' one) + // This way we ignore the device with the volume buttons. + for (int i = 0; i < info->num_classes; i++) { + if (info->classes[i]->type != ButtonClass) + continue; + + const XIButtonClassInfo *btn_info = (XIButtonClassInfo *)info->classes[i]; + for (int j = 0; j < btn_info->num_buttons; j++) { + if (btn_info->labels[j] == button_horiz_wheel_left) { + return true; + } + } + } + + return false; +} + + +static bool find_and_select_devices() +{ + int ndevices; + XIDeviceInfo *info = XIQueryDevice(display, XIAllDevices, &ndevices); + + if (!info) return false; + + for (int i = 0; i < ndevices; i++) { + // Looking for keyboards only currently + if (!is_keyboard(&info[i])) continue; +#if ONLY_SURFACE_COVER + if (!is_surface_touch_cover(&info[i])) continue; +#endif + + g_debug("Capturing events from '%s'", info[i].name); + + // Subscribe to this device's events + XIEventMask mask; + mask.deviceid = info[i].deviceid; + mask.mask_len = XIMaskLen(XI_LASTEVENT); + mask.mask = calloc(mask.mask_len, 1); + XISetMask(mask.mask, XI_KeyPress); + XISelectEvents(display, DefaultRootWindow(display), &mask, 1); + free(mask.mask); + } + + XIFreeDeviceInfo(info); + return true; +} + +static void handle_key_press(const XIDeviceEvent *event) +{ + // Convert into an XKeyEvent + XKeyEvent kevent; + kevent.serial = event->serial; + kevent.send_event = event->send_event; + kevent.display = event->display; + kevent.same_screen = True; + kevent.window = event->event; + kevent.root = event->root; + kevent.subwindow = event->child; + kevent.time = event->time; + kevent.x = event->event_x; + kevent.y = event->event_y; + kevent.x_root = event->root_x; + kevent.y_root = event->root_y; + kevent.state = event->mods.effective; + kevent.keycode = event->detail; + + char buf[4]; + KeySym sym; + + int len = XLookupString(&kevent, buf, sizeof(buf), &sym, NULL); + + bool is_dead_key; + switch (len) { + case 0: + is_dead_key = true; + break; + case 1: + is_dead_key = iscntrl(buf[0]) || isblank(buf[0]); + break; + default: + is_dead_key = false; + break; + } + + handle_keypress(is_dead_key); +} + +static void handle_xi_event(const XGenericEventCookie *event) +{ + switch (event->evtype) { + case XI_HierarchyChanged: + g_debug("Hierarchy changed event"); + find_and_select_devices(); + break; + case XI_KeyPress: + handle_key_press(event->data); + break; + case XI_RawKeyPress: + g_debug("Raw key press"); + break; + } +} + +static gboolean handle_x11_fd(gint fd, GIOCondition condition, gpointer data) +{ + while (XPending(display)) { + XEvent event; + XNextEvent(display, &event); + + switch (event.type) { + case GenericEvent: + if (XGetEventData(display, &event.xcookie)) { + if (event.xcookie.extension == xi_opcode) { + handle_xi_event(&event.xcookie); + } + XFreeEventData(display, &event.xcookie); + } + break; + } + + // Ignoring events + } + + return G_SOURCE_CONTINUE; +} + +bool x11_init() +{ + display = XOpenDisplay(NULL); + if (!display) { + g_warning("Cannot connect to X display"); + return false; + } + + if (!XQueryExtension(display, INAME, &xi_opcode, &xi_event, &xi_error)) { + g_warning("X Input extension not available"); + return false; + } + + int major = 2; + int minor = 0; + if (XIQueryVersion(display, &major, &minor) != Success) { + g_warning("X Input 2.0 not available"); + return false; + } + + product_id = XInternAtom(display, "Device Product ID", False); + button_horiz_wheel_left = XInternAtom(display, "Button Horiz Wheel Left", False); + + // Setup a mask for hierarchy changed events + XIEventMask mask; + mask.deviceid = XIAllDevices; + mask.mask_len = XIMaskLen(XI_LASTEVENT); + mask.mask = calloc(mask.mask_len, 1); + XISetMask(mask.mask, XI_HierarchyChanged); + XISelectEvents(display, DefaultRootWindow(display), &mask, 1); + free(mask.mask); + + if (!find_and_select_devices()) { + return false; + } + + // Prepare integration with glib event loop + g_unix_fd_add(ConnectionNumber(display), G_IO_IN, + handle_x11_fd, NULL); + XFlush(display); + + return true; +} + +void x11_close() +{ + XCloseDisplay(display); +} diff --git a/x11.h b/x11.h new file mode 100644 index 0000000..b5054f6 --- /dev/null +++ b/x11.h @@ -0,0 +1,9 @@ +#ifndef X11_H +#define X11_H + +#include + +bool x11_init(); +void x11_close(); + +#endif // X11_H -- cgit v1.2.3