ALSA snd_pcm_writei() behaves oddly if running in Non-Blocking Mode

Hi Toradex Community,

I’ve been following the article to play sounds via ALSA in C++: How to play audio on TorizonCore using Alsa and C/C++ | Toradex Developer Center

Using the code as is works well. The article points to the following code: torizon-samples/alsa-example.cpp at bullseye · toradex/torizon-samples · GitHub

However, in my case, I would also like to implement a Stop Audio feature, since my application will sometimes require a sound interruption. For example, while a background sound is playing, I may output an error popup + sound, so I’d want to interrupt the background sound as it’s playing.

Using the code as is does not allow to implement such interruption since snd_pcm_writei() is in blocking mode.

I tried to modify the code, primarily by setting the flag SND_PCM_NONBLOCK when opening my PCM device.

So in line 314 of the example, I replaced:

if ((err = snd_pcm_open(&PlaybackHandle, &SoundCardPortName[0], SND_PCM_STREAM_PLAYBACK, 0)) < 0)

with

if ((err = snd_pcm_open(&PlaybackHandle, &SoundCardPortName[0], SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0)

The code is now able to continue after calling snd_pcm_writei(). I plan on implementing the interruption by doing something like this right after since I’m separating this module in a different thread:

while(snd_pcm_state(PlaybackHandle) == SND_PCM_STATE_RUNNING)
{
	mutex.lock();
	if(abortFlag){
		qDebug() << "Dropping PCM" << Qt::endl;
		snd_pcm_drop(PlaybackHandle);
		break;
	}
	mutex.unlock();
}

However, in Non-Blocking mode, Line 256 of the code example frames = snd_pcm_writei(PlaybackHandle, WavePtr + count, WaveSize - count); returns a crazy value: 4294967285. It’s oddly close to the max unsigned int32 value (4294967295).

The sound starts playing for like half a second and stops when this value outputs, resulting in the PCM state to skip SND_PCM_STATE_RUNNING and jump to SND_PCM_STATE_XRUN right away.

I’d like to know if someone else has tried implementing a stop sound feature with this example code, or if someone can identify why the audio doesn’t work in Non-Blocking Mode only? Is this the right approach?

Thanks in advance,
Anthony

. When SND_PCM_NONBLOCK flag is set, the write function (snd_pcm_writei()) returns immediately, without waiting for the data to be actually written to the buffer.

In your case, the write function is returning a negative value (-11), which corresponds to the EAGAIN error code. This error code means that the device is not ready to accept more data at this time, and the application should try again later.

To handle this error code, you can modify your code to call snd_pcm_writei() again with the remaining data until all the data has been written to the buffer. You can use a loop to do this until all the data has been written or until an error other than EAGAIN occurs
For example:

size_t count = 0;
while (count < WaveSize) {
frames = snd_pcm_writei(PlaybackHandle, WavePtr + count, WaveSize - count);
if (frames == -EAGAIN) {
// Device is not ready to accept more data, try again later
continue;
}
if (frames < 0) {
// An error occurred, handle it accordingly
break;
}
count += frames;
}

Hi @alex.tx ,

Thank you for the reply.

Using your suggestion, my play_audio function now looks like this:

void SoundsWorker::play_audio()
{
	register snd_pcm_uframes_t		count, frames;
	// Output the wave data
	count = 0;

    // Write in buffer
	while(count < WaveSize)	{
		frames = snd_pcm_writei(PlaybackHandle, WavePtr + count, WaveSize - count);
		// If an error, try to recover from it
		if(frames == -EAGAIN){
			// Device is not ready to accept more data, try again later
			continue;
		}
		if (frames < 0){
			frames = snd_pcm_recover(PlaybackHandle, frames, 0);
			break;
		}
		// Update our pointer
		count += frames;	
	}

    // Scan for potential abort signal while audio is playing
	while(snd_pcm_state(PlaybackHandle) == SND_PCM_STATE_RUNNING){
          // TODO: Implement Interrupt signal to snd_pcm_drop() on receipt with mutexes
         if(abortSignal){
             snd_pcm_drop(PlaybackHandle);
             break;
         }
	}

	if(!abortSignal){
		qDebug() << "Draining PCM" << Qt::endl;
		snd_pcm_drain(PlaybackHandle);
	}
}

Handling EAGAIN now allows the sound to be played for longer, which is progress! However, it is now stuttering at every X frames (I’m guessing EAGAIN happens very often and creates some sort of underrun?). For example, if my audio file is 10 seconds long, the code will only play the first 5 seconds, but spreaded over 10 seconds with stutters in between (the stutters last the same amount of time).

Looks like that the EAGAIN errors are causing underruns and stutters in your audio playback. One way to reduce the occurrence of EAGAIN errors is to increase the buffer size or decrease the number of frames written at once. This will give the system more time to process the audio data before the buffer runs empty and needs to be refilled.

To increase the buffer size , you can use the snd_pcm_sw_params_set_avail_min function to set the minimum available frames in the buffer for playback.

You could also try using a non-blocking write mode with snd_pcm_writei_nonblock instead of snd_pcm_writei . This function returns immediately and does not wait for the data to be written to the device, which can help reduce the occurrence of EAGAIN errors. You would need to check the return value of snd_pcm_writei_nonblock to see how many frames were actually written and handle errors appropriately.

Hi @alex.tx

To increase the buffer size , you can use the snd_pcm_sw_params_set_avail_min function to set the minimum available frames in the buffer for playback.

So for this attempt, I’ve changed my code to look like the following:

void SoundsWorker::play_audio()
{
	register snd_pcm_uframes_t		count, frames;

	// Output the wave data
	count = 0;
	snd_pcm_sw_params_t *sw_params;
	int err;

    // Set Buffer Size
	snd_pcm_uframes_t bufferSize = WaveSize;
	
	if ((err = snd_pcm_sw_params_malloc(&sw_params)) < 0) {
		qDebug() << "Cannot allocate Software Parameters structure" << Qt::endl;
	}
	
	if ((err = snd_pcm_sw_params_set_avail_min(PlaybackHandle, sw_params, bufferSize)) < 0) {
		qDebug() << "Error setting buffer size" << Qt::endl;
	}

    // While loop to write
	while(count < WaveSize)
	{
		frames = snd_pcm_writei(PlaybackHandle, WavePtr + count, WaveSize - count);

		// If an error, try to recover from it
		if(frames == -EAGAIN){
			// Device is not ready to accept more data, try again later
			continue;
		}
		// If underrun, try to recover from it
		else if(frames == -EPIPE){
			frames = snd_pcm_recover(PlaybackHandle, frames, 0);
			continue;
		}

		// Update our pointer
		count += frames;
	}

	while(snd_pcm_state(PlaybackHandle) == SND_PCM_STATE_RUNNING)
	{
       // TODO
	}

	if(!aborted){
		qDebug() << "Draining PCM" << Qt::endl;
		snd_pcm_drain(PlaybackHandle);
	}
}

image

The sound effect of the underrun does not seem to change upon modifying the bufferSize. I’ve tried 512, and tested all the way up until even WaveSize value (300K+). Difference now is that I hear the whole 10 seconds, but with stutters. Am I changing the buffer size correctly?

You could also try using a non-blocking write mode with snd_pcm_writei_nonblock instead of snd_pcm_writei

Do you have a link to this function’s documentation? It doesn’t seem to exist and I can’t find it in the ALSA API documentation.

https://www.alsa-project.org/alsa-doc/alsa-lib/index.html