0

I am trying to write random noise to to a device and allow my loop to sleep when I have written enough data. My understanding is that for each call to snd_pcm_writei I am writing 162 bytes (81 frames) which at 8khz rate and 16bit format it should be enough audio for ~10ms. I have verified that alsa does tell me I have written 81 frames.

I would expect that I can then sleep for a short amount of time before waking up and pushing the next 10 ms worth of data. However when I sleep for any amount - even a single ms - I start to get buffer underrun errors.

Obviously I have made an incorrect assumption somewhere. Can anyone point me to what I may be missing? I have removed most error checking to shorten the code - but there are no errors initializing the alsa system on my end. I would like to be able to push 10ms of audio and sleep (even for 1 ms) before pushing the next 10ms.

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

int main(int argc, char **argv) {
    snd_pcm_t* handle;
    snd_pcm_hw_params_t* hw;

    unsigned int rate = 8000;
    unsigned long periodSize = rate / 100;  //period every 10 ms

    int err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
    snd_pcm_hw_params_malloc(&hw);
    snd_pcm_hw_params_any(handle, hw);
    snd_pcm_hw_params_set_access(handle, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
    snd_pcm_hw_params_set_format(handle, hw, SND_PCM_FORMAT_S16_LE);
    snd_pcm_hw_params_set_rate(handle, hw, rate, 0);
    snd_pcm_hw_params_set_channels(handle, hw, 1);

    int dir = 1;
    snd_pcm_hw_params_set_period_size_near(handle, hw, &periodSize, &dir);
    snd_pcm_hw_params(handle, hw);

    snd_pcm_uframes_t frames;
    snd_pcm_hw_params_get_period_size(hw, &frames, &dir);
    int size = frames * 2; // two bytes a sample
    char* buffer = (char*)malloc(size);
    unsigned int periodTime;
    snd_pcm_hw_params_get_period_time(hw,&periodTime, &dir);
    snd_pcm_hw_params_free(hw);
    snd_pcm_prepare(handle);

    char* randomNoise = new char[size];
    for(int i = 0; i < size; i++)
        randomNoise[i] = random() % 0xFF;

    while(true) {
        err = snd_pcm_writei(handle, randomNoise, size/2);
        if(err > 0) {
            spdlog::info("Write {} frames", err);
        } else {
            spdlog::error("Error write {}\n", snd_strerror(err));
            snd_pcm_recover(handle, err, 0);
            continue;
        }
        usleep(1000); // <---- This is what causes the buffer underrun
    }
}
Lithium
  • 5,102
  • 2
  • 22
  • 34
  • I don't know C at all, but it would appear that maybe you expected the snd_pcm_writei() function to be non-blocking when in fact it is is blocking... for some reason... documentation: "If the blocking behaviour is selected and it is running, then routine waits until all requested frames are played or put to the playback ring buffer. The returned number of frames can be less only if a signal or underrun occurred. If the non-blocking behaviour is selected, then routine doesn't wait at all." – Nerdy Bunz Mar 19 '22 at 08:49

2 Answers2

0

Try to put in /etc/pulse/daemon.conf :

default-fragments = 5
default-fragment-size-msec = 2

and restart linux.

Philippe
  • 20,025
  • 2
  • 23
  • 32
0

What I don't understand is why you write a buffer of size "size" to the device, and in the approximate calculations of time you rely on the "periodSize" declared by you. Then write a buffer with the size "periodSize" to the device.