Author Topic: PulseAudio Volume Meter. Would like to add VU ticks.  (Read 21046 times)

0 Members and 2 Guests are viewing this topic.

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
PulseAudio Volume Meter. Would like to add VU ticks.
« on: July 21, 2020, 12:31:44 pm »
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/

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?

iratus parum formica
 

Offline gnif

  • Administrator
  • *****
  • Posts: 1934
  • Country: au
  • Views and opinions are my own
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #1 on: January 02, 2021, 01:30:05 am »
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.
 
The following users thanked this post: Ed.Kloonk

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #2 on: January 02, 2021, 03:12:03 am »
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
« Last Edit: January 02, 2021, 03:18:42 am by Ed.Kloonk »
iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #3 on: January 02, 2021, 09:37:42 pm »
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 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.

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 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.
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #4 on: January 02, 2021, 10:12:26 pm »

Why use that shit when 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.

 :)

iratus parum formica
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #5 on: January 02, 2021, 10:20:36 pm »


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.

Quote
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:
iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #6 on: January 02, 2021, 10:34:48 pm »
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?
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #7 on: January 02, 2021, 11:20:15 pm »

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.

« Last Edit: January 02, 2021, 11:22:54 pm by Ed.Kloonk »
iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #8 on: January 03, 2021, 01:46:58 am »
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...
 

Offline PKTKS

  • Super Contributor
  • ***
  • Posts: 1766
  • Country: br
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #9 on: January 03, 2021, 01:17:35 pm »


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

 
The following users thanked this post: Ed.Kloonk

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #10 on: January 03, 2021, 01:51:41 pm »
Something like the following.

vu.h: A simple thread-safe interface for VU metering
Code: [Select]
#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
Code: [Select]
#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
Code: [Select]
#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.
Code: [Select]
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.
 
The following users thanked this post: Ed.Kloonk, RoGeorge, rstofer

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #11 on: January 03, 2021, 04:24:04 pm »
Something like the following.
Wow. That's perfect.  :)

Quote
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!

 :-+



iratus parum formica
 
The following users thanked this post: radiolistener

Offline gnif

  • Administrator
  • *****
  • Posts: 1934
  • Country: au
  • Views and opinions are my own
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #12 on: January 04, 2021, 01:56:56 pm »
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.
 
The following users thanked this post: Ed.Kloonk

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 8437
  • Country: ro
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #13 on: January 04, 2021, 02:54:49 pm »
Something like the following.

vu.h: A simple thread-safe interface for VU metering
...

Wow!   :-+
 
The following users thanked this post: Ed.Kloonk

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #14 on: January 04, 2021, 10:03:30 pm »
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.  :)
iratus parum formica
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #15 on: January 04, 2021, 10:16:16 pm »
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.

iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #16 on: January 05, 2021, 01:52:19 am »
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.
« Last Edit: January 05, 2021, 05:52:56 am by Nominal Animal »
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #17 on: January 05, 2021, 03:10:48 am »
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?



iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #18 on: January 05, 2021, 09:03:19 am »
Sure!  (See, that's one of the things I didn't even think about.)

Updated gui.c:
Code: [Select]
#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.
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #19 on: January 05, 2021, 09:17:54 am »
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.
Quote
I also added the slower-decay peak lines.
Cool. Works great.
Quote
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.

 :)
iratus parum formica
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #20 on: January 05, 2021, 09:26:30 am »
And here is a picture...

iratus parum formica
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #21 on: January 27, 2022, 06:46:01 am »
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:
Code: [Select]
static int              display_monitor = -1;
I have been running vu-bar as is with no command args.
iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #22 on: January 27, 2022, 08:04:57 am »
So far I've got my heart set on fiddling with this line:
Code: [Select]
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...
 

Offline Ed.KloonkTopic starter

  • Super Contributor
  • ***
  • Posts: 4000
  • Country: au
  • Cat video aficionado
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #23 on: January 27, 2022, 08:42:13 am »

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.

Quote
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
iratus parum formica
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: PulseAudio Volume Meter. Would like to add VU ticks.
« Reply #24 on: January 27, 2022, 09:29:45 am »
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
« Last Edit: January 27, 2022, 09:47:20 am by Nominal Animal »
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf