-
Only applies to you if you run Linux and PulseAudio.
Many years ago, Lennart offered up a volume meter program for PA.
http://0pointer.de/lennart/projects/pavumeter/ (http://0pointer.de/lennart/projects/pavumeter/)
My gui/GTK1.2! skills are stuck in a timewarp just as the aforementioned src.
I use the program to check if my mic is not maxxing out when participating in certain occasional d-live streams.
I'd like to add VU marks to the meters. I'd like to be able to read the live meter and then push the fader on the desk to 0dB to -3dB, on the fly.
I am interested to know you think this src software a dead duck. The thing is, it suits me 85%. For the last 15% for it to be perfect would be to add the VU ticks and possibly rotate it horizontal.
Or do I commit the Cardinal sin and re-invent the wheel?
-
While not a solution to your question, if you're using audio for professional work and need such things, why not use JACK and something like KxStudio's Carla for a virtual rack?. There are tons of modules (LDASPA, and LV2) you can load which will do not just DSP, but also metering, etc.
-
While not a solution to your question, if you're using audio for professional work and need such things, why not use JACK and something like KxStudio's Carla for a virtual rack?. There are tons of modules (LDASPA, and LV2) you can load which will do not just DSP, but also metering, etc.
Short answer: too fiddly and I don't want to dick around with low-latency kernels, prefer vanilla ones.
I just wanted something a bit betterer than the terrible stock (mono) meter in pavucontrol. I also asked a dev overseas to quote me a price to quickly add what is needed to the src, only to be told that Pulse is a dead duck. Pipewire is in my future.
It's only been 15 years since I last had Jack running fully. Got fed up with ordinary software always requiring oss/alsa shims to connect to the server. Maybe it's time to look at it again.
:)
https://wiki.archlinux.org/index.php/PipeWire
edit: typo
-
I use the program to check if my mic is not maxxing out when participating in certain occasional d-live streams.
Why use that shit when pavucontrol (https://freedesktop.org/software/pulseaudio/pavucontrol/) exists?
I am interested to know you think this src software a dead duck.
It's developer for sure is.
Also, we use GTK+3 nowadays.
Or do I commit the Cardinal sin and re-invent the wheel?
PulseAudio volume controls are pretty darn simple things, so it's more like customizing your wheel than reinventing one. See here (https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/WritingVolumeControlUIs/).
The only tricky thing is to remember that the audio samples are linear and correspond to amplitude; so 3 dB corresponds to a ratio of sqrt(2) in amplitude. You usually want a peak detect (difference between minimum and maximum amplitudes within a small set of samples, say 50ms worth), but you can use e.g. fftw3 (http://fftw.org/) to do a real spectrum analysis too.
What I've thought about, is to make one that dynamically adjusts the mic volume within given bounds, using the specified time constant, but only when there is input that exceeds a given "silence" level; at "silence", there is no adjustment.
-
Why use that shit when pavucontrol (https://freedesktop.org/software/pulseaudio/pavucontrol/) exists?
I just wanted something a bit betterer than the terrible stock (mono) meter in pavucontrol.
What wasn't mentioned is the other need for the OP. We had a number of analog recordings to bring into the computer. Many of them were a bit weak on one channel, yet we needed a few dBs of headroom for EQ before the mp3 encoder. I used a terminal PCM recorder and I just wanted a spot on a stereo scale to aim for a certain level at a glance. Do you know what I ended up doing? I used a white board marker right on the monitor screen.
:)
-
I am interested to know you think this src software a dead duck.
It's developer for sure is.
No argument here. I avoid anything with systemd and I pay the penalty for it.
Also, we use GTK+3 nowadays.
I'm not into GUI, just like the command line.
I read the GTK1 API at one stage, played around a little bit at the time. Now 4.0 is out. :scared:
-
What wasn't mentioned is the other need for the OP. We had a number of analog recordings to bring into the computer. Many of them were a bit weak on one channel, yet we needed a few dBs of headroom for EQ before the mp3 encoder. I used a terminal PCM recorder and I just wanted a spot on a stereo scale to aim for a certain level at a glance. Do you know what I ended up doing? I used a white board marker right on the monitor screen.
:)
This is exactly why I harp so often about how important it is to understand the real world use cases first, before designing/redesigning/upgrading a tool.
Most VU meters show linear scale amplitude (so a dB scale ticks are at logarithmic intervals). But that's not at all necessary, it just means an additional (logarithmic) conversion at display time (so, what, max. 60 times a second; completely neglible in the scheme of things).
For your use case, I wonder if a undecorated transparent window at left or right edge of the display (i.e., heads-up display style) with just the channel volume bars would work; maybe a tiny bar per dB? Or a continuous (pair of) bar(s) with tick marks at dB intervals? The actual controls could be in a separate window that could be minimized without affecting the display.
So, what is it exactly that you need? Not as a difference to that awful thing, but as a full description of a small applet? Maybe even draw (freehand, Paint-style) an example picture?
-
So, what is it exactly that you need?
edit: I think everyone on here knows what I really need... :popcorn:
What I thought I needed was just a few tics on the scale so I could get the L+R balance closer. An indication, not so much a actual measurement. But yeah, a 0dB downwards log scale would be nice.
I remember in way back in Visual Basic 3(?) you could set ticks on progress bars and sliders and I was hoping someone who knows GTK could add it to the code without breaking the very easy compile.
The only other wish was that it was orientated vertically not horizontal. Transparency apps, I've tended to stay away from, opting for the always-on-top available in lxde, lxqt, mate. But a transparent rectangle with no window widgets is compelling, the more I think about it. I've since realized that the reason these apps don't exist is because they are included in the DAW interface yet I prefer to use command line tools mostly.
My dilemma was that if I had to get my hands dirty, I'd want slider/fader controls on it as well. But it's just to much C++ for this ole dog, as I won't go beyond C. So I took the lazy way out and asked on here if a few little changes could be made without going overboard.
-
But it's just to much C++ for this ole dog, as I won't go beyond C. So I took the lazy way out and asked on here if a few little changes could be made without going overboard.
Both GTK+ and PulseAudio are C, so why not write one in C?
Heck, if you only need it for your default sound source, it'll be a very simple little program, as the "simple" interface suffices. Lemme cobble together something...
-
So, what is it exactly that you need?
edit: I think everyone on here knows what I really need... :popcorn:
ALSA comes (since almost very first releases) with
what you want already.. toolkit independent suited
to work with any sort of audio stream you can think ..
try it a bit by just :
aplay -vv -V stereo -i -B -N -M -D plug:ameter
ameter like any alsa device must already be compiled
with your release... if not you need to enable that in compile time
Later you define you alsa.conf carefully with those required
bindings and data paths to your audio..
Pulse has nothing to do with that.. nor JACK they all will
just attach the data stream
screenshot provided
Paul
-
Something like the following.
vu.h: A simple thread-safe interface for VU metering
#ifndef VU_H
#define VU_H
/**
* Initialize VU measurements
*
* @param server PulseAudio server; NULL for default
* @param appname Application name
* @param devname Source name; NULL for default
* @param stream Descriptive stream name
* @param channels Number of channels
* @param rate Samples per second per channel
* @param samples Samples per update
* @return Zero if success, nonzero if error.
*/
int vu_start(const char *server,
const char *appname,
const char *devname,
const char *stream,
int channels,
int rate,
int samples);
/**
* Convert vu_start() return value to a string
*/
const char *vu_error(int);
/**
* Stop VU measurements
*/
void vu_stop(void);
/**
* Wait for the next VU update
*/
void vu_wait(void);
/**
* Get latest VU peaks per channel; thread-safe
*
* @param peak Array of floats to be populated
* @param channels Number of channels in peak array
* @return Zero if no new data available,
* number of channels available if updated,
* negative if an error occurred.
*/
int vu_peak(float *to, int channels);
/**
* Check if new VU peaks are available; thread-safe
*/
int vu_peak_available(void);
#endif /* VU_H */
vu.c: Implementing the above
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdint.h>
#include <pthread.h>
#include <limits.h>
#include <pulse/simple.h>
#include <pulse/error.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
static volatile int done = 0;
static pa_simple *audio = NULL;
static size_t audio_channels = 0;
static size_t audio_samples = 0;
static int32_t *audio_buffer = NULL; /* audio_buffer[audio_samples][audio_channels] */
static int32_t *audio_min = NULL; /* audio_min[audio_channels] */
static int32_t *audio_max = NULL; /* audio_max[audio_channels] */
static pthread_t audio_thread;
static pthread_mutex_t peak_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t peak_update = PTHREAD_COND_INITIALIZER;
static float *peak_amplitude = NULL;
static volatile int peak_available = 0;
int vu_peak_available(void)
{
return peak_available;
}
static void *worker(void *unused)
{
(void)unused; /* Silence warning about unused parameter. */
while (!done) {
int err = 0;
if (pa_simple_read(audio, audio_buffer, audio_channels * audio_samples * sizeof audio_buffer[0], &err) < 0) {
done = -EIO;
break;
}
for (size_t c = 0; c < audio_channels; c++) {
audio_min[c] = (int32_t)( 2147483647);
audio_max[c] = (int32_t)(-2147483648);
}
int32_t *const end = audio_buffer + audio_channels * audio_samples;
int32_t *ptr = audio_buffer;
/* Min-max peak detect. */
while (ptr < end) {
for (size_t c = 0; c < audio_channels; c++) {
const int32_t s = *(ptr++);
audio_min[c] = (audio_min[c] < s) ? audio_min[c] : s;
audio_max[c] = (audio_max[c] > s) ? audio_max[c] : s;
}
}
/* absolute values. */
for (size_t c = 0; c < audio_channels; c++) {
if (audio_min[c] == (int32_t)(-2147483648))
audio_min[c] = (int32_t)( 2147483647);
else
if (audio_min[c] < 0)
audio_min[c] = -audio_min[c];
else
audio_min[c] = 0;
if (audio_max[c] < 0)
audio_max[c] = 0;
}
/* Update peak amplitudes. */
pthread_mutex_lock(&peak_lock);
if (peak_available++) {
for (size_t c = 0; c < audio_channels; c++) {
const float amplitude = (audio_max[c] > audio_min[c]) ? audio_max[c] / 2147483647.0f : audio_min[c] / 2147483647.0f;
peak_amplitude[c] = (peak_amplitude[c] > amplitude) ? peak_amplitude[c] : amplitude;
}
} else {
for (size_t c = 0; c < audio_channels; c++) {
const float amplitude = (audio_max[c] > audio_min[c]) ? audio_max[c] / 2147483647.0f : audio_min[c] / 2147483647.0f;
peak_amplitude[c] = amplitude;
}
}
pthread_cond_broadcast(&peak_update);
pthread_mutex_unlock(&peak_lock);
}
/* Wake up all waiters on the peak update, too. */
pthread_cond_broadcast(&peak_update);
return NULL;
}
const char *vu_error(int err)
{
if (err < 0)
return strerror(-err);
else
if (err > 0)
return pa_strerror(err);
else
return "OK";
}
void vu_stop(void)
{
if (audio) {
if (!done)
done = 1;
pthread_join(audio_thread, NULL);
pa_simple_free(audio);
audio_thread = pthread_self();
audio = NULL;
}
free(audio_buffer); /* Note: free(NULL) is OK. */
free(audio_min);
free(audio_max);
audio_buffer = NULL;
audio_min = NULL;
audio_max = NULL;
audio_channels = 0;
audio_samples = 0;
pthread_mutex_lock(&peak_lock);
free(peak_amplitude);
peak_amplitude = NULL;
peak_available = 0;
pthread_mutex_unlock(&peak_lock);
}
void vu_wait(void)
{
pthread_mutex_lock(&peak_lock);
if (audio && !done)
pthread_cond_wait(&peak_update, &peak_lock);
pthread_mutex_unlock(&peak_lock);
}
int vu_peak(float *to, int num)
{
pthread_mutex_lock(&peak_lock);
if (!peak_amplitude || !peak_available || audio_channels < 1) {
pthread_mutex_unlock(&peak_lock);
return 0;
}
const int have = (int)audio_channels;
if (num > 0) {
const int cmax = (num < have) ? num : have;
for (int c = 0; c < cmax; c++)
to[c] = peak_amplitude[c];
}
peak_available = 0;
pthread_mutex_unlock(&peak_lock);
return have;
}
int vu_start(const char *server,
const char *appname,
const char *devname,
const char *stream,
int channels,
int rate,
int samples)
{
pa_sample_spec samplespec;
pa_buffer_attr bufferspec;
pthread_attr_t attrs;
int err;
if (!appname || !*appname || !stream || !*stream ||
channels < 1 || channels > 128 || rate < 1 || rate > 1000000 || samples < 1 || samples > 1000000) {
return -EINVAL;
}
/* Empty or "default" server maps to NULL. */
if (server && (!*server || !strcmp(server, "default")))
server = NULL;
/* Empty or "default" devname maps to NULL. */
if (devname && (!*devname || !strcmp(devname, "default")))
devname = NULL;
/* If already running, stop. */
if (audio) {
vu_stop();
}
done = 0;
pthread_mutex_lock(&peak_lock);
samplespec.format = PA_SAMPLE_S32NE;
samplespec.rate = rate;
samplespec.channels = channels;
bufferspec.maxlength = (uint32_t)(-1);
bufferspec.tlength = (uint32_t)(-1);
bufferspec.prebuf = (uint32_t)(-1);
bufferspec.minreq = (uint32_t)(-1);
bufferspec.fragsize = (uint32_t)channels * (uint32_t)samples * (uint32_t)sizeof audio_buffer[0];
err = PA_OK;
audio = pa_simple_new(server, appname, PA_STREAM_RECORD, devname, stream, &samplespec, NULL, &bufferspec, &err);
if (!audio) {
pthread_mutex_unlock(&peak_lock);
return err;
}
/* Allocate memory for the various buffers. */
audio_buffer = calloc((size_t)channels * sizeof audio_buffer[0], (size_t)samples);
audio_min = malloc((size_t)channels * sizeof audio_min[0]);
audio_max = malloc((size_t)channels * sizeof audio_max[0]);
peak_amplitude = malloc((size_t)channels * sizeof peak_amplitude[0]);
if (!audio_buffer || !audio_min || !audio_max || !peak_amplitude) {
free(peak_amplitude);
free(audio_max);
free(audio_min);
free(audio_buffer);
pa_simple_free(audio);
audio = NULL;
audio_buffer = NULL;
audio_min = NULL;
audio_max = NULL;
peak_amplitude = NULL;
pthread_mutex_unlock(&peak_lock);
return -ENOMEM;
}
for (int c = 0; c < channels; c++)
peak_amplitude[c] = 0.0f;
peak_available = 0;
audio_channels = channels;
audio_samples = samples;
pthread_attr_init(&attrs);
pthread_attr_setstacksize(&attrs, 2 * PTHREAD_STACK_MIN);
err = pthread_create(&audio_thread, &attrs, worker, NULL);
if (err) {
pa_simple_free(audio);
free(peak_amplitude);
free(audio_max);
free(audio_min);
free(audio_buffer);
audio = NULL;
audio_buffer = NULL;
audio_min = NULL;
audio_max = NULL;
peak_amplitude = NULL;
audio_channels = 0;
audio_samples = 0;
pthread_mutex_unlock(&peak_lock);
return -err;
}
pthread_attr_destroy(&attrs);
pthread_mutex_unlock(&peak_lock);
return 0;
}
gui.c: A simple Gtk+ UI for the VU meters
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
#include <signal.h>
#include <gtk/gtk.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include "vu.h"
#ifndef MAX_CHANNELS
#define MAX_CHANNELS 32
#endif
#ifndef MAX_RATE
#define MAX_RATE 250000
#endif
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = SA_RESTART;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
struct tickmark {
float amplitude;
float red;
float green;
float blue;
};
enum placement {
PLACEMENT_LEFT = 1,
PLACEMENT_RIGHT = 2,
PLACEMENT_TOP = 3,
PLACEMENT_BOTTOM = 4
};
static const char *server = NULL;
static const char *device = NULL;
static int channels = 2;
static int rate = 48000;
static int updates = 60;
static int bar_size = 4;
static int bar_space = 3;
static int display_monitor = -1;
static enum placement display_placement = PLACEMENT_RIGHT;
static float *peak = NULL;
static float decay = 0.95f;
static float red_limit = 0.891251f; /* Amplitude within 1 dB of clipping */
static float green_limit = 0.707946f; /* Amplitude within 3 dB of clipping */
static struct tickmark tickmarks[] = {
{ .amplitude = 0.891251f, .red = 1.0f, .green = 0.00f, .blue = 0.0f }, /* 1 dB */
{ .amplitude = 0.794328f, .red = 1.0f, .green = 1.00f, .blue = 0.0f }, /* 2 dB */
{ .amplitude = 0.707946f, .red = 0.0f, .green = 1.00f, .blue = 0.0f }, /* 3 dB */
{ .amplitude = 0.630957f, .red = 0.0f, .green = 0.95f, .blue = 0.0f }, /* 4 dB */
{ .amplitude = 0.562341f, .red = 0.0f, .green = 0.90f, .blue = 0.0f }, /* 5 dB */
{ .amplitude = 0.501187f, .red = 0.0f, .green = 0.85f, .blue = 0.0f }, /* 6 dB */
{ .amplitude = 0.446684f, .red = 0.0f, .green = 0.80f, .blue = 0.0f }, /* 7 dB */
{ .amplitude = 0.398107f, .red = 0.0f, .green = 0.75f, .blue = 0.0f }, /* 8 dB */
{ .amplitude = 0.354813f, .red = 0.0f, .green = 0.70f, .blue = 0.0f }, /* 9 dB */
{ .amplitude = 0.316228f, .red = 0.0f, .green = 0.65f, .blue = 0.0f }, /* 10 dB */
{ .amplitude = -1.0f }
};
static gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
GdkRectangle area;
gtk_widget_get_clip(widget, &area);
cairo_save(cr);
cairo_set_source_rgb(cr, 0.0,0.0,0.0);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
for (int i = 0; i < channels; i++) {
if (peak[i] >= red_limit)
cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
else
if (peak[i] <= green_limit)
cairo_set_source_rgb(cr, 0.0, 0.5 + 0.5*peak[i]/green_limit, 0.0);
else {
const double c = (peak[i] - green_limit) / (red_limit - green_limit);
cairo_set_source_rgb(cr, c, 1.0-c, 0.0);
}
const double c = (peak[i] < 0.0f) ? 0.0 : (peak[i] < 1.0f) ? peak[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
cairo_rectangle(cr, area.x + bar_space + i * (bar_space + bar_size),
area.y + bar_space + (1.0 - c)*(area.height - 2*bar_space),
bar_size, c*area.height);
cairo_fill(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
cairo_rectangle(cr, area.x + bar_space,
area.y + bar_space + i * (bar_space + bar_size),
c * (area.width - 2*bar_space), bar_size);
cairo_fill(cr);
}
}
cairo_set_line_width(cr, 1.0);
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int y = area.y + bar_space + (1.0f - tickmarks[i].amplitude)*(area.height - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, area.x + 2, y);
cairo_line_to(cr, area.x + area.width - 2, y);
cairo_stroke(cr);
}
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int x = area.x + bar_space + tickmarks[i].amplitude*(area.width - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, x, area.y + 2);
cairo_line_to(cr, x, area.y + area.width - 2);
cairo_stroke(cr);
}
}
cairo_restore(cr);
return TRUE;
}
static gboolean tick(GtkWidget *widget, GdkFrameClock *fclk, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
(void)fclk;
if (done) {
gtk_window_close(GTK_WINDOW(widget));
return G_SOURCE_REMOVE;
}
float new_peak[channels];
if (vu_peak(new_peak, channels) == channels) {
for (int c = 0; c < channels; c++) {
peak[c] *= decay;
peak[c] = (new_peak[c] > peak[c]) ? new_peak[c] : peak[c];
}
gtk_widget_queue_draw(widget);
}
return G_SOURCE_CONTINUE;
}
static void screen_changed(GtkWidget *widget, GdkScreen *old_screen, gpointer user_data)
{
(void)user_data; (void)old_screen; /* Silence unused parameter warning; generates no code */
gtk_widget_set_visual(widget, gdk_screen_get_system_visual(gtk_widget_get_screen(widget)));
}
static void place(GdkRectangle *to)
{
if (!to)
return;
to->x = 0;
to->y = 0;
to->width = bar_space + (bar_size + bar_space) * channels;
to->height = bar_space + (bar_size + bar_space) * channels;
GdkDisplay *d = gdk_display_get_default();
GdkMonitor *m = gdk_display_get_monitor(d, display_monitor);
GdkRectangle w;
if (!m)
m = gdk_display_get_primary_monitor(d);
if (!m)
return;
gdk_monitor_get_workarea(m, &w);
switch (display_placement) {
case PLACEMENT_LEFT:
to->x = w.x;
to->height = w.height;
return;
case PLACEMENT_RIGHT:
to->x = w.x + w.width - to->width;
to->height = w.height;
return;
case PLACEMENT_TOP:
to->y = w.y;
to->width = w.width;
return;
case PLACEMENT_BOTTOM:
to->y = w.y + w.height - to->height;
to->width = w.width;
return;
}
}
static void activate(GtkApplication *app, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
/* Compute where to place the window */
GdkRectangle pos;
place(&pos);
/* Create window */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "VU-Bar");
gtk_window_set_icon_name(GTK_WINDOW(window), "multimedia-volume-control");
gtk_window_set_default_size(GTK_WINDOW(window), pos.width, pos.height);
gtk_window_set_resizable(GTK_WINDOW(window), TRUE);
gtk_window_move(GTK_WINDOW(window), pos.x, pos.y);
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
gtk_widget_add_tick_callback(window, tick, NULL, NULL);
gtk_widget_set_app_paintable(window, TRUE);
g_signal_connect(window, "draw", G_CALLBACK(draw), NULL);
g_signal_connect(window, "screen-changed", G_CALLBACK(screen_changed), NULL);
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_show_all(window);
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
}
static const char *skip_lws(const char *from)
{
if (!from)
return NULL;
while (isspace((unsigned char)(*from)))
from++;
return from;
}
static const char *parse_int(const char *from, int *to)
{
const char *next = from;
long val;
if (!from || *from == '\0') {
errno = EINVAL;
return NULL;
}
errno = 0;
val = strtol(from, (char **)(&next), 0);
if (errno)
return NULL;
if (next == from) {
errno = EINVAL;
return NULL;
}
if ((long)(int)(val) != val) {
errno = ERANGE;
return NULL;
}
if (to)
*to = val;
errno = 0;
return next;
}
int usage(const char *arg0)
{
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s -h | --help\n", arg0);
fprintf(stderr, " %s [ OPTIONS ]\n", arg0);
fprintf(stderr, "Options:\n");
fprintf(stderr, " -s SERVER PulseAudio server\n");
fprintf(stderr, " -d DEVICE Source to monitor\n");
fprintf(stderr, " -c CHANNELS Number of channels\n");
fprintf(stderr, " -r RATE Samples per second\n");
fprintf(stderr, " -u COUNT Peak calculations per second\n");
fprintf(stderr, " -m MONITOR Display monitor number\n");
fprintf(stderr, " -p WHERE Meter placement on display\n");
fprintf(stderr, " -B PIXELS Bar thickness in pixels\n");
fprintf(stderr, " -S PIXELS Bar spacing in pixels\n");
fprintf(stderr, "Placement:\n");
fprintf(stderr, " -p left Left edge of monitor\n");
fprintf(stderr, " -p right Right edge of monitor\n");
fprintf(stderr, " -p top Top edge of monitor\n");
fprintf(stderr, " -p bottom Bottom edge of monitor\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
int main(int argc, char *argv[])
{
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
const char *p;
int opt, val;
setlocale(LC_ALL, "");
if (argc > 1 && !strcmp(argv[1], "--help"))
return usage(arg0);
gtk_init(&argc, &argv);
while ((opt = getopt(argc, argv, "hs:d:c:r:u:m:p:B:S:")) != -1) {
switch (opt) {
case 'h':
return usage(arg0);
case 's':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
server = NULL;
else
server = optarg;
break;
case 'd':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
device = NULL;
else
device = optarg;
break;
case 'c':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > MAX_CHANNELS) {
fprintf(stderr, "%s: Invalid number of channels.\n", optarg);
return EXIT_FAILURE;
}
channels = val;
break;
case 'r':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 128 || val > MAX_RATE) {
fprintf(stderr, "%s: Invalid sample rate.\n", optarg);
return EXIT_FAILURE;
}
rate = val;
break;
case 'u':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > 200) {
fprintf(stderr, "%s: Invalid number of peak updates per second.\n", optarg);
return EXIT_FAILURE;
}
updates = val;
break;
case 'm':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < -1) {
fprintf(stderr, "%s: Invalid monitor number.\n", optarg);
return EXIT_FAILURE;
}
display_monitor = val;
break;
case 'p':
if (!strcasecmp(optarg, "left"))
display_placement = PLACEMENT_LEFT;
else
if (!strcasecmp(optarg, "right"))
display_placement = PLACEMENT_RIGHT;
else
if (!strcasecmp(optarg, "top"))
display_placement = PLACEMENT_TOP;
else
if (!strcasecmp(optarg, "bottom"))
display_placement = PLACEMENT_BOTTOM;
else {
fprintf(stderr, "%s: Unsupported placement.\n", optarg);
return EXIT_FAILURE;
}
break;
case 'B':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1) {
fprintf(stderr, "%s: Invalid bar thickness in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_size = val;
break;
case 'S':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 0) {
fprintf(stderr, "%s: Invalid bar spacing in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_space = val;
break;
case '?':
/* getopt() has already printed an error message. */
return EXIT_FAILURE;
default:
/* Bug catcher: This should never occur. */
fprintf(stderr, "getopt() returned %d ('%c')!\n", opt, opt);
return EXIT_FAILURE;
}
}
if (install_done(SIGINT) ||
install_done(SIGHUP) ||
install_done(SIGTERM) ||
install_done(SIGQUIT)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (optind < argc) {
fprintf(stderr, "%s: Unsupported parameter.\n", argv[optind]);
return EXIT_FAILURE;
}
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
if (!app) {
fprintf(stderr, "Cannot start GTK+ application.\n");
return EXIT_FAILURE;
}
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
size_t samples = rate / updates;
if (samples < 1)
samples = 1;
val = vu_start(server, "vu-bar", device, "VU monitor", channels, rate, samples);
if (val) {
fprintf(stderr, "Cannot monitor audio source: %s.\n", vu_error(val));
g_object_unref(app);
return EXIT_FAILURE;
}
peak = calloc((size_t)channels * sizeof (float), samples);
if (!peak) {
fprintf(stderr, "Out of memory.\n");
g_object_unref(app);
vu_stop();
return EXIT_FAILURE;
}
val = g_application_run(G_APPLICATION(app), 0, NULL);
g_object_unref(app);
vu_stop();
return val;
}
Makefile: You can run sed -e 's|^ *|\t|' -i Makefile to fix indentation, if you have issues.
CC := gcc
CFLAGS := -Wall -Wextra -O2 `pkg-config --cflags gtk+-3.0 libpulse-simple`
LDFLAGS := -pthread -lm `pkg-config --libs gtk+-3.0 libpulse-simple`
PROGS := vu-bar
all: clean $(PROGS)
.PHONY: clean
clean:
rm -f *.o $(PROGS)
%.o: %.c
$(CC) $(CFLAGS) -c $^
vu-bar: gui.o vu.o
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
Run
make clean all
to compile, and then
./vu-bar -h
to see the usage. Normally, ./vu-bar does the right thing, showing the default audio source as vertical bars on the right side of the main display.
The tick marks are at 1 dB intervals. Close it by making sure it is the current application, then Alt+F4 as usual (or right-clicking on the application in your panel, and Close). Or run pkill -HUP vu-bar to send it a HUP signal, which also causes it to exit cleanly.
Note: This is utter crap, since I just wrote it from scratch in one sitting. Licensed under CC0-1.0 (i.e., do what you will, just don't blame me). Needs better organization and refactoring. Almost certainly contains bugs. But should provide ideas.
-
Something like the following.
Wow. That's perfect. :)
Note: This is utter crap, since I just wrote it from scratch in one sitting. Licensed under CC0-1.0 (i.e., do what you will, just don't blame me). Needs better organization and refactoring. Almost certainly contains bugs. But should provide ideas.
It detects peaks quite well, I send the audio over s/pdif, which is unforgiving. This is great for me to gnaw on, though for the few mins it's been running, I'm not sure I'll need to fiddle with it as it is, but I can comprehend the code. It ticks all the boxes, man!
:-+
[attachimg=1]
-
Short answer: too fiddly and I don't want to dick around with low-latency kernels, prefer vanilla ones.
Fair enough, but please be aware that needing low-latency kernels is a thing of the past, just RT scheduling is all that is needed. I am using JACK to back 6 QEMU VMs, one of which is a workstation, another other a gaming machine (VFIO VGA Passthrough) which all put high loads on the system. With the proper tuning on a stock kernel, it's possible to get perfect audio even under such an extreme workload.
-
Something like the following.
vu.h: A simple thread-safe interface for VU metering
...
Wow! :-+
-
Something like the following.
vu.h: A simple thread-safe interface for VU metering
...
Wow! :-+
I know, right?
I must be the luckiest boy on the whole forum. :)
-
Short answer: too fiddly and I don't want to dick around with low-latency kernels, prefer vanilla ones.
Fair enough, but please be aware that needing low-latency kernels is a thing of the past, just RT scheduling is all that is needed. I am using JACK to back 6 QEMU VMs, one of which is a workstation, another other a gaming machine (VFIO VGA Passthrough) which all put high loads on the system. With the proper tuning on a stock kernel, it's possible to get perfect audio even under such an extreme workload.
Cannot say I had the same luck with virtualbox and a attempt at good old fashioned virtual distro incest. A host and guest both using PA, managed (mangled?) by VB was a complete disaster.
RT kernels. Yeah, I'm a bit out of date. Life got in the way. I started to lose interest when they were squabbling about who had the better scheduler.
-
I must be the luckiest boy on the whole forum. :)
You'd be surprised how many programmers would be willing to cobble something together, if it were not the language barrier.
I don't mean language as in programming language (heh); I mean that users usually cannot describe their actual need in terms that programmers can act upon. Things like Paint sketches of what the user wants to see are way underappreciated; yet, they describe the needed thingy much better than dozens if not hundreds of words.
Me, I've dealt with the same problem, trying to adjust the microphone volume (or amplification, or source audio volume) to keep most of dynamic range, but with enough headroom to not clip in the end result. I've used pavucontrol – it has a nice wide volume display, and worked fine for me – and although not an audio person, have seen proper studio audio setups and been interested enough to look up the details like amplitude, loudness, compression (the audio effect), and the relationships between perceived volume, amplitude, and the dB scale. I only thought I understood your problem you were trying to solve, because it has bugged me too.
I still don't know what kind of tool would solve it better – I think at minimum we should add lingering peak lines to the live bars –, and as it is, it really needs to be configured to suit ones needs by supplying suitable command-line parameters. Switching to the PulseAudio Async interface would make it possible to reduce the I/O (and thus CPU load), as it can provide only the peak samples instead of the full sample stream. We could have a context window to select the audio source, and optionally to control the volume of the source, which initially pops up as the application is started, but which can be closed, and reopened via right-clicking on the VU bar. We could even make the VU bar freely placeable and resizable, say whenever the context window is open; and have that location be the default for the next startup (via gsettings).
To make it more useful, the optional volume control would be very interesting to make scriptable. You'd have additional markers, like tab stops, on the volume bar to restrict the allowed auto-control volume range, and use an external program (plugin, simply by putting them in a specific directory) that consumes the audio peak data (at ~ 100 peaks per second) in floating-point format (0.0 - 1.0), and which outputs (at arbitrary intervals) the desired audio volume level (0.0 - 1.0), both in linear amplitude scale. The application would update the volume (within the set limits), so the script would be very simple, and users could write their own scripts for their own needs.
If anyone finds a real good algorithm, those can be built-in to the application, reducing the overall CPU load again.
(The most commonly used algorithm just bumps up the volume whenever the peaks are too low, and drops the volume whenever the peaks are too high. Another uses a low-pass filter (with pass-band on the order of 1 Hz or less) on A/peak, where A is the desired peak amplitude, and the filtered output is the source volume or amplification in terms of amplitude. So, this plugin interface is really simple, and a few examples are enough to get people with basic Python skills to implement their own logic as to how the volume should be adjusted. I think I can safely assume we both know there is no auto-gain algorithm that works in all cases; this plugin method would be a nice simple platform of exploring and experimenting with auto-gain algorithms in a simple numerical domain that requires basically no audio domain knowledge.)
See? Since you and me have battled with this, I'm sure there are lots of others wasting time with this too, on Linux machines. Making something better to better cater our needs is how Free/Open Source ecosystems work. The hard part is not the code; I've already proven by the example that these things can be whipped up within a few hours. The hard part is designing the tool so that it fits the underlying problem; that it fixes the problem at hand. And that it and the problem are described in an interesting way, to attract the attention of both developers and users. Otherwise, it's just ... kinda wasted time. A one-off.
The one I showed is crap because it hasn't been checked for bugs and has a horribly messy structure (it was written, not designed!), but moreso because it is just my guess of what might work.
-
The only quibble I have is that I wish it could 'hog' the desktop like the way the task panel does when another window is maximized. I mean, it's nice that the meter is always on top, but it covers the slider bar on a maximized browser, for example. I've adjusted the size and pos of firefox to suit, but I wonder how hard it might be to have the meter just push the windows out of the way when they are being maximized?
-
Sure! (See, that's one of the things I didn't even think about.)
Updated gui.c:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
#include <signal.h>
#include <gtk/gtk.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include "vu.h"
#ifndef MAX_CHANNELS
#define MAX_CHANNELS 32
#endif
#ifndef MAX_RATE
#define MAX_RATE 250000
#endif
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = SA_RESTART;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
struct tickmark {
float amplitude;
float red;
float green;
float blue;
};
enum placement {
PLACEMENT_LEFT = 1,
PLACEMENT_RIGHT = 2,
PLACEMENT_TOP = 3,
PLACEMENT_BOTTOM = 4
};
static const char *server = NULL;
static const char *device = NULL;
static int channels = 2;
static int rate = 48000;
static int updates = 60;
static int bar_size = 4;
static int bar_space = 3;
static int display_monitor = -1;
static enum placement display_placement = PLACEMENT_RIGHT;
static float *peak_line = NULL;
static float *peak = NULL;
static float decay_line = 0.99f;
static float decay = 0.95f;
static float red_limit = 0.891251f; /* Amplitude within 1 dB of clipping */
static float green_limit = 0.707946f; /* Amplitude within 3 dB of clipping */
static struct tickmark tickmarks[] = {
{ .amplitude = 0.891251f, .red = 1.0f, .green = 0.00f, .blue = 0.0f }, /* 1 dB */
{ .amplitude = 0.794328f, .red = 1.0f, .green = 1.00f, .blue = 0.0f }, /* 2 dB */
{ .amplitude = 0.707946f, .red = 0.0f, .green = 1.00f, .blue = 0.0f }, /* 3 dB */
{ .amplitude = 0.630957f, .red = 0.0f, .green = 0.95f, .blue = 0.0f }, /* 4 dB */
{ .amplitude = 0.562341f, .red = 0.0f, .green = 0.90f, .blue = 0.0f }, /* 5 dB */
{ .amplitude = 0.501187f, .red = 0.0f, .green = 0.85f, .blue = 0.0f }, /* 6 dB */
{ .amplitude = 0.446684f, .red = 0.0f, .green = 0.80f, .blue = 0.0f }, /* 7 dB */
{ .amplitude = 0.398107f, .red = 0.0f, .green = 0.75f, .blue = 0.0f }, /* 8 dB */
{ .amplitude = 0.354813f, .red = 0.0f, .green = 0.70f, .blue = 0.0f }, /* 9 dB */
{ .amplitude = 0.316228f, .red = 0.0f, .green = 0.65f, .blue = 0.0f }, /* 10 dB */
{ .amplitude = -1.0f }
};
static gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
GdkRectangle area;
gtk_widget_get_clip(widget, &area);
cairo_save(cr);
cairo_set_source_rgb(cr, 0.0,0.0,0.0);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
for (int i = 0; i < channels; i++) {
if (peak[i] >= red_limit)
cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
else
if (peak[i] <= green_limit)
cairo_set_source_rgb(cr, 0.0, 0.5 + 0.5*peak[i]/green_limit, 0.0);
else {
const double c = (peak[i] - green_limit) / (red_limit - green_limit);
cairo_set_source_rgb(cr, c, 1.0-c, 0.0);
}
const double c = (peak[i] < 0.0f) ? 0.0 : (peak[i] < 1.0f) ? peak[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
cairo_rectangle(cr, area.x + bar_space + i * (bar_space + bar_size),
area.y + bar_space + (1.0 - c)*(area.height - 2*bar_space),
bar_size, c*area.height);
cairo_fill(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
cairo_rectangle(cr, area.x + bar_space,
area.y + bar_space + i * (bar_space + bar_size),
c * (area.width - 2*bar_space), bar_size);
cairo_fill(cr);
}
}
cairo_set_line_width(cr, 1.0);
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int y = area.y + bar_space + (1.0f - tickmarks[i].amplitude)*(area.height - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, area.x + 2, y);
cairo_line_to(cr, area.x + area.width - 2, y);
cairo_stroke(cr);
}
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int x = area.x + bar_space + tickmarks[i].amplitude*(area.width - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, x, area.y + 2);
cairo_line_to(cr, x, area.y + area.width - 2);
cairo_stroke(cr);
}
}
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
cairo_set_line_width(cr, 2.0);
for (int i = 0; i < channels; i++) {
const double c = (peak_line[i] < 0.0f) ? 0.0 : (peak_line[i] < 1.0f) ? peak_line[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
const int y = area.y + bar_space + (1.0f - c)*(area.height - 2*bar_space);
cairo_move_to(cr, area.x + i *(bar_space + bar_size), y);
cairo_line_to(cr, area.x + (i+1)*(bar_space + bar_size), y);
cairo_stroke(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
const int x = area.x + bar_space + c*(area.width - 2*bar_space);
cairo_move_to(cr, x, area.y + i *(bar_space + bar_size));
cairo_line_to(cr, x, area.y + (i+1)*(bar_space + bar_size));
cairo_stroke(cr);
}
}
cairo_restore(cr);
return TRUE;
}
static gboolean tick(GtkWidget *widget, GdkFrameClock *fclk, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
(void)fclk;
if (done) {
gtk_window_close(GTK_WINDOW(widget));
return G_SOURCE_REMOVE;
}
float new_peak[channels];
if (vu_peak(new_peak, channels) == channels) {
for (int c = 0; c < channels; c++) {
peak[c] *= decay;
peak[c] = (new_peak[c] > peak[c]) ? new_peak[c] : peak[c];
peak_line[c] *= decay_line;
peak_line[c] = (new_peak[c] > peak_line[c]) ? new_peak[c] : peak_line[c];
}
gtk_widget_queue_draw(widget);
}
return G_SOURCE_CONTINUE;
}
static void screen_changed(GtkWidget *widget, GdkScreen *old_screen, gpointer user_data)
{
(void)user_data; (void)old_screen; /* Silence unused parameter warning; generates no code */
gtk_widget_set_visual(widget, gdk_screen_get_system_visual(gtk_widget_get_screen(widget)));
}
static void place(GdkRectangle *to, gulong *reserve)
{
if (!to || !reserve)
return;
to->x = 0;
to->y = 0;
to->width = bar_space + (bar_size + bar_space) * channels;
to->height = bar_space + (bar_size + bar_space) * channels;
reserve[ 0] = 0; /* left */
reserve[ 1] = 0; /* right */
reserve[ 2] = 0; /* top */
reserve[ 3] = 0; /* bottom */
reserve[ 4] = 0; /* left_start_y */
reserve[ 5] = 0; /* left_end_y */
reserve[ 6] = 0; /* right_start_y */
reserve[ 7] = 0; /* right_end_y */
reserve[ 8] = 0; /* top_start_x */
reserve[ 9] = 0; /* top_end_x */
reserve[10] = 0; /* bottom_start_x */
reserve[11] = 0; /* bottom_end_x */
GdkDisplay *d = gdk_display_get_default();
GdkMonitor *m = gdk_display_get_monitor(d, display_monitor);
GdkRectangle w;
if (!m)
m = gdk_display_get_primary_monitor(d);
if (!m)
return;
gdk_monitor_get_workarea(m, &w);
switch (display_placement) {
case PLACEMENT_LEFT:
to->x = w.x;
to->height = w.height;
reserve[0] = to->width;
reserve[4] = w.y;
reserve[5] = w.y + w.height - 1;
return;
case PLACEMENT_RIGHT:
to->x = w.x + w.width - to->width;
to->height = w.height;
reserve[1] = to->width;
reserve[6] = w.y;
reserve[7] = w.y + w.height - 1;
return;
case PLACEMENT_TOP:
to->y = w.y;
to->width = w.width;
reserve[2] = to->height;
reserve[8] = w.x;
reserve[9] = w.x + w.width - 1;
return;
case PLACEMENT_BOTTOM:
to->y = w.y + w.height - to->height;
to->width = w.width;
reserve[3] = to->height;
reserve[10] = w.x;
reserve[11] = w.x + w.width - 1;
return;
}
}
static void activate(GtkApplication *app, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
gulong reserve[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
/* Compute where to place the window */
GdkRectangle pos;
place(&pos, reserve);
/* Create window */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "VU-Bar");
gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DOCK);
gtk_window_set_icon_name(GTK_WINDOW(window), "multimedia-volume-control");
gtk_window_set_default_size(GTK_WINDOW(window), pos.width, pos.height);
gtk_window_set_resizable(GTK_WINDOW(window), TRUE);
gtk_window_move(GTK_WINDOW(window), pos.x, pos.y);
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
gtk_widget_add_tick_callback(window, tick, NULL, NULL);
gtk_widget_set_app_paintable(window, TRUE);
g_signal_connect(window, "draw", G_CALLBACK(draw), NULL);
g_signal_connect(window, "screen-changed", G_CALLBACK(screen_changed), NULL);
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_show_all(window);
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
GdkWindow *w = gtk_widget_get_window(window);
if (w) {
GdkAtom strut = gdk_atom_intern_static_string("_NET_WM_STRUT");
GdkAtom partial = gdk_atom_intern_static_string("_NET_WM_STRUT_PARTIAL");
GdkAtom cardinal = gdk_atom_intern_static_string("CARDINAL");
gdk_property_change(w, strut, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)reserve, 4);
gdk_property_change(w, partial, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)reserve, 12);
}
}
static const char *skip_lws(const char *from)
{
if (!from)
return NULL;
while (isspace((unsigned char)(*from)))
from++;
return from;
}
static const char *parse_int(const char *from, int *to)
{
const char *next = from;
long val;
if (!from || *from == '\0') {
errno = EINVAL;
return NULL;
}
errno = 0;
val = strtol(from, (char **)(&next), 0);
if (errno)
return NULL;
if (next == from) {
errno = EINVAL;
return NULL;
}
if ((long)(int)(val) != val) {
errno = ERANGE;
return NULL;
}
if (to)
*to = val;
errno = 0;
return next;
}
int usage(const char *arg0)
{
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s -h | --help\n", arg0);
fprintf(stderr, " %s [ OPTIONS ]\n", arg0);
fprintf(stderr, "Options:\n");
fprintf(stderr, " -s SERVER PulseAudio server\n");
fprintf(stderr, " -d DEVICE Source to monitor\n");
fprintf(stderr, " -c CHANNELS Number of channels\n");
fprintf(stderr, " -r RATE Samples per second\n");
fprintf(stderr, " -u COUNT Peak calculations per second\n");
fprintf(stderr, " -m MONITOR Display monitor number\n");
fprintf(stderr, " -p WHERE Meter placement on display\n");
fprintf(stderr, " -B PIXELS Bar thickness in pixels\n");
fprintf(stderr, " -S PIXELS Bar spacing in pixels\n");
fprintf(stderr, "Placement:\n");
fprintf(stderr, " -p left Left edge of monitor\n");
fprintf(stderr, " -p right Right edge of monitor\n");
fprintf(stderr, " -p top Top edge of monitor\n");
fprintf(stderr, " -p bottom Bottom edge of monitor\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
int main(int argc, char *argv[])
{
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
const char *p;
int opt, val;
setlocale(LC_ALL, "");
if (argc > 1 && !strcmp(argv[1], "--help"))
return usage(arg0);
gtk_init(&argc, &argv);
while ((opt = getopt(argc, argv, "hs:d:c:r:u:m:p:B:S:")) != -1) {
switch (opt) {
case 'h':
return usage(arg0);
case 's':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
server = NULL;
else
server = optarg;
break;
case 'd':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
device = NULL;
else
device = optarg;
break;
case 'c':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > MAX_CHANNELS) {
fprintf(stderr, "%s: Invalid number of channels.\n", optarg);
return EXIT_FAILURE;
}
channels = val;
break;
case 'r':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 128 || val > MAX_RATE) {
fprintf(stderr, "%s: Invalid sample rate.\n", optarg);
return EXIT_FAILURE;
}
rate = val;
break;
case 'u':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > 200) {
fprintf(stderr, "%s: Invalid number of peak updates per second.\n", optarg);
return EXIT_FAILURE;
}
updates = val;
break;
case 'm':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < -1) {
fprintf(stderr, "%s: Invalid monitor number.\n", optarg);
return EXIT_FAILURE;
}
display_monitor = val;
break;
case 'p':
if (!strcasecmp(optarg, "left"))
display_placement = PLACEMENT_LEFT;
else
if (!strcasecmp(optarg, "right"))
display_placement = PLACEMENT_RIGHT;
else
if (!strcasecmp(optarg, "top"))
display_placement = PLACEMENT_TOP;
else
if (!strcasecmp(optarg, "bottom"))
display_placement = PLACEMENT_BOTTOM;
else {
fprintf(stderr, "%s: Unsupported placement.\n", optarg);
return EXIT_FAILURE;
}
break;
case 'B':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1) {
fprintf(stderr, "%s: Invalid bar thickness in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_size = val;
break;
case 'S':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 0) {
fprintf(stderr, "%s: Invalid bar spacing in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_space = val;
break;
case '?':
/* getopt() has already printed an error message. */
return EXIT_FAILURE;
default:
/* Bug catcher: This should never occur. */
fprintf(stderr, "getopt() returned %d ('%c')!\n", opt, opt);
return EXIT_FAILURE;
}
}
if (install_done(SIGINT) ||
install_done(SIGHUP) ||
install_done(SIGTERM) ||
install_done(SIGQUIT)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (optind < argc) {
fprintf(stderr, "%s: Unsupported parameter.\n", argv[optind]);
return EXIT_FAILURE;
}
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
if (!app) {
fprintf(stderr, "Cannot start GTK+ application.\n");
return EXIT_FAILURE;
}
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
size_t samples = rate / updates;
if (samples < 1)
samples = 1;
val = vu_start(server, "vu-bar", device, "VU monitor", channels, rate, samples);
if (val) {
fprintf(stderr, "Cannot monitor audio source: %s.\n", vu_error(val));
g_object_unref(app);
return EXIT_FAILURE;
}
peak = calloc((size_t)channels * sizeof (float), samples);
peak_line = calloc((size_t)channels * sizeof (float), samples);
if (!peak) {
fprintf(stderr, "Out of memory.\n");
g_object_unref(app);
vu_stop();
return EXIT_FAILURE;
}
val = g_application_run(G_APPLICATION(app), 0, NULL);
g_object_unref(app);
vu_stop();
return val;
}
I also added the slower-decay peak lines.
Do note that I still think this is crappy, because it hasn't been checked for bugs, could be more efficient, et cetera.
-
Sure! (See, that's one of the things I didn't even think about.)
Neither did I. Since it's so slimline, it deserves it's own sanctioned territory.
I also added the slower-decay peak lines.
Cool. Works great.
Do note that I still think this is crappy, because it hasn't been checked for bugs, could be more efficient, et cetera.
It's so crappy, I've just gone and added it to the auto start file.
:)
-
And here is a picture...
[attachimg=1]
-
Update.
A whole year has passed and the Vu meter program (above) has been perfect for my needs. I owe NA a beer.
Well, that multi-monitor thread the other day motivated me to dust off a 32" TV laying around and constructed the most horrific wall bracket so now I have a "L" shaped twin monitor setup. Yay!
My problem now is I need to tweak the gui code because the wrong monitor is pushing the windows out of the way and leaving a gap and the monitor displaying the vu meter covers the programs under it (the way it did in the first gui version).
I don't think that the 2nd monitor being rotated 90deg has anything to do with this issue.
:-/O
So far I've got my heart set on fiddling with this line:
static int display_monitor = -1;
I have been running vu-bar as is with no command args.
-
So far I've got my heart set on fiddling with this line:
static int display_monitor = -1;
Or, you know, just run vu-bar -m 1 or vu-bar -m 2 as they achieve the same thing.
I'm pretty sure it's the region reserving code (place() function) that needs fixing, but darnit, I don't have an extra display right now to test and verify...
-
Or, you know, just run vu-bar -m 1 or vu-bar -m 2 as they achieve the same thing.
vu-bar -m 1 runs it in the new monitor.
All values not 1, it runs in the original monitor.
It pushes windows around in the other monitor. And puts them back when you kill the program.
I'm pretty sure it's the region reserving code (place() function) that needs fixing, but darnit, I don't have an extra display right now to test and verify...
I'll have a poke around, thanks.
For years, I've had a spare HDMI cable that I thought was kept near the TV so I could fire it up quickly when I got around to it. Couldn't find the cable today, of course! |O
-
So, let me see if I understand this right.
If you run
vu-bar -m 0
the meter is displayed in the primary monitor, but the windows are pushed around in the second monitor; meaning windows cover the meter in the primary monitor and there is unusable space in the second monitor.
If you run
vu-bar -m 1
the meter is displayed in the secondary monitor, but the windows are pushed around in the primary monitor; meaning again windows cover the meter, with the other monitor having unusable space.
Or, do the windows get pushed around always only in the secondary monitor?
I need to know the actual error pattern, because I cannot duplicate it right now, to fix it. I think I understand how the error occurs, but I need to know the exact symptoms before I can suggest a fix.
Edit: Actually, I should just make it draggable, with a context menu (right-clickable) to select orientation et cetera, with the command-line parameters just setting the defaults (so that one can make trivial .desktop shortcuts or menu entries for specific configurations). Let me see if I can be arsed to, in a day or two; brain too tired today... :P
-
So, let me see if I understand this right.
Or, do the windows get pushed around always only in the secondary monitor?
Yes, that's it. I might have misunderstood the behaviour, but now realise it only pushes around the windows on the 2nd monitor but the meter window can indeed be selected properly.
-
GdkDisplay *d = gdk_display_get_default();
GdkMonitor *m = gdk_display_get_monitor(d, display_monitor);
GdkRectangle w;
fprintf(stderr, "display_monitor = %i\n", display_monitor );
fprintf(stderr, "d = %p\n", d );
fprintf(stderr, "m = %p\n", m );
GdkMonitor *mon0 = gdk_display_get_monitor(d, 0);
GdkMonitor *mon1 = gdk_display_get_monitor(d, 1);
fprintf(stderr, "mon0 = %p\n", mon0 );
fprintf(stderr, "mon1 = %p\n", mon1 );
if (!m)
fprintf(stderr, "Not a monitor. display_monitor = %i\n", display_monitor );
if (!m)
m = gdk_display_get_primary_monitor(d);
if (!m)
return;
gdk_monitor_get_workarea(m, &w);
[ed@kloonkmanor ~]$ ./vu-bar -m 0
display_monitor = 0
d = 0x11af0e0
m = 0x119b240
mon0 = 0x119b240
mon1 = 0x119b2b0
[ed@kloonkmanor ~]$ ./vu-bar -m 1
display_monitor = 1
d = 0x1a1d0e0
m = 0x1a092b0
mon0 = 0x1a09240
mon1 = 0x1a092b0
[ed@kloonkmanor ~]$ ./vu-bar -m 2
display_monitor = 2
d = 0x21210e0
m = (nil)
mon0 = 0x210d240
mon1 = 0x210d2b0
Not a monitor. display_monitor = 2
So -m 2 still pushes the windows, so maybe the function is barking up the wrong tree.
-
This example in Python suffers the same problem..
https://gist.github.com/johnlane/351adff97df196add08a
[ed@kloonkmanor ~]$ python2.7 dockbar-gtk3.py
Gtk 3.24.31
width: 2688
there are 2 monitors
monitor 0: 1920 x 1080
monitor 1: 768 x 1366
monitor 0: 1920 x 1080 (current, offset 0)
bar: start=0 end=1919
Red bar appears at the top of monitor 0. Windows are pushed around at the top of Monitor 1.
:(
-
(The monitors are actually numbered 0, 1, ...; not 1, 2, ...; and when given an invalid monitor number, it defaults to the default one automatically.)
Okay, I think I understand where the error lies. I'll need to do some simple tests first to ensure I can fix it right; I don't 'do' scotch tape fixes. In the mean time, sit tight; I'm not dropping this on the floor. :)
-
Ed.Kloonk –– and anyone else running Linux with multiple monitors, having GTK+3.0 development packages installed! –– , could you compile and run the following GDK information dumper program, when you have more than one monitor connected, and either post here or send me the output via PM or email?
It does not open any windows. It only connects to the default display (which you can override using the --display parameter, if you have more than one X server running), and then dumps the information on the monitors and their geometry. This information on multi-monitor setups would be very, very informative.
// SPDX-License-Identifier: CC0-1.0
//
// Save this file as 'list.c', and compile to './list' using for example
// gcc -Wall -O2 `pkg-config --cflags gdk-3.0` list.c `pkg-config --libs gdk-3.0` -o list
//
#include <stdlib.h>
#include <gdk/gdk.h>
int main(int argc, char *argv[])
{
/* Initialize GDK. Supports display selection via --display
parameter and the DISPLAY environment variable. */
if (!gdk_init_check(&argc, &argv)) {
fprintf(stderr, "Cannot initialize GDK.\n");
return EXIT_FAILURE;
}
/* Get access to the default display. */
GdkDisplay *display = gdk_display_get_default();
if (!display) {
fprintf(stderr, "No default display.\n");
return EXIT_FAILURE;
}
const char *display_name = gdk_display_get_name(display);
if (!display_name || !*display_name)
display_name = "(Unnamed default)";
/* Get number of monitors */
int monitors = gdk_display_get_n_monitors(display);
if (monitors < 0) {
fprintf(stderr, "No monitors in default display.\n");
return EXIT_FAILURE;
}
printf("Display %s has %d %s:\n", display_name, monitors, (monitors > 1) ? "monitors" : "monitor");
/* Loop over each monitor. */
for (int i = 0; i < monitors; i++) {
GdkMonitor *monitor = gdk_display_get_monitor(display, i);
if (!monitor)
continue;
GdkRectangle geometry, workarea;
gdk_monitor_get_geometry(monitor, &geometry);
gdk_monitor_get_workarea(monitor, &workarea);
const char *manufacturer = gdk_monitor_get_manufacturer(monitor);
const char *model = gdk_monitor_get_model(monitor);
gboolean primary = gdk_monitor_is_primary(monitor);
int rate = gdk_monitor_get_refresh_rate(monitor);
int scale = gdk_monitor_get_scale_factor(monitor);
int width_mm = gdk_monitor_get_width_mm(monitor);
int height_mm = gdk_monitor_get_height_mm(monitor);
printf("Monitor %d%s:\n", i, (primary) ? " (Primary monitor)" : "");
printf(" Manufacturer: %s\n", (manufacturer && *manufacturer) ? manufacturer : "(unknown manufacturer)");
printf(" Model: %s\n", (model && *model) ? model : "(unknown model)");
printf(" Physical size: %d mm by %d mm\n", width_mm, height_mm);
printf(" Pixel size: %d x %d pixels\n", geometry.width * scale, geometry.height * scale);
printf(" Refresh rate: %.1f Hz\n", rate/1000.0);
printf(" Viewport: (%d, %d) - (%d, %d)\n", geometry.x, geometry.y, geometry.x + geometry.width, geometry.y + geometry.height);
printf(" Work area: (%d, %d) - (%d, %d)\n", workarea.x, workarea.y, workarea.x + workarea.width, workarea.y + workarea.height);
fflush(stdout);
}
return EXIT_SUCCESS;
}
On my HP EliteBook 840 G4 running Cinnamon desktop environment, the output is
Display :0 has 1 monitor:
Monitor 0 (Primary monitor):
Manufacturer: (unknown manufacturer)
Model: eDP-1
Physical size: 309 mm by 173 mm
Pixel size: 1920 x 1080 pixels
Refresh rate: 60.0 Hz
Viewport: (0, 0) - (1920, 1080)
Work area: (0, 0) - (1920, 1054)
If you want, you can "anonymize" the Manufacturer and Model. I included them, because I'd like to know whether one could use the Manufacturer and Model strings in a menu list, for example as "Manufacturer Model (pixel size, refresh rate; physical size)", or whether "Monitor N (pixel size, refresh rate; physical size)" is more human-friendly.
Pixel size, Viewport, and Work area are very important. For HiDPI displays (4K and such), the Pixel size should be correct, but Viewport and Work area should reflect the application size, i.e. can be smaller for HiDPI displays; this too is expected. Work area is the part of the Viewport that is not excluded from application windows. In other words, the area in Viewport but not in Work area is taken up by panels and stuff like the VU-Bar thingy.
I assume, and wish to confirm, that in multi-monitor setups, the Viewports do not coincide. (The first point is inclusive, latter point exclusive; I deliberately chose this to make size calculation in ones head easier. One really should subtract one.) That is, they may overlap if the desktop view in them overlaps. The coordinates of each viewport should reflect the displays' relative views of the entire desktop.
If so, it is just a matter of moving the displayed "window" where the bars are drawn to the proper monitor.
I do suspect that the easiest way to make the widget work better is to have it present a simple pop-up menu, where the default (or command-line selected) options are (audio source and meter placement as drop-down lists, with Show, Cancel, and Quit), and let the user drag that menu to the display they want to use. Closing it wouldn't affect the VU-meter, unless you choose Quit. One could close the pop-up menu (by selecting Show or Cancel), without affecting the VU-meter.
I'll probably need to do a second test program to verify that approach works. It would be nice in that it would let one have more than one VU-meter active, too, for example mic input and global output, just by selecting the source at startup...
-
Ed.Kloonk –– and anyone else running Linux with multiple monitors, having GTK+3.0 development packages installed! –– , could you compile and run the following GDK information dumper program, when you have more than one monitor connected, and either post here or send me the output via PM or email?
It does not open any windows. It only connects to the default display (which you can override using the --display parameter, if you have more than one X server running), and then dumps the information on the monitors and their geometry. This information on multi-monitor setups would be very, very informative.
Display :0 has 2 monitors:
Monitor 0 (Primary monitor):
Manufacturer: AOC
Model: HDMI1
Physical size: 600 mm by 340 mm
Pixel size: 1920 x 1080 pixels
Refresh rate: 60.0 Hz
Viewport: (0, 1003) - (1920, 2083)
Work area: (0, 1003) - (1920, 2047)
Monitor 1:
Manufacturer: XXX
Model: HDMI2
Physical size: 203 mm by 361 mm
Pixel size: 768 x 1366 pixels
Refresh rate: 60.0 Hz
Viewport: (1920, 0) - (2688, 1366)
Work area: (1920, 0) - (2688, 1366)
The layout pic shows the setup in plain ole mate-display-properties, and it represents the physical layout also. What is interesting is the different results I'm getting where the reserved area is connected to the another monitor. For example, setting '-p left -m 0' does work fine without interfering with the other monitor. <- Not placing meter correctly. Bottom of meter is in-line with bottom of mon 1. |O
[attachimg=1]
-
I edited the post above and updated some incorrect info.
-
Could you check if the modified gui.c below works?
It is based on the idea that the problem is not the space reservation, but where the actual vu bar area ends up, and attempts to fix that by resizing and moving the "window" after it has been mapped. Pay attention to the output: it will print "Window mispositioned at ..." or "Window size is ..." if the window position is detected to not apply correctly. (I expect it to print the former, if the vu bar ends up in the wrong monitor.)
I'd like you to try the following four scenarios:- ./vu-bar -m 0 -p left
This should put the vu-bar on the left edge of the primary (bottom/left) monitor - ./vu-bar -m 0 -p right
This should put the vu-bar on the right edge of the primary (bottom/left) monitor - ./vu-bar -m 1 -p left
This should put the vu-bar on the left edge of the secondary (top/right) monitor - ./vu-bar -m 1 -p right
This should put the vu-bar on the right edge of the secondary (top/right) monitor
and report whether the bar is in the correct monitor and correct location, with the correct area reserved, and whether there was any output from the program, for each of the four cases.
(Dammit, I need to get me a display, instead of getting others to do my testing and debugging for me :-[. I've been putting off getting one, since I'm very sensitive to panel/pixel/subpixel errors, and I want one with good viewing angles and color reproduction too – so IPS panel, instead of TN or VA ––, and I really dislike having to return a product...)
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
#include <signal.h>
#include <gtk/gtk.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include "vu.h"
#ifndef MAX_CHANNELS
#define MAX_CHANNELS 32
#endif
#ifndef MAX_RATE
#define MAX_RATE 250000
#endif
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = SA_RESTART;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
struct tickmark {
float amplitude;
float red;
float green;
float blue;
};
enum placement {
PLACEMENT_LEFT = 1,
PLACEMENT_RIGHT = 2,
PLACEMENT_TOP = 3,
PLACEMENT_BOTTOM = 4
};
static const char *server = NULL;
static const char *device = NULL;
static int channels = 2;
static int rate = 48000;
static int updates = 60;
static int bar_size = 4;
static int bar_space = 3;
static int display_monitor = -1;
static enum placement display_placement = PLACEMENT_RIGHT;
static float *peak_line = NULL;
static float *peak = NULL;
static float decay_line = 0.99f;
static float decay = 0.95f;
static float red_limit = 0.891251f; /* Amplitude within 1 dB of clipping */
static float green_limit = 0.707946f; /* Amplitude within 3 dB of clipping */
static struct tickmark tickmarks[] = {
{ .amplitude = 0.891251f, .red = 1.0f, .green = 0.00f, .blue = 0.0f }, /* 1 dB */
{ .amplitude = 0.794328f, .red = 1.0f, .green = 1.00f, .blue = 0.0f }, /* 2 dB */
{ .amplitude = 0.707946f, .red = 0.0f, .green = 1.00f, .blue = 0.0f }, /* 3 dB */
{ .amplitude = 0.630957f, .red = 0.0f, .green = 0.95f, .blue = 0.0f }, /* 4 dB */
{ .amplitude = 0.562341f, .red = 0.0f, .green = 0.90f, .blue = 0.0f }, /* 5 dB */
{ .amplitude = 0.501187f, .red = 0.0f, .green = 0.85f, .blue = 0.0f }, /* 6 dB */
{ .amplitude = 0.446684f, .red = 0.0f, .green = 0.80f, .blue = 0.0f }, /* 7 dB */
{ .amplitude = 0.398107f, .red = 0.0f, .green = 0.75f, .blue = 0.0f }, /* 8 dB */
{ .amplitude = 0.354813f, .red = 0.0f, .green = 0.70f, .blue = 0.0f }, /* 9 dB */
{ .amplitude = 0.316228f, .red = 0.0f, .green = 0.65f, .blue = 0.0f }, /* 10 dB */
{ .amplitude = -1.0f }
};
static gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
GdkRectangle area;
gtk_widget_get_clip(widget, &area);
cairo_save(cr);
cairo_set_source_rgb(cr, 0.0,0.0,0.0);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
for (int i = 0; i < channels; i++) {
if (peak[i] >= red_limit)
cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
else
if (peak[i] <= green_limit)
cairo_set_source_rgb(cr, 0.0, 0.5 + 0.5*peak[i]/green_limit, 0.0);
else {
const double c = (peak[i] - green_limit) / (red_limit - green_limit);
cairo_set_source_rgb(cr, c, 1.0-c, 0.0);
}
const double c = (peak[i] < 0.0f) ? 0.0 : (peak[i] < 1.0f) ? peak[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
cairo_rectangle(cr, area.x + bar_space + i * (bar_space + bar_size),
area.y + bar_space + (1.0 - c)*(area.height - 2*bar_space),
bar_size, c*area.height);
cairo_fill(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
cairo_rectangle(cr, area.x + bar_space,
area.y + bar_space + i * (bar_space + bar_size),
c * (area.width - 2*bar_space), bar_size);
cairo_fill(cr);
}
}
cairo_set_line_width(cr, 1.0);
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int y = area.y + bar_space + (1.0f - tickmarks[i].amplitude)*(area.height - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, area.x + 2, y);
cairo_line_to(cr, area.x + area.width - 2, y);
cairo_stroke(cr);
}
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int x = area.x + bar_space + tickmarks[i].amplitude*(area.width - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, x, area.y + 2);
cairo_line_to(cr, x, area.y + area.width - 2);
cairo_stroke(cr);
}
}
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
cairo_set_line_width(cr, 2.0);
for (int i = 0; i < channels; i++) {
const double c = (peak_line[i] < 0.0f) ? 0.0 : (peak_line[i] < 1.0f) ? peak_line[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
const int y = area.y + bar_space + (1.0f - c)*(area.height - 2*bar_space);
cairo_move_to(cr, area.x + i *(bar_space + bar_size), y);
cairo_line_to(cr, area.x + (i+1)*(bar_space + bar_size), y);
cairo_stroke(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
const int x = area.x + bar_space + c*(area.width - 2*bar_space);
cairo_move_to(cr, x, area.y + i *(bar_space + bar_size));
cairo_line_to(cr, x, area.y + (i+1)*(bar_space + bar_size));
cairo_stroke(cr);
}
}
cairo_restore(cr);
return TRUE;
}
static gboolean tick(GtkWidget *widget, GdkFrameClock *fclk, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
(void)fclk;
if (done) {
gtk_window_close(GTK_WINDOW(widget));
return G_SOURCE_REMOVE;
}
float new_peak[channels];
if (vu_peak(new_peak, channels) == channels) {
for (int c = 0; c < channels; c++) {
peak[c] *= decay;
peak[c] = (new_peak[c] > peak[c]) ? new_peak[c] : peak[c];
peak_line[c] *= decay_line;
peak_line[c] = (new_peak[c] > peak_line[c]) ? new_peak[c] : peak_line[c];
}
gtk_widget_queue_draw(widget);
}
return G_SOURCE_CONTINUE;
}
static void screen_changed(GtkWidget *widget, GdkScreen *old_screen, gpointer user_data)
{
(void)user_data; (void)old_screen; /* Silence unused parameter warning; generates no code */
gtk_widget_set_visual(widget, gdk_screen_get_system_visual(gtk_widget_get_screen(widget)));
}
static void place(GdkRectangle *to, gulong *reserve)
{
if (!to || !reserve)
return;
to->x = 0;
to->y = 0;
to->width = bar_space + (bar_size + bar_space) * channels;
to->height = bar_space + (bar_size + bar_space) * channels;
reserve[ 0] = 0; /* left */
reserve[ 1] = 0; /* right */
reserve[ 2] = 0; /* top */
reserve[ 3] = 0; /* bottom */
reserve[ 4] = 0; /* left_start_y */
reserve[ 5] = 0; /* left_end_y */
reserve[ 6] = 0; /* right_start_y */
reserve[ 7] = 0; /* right_end_y */
reserve[ 8] = 0; /* top_start_x */
reserve[ 9] = 0; /* top_end_x */
reserve[10] = 0; /* bottom_start_x */
reserve[11] = 0; /* bottom_end_x */
GdkDisplay *d = gdk_display_get_default();
GdkMonitor *m = gdk_display_get_monitor(d, display_monitor);
GdkRectangle w;
if (!m)
m = gdk_display_get_primary_monitor(d);
if (!m)
return;
gdk_monitor_get_workarea(m, &w);
switch (display_placement) {
case PLACEMENT_LEFT:
to->x = w.x;
to->height = w.height;
reserve[0] = to->width;
reserve[4] = w.y;
reserve[5] = w.y + w.height - 1;
return;
case PLACEMENT_RIGHT:
to->x = w.x + w.width - to->width;
to->height = w.height;
reserve[1] = to->width;
reserve[6] = w.y;
reserve[7] = w.y + w.height - 1;
return;
case PLACEMENT_TOP:
to->y = w.y;
to->width = w.width;
reserve[2] = to->height;
reserve[8] = w.x;
reserve[9] = w.x + w.width - 1;
return;
case PLACEMENT_BOTTOM:
to->y = w.y + w.height - to->height;
to->width = w.width;
reserve[3] = to->height;
reserve[10] = w.x;
reserve[11] = w.x + w.width - 1;
return;
}
}
static void activate(GtkApplication *app, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
gulong reserve[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
/* Compute where to place the window */
GdkRectangle pos;
place(&pos, reserve);
/* Create window */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "VU-Bar");
gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DOCK);
gtk_window_set_icon_name(GTK_WINDOW(window), "multimedia-volume-control");
gtk_window_set_default_size(GTK_WINDOW(window), pos.width, pos.height);
gtk_window_resize(GTK_WINDOW(window), pos.width, pos.height);
gtk_window_set_resizable(GTK_WINDOW(window), TRUE);
gtk_window_move(GTK_WINDOW(window), pos.x, pos.y);
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
gtk_widget_add_tick_callback(window, tick, NULL, NULL);
gtk_widget_set_app_paintable(window, TRUE);
g_signal_connect(window, "draw", G_CALLBACK(draw), NULL);
g_signal_connect(window, "screen-changed", G_CALLBACK(screen_changed), NULL);
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_show_all(window);
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
gtk_window_resize(GTK_WINDOW(window), 1, 1);
gtk_window_move(GTK_WINDOW(window), pos.x, pos.y);
GdkWindow *w = gtk_widget_get_window(window);
if (w) {
GdkAtom strut = gdk_atom_intern_static_string("_NET_WM_STRUT");
GdkAtom partial = gdk_atom_intern_static_string("_NET_WM_STRUT_PARTIAL");
GdkAtom cardinal = gdk_atom_intern_static_string("CARDINAL");
gdk_property_change(w, strut, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)reserve, 4);
gdk_property_change(w, partial, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)reserve, 12);
}
gtk_window_move(GTK_WINDOW(window), pos.x, pos.y);
gtk_window_resize(GTK_WINDOW(window), pos.width, pos.height);
// Check if the window position matches the required position.
{
gint x, y, w, h;
gtk_window_get_position(GTK_WINDOW(window), &x, &y);
gtk_window_get_size(GTK_WINDOW(window), &w, &h);
if (x != pos.x || y != pos.y)
fprintf(stderr, "Window mispositioned at (%d,%d) instead of at (%d, %d) as requested.\n", x, y, pos.x, pos.y);
if (w != pos.width || h != pos.height)
fprintf(stderr, "Window size is %dx%d instead of the requested %dx%d\n", w, h, pos.width, pos.height);
}
}
static const char *skip_lws(const char *from)
{
if (!from)
return NULL;
while (isspace((unsigned char)(*from)))
from++;
return from;
}
static const char *parse_int(const char *from, int *to)
{
const char *next = from;
long val;
if (!from || *from == '\0') {
errno = EINVAL;
return NULL;
}
errno = 0;
val = strtol(from, (char **)(&next), 0);
if (errno)
return NULL;
if (next == from) {
errno = EINVAL;
return NULL;
}
if ((long)(int)(val) != val) {
errno = ERANGE;
return NULL;
}
if (to)
*to = val;
errno = 0;
return next;
}
int usage(const char *arg0)
{
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s -h | --help\n", arg0);
fprintf(stderr, " %s [ OPTIONS ]\n", arg0);
fprintf(stderr, "Options:\n");
fprintf(stderr, " -s SERVER PulseAudio server\n");
fprintf(stderr, " -d DEVICE Source to monitor\n");
fprintf(stderr, " -c CHANNELS Number of channels\n");
fprintf(stderr, " -r RATE Samples per second\n");
fprintf(stderr, " -u COUNT Peak calculations per second\n");
fprintf(stderr, " -m MONITOR Display monitor number\n");
fprintf(stderr, " -p WHERE Meter placement on display\n");
fprintf(stderr, " -B PIXELS Bar thickness in pixels\n");
fprintf(stderr, " -S PIXELS Bar spacing in pixels\n");
fprintf(stderr, "Placement:\n");
fprintf(stderr, " -p left Left edge of monitor\n");
fprintf(stderr, " -p right Right edge of monitor\n");
fprintf(stderr, " -p top Top edge of monitor\n");
fprintf(stderr, " -p bottom Bottom edge of monitor\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
int main(int argc, char *argv[])
{
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
const char *p;
int opt, val;
setlocale(LC_ALL, "");
if (argc > 1 && !strcmp(argv[1], "--help"))
return usage(arg0);
gtk_init(&argc, &argv);
while ((opt = getopt(argc, argv, "hs:d:c:r:u:m:p:B:S:")) != -1) {
switch (opt) {
case 'h':
return usage(arg0);
case 's':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
server = NULL;
else
server = optarg;
break;
case 'd':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
device = NULL;
else
device = optarg;
break;
case 'c':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > MAX_CHANNELS) {
fprintf(stderr, "%s: Invalid number of channels.\n", optarg);
return EXIT_FAILURE;
}
channels = val;
break;
case 'r':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 128 || val > MAX_RATE) {
fprintf(stderr, "%s: Invalid sample rate.\n", optarg);
return EXIT_FAILURE;
}
rate = val;
break;
case 'u':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > 200) {
fprintf(stderr, "%s: Invalid number of peak updates per second.\n", optarg);
return EXIT_FAILURE;
}
updates = val;
break;
case 'm':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < -1) {
fprintf(stderr, "%s: Invalid monitor number.\n", optarg);
return EXIT_FAILURE;
}
display_monitor = val;
break;
case 'p':
if (!strcasecmp(optarg, "left"))
display_placement = PLACEMENT_LEFT;
else
if (!strcasecmp(optarg, "right"))
display_placement = PLACEMENT_RIGHT;
else
if (!strcasecmp(optarg, "top"))
display_placement = PLACEMENT_TOP;
else
if (!strcasecmp(optarg, "bottom"))
display_placement = PLACEMENT_BOTTOM;
else {
fprintf(stderr, "%s: Unsupported placement.\n", optarg);
return EXIT_FAILURE;
}
break;
case 'B':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1) {
fprintf(stderr, "%s: Invalid bar thickness in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_size = val;
break;
case 'S':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 0) {
fprintf(stderr, "%s: Invalid bar spacing in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_space = val;
break;
case '?':
/* getopt() has already printed an error message. */
return EXIT_FAILURE;
default:
/* Bug catcher: This should never occur. */
fprintf(stderr, "getopt() returned %d ('%c')!\n", opt, opt);
return EXIT_FAILURE;
}
}
if (install_done(SIGINT) ||
install_done(SIGHUP) ||
install_done(SIGTERM) ||
install_done(SIGQUIT)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (optind < argc) {
fprintf(stderr, "%s: Unsupported parameter.\n", argv[optind]);
return EXIT_FAILURE;
}
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
if (!app) {
fprintf(stderr, "Cannot start GTK+ application.\n");
return EXIT_FAILURE;
}
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
size_t samples = rate / updates;
if (samples < 1)
samples = 1;
val = vu_start(server, "vu-bar", device, "VU monitor", channels, rate, samples);
if (val) {
fprintf(stderr, "Cannot monitor audio source: %s.\n", vu_error(val));
g_object_unref(app);
return EXIT_FAILURE;
}
peak = calloc((size_t)channels * sizeof (float), samples);
peak_line = calloc((size_t)channels * sizeof (float), samples);
if (!peak) {
fprintf(stderr, "Out of memory.\n");
g_object_unref(app);
vu_stop();
return EXIT_FAILURE;
}
val = g_application_run(G_APPLICATION(app), 0, NULL);
g_object_unref(app);
vu_stop();
return val;
}
-
(Dammit, I need to get me a display, instead of getting others to do my testing and debugging for me :-[. I've been putting off getting one, since I'm very sensitive to panel/pixel/subpixel errors, and I want one with good viewing angles and color reproduction too – so IPS panel, instead of TN or VA ––, and I really dislike having to return a product...)
It's taken me 25 years to mount a 2nd monitor. Who knows, in another 25 years, I might be ready to try the triple monitor setup you all the kids rave about.
-
Could you check if the modified gui.c below works?
It is based on the idea that the problem is not the space reservation, but where the actual vu bar area ends up, and attempts to fix that by resizing and moving the "window" after it has been mapped. Pay attention to the output: it will print "Window mispositioned at ..." or "Window size is ..." if the window position is detected to not apply correctly. (I expect it to print the former, if the vu bar ends up in the wrong monitor.)
I'd like you to try the following four scenarios:- ./vu-bar -m 0 -p left
This should put the vu-bar on the left edge of the primary (bottom/left) monitor - ./vu-bar -m 0 -p right
This should put the vu-bar on the right edge of the primary (bottom/left) monitor - ./vu-bar -m 1 -p left
This should put the vu-bar on the left edge of the secondary (top/right) monitor - ./vu-bar -m 1 -p right
This should put the vu-bar on the right edge of the secondary (top/right) monitor
and report whether the bar is in the correct monitor and correct location, with the correct area reserved, and whether there was any output from the program, for each of the four cases.
Monitor 1:
'-m 1 -p left' and '-m 1 -p right' work perfect on monitor 1 (the secondary monitor). And they push windows around as they should.
Monitor 0:
'-m 0 -p left' pushes the windows away in the correct spot but places the bottom of the meter too high (in-line with mon1?), and meter runs off past the top of the screen.
'-m 0 -p right' pushes the windows away on the wrong monitor (mon 1) but places the meter on the correct monitor but too high as indicated above.
Hope that makes sense.
-
Could you retry with the attached gui.c? And also report if vu-bar outputs anything in the terminal?
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
#include <signal.h>
#include <gtk/gtk.h>
#include <ctype.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include "vu.h"
#ifndef MAX_CHANNELS
#define MAX_CHANNELS 32
#endif
#ifndef MAX_RATE
#define MAX_RATE 250000
#endif
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = SA_RESTART;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
struct tickmark {
float amplitude;
float red;
float green;
float blue;
};
enum placement {
PLACEMENT_LEFT = 1,
PLACEMENT_RIGHT = 2,
PLACEMENT_TOP = 3,
PLACEMENT_BOTTOM = 4
};
static const char *server = NULL;
static const char *device = NULL;
static int channels = 2;
static int rate = 48000;
static int updates = 60;
static int bar_size = 4;
static int bar_space = 3;
static int display_monitor = -1;
static enum placement display_placement = PLACEMENT_RIGHT;
static float *peak_line = NULL;
static float *peak = NULL;
static float decay_line = 0.99f;
static float decay = 0.95f;
static float red_limit = 0.891251f; /* Amplitude within 1 dB of clipping */
static float green_limit = 0.707946f; /* Amplitude within 3 dB of clipping */
static struct tickmark tickmarks[] = {
{ .amplitude = 0.891251f, .red = 1.0f, .green = 0.00f, .blue = 0.0f }, /* 1 dB */
{ .amplitude = 0.794328f, .red = 1.0f, .green = 1.00f, .blue = 0.0f }, /* 2 dB */
{ .amplitude = 0.707946f, .red = 0.0f, .green = 1.00f, .blue = 0.0f }, /* 3 dB */
{ .amplitude = 0.630957f, .red = 0.0f, .green = 0.95f, .blue = 0.0f }, /* 4 dB */
{ .amplitude = 0.562341f, .red = 0.0f, .green = 0.90f, .blue = 0.0f }, /* 5 dB */
{ .amplitude = 0.501187f, .red = 0.0f, .green = 0.85f, .blue = 0.0f }, /* 6 dB */
{ .amplitude = 0.446684f, .red = 0.0f, .green = 0.80f, .blue = 0.0f }, /* 7 dB */
{ .amplitude = 0.398107f, .red = 0.0f, .green = 0.75f, .blue = 0.0f }, /* 8 dB */
{ .amplitude = 0.354813f, .red = 0.0f, .green = 0.70f, .blue = 0.0f }, /* 9 dB */
{ .amplitude = 0.316228f, .red = 0.0f, .green = 0.65f, .blue = 0.0f }, /* 10 dB */
{ .amplitude = -1.0f }
};
static gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
GdkRectangle area;
gtk_widget_get_clip(widget, &area);
cairo_save(cr);
cairo_set_source_rgb(cr, 0.0,0.0,0.0);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
for (int i = 0; i < channels; i++) {
if (peak[i] >= red_limit)
cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
else
if (peak[i] <= green_limit)
cairo_set_source_rgb(cr, 0.0, 0.5 + 0.5*peak[i]/green_limit, 0.0);
else {
const double c = (peak[i] - green_limit) / (red_limit - green_limit);
cairo_set_source_rgb(cr, c, 1.0-c, 0.0);
}
const double c = (peak[i] < 0.0f) ? 0.0 : (peak[i] < 1.0f) ? peak[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
cairo_rectangle(cr, area.x + bar_space + i * (bar_space + bar_size),
area.y + bar_space + (1.0 - c)*(area.height - 2*bar_space),
bar_size, c*area.height);
cairo_fill(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
cairo_rectangle(cr, area.x + bar_space,
area.y + bar_space + i * (bar_space + bar_size),
c * (area.width - 2*bar_space), bar_size);
cairo_fill(cr);
}
}
cairo_set_line_width(cr, 1.0);
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int y = area.y + bar_space + (1.0f - tickmarks[i].amplitude)*(area.height - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, area.x + 2, y);
cairo_line_to(cr, area.x + area.width - 2, y);
cairo_stroke(cr);
}
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
for (int i = 0; tickmarks[i].amplitude >= 0.0f; i++) {
const int x = area.x + bar_space + tickmarks[i].amplitude*(area.width - 2*bar_space);
cairo_set_source_rgb(cr, tickmarks[i].red, tickmarks[i].green, tickmarks[i].blue);
cairo_move_to(cr, x, area.y + 2);
cairo_line_to(cr, x, area.y + area.width - 2);
cairo_stroke(cr);
}
}
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
cairo_set_line_width(cr, 2.0);
for (int i = 0; i < channels; i++) {
const double c = (peak_line[i] < 0.0f) ? 0.0 : (peak_line[i] < 1.0f) ? peak_line[i] : 1.0;
if (display_placement == PLACEMENT_LEFT || display_placement == PLACEMENT_RIGHT) {
const int y = area.y + bar_space + (1.0f - c)*(area.height - 2*bar_space);
cairo_move_to(cr, area.x + i *(bar_space + bar_size), y);
cairo_line_to(cr, area.x + (i+1)*(bar_space + bar_size), y);
cairo_stroke(cr);
} else
if (display_placement == PLACEMENT_TOP || display_placement == PLACEMENT_BOTTOM) {
const int x = area.x + bar_space + c*(area.width - 2*bar_space);
cairo_move_to(cr, x, area.y + i *(bar_space + bar_size));
cairo_line_to(cr, x, area.y + (i+1)*(bar_space + bar_size));
cairo_stroke(cr);
}
}
cairo_restore(cr);
return TRUE;
}
static gboolean tick(GtkWidget *widget, GdkFrameClock *fclk, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
(void)fclk;
if (done) {
gtk_window_close(GTK_WINDOW(widget));
return G_SOURCE_REMOVE;
}
float new_peak[channels];
if (vu_peak(new_peak, channels) == channels) {
for (int c = 0; c < channels; c++) {
peak[c] *= decay;
peak[c] = (new_peak[c] > peak[c]) ? new_peak[c] : peak[c];
peak_line[c] *= decay_line;
peak_line[c] = (new_peak[c] > peak_line[c]) ? new_peak[c] : peak_line[c];
}
gtk_widget_queue_draw(widget);
}
return G_SOURCE_CONTINUE;
}
static void screen_changed(GtkWidget *widget, GdkScreen *old_screen, gpointer user_data)
{
(void)user_data; (void)old_screen; /* Silence unused parameter warning; generates no code */
gtk_widget_set_visual(widget, gdk_screen_get_system_visual(gtk_widget_get_screen(widget)));
}
static void place(GdkRectangle *to, gulong *reserve)
{
if (!to || !reserve)
return;
to->x = 0;
to->y = 0;
to->width = bar_space + (bar_size + bar_space) * channels;
to->height = bar_space + (bar_size + bar_space) * channels;
reserve[ 0] = 0; /* left */
reserve[ 1] = 0; /* right */
reserve[ 2] = 0; /* top */
reserve[ 3] = 0; /* bottom */
reserve[ 4] = 0; /* left_start_y */
reserve[ 5] = 0; /* left_end_y */
reserve[ 6] = 0; /* right_start_y */
reserve[ 7] = 0; /* right_end_y */
reserve[ 8] = 0; /* top_start_x */
reserve[ 9] = 0; /* top_end_x */
reserve[10] = 0; /* bottom_start_x */
reserve[11] = 0; /* bottom_end_x */
GdkDisplay *d = gdk_display_get_default();
GdkMonitor *m = gdk_display_get_monitor(d, display_monitor);
GdkRectangle w;
if (!m)
m = gdk_display_get_primary_monitor(d);
if (!m)
return;
gdk_monitor_get_workarea(m, &w);
switch (display_placement) {
case PLACEMENT_LEFT:
to->x = w.x;
to->height = w.height;
reserve[0] = to->width;
reserve[4] = w.y;
reserve[5] = w.y + w.height - 1;
break;
case PLACEMENT_RIGHT:
to->x = w.x + w.width - to->width;
to->height = w.height;
reserve[1] = to->width;
reserve[6] = w.y;
reserve[7] = w.y + w.height - 1;
break;
case PLACEMENT_TOP:
to->y = w.y;
to->width = w.width;
reserve[2] = to->height;
reserve[8] = w.x;
reserve[9] = w.x + w.width - 1;
break;
case PLACEMENT_BOTTOM:
to->y = w.y + w.height - to->height;
to->width = w.width;
reserve[3] = to->height;
reserve[10] = w.x;
reserve[11] = w.x + w.width - 1;
break;
}
reserve[0] = to->x;
reserve[1] = to->y;
reserve[2] = to->x + to->width - 1;
reserve[3] = to->y + to->height - 1;
}
static void activate(GtkApplication *app, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
gulong reserve[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
/* Compute where to place the window */
GdkRectangle pos;
place(&pos, reserve);
/* Create window */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "VU-Bar");
gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DOCK);
gtk_window_set_icon_name(GTK_WINDOW(window), "multimedia-volume-control");
gtk_window_set_default_size(GTK_WINDOW(window), pos.width, pos.height);
gtk_window_resize(GTK_WINDOW(window), pos.width, pos.height);
gtk_window_set_resizable(GTK_WINDOW(window), TRUE);
gtk_window_move(GTK_WINDOW(window), pos.x, pos.y);
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
gtk_widget_add_tick_callback(window, tick, NULL, NULL);
gtk_widget_set_app_paintable(window, TRUE);
g_signal_connect(window, "draw", G_CALLBACK(draw), NULL);
g_signal_connect(window, "screen-changed", G_CALLBACK(screen_changed), NULL);
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_show_all(window);
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
GdkWindow *w = gtk_widget_get_window(window);
if (w) {
GdkAtom strut = gdk_atom_intern_static_string("_NET_WM_STRUT");
GdkAtom partial = gdk_atom_intern_static_string("_NET_WM_STRUT_PARTIAL");
GdkAtom cardinal = gdk_atom_intern_static_string("CARDINAL");
gdk_property_change(w, strut, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)reserve, 4);
gdk_property_change(w, partial, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)reserve, 12);
}
// Check if the window position matches the required position.
{
gint x, y, w, h;
gtk_window_get_position(GTK_WINDOW(window), &x, &y);
gtk_window_get_size(GTK_WINDOW(window), &w, &h);
if (x != pos.x || y != pos.y)
fprintf(stderr, "Window mispositioned at (%d,%d) instead of at (%d, %d) as requested.\n", x, y, pos.x, pos.y);
if (w != pos.width || h != pos.height)
fprintf(stderr, "Window size is %dx%d instead of the requested %dx%d\n", w, h, pos.width, pos.height);
}
}
static const char *skip_lws(const char *from)
{
if (!from)
return NULL;
while (isspace((unsigned char)(*from)))
from++;
return from;
}
static const char *parse_int(const char *from, int *to)
{
const char *next = from;
long val;
if (!from || *from == '\0') {
errno = EINVAL;
return NULL;
}
errno = 0;
val = strtol(from, (char **)(&next), 0);
if (errno)
return NULL;
if (next == from) {
errno = EINVAL;
return NULL;
}
if ((long)(int)(val) != val) {
errno = ERANGE;
return NULL;
}
if (to)
*to = val;
errno = 0;
return next;
}
int usage(const char *arg0)
{
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s -h | --help\n", arg0);
fprintf(stderr, " %s [ OPTIONS ]\n", arg0);
fprintf(stderr, "Options:\n");
fprintf(stderr, " -s SERVER PulseAudio server\n");
fprintf(stderr, " -d DEVICE Source to monitor\n");
fprintf(stderr, " -c CHANNELS Number of channels\n");
fprintf(stderr, " -r RATE Samples per second\n");
fprintf(stderr, " -u COUNT Peak calculations per second\n");
fprintf(stderr, " -m MONITOR Display monitor number\n");
fprintf(stderr, " -p WHERE Meter placement on display\n");
fprintf(stderr, " -B PIXELS Bar thickness in pixels\n");
fprintf(stderr, " -S PIXELS Bar spacing in pixels\n");
fprintf(stderr, "Placement:\n");
fprintf(stderr, " -p left Left edge of monitor\n");
fprintf(stderr, " -p right Right edge of monitor\n");
fprintf(stderr, " -p top Top edge of monitor\n");
fprintf(stderr, " -p bottom Bottom edge of monitor\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
int main(int argc, char *argv[])
{
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
const char *p;
int opt, val;
setlocale(LC_ALL, "");
if (argc > 1 && !strcmp(argv[1], "--help"))
return usage(arg0);
gtk_init(&argc, &argv);
while ((opt = getopt(argc, argv, "hs:d:c:r:u:m:p:B:S:")) != -1) {
switch (opt) {
case 'h':
return usage(arg0);
case 's':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
server = NULL;
else
server = optarg;
break;
case 'd':
if (!optarg || optarg[0] == '\0' || !strcmp(optarg, "default"))
device = NULL;
else
device = optarg;
break;
case 'c':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > MAX_CHANNELS) {
fprintf(stderr, "%s: Invalid number of channels.\n", optarg);
return EXIT_FAILURE;
}
channels = val;
break;
case 'r':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 128 || val > MAX_RATE) {
fprintf(stderr, "%s: Invalid sample rate.\n", optarg);
return EXIT_FAILURE;
}
rate = val;
break;
case 'u':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1 || val > 200) {
fprintf(stderr, "%s: Invalid number of peak updates per second.\n", optarg);
return EXIT_FAILURE;
}
updates = val;
break;
case 'm':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < -1) {
fprintf(stderr, "%s: Invalid monitor number.\n", optarg);
return EXIT_FAILURE;
}
display_monitor = val;
break;
case 'p':
if (!strcasecmp(optarg, "left"))
display_placement = PLACEMENT_LEFT;
else
if (!strcasecmp(optarg, "right"))
display_placement = PLACEMENT_RIGHT;
else
if (!strcasecmp(optarg, "top"))
display_placement = PLACEMENT_TOP;
else
if (!strcasecmp(optarg, "bottom"))
display_placement = PLACEMENT_BOTTOM;
else {
fprintf(stderr, "%s: Unsupported placement.\n", optarg);
return EXIT_FAILURE;
}
break;
case 'B':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 1) {
fprintf(stderr, "%s: Invalid bar thickness in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_size = val;
break;
case 'S':
p = skip_lws(parse_int(optarg, &val));
if (!p || *p != '\0' || val < 0) {
fprintf(stderr, "%s: Invalid bar spacing in pixels.\n", optarg);
return EXIT_FAILURE;
}
bar_space = val;
break;
case '?':
/* getopt() has already printed an error message. */
return EXIT_FAILURE;
default:
/* Bug catcher: This should never occur. */
fprintf(stderr, "getopt() returned %d ('%c')!\n", opt, opt);
return EXIT_FAILURE;
}
}
if (install_done(SIGINT) ||
install_done(SIGHUP) ||
install_done(SIGTERM) ||
install_done(SIGQUIT)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (optind < argc) {
fprintf(stderr, "%s: Unsupported parameter.\n", argv[optind]);
return EXIT_FAILURE;
}
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
if (!app) {
fprintf(stderr, "Cannot start GTK+ application.\n");
return EXIT_FAILURE;
}
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
size_t samples = rate / updates;
if (samples < 1)
samples = 1;
val = vu_start(server, "vu-bar", device, "VU monitor", channels, rate, samples);
if (val) {
fprintf(stderr, "Cannot monitor audio source: %s.\n", vu_error(val));
g_object_unref(app);
return EXIT_FAILURE;
}
peak = calloc((size_t)channels * sizeof (float), samples);
peak_line = calloc((size_t)channels * sizeof (float), samples);
if (!peak) {
fprintf(stderr, "Out of memory.\n");
g_object_unref(app);
vu_stop();
return EXIT_FAILURE;
}
val = g_application_run(G_APPLICATION(app), 0, NULL);
g_object_unref(app);
vu_stop();
return val;
}
-
'-m 0 -p left' pushes no windows. Places the meter too high on the left side of monitor 0
'-m 0 -p right' pushes no windows. Places the meter too high on the right hand side of monitor 0
'-m 1 -p left' places the meter on the left of m1, pushes no windows but does move the windows from the top 1/3 of the screen of m0
'-m 1 -p right' works as it should. Meter on the right of m1, windows are pushed out of it's way.
-
I just fired up virtual box and gave it two small monitors (640x480). What I found was the params seem to work fine while ever the two monitors are dead in-line horizontally. When I bumped mon1 up a bit, all bets were off.
-
What about the output?
-
What about the output?
Command line output is very tight-lipped.
No output.
-
Okay, I think I understand the problem now.
I am using the _NET_WM_STRUT_PARTIAL (https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html#NETWMSTRUTPARTIAL) application window property (with its simpler, older _NET_WM_STRUT as a backup) to reserve a region for the VU bar, and didn't realize its limitations correctly.
Let's call the union of all your monitors, an axis-aligned rectangle, the desktop rectangle. This rectangle contains all of your visual desktop, plus possibly some un-displayed areas.
The _NET_WM_STRUT_PARTIAL property can only reserve a rectangular area with at least one edge at the edge of the desktop rectangle.
Some of the regions are however miscalculated right now, because the desired region is defined with respect to that desktop rectangle.
A bit of logic is needed to choose how to define the reserved rectangle, because for example in your case, if one wants the vu-bar in either monitor on the shared edge, that can be done by extending a narrow rectangle from the bottom or top of the desktop rectangle. It will need some testing, but theoretically all edges for up to four monitors should be supportable, if there are no panels. The fact that already existing reservations affect the placing of the vu-bar makes it a bit more complicated.
I'll do some testing, then post the next version for you to try; probably tomorrow.
-
Funnily enough, the 'format 32' for window manager properties like _NET_WM_PARTIAL_STRUT (and all others) does not mean 32-bit unsigned integers; it means unsigned long on the current architecture. :palm:
That's what the deleted post was about: I thought I might have a bug in using gulong, but no, format==32 uses gulong elements in the array.
So, the next thing is to explore how the struts (reserved areas on a workspace) actually work. Could you please compile and run the following strut.c:
// SPDX-License-Identifier: CC0-1.0
//
// Save this file as 'strut.c', and compile to './strut' using for example
// gcc -Wall -O2 `pkg-config --cflags gtk+-3.0` strut.c `pkg-config --libs gtk+-3.0` -o strut
//
#include <stdlib.h>
#include <stdint.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <stdio.h>
#include <errno.h>
enum {
ANCHOR_NONE = 0,
ANCHOR_LEFT = 1,
ANCHOR_TOP = 2,
ANCHOR_RIGHT = 3,
ANCHOR_BOTTOM = 4,
};
#define PARTIAL_STRUT_LEFT 0
#define PARTIAL_STRUT_RIGHT 1
#define PARTIAL_STRUT_TOP 2
#define PARTIAL_STRUT_BOTTOM 3
#define PARTIAL_STRUT_LEFT_YMIN 4
#define PARTIAL_STRUT_LEFT_YMAX 5
#define PARTIAL_STRUT_RIGHT_YMIN 6
#define PARTIAL_STRUT_RIGHT_YMAX 7
#define PARTIAL_STRUT_TOP_XMIN 8
#define PARTIAL_STRUT_TOP_XMAX 9
#define PARTIAL_STRUT_BOTTOM_XMIN 10
#define PARTIAL_STRUT_BOTTOM_XMAX 11
struct rect {
int xmin;
int ymin;
int xmax;
int ymax;
};
struct monitor {
struct rect geometry;
struct rect workarea;
};
static GdkDisplay *display = NULL;
static struct rect desktop = { .xmin = 0, .ymin = 0, .xmax = 0, .ymax = 0 };
static int monitors = 0;
static struct monitor *monitor = NULL;
static int anchor = ANCHOR_NONE;
static struct rect strut = { .xmin = -1, .ymin = -1, .xmax = -1, .ymax = -1 };
static gulong partial_strut[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
static int rescan_monitors(void)
{
desktop.xmin = 0;
desktop.ymin = 0;
desktop.xmax = 0;
desktop.ymax = 0;
monitors = 0;
if (monitor) {
free(monitor);
monitor = NULL;
}
/* Find out the maximum number of monitors */
int max_monitors = (display) ? gdk_display_get_n_monitors(display) : 0;
if (max_monitors < 1) {
errno = ENOENT;
return -1;
}
/* Allocate a monitor array */
monitor = malloc(max_monitors * sizeof monitor[0]);
if (!monitor) {
errno = ENOMEM;
return -1;
}
for (int i = 0; i < max_monitors; i++) {
GdkRectangle box;
GdkMonitor *one = gdk_display_get_monitor(display, i);
if (!one)
continue;
gdk_monitor_get_geometry(one, &box);
if (box.x < 0 || box.y < 0 || box.width < 1 || box.height < 1)
continue;
monitor[monitors].geometry.xmin = box.x;
monitor[monitors].geometry.ymin = box.y;
monitor[monitors].geometry.xmax = box.x + box.width - 1;
monitor[monitors].geometry.ymax = box.y + box.height - 1;
if (desktop.xmax < monitor[monitors].geometry.xmax)
desktop.xmax = monitor[monitors].geometry.xmax;
if (desktop.ymax < monitor[monitors].geometry.ymax)
desktop.ymax = monitor[monitors].geometry.ymax;
gdk_monitor_get_workarea(one, &box);
if (box.x < 0 || box.y < 0 || box.width < 1 || box.height < 1)
continue;
monitor[monitors].workarea.xmin = box.x;
monitor[monitors].workarea.ymin = box.y;
monitor[monitors].workarea.xmax = box.x + box.width - 1;
monitor[monitors].workarea.ymax = box.y + box.height - 1;
if (monitor[monitors].workarea.xmin < monitor[monitors].geometry.xmin ||
monitor[monitors].workarea.xmax > monitor[monitors].geometry.xmax ||
monitor[monitors].workarea.ymin < monitor[monitors].geometry.ymin ||
monitor[monitors].workarea.ymax > monitor[monitors].geometry.ymax)
continue;
monitors++;
}
if (!monitors) {
free(monitor);
monitor = NULL;
}
return monitors;
}
static gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{
(void)user_data; /* Silence unused parameter warning; generates no code */
GdkRectangle area;
gtk_widget_get_clip(widget, &area);
cairo_save(cr);
cairo_set_source_rgb(cr, 0.0,0.0,0.0);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_paint(cr);
cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
cairo_rectangle(cr, area.x, area.y, area.width, area.height);
cairo_fill(cr);
cairo_restore(cr);
return TRUE;
}
static void activate(GtkApplication *app, gpointer user_data)
{
(void)user_data; /* Silence warning about unused parameter. */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DOCK);
gtk_window_set_default_size(GTK_WINDOW(window), strut.xmax - strut.xmin + 1, strut.ymax - strut.ymin + 1);
gtk_window_set_resizable(GTK_WINDOW(window), TRUE);
gtk_window_resize(GTK_WINDOW(window), strut.xmax - strut.xmin + 1, strut.ymax - strut.ymin + 1);
gtk_window_move(GTK_WINDOW(window), strut.xmin, strut.ymin);
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_set_app_paintable(window, TRUE);
g_signal_connect(window, "draw", G_CALLBACK(draw), NULL);
g_signal_connect(window, "button-release-event", G_CALLBACK(gtk_window_close), window);
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
gtk_widget_show_all(window);
GdkWindow *w = gtk_widget_get_window(window);
if (w) {
gdk_window_move_resize(w, strut.xmin, strut.ymin, strut.xmax - strut.xmin + 1, strut.ymax - strut.ymin + 1);
GdkAtom partial = gdk_atom_intern_static_string("_NET_WM_STRUT_PARTIAL");
GdkAtom cardinal = gdk_atom_intern_static_string("CARDINAL");
gdk_property_change(w, partial, cardinal, 32, GDK_PROP_MODE_REPLACE, (guchar *)partial_strut, 12);
fprintf(stderr, "Note: Set _NET_WM_STRUT_PARTIAL: %d %d %d %d %d %d %d %d %d %d %d %d\n",
(int)partial_strut[0], (int)partial_strut[1], (int)partial_strut[2], (int)partial_strut[3],
(int)partial_strut[4], (int)partial_strut[5], (int)partial_strut[6], (int)partial_strut[7],
(int)partial_strut[8], (int)partial_strut[9], (int)partial_strut[10], (int)partial_strut[11]);
}
}
static int match(const char *src, const char *alt1, const char *alt2)
{
if (!src || !alt1 || !alt2) {
return 0;
}
while (*src) {
if (*src == *alt1 || *src == *alt2) {
src++;
alt1++;
alt2++;
} else {
return 0;
}
}
return (*alt1 == '\0' || *alt2 == '\0');
}
static int parse_int(const char *src, int *dst)
{
const char *end;
long val;
end = src;
errno = 0;
val = strtol(src, (char **)&end, 0);
if (errno)
return -1;
if (end == src)
return -1;
while (*end == '\t' || *end == '\n' || *end == '\v' ||
*end == '\f' || *end == '\r' || *end == ' ')
end++;
if (*end)
return -1;
if ((long)((int)val) != val)
return -1;
if (dst)
*dst = (int)val;
return 0;
}
int main(int argc, char *argv[])
{
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
/* Initialize GDK. Supports display selection via --display
parameter and the DISPLAY environment variable. */
if (!gdk_init_check(&argc, &argv)) {
fprintf(stderr, "Cannot initialize GDK.\n");
return EXIT_FAILURE;
}
/* Get access to the default display. */
display = gdk_display_get_default();
if (!display) {
fprintf(stderr, "No default display.\n");
return EXIT_FAILURE;
}
/* Rescan monitors. */
int n = rescan_monitors();
if (n < 0) {
fprintf(stderr, "%s.\n", strerror(errno));
return EXIT_FAILURE;
} else
if (!n) {
fprintf(stderr, "No monitors.\n");
return EXIT_FAILURE;
}
if (argc < 2) {
printf("Desktop: (%d,%d)-(%d,%d)\n", desktop.xmin, desktop.ymin,
desktop.xmax, desktop.ymax);
for (int i = 0; i < monitors; i++)
printf("Monitor: (%d,%d)-(%d,%d) with work area (%d,%d)-(%d,%d)\n",
monitor[i].geometry.xmin, monitor[i].geometry.ymin,
monitor[i].geometry.xmax, monitor[i].geometry.ymax,
monitor[i].workarea.xmin, monitor[i].workarea.ymin,
monitor[i].workarea.xmax, monitor[i].workarea.ymax);
return EXIT_SUCCESS;
} else
if (argc != 6) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s -h | --help\n", arg0);
fprintf(stderr, " %s\n", arg0);
fprintf(stderr, " %s ANCHOR XMIN YMIN XMAX YMAX\n", arg0);
fprintf(stderr, "Where ANCHOR is one of:\n");
fprintf(stderr, " L or LEFT\n");
fprintf(stderr, " T or TOP\n");
fprintf(stderr, " R or RIGHT\n");
fprintf(stderr, " B or BOTTOM\n");
fprintf(stderr, "and (XMIN,YMIN)-(XMAX,YMAX) is the reserved strut area.\n");
fprintf(stderr, "Run without parameters to see current desktop and monitors.\n");
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
if (match(argv[1], "left", "LEFT") || match(argv[1], "l", "L"))
anchor = ANCHOR_LEFT;
else
if (match(argv[1], "top", "TOP") || match(argv[1], "t", "T") || match(argv[1], "up", "UP") || match(argv[1], "u", "U"))
anchor = ANCHOR_TOP;
else
if (match(argv[1], "right", "RIGHT") || match(argv[1], "r", "R"))
anchor = ANCHOR_RIGHT;
else
if (match(argv[1], "bottom", "BOTTOM") || match(argv[1], "b", "B") || match(argv[1], "down", "DOWN") || match(argv[1], "d", "D"))
anchor = ANCHOR_BOTTOM;
else {
fprintf(stderr, "%s: Invalid anchor direction.\n", argv[1]);
return EXIT_FAILURE;
}
if (parse_int(argv[2], &strut.xmin) || strut.xmin < desktop.xmin || strut.xmin > desktop.xmax) {
fprintf(stderr, "%s: Invalid strut rectangle minimum x coordinate.\n", argv[2]);
return EXIT_FAILURE;
}
if (parse_int(argv[3], &strut.ymin) || strut.ymin < desktop.ymin || strut.ymin > desktop.ymax) {
fprintf(stderr, "%s: Invalid strut rectangle minimum y coordinate.\n", argv[3]);
return EXIT_FAILURE;
}
if (parse_int(argv[4], &strut.xmax) || strut.xmax < strut.xmin || strut.xmax > desktop.xmax) {
fprintf(stderr, "%s: Invalid strut rectangle maximum x coordinate.\n", argv[4]);
return EXIT_FAILURE;
}
if (parse_int(argv[5], &strut.ymax) || strut.ymax < strut.ymin || strut.ymax > desktop.ymax) {
fprintf(stderr, "%s: Invalid strut rectangle maximum y coordinate.\n", argv[5]);
return EXIT_FAILURE;
}
switch (anchor) {
case ANCHOR_LEFT:
partial_strut[PARTIAL_STRUT_LEFT] = strut.xmax + 1;
partial_strut[PARTIAL_STRUT_LEFT_YMIN] = strut.ymin;
partial_strut[PARTIAL_STRUT_LEFT_YMAX] = strut.ymax;
break;
case ANCHOR_RIGHT:
partial_strut[PARTIAL_STRUT_RIGHT] = desktop.xmax - strut.xmin + 1;
partial_strut[PARTIAL_STRUT_RIGHT_YMIN] = strut.ymin;
partial_strut[PARTIAL_STRUT_RIGHT_YMAX] = strut.ymax;
break;
case ANCHOR_TOP:
partial_strut[PARTIAL_STRUT_TOP] = strut.ymax + 1;
partial_strut[PARTIAL_STRUT_TOP_XMIN] = strut.xmin;
partial_strut[PARTIAL_STRUT_TOP_XMAX] = strut.xmax;
break;
case ANCHOR_BOTTOM:
partial_strut[PARTIAL_STRUT_BOTTOM] = desktop.ymax - strut.ymin + 1;
partial_strut[PARTIAL_STRUT_BOTTOM_XMIN] = strut.xmin;
partial_strut[PARTIAL_STRUT_BOTTOM_XMAX] = strut.xmax;
break;
default:
fprintf(stderr, "Invalid anchor.\n");
return EXIT_FAILURE;
}
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
if (!app) {
fprintf(stderr, "Cannot start GTK+ application.\n");
return EXIT_FAILURE;
}
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
int val = g_application_run(G_APPLICATION(app), 0, NULL);
g_object_unref(app);
return val;
}
Run ./strut --help to see a short usage info.
Run
./strut
to see the desktop rectangle (which is (0,0)-(xmax,ymax) always), plus the rectangles and work areas of all monitors. All monitors will reside within the desktop rectangle.
A strut, or a reserved area for a panel or similar, is a rectangular region connected to one of the four edges of the desktop rectangle.
Run
./strut ANCHOR XMIN YMIN XMAX YMAX
where ANCHOR is left, top, right, or bottom; and the rectangular region in desktop coordinates is (XMIN,YMIN)-(XMAX,YMAX), to test the strut. It will be shown as a simple red rectangle. Clicking on the rectangle will close the test program.
Now, I assume that when Ed.Kloonk runs ./strut, he'll see
Desktop: (0, 0) - (2687, 2082)
Monitor: (0, 1003) - (1919, 2082) with work area (0, 1003) - (1919, 2046)
Monitor: (1920, 0) - (2687, 1365) with work area (1920, 0) - (2687, 1365)
Here are the most interesting test cases. Note that if the desktop gets wonky, you can just press Ctrl+C in the terminal running the command, and it should revert back to normal instantly. Clicking on the red area to close the window is just a nicety, not a requirement.
- These should put the strut on the left edge of the bottom left monitor:
./strut left 0 1003 15 2046
./strut top 0 1003 15 2046
- These should put the strut on the top edge of the bottom left monitor:
./strut left 0 1003 1919 1018
./strut top 0 1003 1919 1018
- These should put the strut on the top edge of the top right monitor:
./strut top 1920 0 2687 15
./strut right 1920 0 2687 15
- These should put the strut on the right edge of the top right monitor:
./strut top 2672 0 2687 1365
./strut right 2672 0 2687 1365
./strut bottom 2672 0 2687 1365
- These should put the strut on the bottom edge of the top right monitor:
./strut left 1920 1350 2687 1365
./strut right 1920 1350 2687 1365
./strut bottom 1920 1350 2687 1365
- These should put the strut on the right edge of the bottom left monitor:
./strut top 1913 1003 1919 2046
./strut bottom 1913 1003 1919 2046
- These should put the strut on the left edge of the top right monitor:
./strut top 1920 0 1935 1365
./strut bottom 1920 0 1935 1365
- These should put the strut near the bottom edge of the bottom left monitor, but above the existing panel:
./strut bottom 0 2032 1919 2047
./strut left 0 2032 1919 2047
I really only need to know which commands, if any, do exactly what the description says, "pushing" other windows correctly, without wonky effects.
(The note the program prints is not important, unless something really wonky happens. It just describes the exact partial strut setup.)
-
stby
:)
-
Now we're cooking with gas! :)
It seems that I have changed the screen metrics on you. So I took the liberty of adjusting the numbers where obvious. I haven't checked my work yet.
Desktop: (0,0)-(2687,1732)
Monitor: (0,653)-(1919,1732) with work area (0,653)-(1919,1696)
Monitor: (1920,0)-(2687,1365) with work area (1920,0)-(2687,1365)
with that in mind...
Now, I assume that when Ed.Kloonk runs ./strut, he'll see
Desktop: (0, 0) - (2687, 1732)
Monitor: (0, 653) - (1919, 1732) with work area (0, 653) - (1919, 1696)
Monitor: (1920, 0) - (2687, 1365) with work area (1920, 0) - (2687, 1365)
Here are the most interesting test cases. Note that if the desktop gets wonky, you can just press Ctrl+C in the terminal running the command, and it should revert back to normal instantly. Clicking on the red area to close the window is just a nicety, not a requirement.
These should put the strut on the left edge of the bottom left monitor:
./strut left 0 653 15 1696
./strut top 0 653 15 1696
These should put the strut on the top edge of the bottom left monitor:
./strut left 0 653 1919 1018
./strut top 0 653 1919 1018
[b]* occupies 1/3 of the top of the left monitor. (might be my fault)[/b]
These should put the strut on the top edge of the top right monitor:
./strut top 1920 0 2687 15
./strut right 1920 0 2687 15
These should put the strut on the right edge of the top right monitor:
./strut top 2672 0 2687 1365
./strut right 2672 0 2687 1365
./strut bottom 2672 0 2687 1365
These should put the strut on the bottom edge of the top right monitor:
./strut left 1920 1350 2687 1365
./strut right 1920 1350 2687 1365
./strut bottom 1920 1350 2687 1365
These should put the strut on the right edge of the bottom left monitor:
./strut top 1913 653 1919 1696
./strut bottom 1913 653 1919 1696
[b]* these two are thinner than the others. [/b]
These should put the strut on the left edge of the top right monitor:
./strut top 1920 0 1935 1365
./strut bottom 1920 0 1935 1365
These should put the strut near the bottom edge of the bottom left monitor, but above the existing panel:
./strut bottom 0 2032 1919 2047
[b]* "2032: Invalid strut rectangle minimum y coordinate."[/b]
./strut left 0 2032 1919 2047
[b]* "2032: Invalid strut rectangle minimum y coordinate."[/b]
I really only need to know which commands, if any, do exactly what the description says, "pushing" other windows correctly, without wonky effects.
(The note the program prints is not important, unless something really wonky happens. It just describes the exact partial strut setup.)
-
The examples with no footnote work as intended, if that wasn't obvious. :)
-
I think I had some goofs in the numbers, too. These should yield a 16-pixel sized red bar, given
Desktop: (0,0)-(2687,1732)
Monitor: (0,653)-(1919,1732) with work area (0,653)-(1919,1696)
Monitor: (1920,0)-(2687,1365) with work area (1920,0)-(2687,1365)
Edit: The below had a typo (635,650 instead of 653,668).
Note that we really use just Desktop, and the two work areas.
The logic is that expanding the specified region in the anchor direction to the edge of the desktop should not overlap with the work area of any other monitor; and we do not overlap across the monitor itself (that is, if we calculate a region in the left edge, we don't use right as the anchor direction).
- These should put the strut on the left edge of the bottom left monitor:
./strut left 0 653 15 1696
./strut top 0 653 15 1696
- These should put the strut on the top edge of the bottom left monitor:
./strut left 0 653 1919 668
./strut top 0 653 1919 668
- These should put the strut on the right edge of the bottom left monitor:
./strut top 1904 653 1919 1696
./strut bottom 1904 653 1919 1696
- These should put the strut near the bottom edge of the bottom left monitor, but above the existing panel:
./strut bottom 0 1681 1919 1696
./strut left 0 1681 1919 1696
- These should put the strut on the top edge of the top right monitor:
./strut top 1920 0 2687 15
./strut right 1920 0 2687 15
- These should put the strut on the right edge of the top right monitor:
./strut top 2672 0 2687 1365
./strut right 2672 0 2687 1365
./strut bottom 2672 0 2687 1365
- These should put the strut on the bottom edge of the top right monitor:
./strut left 1920 1350 2687 1365
./strut right 1920 1350 2687 1365
./strut bottom 1920 1350 2687 1365
- These should put the strut on the left edge of the top right monitor:
./strut top 1920 0 1935 1365
./strut bottom 1920 0 1935 1365
-
4 works now. (Above the existing panel)
but not pushing the windows away. :(
2 yields no response from the displays or any backchat from the console.
3 is the correct width now.
Edit: beg pardon. 4 is indeed pushing windows away. :rant:
-
Gah! I read 653 as 635, so the first three test cases were actually above the visible display. Could you recheck 1-3 too?
(It is Most Definitely a Monday today, for me at least. :palm:)
Oh yeah: for cases 3 and 5 8, I'd like to know if the strut (reserved area) behaves intuitively and as expected when you try to move small windows against/around/beneath it, since it is between the two monitors. You should be able to put a wide window across both monitors; in that case, the reserved area would stay visible and just hide the overlapped part of the wide window.
-
Gah! I read 653 as 635, so the first three test cases were actually above the visible display. Could you recheck 1-3 too?
Oh yeah: for cases 3 and 5, I'd like to know if the strut (reserved area) behaves intuitively and as expected when you try to move small windows against/around/beneath it, since it is between the two monitors. You should be able to put a wide window across both monitors; in that case, the reserved area would stay visible and just hide the overlapped part of the wide window.
Seems fine. The space reserved in between monitors is observed by the WM when moving a window. But there are two distinct snap-to positions.
In the case of (3), dragging a window...
Window can straddle both monitors with the window underneath the bar.
Window can snap-to the position to the left of the bar and therefore covering that part of the left of the window getting dragged.
Window can snap to the left of the right-hand monitor.
All fine.
-
When you say 5 I wonder if you mean 2. (top of left monitor)
Anyway. Seems to work OK.
-
No, I meant 8 (and not 5).
3 and 8 are between the two monitors. 3 is the right edge of the bottom left monitor, and 8 is the left edge of the top right monitor.
-
No, I meant 8 (and not 5).
3 and 8 are between the two monitors. 3 is the right edge of the bottom left monitor, and 8 is the left edge of the top right monitor.
Roger.
A window straddled across both monitors gets shoved to the left all the way to the right hand side of the left monitor when running strut.
A Window on the hard left of the right hand monitor gets bumped out of the way of the strut, when ran.
Looks OK.
-
Ok. I'll refactor the code, and post a fixed version of the volume meter later (today or tomorrow).
-
Ok. I'll refactor the code, and post a fixed version of the volume meter later (today or tomorrow).
:)
I threw them all in a script so I could whiz through them and found out that the first one of number 7 ( ./strut left 1920 1350 2687 1365) places the bar on the bottom of the right-hand monitor and works ok pushing the windows up, but it also severely interferes with the windows on the left monitor.
The remaining two in (7) are fine.
-
That makes sense, because the ./strut left 1920 1350 2687 1365 case, pulling (1920,1350)-(2687,1365) left makes it into (0,1350)-(2687,1365), which intersects with the first monitor, (0,653)-(1919,1732), and therefore should not have been included in the list of tests!
That is, you caught another of my bugs. :-[
Good thing is, I think I have a sound logic now.
Basically, we use the selected monitor and desired edge to find the rectangle where the VU meter should be displayed. Then, we try extending it to the edge of the desktop in the direction of the selected edge. If the extended rectangle does not intersect with any of the other monitors, we're golden. Otherwise, we try extending in the two directions perpendicular to the previous direction, and if one does not intersect with any of the other monitors, we use that; otherwise, we fail. (For example, if someone has a 3×3 grid of displays, we cannot really put it anywhere in the center display.)
-
Oh my, PulseAudio is annoyingly bad. (Before you label me, just look at some example documentation, for example stream.h:pa_stream_peek() (https://freedesktop.org/software/pulseaudio/doxygen/stream_8h.html), and try to find out if the buffer contains a full number of samples for all channels, or whether it is just a byte stream the reader must synchronize itself; i.e., whether each callback starts at a sample for the first channel or not. All the documentation says is "it can be less or more than a complete fragment", a fragment being the basic self-contained block of samples on the server side.)
You see, to make this "worthwhile", I want to add run-time source selection (by right-clicking on the bar) and some other goodies. To do that, I need to switch from the "simple" interface to the asynchronous interface. No problem; I'm very familiar and comfy with async interfaces. Except this one is 1) designed by a baboon using crayons, and 2) with even worse documentation. Sure, it is *easy* to get something to seemingly work, and that's fine for proof of concept and quick crappy stuff, but my OCD just don't let me keep doing that with this thing, knowing Ed.Kloonk has been using it for a year already. Besides, if I do it right, I could add Debian packaging in the sources, maybe visual plugin support (if one wants to write their own peak visualizer), and be happy with the result. It might be useful for others, too.
Ed.Kloonk, do you mind if I use GPL-3.0+ for the rewritten version, and publish the sources somewhere, maybe GitHub? (I think it'd be the most appropriate license for this kind of application on Linux. But, if you or anyone else has a reasonable argument for some other license, I'm open.)
-
Ed.Kloonk, do you mind if I use GPL-3.0+ for the rewritten version, and publish the sources somewhere, maybe GitHub? (I think it'd be the most appropriate license for this kind of application on Linux. But, if you or anyone else has a reasonable argument for some other license, I'm open.)
I don't care. I'm just here to learn. :)
-
Having had a moment to dwell on this more, I feel the Beer-ware licence is more my speed...
/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <phk@FreeBSD.ORG> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp
* ----------------------------------------------------------------------------
*/
https://fedoraproject.org/wiki/Licensing/Beerware
;D
;)
-
Just a progress report: I'm still trying to decide whether threaded operation (having the audio code run in its own thread, completely separate from the UI; except for reporting changes in available sources using gdk_threads_add_timeout_full() (https://docs.gtk.org/gdk3/func.threads_add_timeout_full.html) with the single-shot callee (that returns FALSE) function sending a custom 'audio-sources' signal to the GtkApplication or GtkWindow or GtkWidget that uses the sources), or a unified main loop in a single thread, is the way to go. Both have their benefits and downsides.
It is funny how easy it is to write the code, compared to how difficult it is to select a robust, reliable approach.
I know other developers often pooh-pooh this, and just implement whatever comes to mind first, and then move on; but I don't like that, because it leads to inferior tools.
Sure, it does make much more commercial sense, because then your output and product portfolio gets bigger. But, I'm after quality, not quantity here.
Apologies if it annoys anyone, but me being me, this is the way for me.