PeppyMeter

I have done the "old school" debugging suggested by Serge. By commenting out the function bodies I found the line which caused the issue:
peppyalsa/peppyalsa.c at ddd631ca6f9ba3fc4c5df0f1912ac737b7eddadb * project-owner/peppyalsa * GitHub

That block of code exists in all peppyalsa predecessors: ameter and pivumeter.
The next conditional block if (err < 0) was never executed during my testing.

The execution hits that line in both cases - with network caching enabled and disabled. So probably the size of the 'pcm' makes the difference.
 
Last edited:
The good news is a huge step forward.
Thus it’s an alsa bug or (much more probable) incorrect (historical) alsa usage
Look
s16 = snd_pcm_meter_search_scope(pcm, "s16"); is searching for the scope
and if the search fails (“if (!s16)”, no available scope yet) the plugin tries to (probably forcibly) open the same scope as
“err = snd_pcm_scope_s16_open(pcm, "s16", &s16);”.
The latter loops while the scope will be available and while the caching is not finished it continues looping.
I’m definitely not an alsa guru, the above is just a common sense.


- What other (not meter) plugins do at that point. Are there any sources?
- Could you try instead of
s16 = snd_pcm_meter_search_scope(pcm, "s16");
something like that
int count=0;
do
{
s16 = snd_pcm_meter_search_scope(pcm, "s16");
sleep(1000);
if (count++> MAX_COUNT) break;
} while (!s16);




Edit
Actually a bit unclear logic is implemented in snd_pcm_scope_peppyalsa_open
It starts from searching for the scope (snd_pcm_meter_search_scope).
If it fails it tries to open (snd_pcm_scope_s16_open)
and then anyway adds the scope (snd_pcm_meter_add_scope )
Looks like in “normal life” snd_pcm_scope_s16_open would never be called.
I might miss something important obviously.
Are there any alsa-lib tutorials, not just the functions descriptions?
 
Last edited:
rpi - when commenting out the creation of the virtual scope s16, does your code still work correctly? IMO the code at peppyalsa/peppyalsa.c at ddd631ca6f9ba3fc4c5df0f1912ac737b7eddadb * project-owner/peppyalsa * GitHub would access to struct fields at NULL if the s16 scope were left NULL.

SergeG: that code makes sure the first scope in the list is always the virtual scope s16 which takes care of converting all possible input samples to common format s16 - look at alsa-lib/pcm_meter.c at 3ec6dce5198f100fa8dd2abfc1258fa4138ceb1a * alsa-project/alsa-lib * GitHub . That scope is used by the real scopes.
 
Last edited:
This wiki page explains the whole story about meter, pivumeter and peppyalsa:
History * project-owner/peppyalsa.doc Wiki * GitHub
It explains that I’m not the author of the major part of the plugin. I just replaced the output to I2C by the output to the ‘named pipe’. Therefore I cannot answer most of your questions as I’m probably even less knowledgeable than you about ALSA internals.

Here is the same code from the pivumeter which Pimoroni is using in its commercial products:
pivumeter/pivumeter.c at 5e4afea8813beeec2f81d877f3ddc9a84eaa177e * pimoroni/pivumeter * GitHub
It was in turn borrowed as is from the ameter.

It’s problematic to find a good tutorial even about ALSA usage. I’m not sure that any info exists about its code/design. Only source files are there with doc comments.

Serge, I think with do-while loop you are suggesting the same logic which snd_pcm_scope_s16_open probably does internally.
Maybe while caching the players use some format/scope other than s16?

phofman, which lines of the code do you suggest to comment out?
 
So asla is a black-box with full (I hope) source and without audible manual.
Not too good, but not for the first time ;-)


The first idea is asking the alsa support (if any)


While waiting for answer the second idea is
grabbing the snd_pcm_scope_s16_open function body from the original code,
inserting the body into peppyalsa (renaming the function to avoid clashing)
and continue commenting in the cloned alsa code to find the CPU spiking point inside alsa.
Maybe it could give an idea where exactly and what for alsa loops aggressively.


>the same logic which snd_pcm_scope_s16_open probably does internally.


It’s the subject to verify I think if a sleep were used there would be no such CPU spikes.


>Maybe while caching the players use some format/scope other than s16?


I suspect a different behavior.
While caching is happening, the plugin is already started and needs to process something.
But there is nothing to process yet, so alsa waits aggressively.
The solutions is probably in inserting a sleep() to make the lopping less CPU consumptive.
I don’t claim the sleep should be inserted into the alsa code. Rebuilding alsa is out of the scope.
Sleep could be inserted into alsaplugin to loop “gently” (with less CPU consumption) I believe the above makes sense as you do see the CPU goes to normal after the caching is completed.


Meanwhile I will try to review the alsa code
 
Reviewing the alsa code…
Are you sure snd_pcm_scope_s16_open call causes the spikes?


int snd_pcm_scope_s16_open(snd_pcm_t *pcm, const char *name,
snd_pcm_scope_t **scopep)
{
snd_pcm_meter_t *meter;
snd_pcm_scope_t *scope;
snd_pcm_scope_s16_t *s16;
assert(pcm->type == SND_PCM_TYPE_METER);
meter = pcm->private_data;
scope = calloc(1, sizeof(*scope));
if (!scope)
return -ENOMEM;
s16 = calloc(1, sizeof(*s16));
if (!s16) {
free(scope);
return -ENOMEM;
}
if (name)
scope->name = strdup(name);
s16->pcm = pcm;
scope->ops = &s16_ops;
scope->private_data = s16;
list_add_tail(&scope->list, &meter->scopes);
*scopep = scope;
return 0;
}


Just list_add_tail is called.


Could it be the spike happens somewhere near? snd_pcm_meter_search_scope is a better candidate,
it has a loop at least
 
This code caused CPU spikes:
Code:
int snd_pcm_scope_peppyalsa_open(snd_pcm_t * pcm,
        const char *name,
        unsigned int decay_ms,
        snd_pcm_scope_t ** scopep)
{

    snd_pcm_scope_t *scope, *s16;
    snd_pcm_scope_peppyalsa_t *level;
    int err = snd_pcm_scope_malloc(&scope);
    if (err < 0){
        return err;
    }
    level = calloc(1, sizeof(*level));
    if (!level) {
        if (scope) free(scope);
        return -ENOMEM;
    }
    level->pcm = pcm;
    level->decay_ms = decay_ms;
    s16 = snd_pcm_meter_search_scope(pcm, "s16");

    if (!s16) {
        err = snd_pcm_scope_s16_open(pcm, "s16", &s16);
        if (err < 0) {
            if (scope) free(scope);
            if (level)free(level);
            return err;
        }
    }
/*
    level->s16 = s16;
    snd_pcm_scope_set_ops(scope, &level_ops);
    snd_pcm_scope_set_callback_private(scope, level);
    if (name){
        snd_pcm_scope_set_name(scope, strdup(name));
    }
    snd_pcm_meter_add_scope(pcm, scope);
    *scopep = scope;
*/
    return 0;
}
and this didn't cause spikes:
Code:
int snd_pcm_scope_peppyalsa_open(snd_pcm_t * pcm,
        const char *name,
        unsigned int decay_ms,
        snd_pcm_scope_t ** scopep)
{

    snd_pcm_scope_t *scope, *s16;
    snd_pcm_scope_peppyalsa_t *level;
    int err = snd_pcm_scope_malloc(&scope);
    if (err < 0){
        return err;
    }
    level = calloc(1, sizeof(*level));
    if (!level) {
        if (scope) free(scope);
        return -ENOMEM;
    }
    level->pcm = pcm;
    level->decay_ms = decay_ms;
    s16 = snd_pcm_meter_search_scope(pcm, "s16");
/*
    if (!s16) {
        err = snd_pcm_scope_s16_open(pcm, "s16", &s16);
        if (err < 0) {
            if (scope) free(scope);
            if (level)free(level);
            return err;
        }
    }

    level->s16 = s16;
    snd_pcm_scope_set_ops(scope, &level_ops);
    snd_pcm_scope_set_callback_private(scope, level);
    if (name){
        snd_pcm_scope_set_name(scope, strdup(name));
    }
    snd_pcm_meter_add_scope(pcm, scope);
    *scopep = scope;
*/
    return 0;
}
When I've tried to embed that function snd_pcm_scope_s16_open into the code the usual story happened - compiler started complaining that it cannot find definition of the snd_pcm_meter_t and snd_pcm_scope_s16_t. I'm not sure where is the header file with those declarations.


While searching for the header files found this info:
Meter causes freeze when closing audio device - Patchwork
 
Last edited:
How specifically did you identify that particular code causes the peak? In the YES CPU case - the s16 scope is the only scope added. The NO CPU case - no scope is added. Could it perhaps be the processing within s16 running (e.g. the xxx_update method) is causing the peak? No processing is running in NO case as the s16 is not used at all.

My 2 cents - the caching causes calling snd_pcm_pause/snd_pcm_resume which may not be properly handled by the alsa meter/scope/scope_s16 layer.
 
snd_pcm_meter_t and snd_pcm_scope_s16_t are defined in src/pcm/pcm_meter.c of alsa


>While searching for the header files found this info:


Not sure it’s relevant




Very convincing the code samples, so it’s in list_add_tail of snd_pcm_scope_s16_open.
I just have no other suspects /
The list related operations should have a lock mechanism for multi threading.
The lock can be implemented by polling (looping)
I’m trying to find list_add_tail implementation.




BTW. Didn’t you think to try the same on a different hardware/linux installation?
Just to exclude a “surprise”
 
>Do you use Raspberry OS


Yes. And I will try once more

What I could miss beyond mpd.conf, asoundrc libpeppyalsa.so and the playlist?

If you are on PI4 as well you may want to send me the SD image (better <=8GB)

I could try your image


Edit:
Shouldn't we compare mpd/alsa/etc versions?
 
Last edited:
I use Pi 3B+, though I don't think that makes any difference. You can try to use the headless image:
Disk Images * project-owner/PeppyPlayers.doc Wiki * GitHub
I use it right now for testing. To login through SSH use pi/Turner as name/pass. Then kill the python3 process which starts by default and disable it in the /etc/rc.local. After that it will not start after the booting process.

Using SSH start the player from the folder /home/pi/Peppy (assuming you stopped auto-started process):
python3 peppy.py

To see the Web UI use this URL: http://IP_OF_YOUR_PI:8080

Start 'top' in the separate SSH session and watch 'python3' process. By default the player is using Python binding for VLC. Therefore you won't see vlc as a separate process. python3 process includes both: player and vlc.

There are several .asoundrc* files in the /home/pi folder. Use the one for the peppyalsa plugin.

In the Web UI go to the Radio, click on stations and check SSH terminal with 'top'.

You can switch to 'mpd' just changing the first line in the /home/pi/Peppy/players.txt from 'vlc' to 'mpd'. The player should be restarted after that.

I don't think I made any specific settings for ALSA or players. All of them are available in the image.

Thank you!
 
I will try the image as soon as possible


A weak hypothesis.
Alsa has it’s own meter “master” plugin, looks like that at least.
The plugin initializes list that is used in snd_pcm_scope_s16_open/list_add_tail.
If peppyalsa starts before the master plugin (anyhow) the list is not initialized yet, so adding an item to the uninitialized (yet) list may cause “miracles”.
How to very the plugins load sequence?
 
I'm not sure about the plugin load sequence. That's probably defined by the .asoundrc content (?)

I've tried to use that function in my code but there are too many dependencies. They are like snowball - you add one then there is another one... I'm afraid I need to setup the whole ALSA build environment.
 
Weird, try to use that auto-started python3 process for testing. Starting the player manually could simplify debugging - you could stop the player, modify something and than start it from the command line.


I've never faced this kind of issue. The player process should not affect any open SSH connections.