~kennylevinsen/wlavu

2d701338e6ad2fe43f31443de11d12afd4a30608 — Kenny Levinsen 3 years ago 66826ba
Swap ALSA out with PipeWire
3 files changed, 308 insertions(+), 247 deletions(-)

M README.md
M main.c
M meson.build
M README.md => README.md +8 -3
@@ 1,6 1,6 @@
# wlavu

Wayland-based ALSA VU-meter with peaking, with (optional) support for
Wayland-based PipeWire VU-meter with peaking, with (optional) support for
wlr-layer-shell.

It shows an audio input level as a vertical or horizontal bar of variable


@@ 14,7 14,7 @@ as a single pixel of screen height/width, depending on taste.
```
meson build
ninja -C build
./build/wlavu 'dsnoop:0,0'
./build/wlavu
```

## How to use (layer-shell)


@@ 22,9 22,14 @@ ninja -C build
```
meson build
ninja -C build
./build/wlavu -l -a left -w 8 'dsnoop:0,0'
./build/wlavu -l -a left -w 8
```

## Wasn't this ALSA earlier?

Yup, but dsnoop was annoying, and upon looking at PipeWire I found it decently
efficient, so I decided to go with that.

# Help

Go to #kennylevinsen @ chat.freenode.net to discuss, or use [~kennylevinsen/public-inbox@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/public-inbox)

M main.c => main.c +297 -242
@@ 1,5 1,6 @@
#define _POSIX_C_SOURCE 200809L
#include <alsa/asoundlib.h>
#include <assert.h>
#include <sys/poll.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <GL/glut.h>


@@ 20,10 21,27 @@
#include <dev/evdev/input-event-codes.h>
#endif

#include <spa/param/audio/layout.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/props.h>
#include <spa/utils/result.h>

#include <pipewire/pipewire.h>
#include <pipewire/global.h>

#include "wlr-layer-shell-unstable-v1-client-protocol.h"
#include "xdg-shell-client-protocol.h"

#define PEAK_DURATION 20
#define HYSTERESIS 0.0005

enum event_loop_fd {
	EVENT_LOOP_WAYLAND,
	EVENT_LOOP_PIPEWIRE,
};

struct context {
	struct wl_display *display;
	struct wl_compositor *compositor;
	struct wl_surface *surface;
	struct xdg_wm_base *xdg_wm_base;


@@ 33,22 51,35 @@ struct context {
	struct zwlr_layer_surface_v1 *layer_surface;
	struct wl_seat *seat;

	struct spa_hook stream_listener;
	struct pw_context *pw_context;
	struct pw_core *pw_core;
	struct pw_loop *pw_loop;
	struct pw_stream *pw_stream;

	int width;
	int height;

	bool dirty;
	bool pending;
	bool running;

	int channels;
	int rate;
	int target;
	int latency;

	double val;
	double peak;
	double last_val;
	double last_peak;
	double peak_decay;

	struct wl_egl_window *wl_egl_window;
	EGLConfig egl_config;
	EGLContext egl_context;
	EGLDisplay egl_display;
	EGLSurface egl_surface;

	bool running;	
};

static void layer_surface_configure(void *data,


@@ 251,6 282,9 @@ static int draw(struct context *ctx) {
	glEnd();

	ctx->pending = true;
	if (ctx->peak_decay != 0) {
		ctx->peak_decay--;
	}
	struct wl_callback *callback = wl_surface_frame(ctx->surface);
	wl_callback_add_listener(callback, &frame_listener, ctx);



@@ 262,36 296,13 @@ static int draw(struct context *ctx) {
	return 0;
}

#define PEAK_DURATION 20
#define HYSTERESIS 0.0005

static int max_s16(void *buffer, int frames, int channels) {
	int max = 0;
	for (int i = 0; i < frames*channels; i++) {
		int b = abs(((short*)buffer)[i]);
		if (b > max)
			max = b;
	}
	return max;
}

static int max_s32(void *buffer, int frames, int channels) {
	int max = 0;
	for (int i = 0; i < frames*channels; i++) {
		int b = abs(((int*)buffer)[i]);
		if (b > max)
			max = b;
	}
	return max;
}

static int display_dispatch(struct pollfd *pfd, int polln, struct wl_display *display, int timeout) {
	assert(polln > 0);
	if (wl_display_prepare_read(display) == -1) {
		return wl_display_dispatch_pending(display);
	}

	pfd[0].events = POLLOUT;
	pfd[EVENT_LOOP_WAYLAND].events = POLLOUT;
	while (wl_display_flush(display) == -1) {
		if (errno != EAGAIN && errno != EPIPE) {
			wl_display_cancel_read(display);


@@ 307,7 318,7 @@ static int display_dispatch(struct pollfd *pfd, int polln, struct wl_display *di
		}
	}

	pfd[0].events = POLLIN;
	pfd[EVENT_LOOP_WAYLAND].events = POLLIN;
	while (poll(pfd, polln, timeout) == -1) {
		if (errno != EINTR) {
			wl_display_cancel_read(display);


@@ 327,177 338,163 @@ static int display_dispatch(struct pollfd *pfd, int polln, struct wl_display *di
	return wl_display_dispatch_pending(display);
}

static snd_pcm_t *open_pcm(char *device, void **buffer, int *frame_capacity, unsigned int *channels, int *bits) {
	int ret;
	snd_pcm_t *handle_capture;
	if ((ret = snd_pcm_open(&handle_capture, device, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)) != 0) {
		fprintf(stderr, "Could not open capture device '%s': %s\n", device, snd_strerror(ret));
		return NULL;
static bool equal_within(double a, double b, double hysteresis) {
	return a > b - hysteresis && a < b + hysteresis;
}

static double max_f32(void *buffer, int frames) {
	float max = 0;
	for (int idx = 0; idx < frames; idx++) {
		float val = fabsf(((float*)buffer)[idx]);
		if (val > max) {
			max = val;
		}
	}

	return max;
}

static void on_process(void *userdata) {
	struct context *ctx = userdata;

	struct pw_buffer *b = pw_stream_dequeue_buffer(ctx->pw_stream);
	if (b == NULL) {
		fprintf(stderr, "Out of buffers: %s\n", strerror(errno));
		return;
	}

	snd_pcm_hw_params_t *hw_params;
	if ((ret = snd_pcm_hw_params_malloc(&hw_params)) != 0) {
		fprintf(stderr, "Could not allocate hardware parameter structure (%s)\n",
				snd_strerror(ret));
		return NULL;
	struct spa_buffer *buf = b->buffer;
	struct spa_data *d = &buf->datas[0];
	uint8_t *p = d->data;
	if (p == NULL)
		return;

	int offset = SPA_MIN(d->chunk->offset, d->maxsize);
	int size = SPA_MIN(d->chunk->size, d->maxsize - offset);
	p += offset;

	ctx->val = max_f32(p, size / (4 * ctx->channels));
	if (ctx->peak_decay == 0 || ctx->val > ctx->peak) {
		ctx->peak = ctx->val;
		ctx->peak_decay = PEAK_DURATION;
	}
	
	if ((ret = snd_pcm_hw_params_any(handle_capture, hw_params)) != 0) {
		fprintf(stderr, "Could not initialize hardware parameter structure (%s)\n",
				snd_strerror(ret));
		return NULL;

	pw_stream_queue_buffer(ctx->pw_stream, b);

	if (equal_within(ctx->last_val, ctx->val, HYSTERESIS) &&
			equal_within(ctx->last_peak, ctx->peak, HYSTERESIS)) {
		return;
	}

	snd_pcm_hw_params_get_channels(hw_params, channels);
	
	unsigned int rate = 44100;
	snd_pcm_hw_params_get_rate(hw_params, &rate, 0);

	snd_pcm_format_t format;
	snd_pcm_hw_params_get_format(hw_params, &format);

	switch (format) {
	case SND_PCM_FORMAT_S32_LE:
		*bits = 32;
		break;
	case SND_PCM_FORMAT_S16_LE:
		*bits = 16;
		break;
	default:
		fprintf(stderr, "Unknown format for input device: %d\n", format);
		return NULL;
	}

	snd_pcm_hw_params_set_access(handle_capture, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
	if ((ret = snd_pcm_hw_params(handle_capture, hw_params)) != 0) {
		fprintf(stderr, "Could not set hardware parameters (%s)\n",
				snd_strerror(ret));
		return NULL;
	}

	int bufsize = rate * (*bits / 8) / PEAK_DURATION;
	*buffer = calloc(*channels, bufsize);
	if (*buffer == NULL) {
		fprintf(stderr, "Could not allocate buffer: %s\n", strerror(errno));
		return NULL;
	}
	*frame_capacity = bufsize / 4;
	return handle_capture;
	ctx->last_val = ctx->val;
	ctx->last_peak = ctx->peak;
	ctx->dirty = true;
	draw(ctx);
}

static const char usage[] = ""
"usage: %s [options] device\n"
"  -h		show this help message\n"
"  -l           use layer shell\n"
"  -w <width>   bar width when using layer shell\n"
"  -a <side>    side to anchor to (left, right, top, bottom)\n"
"  device	ALSA device (e.g. 'dsnoop:0,0')\n";
static const struct pw_stream_events stream_events = {
	PW_VERSION_STREAM_EVENTS,
	.process = on_process,
};

int main(int argc, char **argv) {
	bool use_layer_shell = false;
	int bar_width = 8;
	int anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
			ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
			ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
	bool vertical = true;
static int setup_pipewire(struct context *ctx) {
	pw_init(NULL, NULL);
	ctx->pw_loop = pw_loop_new(NULL);
	if (ctx->pw_loop == NULL) {
		fprintf(stderr, "Could not create pipewire event loop\n");
		return -1;
	}

	int opt;
	while ((opt = getopt(argc, argv, "hlw:a:")) != -1) {
		switch (opt) {
		case 'l':
			use_layer_shell = true;
			break;
		case 'w':
			bar_width = strtol(optarg, NULL, 10);
			break;
		case 'a':
			if (strcmp(optarg, "left") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
				vertical = true;
			} else if (strcmp(optarg, "right") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
				vertical = true;
			} else if (strcmp(optarg, "top") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
				vertical = false;
			} else if (strcmp(optarg, "bottom") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
				vertical = false;
			} else {
				fprintf(stderr, "unknown side: %s\n", optarg);
				return EXIT_FAILURE;
			}
			break;
		default:
			fprintf(stderr, usage, argv[0]);
			return opt == 'h' ? EXIT_SUCCESS : EXIT_FAILURE;
		}
	ctx->pw_context = pw_context_new(ctx->pw_loop, NULL, 0);
	if (ctx->pw_context == NULL) {
		fprintf(stderr, "Could not create pipewire context\n");
		return -1;
	}

	if (optind >= argc) {
		fprintf(stderr, "Expected an ALSA device name after arguments\n");
		return EXIT_FAILURE;
	ctx->pw_core = pw_context_connect(ctx->pw_context, NULL, 0);
	if (ctx->pw_core == NULL) {
		fprintf(stderr, "Could not connect to pipewire context\n");
		return -1;
	}
	char *device = argv[optind];

	struct context ctx = {
		.height = bar_width,
		.width = 1024,
		.running = true,
		.val = 0.0,
		.peak = 0.0,
		.dirty = true,
	};
	struct pw_properties *core_props = pw_core_get_properties(ctx->pw_core);

	void *buffer = NULL;
	int frame_capacity = 0, bits = 0;
	unsigned int channels = 0;
	snd_pcm_t *handle_capture = open_pcm(device, &buffer, &frame_capacity, &channels, &bits);
	if (handle_capture == NULL) {
		return EXIT_FAILURE;
	int rate = 48000;
	const char *default_rate = pw_properties_get(core_props, "default.clock.rate");
	if (default_rate != NULL) {
		rate = strtol(default_rate, NULL, 10);
	}

	struct pw_properties *props = pw_properties_new(
				PW_KEY_MEDIA_TYPE, "Audio",
				PW_KEY_MEDIA_CATEGORY, "Capture",
				PW_KEY_MEDIA_ROLE, "Monitor",
				PW_KEY_STREAM_MONITOR, "true",
				PW_KEY_STREAM_DONT_REMIX, "true",
				NULL);

	pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", rate / ctx->latency, rate);

	ctx->pw_stream = pw_stream_new(ctx->pw_core, "wlavu", props);

	pw_stream_add_listener(ctx->pw_stream, &ctx->stream_listener, &stream_events, ctx);

	void *sound_buffer[8192];
	struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(sound_buffer, sizeof(sound_buffer));

	const struct spa_pod *params[1];
	params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat,
			&SPA_AUDIO_INFO_RAW_INIT(
				.format = SPA_AUDIO_FORMAT_F32,
				.channels = ctx->channels,
				.rate = rate,
			));

	int ret = pw_stream_connect(ctx->pw_stream,
			PW_DIRECTION_INPUT,
			ctx->target,
			PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS,
			params, 1);
	if (ret < 0) {
		fprintf(stderr, "Could not connect to stream: %s\n", spa_strerror(ret));
		return -1;
	}
	return 0;
}

	struct wl_display *display = wl_display_connect(NULL);
	if (display == NULL) {
static int setup_wayland(struct context *ctx, bool layer_shell, bool vertical, int anchor) {
	ctx->display = wl_display_connect(NULL);
	if (ctx->display == NULL) {
		fprintf(stderr, "failed to create display\n");
		return EXIT_FAILURE;
		return -1;
	}

	struct wl_registry *registry = wl_display_get_registry(display);
	wl_registry_add_listener(registry, &registry_listener, &ctx);
	wl_display_dispatch(display);
	wl_display_roundtrip(display);
	struct wl_registry *registry = wl_display_get_registry(ctx->display);
	wl_registry_add_listener(registry, &registry_listener, ctx);
	wl_display_dispatch(ctx->display);
	wl_display_roundtrip(ctx->display);

	if (ctx.compositor == NULL || ctx.xdg_wm_base == NULL) {
	if (ctx->compositor == NULL) {
		fprintf(stderr, "no wl_compositor support\n");
		return EXIT_FAILURE;
		return -1;
	}

	ctx.egl_display = eglGetDisplay((EGLNativeDisplayType)display);
	if (ctx.egl_display == EGL_NO_DISPLAY) {
	ctx->egl_display = eglGetDisplay((EGLNativeDisplayType)ctx->display);
	if (ctx->egl_display == EGL_NO_DISPLAY) {
		fprintf(stderr, "failed to create EGL display\n");
		return EXIT_FAILURE;
		return -1;
	}

	eglBindAPI(EGL_OPENGL_API);
	glutInit(&argc, argv);

	EGLint major, minor;
	if (!eglInitialize(ctx.egl_display, &major, &minor)) {
	if (!eglInitialize(ctx->egl_display, &major, &minor)) {
		fprintf(stderr, "failed to initialize EGL\n");
		return EXIT_FAILURE;
		return -1;
	}

	EGLint count;
	eglGetConfigs(ctx.egl_display, NULL, 0, &count);
	eglGetConfigs(ctx->egl_display, NULL, 0, &count);

	EGLint config_attribs[] = {
		EGL_SURFACE_TYPE, EGL_WINDOW_BIT,


@@ 509,121 506,179 @@ int main(int argc, char **argv) {
	};
	EGLint n = 0;
	EGLConfig *configs = calloc(count, sizeof(EGLConfig));
	eglChooseConfig(ctx.egl_display, config_attribs, configs, count, &n);
	eglChooseConfig(ctx->egl_display, config_attribs, configs, count, &n);
	if (n == 0) {
		fprintf(stderr, "failed to choose an EGL config\n");
		return EXIT_FAILURE;
		return -1;
	}
	ctx.egl_config = configs[0];
	ctx->egl_config = configs[0];

	EGLint context_attribs[] = {
		EGL_CONTEXT_CLIENT_VERSION, 2,
		EGL_NONE,
	};
	ctx.egl_context = eglCreateContext(ctx.egl_display, ctx.egl_config,
	ctx->egl_context = eglCreateContext(ctx->egl_display, ctx->egl_config,
		EGL_NO_CONTEXT, context_attribs);

	ctx.surface = wl_compositor_create_surface(ctx.compositor);
	ctx->surface = wl_compositor_create_surface(ctx->compositor);

	if (!use_layer_shell) {
		if (ctx.xdg_wm_base == NULL) {
	if (!layer_shell) {
		if (ctx->xdg_wm_base == NULL) {
			fprintf(stderr, "no xdg_wm_base support\n");
			return EXIT_FAILURE;
			return -1;
		}
		ctx.xdg_surface = xdg_wm_base_get_xdg_surface(ctx.xdg_wm_base, ctx.surface);
		ctx.xdg_toplevel = xdg_surface_get_toplevel(ctx.xdg_surface);
		ctx->xdg_surface = xdg_wm_base_get_xdg_surface(ctx->xdg_wm_base, ctx->surface);
		ctx->xdg_toplevel = xdg_surface_get_toplevel(ctx->xdg_surface);

		xdg_surface_add_listener(ctx.xdg_surface, &xdg_surface_listener, &ctx);
		xdg_toplevel_add_listener(ctx.xdg_toplevel, &xdg_toplevel_listener, &ctx);
		xdg_surface_add_listener(ctx->xdg_surface, &xdg_surface_listener, ctx);
		xdg_toplevel_add_listener(ctx->xdg_toplevel, &xdg_toplevel_listener, ctx);
	} else {
		if (ctx.layer_shell == NULL) {
		if (ctx->layer_shell == NULL) {
			fprintf(stderr, "no layer_shell support\n");
			return EXIT_FAILURE;
			return -1;
		}
		ctx.layer_surface = zwlr_layer_shell_v1_get_layer_surface(ctx.layer_shell, ctx.surface,
		ctx->layer_surface = zwlr_layer_shell_v1_get_layer_surface(ctx->layer_shell, ctx->surface,
				NULL, ZWLR_LAYER_SHELL_V1_LAYER_TOP, "wlavu");
		if (vertical) {
			zwlr_layer_surface_v1_set_size(ctx.layer_surface, bar_width, 0);
			zwlr_layer_surface_v1_set_size(ctx->layer_surface, ctx->width, 0);
		} else {
			zwlr_layer_surface_v1_set_size(ctx.layer_surface, 0, bar_width);
			zwlr_layer_surface_v1_set_size(ctx->layer_surface, 0, ctx->width);
		}
		zwlr_layer_surface_v1_set_anchor(ctx.layer_surface, anchor);
		zwlr_layer_surface_v1_set_exclusive_zone(ctx.layer_surface, bar_width);
		zwlr_layer_surface_v1_set_layer(ctx.layer_surface, ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM);
		zwlr_layer_surface_v1_add_listener(ctx.layer_surface, &layer_surface_listener, &ctx);
		zwlr_layer_surface_v1_set_anchor(ctx->layer_surface, anchor);
		zwlr_layer_surface_v1_set_exclusive_zone(ctx->layer_surface, ctx->width);
		zwlr_layer_surface_v1_set_layer(ctx->layer_surface, ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM);
		zwlr_layer_surface_v1_add_listener(ctx->layer_surface, &layer_surface_listener, ctx);
	}

	wl_surface_commit(ctx.surface);
	wl_display_roundtrip(display);
	wl_surface_commit(ctx->surface);
	wl_display_roundtrip(ctx->display);
	
	ctx.wl_egl_window = wl_egl_window_create(ctx.surface, ctx.width, ctx.height);
	ctx.egl_surface = eglCreateWindowSurface(ctx.egl_display, ctx.egl_config,
		(EGLNativeWindowType)ctx.wl_egl_window, NULL);
	ctx->wl_egl_window = wl_egl_window_create(ctx->surface, ctx->width, ctx->height);
	ctx->egl_surface = eglCreateWindowSurface(ctx->egl_display, ctx->egl_config,
		(EGLNativeWindowType)ctx->wl_egl_window, NULL);

	if (!eglMakeCurrent(ctx->egl_display, ctx->egl_surface, ctx->egl_surface, ctx->egl_context)) {
		fprintf(stderr, "Could not make EGL context current\n");
		return -1;
	}

	eglSwapInterval(ctx->egl_display, 0);

	return 0;
}

static const char usage[] = ""
"usage: %s [options]\n"
"  -h		  show this help message\n"
"  -l             use layer shell\n"
"  -w <width>     bar width when using layer shell\n"
"  -a <side>      side to anchor to (left, right, top, bottom)\n"
"  -c <channels>  channels (default: 2)\n"
"  -t <target>    target device (default: any)\n"
"  -L <latency>   latency in fractions of a second (defualt: 20)\n";

int main(int argc, char **argv) {
	bool use_layer_shell = false;
	int anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
			ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
			ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
	bool vertical = true;

	struct context ctx = {
		.height = 8,
		.width = 1024,
		.running = true,
		.val = 0.0,
		.peak = 0.0,
		.dirty = true,
		.channels = 2,
		.target = PW_ID_ANY,
		.latency = 20,
	};

	int opt;
	while ((opt = getopt(argc, argv, "hlw:a:r:c:t:L:")) != -1) {
		switch (opt) {
		case 'l':
			use_layer_shell = true;
			break;
		case 'w':
			ctx.width = strtol(optarg, NULL, 10);
			break;
		case 'a':
			if (strcmp(optarg, "left") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
				vertical = true;
			} else if (strcmp(optarg, "right") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
				vertical = true;
			} else if (strcmp(optarg, "top") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
				vertical = false;
			} else if (strcmp(optarg, "bottom") == 0) {
				anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
					ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
				vertical = false;
			} else {
				fprintf(stderr, "unknown side: %s\n", optarg);
				return EXIT_FAILURE;
			}
			break;
		case 'c':
			ctx.channels = strtol(optarg, NULL, 10);
			break;
		case 't':
			ctx.target = strtol(optarg, NULL, 10);
			break;
		case 'L':
			ctx.latency = strtol(optarg, NULL, 10);
			break;
		default:
			fprintf(stderr, usage, argv[0]);
			return opt == 'h' ? EXIT_SUCCESS : EXIT_FAILURE;
		}
	}

	if (!eglMakeCurrent(ctx.egl_display, ctx.egl_surface, ctx.egl_surface, ctx.egl_context)) {
		fprintf(stderr, "eglMakeCurrent failed\n");
	if (setup_pipewire(&ctx) == -1) {
		return EXIT_FAILURE;
	}

	eglSwapInterval(ctx.egl_display, 0);
	glutInit(&argc, argv);

	int polln = snd_pcm_poll_descriptors_count(handle_capture) + 1;
	struct pollfd *pfd = calloc(polln, sizeof(struct pollfd));
	if (pfd == NULL) {
		fprintf(stderr, "Could not allocate poll descriptors\n");
	if (setup_wayland(&ctx, use_layer_shell, vertical, anchor) == -1) {
		return EXIT_FAILURE;
	}
	pfd[0].fd = wl_display_get_fd(display);
	polln = snd_pcm_poll_descriptors(handle_capture, &pfd[1], polln-1) + 1;
	// For some reason we must read at least once
	snd_pcm_readi(handle_capture, buffer, frame_capacity);

	struct pollfd pollfds[] = {
		[EVENT_LOOP_WAYLAND] = {
			.fd = wl_display_get_fd(ctx.display),
			.events = POLLIN,
		},
		[EVENT_LOOP_PIPEWIRE] = {
			.fd = pw_loop_get_fd(ctx.pw_loop),
			.events = POLLIN,
		},
	};

	draw(&ctx);
	
	double peak;
	int peak_timer = 0;
	double divisor = INT_MAX;
	int (*max)(void *buffer, int frames, int channels) = bits == 32 ? max_s32 : max_s16;

	int ret;
	while (display_dispatch(pfd, polln, display, -1) != -1 && ctx.running) {
		unsigned short revent = 0;
		if ((ret = snd_pcm_poll_descriptors_revents(handle_capture, &pfd[1], polln-1, &revent)) != 0) {
			fprintf(stderr, "Could not convert alsa revents (%s)\n",
					snd_strerror(ret));
			return EXIT_FAILURE;
		}
		if ((revent & POLLIN) == 0) {
			continue;
		}
		snd_pcm_sframes_t frames = snd_pcm_readi(handle_capture, buffer, frame_capacity);

		if (frames < 0) {
			if ((ret = snd_pcm_recover(handle_capture, frames, 0)) != 0) {
				fprintf(stderr, "Could not recover from read error: %s\n", snd_strerror(ret));
	while (display_dispatch(pollfds, 2, ctx.display, -1) != -1 && ctx.running) {
		if (pollfds[EVENT_LOOP_PIPEWIRE].revents & POLLIN) {
			int ret = pw_loop_iterate(ctx.pw_loop, 0);
			if (ret < 0) {
				fprintf(stderr, "Could not iterate pipewire loop: %s", spa_strerror(ret));
				return EXIT_FAILURE;
			}
			continue;
		}

		double val = ((double)max(buffer, frames, channels)) / divisor;
		if (val >= peak || peak_timer == 0) {
			peak = val;
			peak_timer = PEAK_DURATION;
		} else if (peak_timer > 0) {
			peak_timer--;
		}

		if (val < ctx.val - HYSTERESIS || val > ctx.val + HYSTERESIS ||
				peak < ctx.peak - HYSTERESIS || peak > ctx.peak + HYSTERESIS) {
			ctx.dirty = true;
			ctx.val = val;
			ctx.peak = peak;
			draw(&ctx);
		}
	}

	snd_pcm_close(handle_capture);
	wl_surface_destroy(ctx.surface);

	return EXIT_SUCCESS;

M meson.build => meson.build +3 -2
@@ 55,8 55,9 @@ executable(
	dependencies: [
		protocols_dep,
		cc.find_library('m'),
		cc.find_library('asound'),
		dependency('freeglut'),
		dependency('libpipewire-0.3'),
		dependency('libspa-0.2'),
		cc.find_library('glut'),
		dependency('gl'),
		dependency('glesv2'),
		dependency('egl'),