diff --git a/README.md b/README.md index f830b7f..388c525 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,21 @@ GLava aims to be compatible with _most_ EWMH compliant window managers. Below is Note that some WMs listed without issues have specific overrides when using the `--desktop` flag. See `shaders/env_*.glsl` files for details. +## Reading from MPD's FIFO output + +Add the following to your `~/.config/mpd.conf`: + +``` +audio_output { + type "fifo" + name "glava_fifo" + path "/tmp/mpd.fifo" + format "22050:16:2" +} +``` + +Note the `22050` sample rate -- this is the reccommended setting for GLava. Restart MPD (if nessecary) and start GLava with `glava --audio=fifo`. + ## Licensing GLava is licensed under the terms of the GPLv3, with the exemption of `khrplatform.h`, which is licensed under the terms in its header. GLava includes some (heavily modified) source code that originated from [cava](https://github.com/karlstav/cava), which was initially provided under the MIT license. The source files that originated from cava are the following: diff --git a/fifo.c b/fifo.c index d511c17..4aeff08 100644 --- a/fifo.c +++ b/fifo.c @@ -7,74 +7,123 @@ #include #include #include +#include +#include +#include #include "fifo.h" -void* input_fifo(void* data) { - struct audio_data* audio = (struct audio_data *) data; - int fd; - int n = 0; - signed char buf[1024]; - int tempr, templ, lo; - int q, i; - int t = 0; - int size = 1024; - int bytes = 0; - int flags; - struct timespec req = { .tv_sec = 0, .tv_nsec = 10000000 }; - - fd = open(audio->source, O_RDONLY); - flags = fcntl(fd, F_GETFL, 0); - fcntl(fd, F_SETFL, flags | O_NONBLOCK); - - while (1) { - bytes = read(fd, buf, sizeof(buf)); - - if (bytes == -1) { /* if no bytes read, sleep 10ms and zero shared buffer */ - nanosleep (&req, NULL); - t++; - if (t > 10) { - for (i = 0; i < 2048; i++)audio->audio_out_l[i] = 0; - for (i = 0; i < 2048; i++)audio->audio_out_r[i] = 0; - t = 0; - } - } else { /* if bytes read, go ahead */ - t = 0; - for (q = 0; q < (size / 4); q++) { - - tempr = (buf[4 * q + 3] << 2); - - lo = (buf[4 * q + 2] >> 6); - if (lo < 0) lo = abs(lo) + 1; - if (tempr >= 0) tempr = tempr + lo; - else tempr = tempr - lo; - - templ = (buf[4 * q + 1] << 2); - - lo = (buf[4 * q] >> 6); - if (lo < 0) lo = abs(lo) + 1; - if (templ >= 0) templ = templ + lo; - else templ = templ - lo; - - if (audio->channels == 1) audio->audio_out_l[n] = (tempr + templ) / 2; - - /* stereo storing channels in buffer */ - if (audio->channels == 2) { - audio->audio_out_l[n] = templ; - audio->audio_out_r[n] = tempr; - } - - n++; - if (n == 2048 - 1)n = 0; - } - } - - if (audio->terminate == 1) { - close(fd); - break; - } - - } - - return 0; +/* Implementation struct storage */ + +typeof(*audio_impls) audio_impls[sizeof(audio_impls) / sizeof(struct audio_impl*)] = {}; +size_t audio_impls_idx = 0; + +/* FIFO backend */ + +static void init(struct audio_data* audio) { + if (!audio->source) { + audio->source = strdup("/tmp/mpd.fifo"); + } } + +static void* entry(void* data) { + struct audio_data* audio = (struct audio_data *) data; + + float* bl = (float*) audio->audio_out_l; + float* br = (float*) audio->audio_out_r; + size_t fsz = audio->audio_buf_sz; + size_t ssz = audio->sample_sz; + + int fd; + int16_t buf[ssz / 2]; + size_t q; + int timeout = 50; + + struct timespec tv_last = {}, tv; + bool measured = false; + + if ((fd = open(audio->source, O_RDONLY)) == -1) { + fprintf(stderr, "failed to open FIFO audio source \"%s\": %s\n", audio->source, strerror(errno)); + exit(EXIT_FAILURE); + } + + struct pollfd pfd = { + .fd = fd, + .events = POLLIN + }; + + size_t buffer_offset = (fsz - (ssz / 4)); + + while (true) { + + /* The poll timeout is set to accommodate an approximate UPS, but has little purpose except + for effectively setting the rate of empty samples in the event of the FIFO descriptor + blocking for long periods of time. */ + + switch (poll(&pfd, 1, timeout)) { + case -1: + fprintf(stderr, "FIFO backend: poll() failed (%s)\n", strerror(errno)); + exit(EXIT_FAILURE); + case 0: + pthread_mutex_lock(&audio->mutex); + + memmove(bl, &bl[ssz / 4], buffer_offset * sizeof(float)); + memmove(br, &br[ssz / 4], buffer_offset * sizeof(float)); + + for (q = 0; q < (ssz / 4); ++q) bl[buffer_offset + q] = 0; + for (q = 0; q < (ssz / 4); ++q) br[buffer_offset + q] = 0; + + audio->modified = true; + + pthread_mutex_unlock(&audio->mutex); + break; + default: { + read(fd, buf, sizeof(buf)); + clock_gettime(CLOCK_REALTIME, measured ? &tv : &tv_last); + if (measured) { + /* Set the timeout slightly higher than the delay between samples to prevent empty writes */ + timeout = (((tv.tv_sec - tv_last.tv_sec) * 1000) + ((tv.tv_nsec - tv_last.tv_nsec) / 1000000)) + 1; + tv_last = tv; + } else measured = true; + + pthread_mutex_lock(&audio->mutex); + + memmove(bl, &bl[ssz / 4], buffer_offset * sizeof(float)); + memmove(br, &br[ssz / 4], buffer_offset * sizeof(float)); + + for (size_t n = 0, q = 0; q < (ssz / 2); q += 2) { + + size_t idx = (fsz - (ssz / 4)) + n; + + if (audio->channels == 1) { + float sample = ((buf[q] + buf[q + 1]) / 2) / (float) 65535; + bl[idx] = sample; + br[idx] = sample; + } + + if (audio->channels == 2) { + bl[idx] = buf[q] / (float) 65535; + br[idx] = buf[q + 1] / (float) 65535; + } + + n++; + } + + audio->modified = true; + + pthread_mutex_unlock(&audio->mutex); + break; + } + } + + if (audio->terminate == 1) { + close(fd); + break; + } + + } + + return 0; +} + +AUDIO_ATTACH(fifo); diff --git a/fifo.h b/fifo.h index 8a3ad06..642ffdc 100644 --- a/fifo.h +++ b/fifo.h @@ -14,4 +14,26 @@ struct audio_data { pthread_mutex_t mutex; }; -void* input_fifo(void* data); +struct audio_impl { + const char* name; + void (*init)(struct audio_data* data); + void* (*entry)(void* data); +}; + +#define AUDIO_FUNC(F) \ + .F = (typeof(((struct audio_impl*) NULL)->F)) &F + +extern struct audio_impl* audio_impls[4]; +extern size_t audio_impls_idx; + +static inline void register_audio_impl(struct audio_impl* impl) { audio_impls[audio_impls_idx++] = impl; } + +#define AUDIO_ATTACH(N) \ + static struct audio_impl N##_var = { \ + .name = #N, \ + AUDIO_FUNC(init), \ + AUDIO_FUNC(entry), \ + }; \ + void __attribute__((constructor)) _##N##_construct(void) { \ + register_audio_impl(&N##_var); \ + } diff --git a/glava.c b/glava.c index ea1e054..dcb650f 100644 --- a/glava.c +++ b/glava.c @@ -177,6 +177,7 @@ static const char* help_str = "-b, --backend specifies a window creation backend to use. By default, the most\n" " appropriate backend will be used for the underlying windowing\n" " system.\n" + "-a, --audio=BACKEND specifies an audio input backend to use.\n" "-V, --version print application version and exit\n" "\n" "The REQUEST argument is evaluated identically to the \'#request\' preprocessor directive\n" @@ -188,13 +189,17 @@ static const char* help_str = "The FILE argument may be any file path. All specified file paths are relative to the\n" "active configuration root (usually ~/.config/glava if present).\n" "\n" + "The BACKEND argument may be any of the following strings (for this particular build):\n" + "%s" + "\n" GLAVA_VERSION_STRING "\n"; -static const char* opt_str = "dhvVe:Cm:b:r:"; +static const char* opt_str = "dhvVe:Cm:b:r:a:"; static struct option p_opts[] = { {"help", no_argument, 0, 'h'}, {"verbose", no_argument, 0, 'v'}, {"desktop", no_argument, 0, 'd'}, + {"audio", required_argument, 0, 'a'}, {"request", required_argument, 0, 'r'}, {"entry", required_argument, 0, 'e'}, {"force-mod", required_argument, 0, 'm'}, @@ -222,11 +227,12 @@ int main(int argc, char** argv) { /* Evaluate these macros only once, since they allocate */ const char - * install_path = SHADER_INSTALL_PATH, - * user_path = SHADER_USER_PATH, - * entry = "rc.glsl", - * force = NULL, - * backend = NULL; + * install_path = SHADER_INSTALL_PATH, + * user_path = SHADER_USER_PATH, + * entry = "rc.glsl", + * force = NULL, + * backend = NULL, + * audio_impl_name = "pulseaudio"; const char* system_shader_paths[] = { user_path, install_path, NULL }; char** requests = malloc(1); @@ -241,19 +247,26 @@ int main(int argc, char** argv) { case 'C': copy_mode = true; break; case 'd': desktop = true; break; case 'r': append_buf(requests, &requests_sz, optarg); break; - case 'e': entry = optarg; break; - case 'm': force = optarg; break; - case 'b': backend = optarg; break; + case 'e': entry = optarg; break; + case 'm': force = optarg; break; + case 'b': backend = optarg; break; + case 'a': audio_impl_name = optarg; break; case '?': exit(EXIT_FAILURE); break; case 'V': puts(GLAVA_VERSION_STRING); exit(EXIT_SUCCESS); break; default: - case 'h': - printf(help_str, argc > 0 ? argv[0] : "glava"); + case 'h': { + char buf[2048]; + size_t bsz = 0; + for (size_t t = 0; t < audio_impls_idx; ++t) + bsz += snprintf(buf + bsz, sizeof(buf) - bsz, "\t\"%s\"%s\n", audio_impls[t]->name, + !strcmp(audio_impls[t]->name, audio_impl_name) ? " (default)" : ""); + printf(help_str, argc > 0 ? argv[0] : "glava", buf); exit(EXIT_SUCCESS); break; + } } } @@ -306,13 +319,27 @@ int main(int argc, char** argv) { .sample_sz = rd->samplesize_request, .modified = false }; - if (!audio.source) { - get_pulse_default_sink(&audio); - if (verbose) printf("Using default PulseAudio sink: %s\n", audio.source); + + struct audio_impl* impl = NULL; + + for (size_t t = 0; t < audio_impls_idx; ++t) { + if (!strcmp(audio_impls[t]->name, audio_impl_name)) { + impl = audio_impls[t]; + break; + } + } + + if (!impl) { + fprintf(stderr, "The specified audio backend (\"%s\") is not available.\n", audio_impl_name); + exit(EXIT_FAILURE); } + impl->init(&audio); + + if (verbose) printf("Using audio source: %s\n", audio.source); + pthread_t thread; - pthread_create(&thread, NULL, input_pulse, (void*) &audio); + pthread_create(&thread, NULL, impl->entry, (void*) &audio); float lb[rd->bufsize_request], rb[rd->bufsize_request]; while (rd->alive) { diff --git a/pulse_input.c b/pulse_input.c index cc620f0..198e20d 100644 --- a/pulse_input.c +++ b/pulse_input.c @@ -54,7 +54,9 @@ static void pulseaudio_context_state_callback(pa_context* pulseaudio_context, vo } -void get_pulse_default_sink(struct audio_data* audio) { +static void init(struct audio_data* audio) { + + if (audio->source) return; pa_mainloop_api* mainloop_api; pa_context* pulseaudio_context; @@ -106,7 +108,7 @@ void get_pulse_default_sink(struct audio_data* audio) { #error "Unsupported float format (requires 32 bit IEEE (little or big endian) floating point support)" #endif -void* input_pulse(void* data) { +static void* entry(void* data) { struct audio_data* audio = (struct audio_data*) data; int i, n; size_t ssz = audio->sample_sz; @@ -188,3 +190,5 @@ void* input_pulse(void* data) { return 0; } + +AUDIO_ATTACH(pulseaudio); diff --git a/shaders/rc.glsl b/shaders/rc.glsl index 1eed4b5..422d36f 100644 --- a/shaders/rc.glsl +++ b/shaders/rc.glsl @@ -98,9 +98,14 @@ default when GLava itself is a desktop window. */ #request setclickthrough false -/* PulseAudio source. Can be a number or a name of an audio - sink or device to record from. Set to "auto" to use the - default output device. */ +/* Audio source + + When the "pulseaudio" backend is set, this can be a number or + a name of an audio sink or device to record from. Set to "auto" + to use the default output device. + + When the "fifo" backend is set, "auto" is interpreted as + "/tmp/mpd.fifo". Otherwise, a valid path should be provided. */ #request setsource "auto" /* Buffer swap interval (vsync), set to '0' to prevent @@ -189,7 +194,11 @@ Lower sample rates also can make output more choppy, when not using interpolation. It's generally OK to leave this - value unless you have a strange PulseAudio configuration. */ + value unless you have a strange PulseAudio configuration. + + This option does nothing when using the "fifo" audio + backend. Instead, an ideal rate should be be configured + in the application generating the output. */ #request setsamplerate 22050 /* ** DEPRECATED **