Search code examples
c++alsa

Set snd_pcm_sw_params_set_stop_threshold to boundary, still getting underrun on snd_pcm_writei


The question says it all. I am going in circles here. I set snd_pcm_sw_params_set_stop_threshold to boundary (and zero too just for fun) and I am still getting buffer underrun errors on snd_pcm_writei. I cannot understand why. The documentation is pretty clear on this:

If the stop threshold is equal to boundary (also software parameter - sw_param) then automatic stop will be disabled

Here is a minimally reproducible example:

#include <alsa/asoundlib.h>
#include <iostream>

#define AUDIO_DEV "default"

#define AC_FRAME_SIZE 960
#define AC_SAMPLE_RATE 48000
#define AC_CHANNELS 2

//BUILD g++ -o main main.cpp -lasound

using namespace std;

int main() {
    int err;
    unsigned int i;
    snd_pcm_t *handle;
    snd_pcm_sframes_t frames;
    snd_pcm_uframes_t boundary;
    snd_pcm_sw_params_t *sw;
    snd_pcm_hw_params_t *params;
    unsigned int s_rate;
    unsigned int buffer_time;
    snd_pcm_uframes_t f_size;
    unsigned char buffer[AC_FRAME_SIZE * 2];
    int rc;

    for (i = 0; i < sizeof(buffer); i++)
        buffer[i] = random() & 0xff;

    if ((err = snd_pcm_open(&handle, AUDIO_DEV, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
        cout << "open error " << snd_strerror(err) << endl;
        return 0;
    }

    s_rate = AC_SAMPLE_RATE;
    f_size = AC_FRAME_SIZE;
    buffer_time = 2500;

    cout << s_rate << " " << f_size << endl;

    snd_pcm_hw_params_alloca(&params);
    snd_pcm_hw_params_any(handle, params);
    snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
    snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
    snd_pcm_hw_params_set_channels(handle, params, AC_CHANNELS);
    snd_pcm_hw_params_set_rate_near(handle, params, &s_rate, 0);
    snd_pcm_hw_params_set_period_size_near(handle, params, &f_size, 0);

    cout << s_rate << " " << f_size << endl;

    rc = snd_pcm_hw_params(handle, params);
    if (rc < 0) {
        cout << "open error " << snd_strerror(err) << endl;
        return 0;
    }

    snd_pcm_sw_params_alloca(&sw);
    snd_pcm_sw_params_current(handle, sw);
    snd_pcm_sw_params_get_boundary(sw, &boundary);
    snd_pcm_sw_params_set_stop_threshold(handle, sw, boundary);

    rc = snd_pcm_sw_params(handle, sw);
    if (rc < 0) {
        cout << "open error " << snd_strerror(err) << endl;
        return 0;
    }

    snd_pcm_sw_params_current(handle, sw);

    snd_pcm_sw_params_get_stop_threshold(sw, &boundary);
    cout << "VALUE " << boundary << endl;

    for (i = 0; i < 1600; i++) {
        usleep(100 * 1000);
        frames = snd_pcm_writei(handle, buffer, f_size);
        if (frames < 0)
            frames = snd_pcm_recover(handle, frames, 0);
        if (frames < 0) {
            cout << "open error " << snd_strerror(frames) << endl;
            break;
        }
    }

    return 0;
}

Solution

  • Okay I figured it out. To anyone who runs into this issue who also has pipewire or pulse (or any other thirdparty non-alsa audio card) enabled as the "default" card the solution is to not use pipewire or pulse directly. It seems that snd_pcm_sw_params_set_stop_threshold is not implemented properly in pipewire/pulseaudio. You'll notice that if you disable pipewire or pulse this code will run exactly the way you want it to run.

    Here is how you can disable pulseaudio (which was the issue on my system):

    systemctl --user stop pulseaudio.socket
    systemctl --user stop pulseaudio.service
    

    Although a much better solution is to just set the AUDIO_DEV to write directly to an alsa card. You can find the names of these cards by running aplay -L. But in 95% of cases updating AUDIO_DEV in my sample code to the following:

    #define AUDIO_DEV "hw:0,0"
    

    Will usually fix the issue.