Since last summer, when I changed Klipspringer to access ALSA directly, I’ve had problems with occasional sudden stops in playback and capture, caused by broken pipe errors from the ALSA interface. This post contains the information I would have needed – but failed to find when searching the internet – to solve the problem when it first appeared.
The key piece of information is that broken pipe, errno 32, EPIPE, from ALSA doesn’t mean anything close to what it means in socket communication, but instead signals that there has been a buffer underrun or overrun.
The ring buffer is where the sound hardware picks up samples to send to the output during playback. If the ring buffer is empty when the next sample is supposed to be played, the stream goes into underrun state. Likewise during capture, the hardware places samples in the ring buffer, expecting it to be picked up by recording software, and if the ring buffer is full it goes into overrun state.
Underrun due to small playback batch
It’s rather obvious that if the ring buffer is very small, the risk is large that it’s exhausted by hardware playback before the the player has had the opportunity to write the next batch of samples. But what’s less obvious and didn’t occur to me when I first encountered underruns, is that even if the buffer is large enough, it might get exhausted if the first batch of audio data happens to be very small. Playback starts as soon as the first few samples are written to the stream (unless you set the ALSA start threshold software parameter to higher than the default), and if the stream has only received a few samples, an underrun is rather likely to happen almost immediately.
The ALSA documentation clearly states that snd_pcm_writei and snd_pcm_writen, return -EPIPE if an underrun has occurred, but does not mention it in the specification of snd_pcm_avail, which was where it first happened in Klipspringer. Therefore, I didn’t understand what it meant, and it didn’t occur to me that if a very small batch of samples was written to a new stream in connection with a seek, pause, or something to do with crossfading, it might cause an underrun. I couldn’t figure out what was behind the error code, and thought that there may be something wrong with how snd_pcm_avail interacted with my hardware, something I couldn’t do much about.
My first workaround was therefore to stop using snd_pcm_avail, which was was no big problem. The main loss was that the time display in the user interface became less exact, because it could now only know how many samples had been sent to the audio stream, not how many had actually been played. With a ring buffer large enough to hold several seconds of audio, this meant that the time display, as well as the position skipped to by the rewind and fast forward buttons, could be several seconds off. To make the time error less annoying, I configured the ring buffer to be only 0.2 seconds by default, which meant that the discrepancy was hardly noticeable. But in doing so, I had set myself up for the next problem…
Underrun due to small buffer and high sample precision
Although 0.2 seconds is a rather long time for a computer, it was apparently not a large enough ring buffer on my stereo NUC. Occasionally, especially when playing music with a large bit depth and/or sample frequency, there would be an underrun, and snd_pcm_writei would return -EPIPE. When I noted the error code that I had already failed work out before, I again assumed that it was beyond my power to do anything about it, so rather than getting rid of the underrun, I mimicked what the Java sound system does when it gets a -EPIPE from ALSA, which is to simply call snd_pcm_prepare to reset the stream. You can see how in in Alsa.c: the implementation of recover_xrun and the use of it in METHOD(write).
Instead of playback suddenly stopping, it now suffered a slight stutter from each underrun, which was a bit annoying, but rather rare, and I assumed I would have to live with it.
Overrun due to small buffer and high sample precision
Less tolerable was that audio capture on my Hifiberry ADC also occasionally came to a sudden stop.
I use the ADC mostly for digitalizing music from vinyl records, and when capture suddenly stopped I would have to restart the recording process. Without any real reason, I had set the default ring buffer size down to 0.2 seconds for capture as well, and also, to make sure that throughput didn’t suffer from too frequent context switching, I had the input read loop wait until the ring buffer was at least half full before reading. With a ring buffer size of 0.2 seconds, this meant that the margin was only 0.1 seconds, which again apparently wasn’t always enough, but would result in an occasional overrun.
I didn’t consider using the same reset mechanism as for playback in this case, because then I would get a stutter in the recording, repeated in the same place every time I listened to it. Hence, when I found that this was also caused by -EPIPE, I resorted to digging down into the ALSA source code, and rather quickly realized what the problem was.
Problems solved
Solving all the problems was then embarrassingly simple: increase the ring buffer size to several seconds, bring back the use of snd_pcm_avail to get the user interface precision back, change the capture read loop to read from the stream as soon as a tenth of the buffer was filled, and keep the catch-and-reset logic for underrun in order to keep playback operating on the rare occasions that it happens in connection with a skip/pause/play (the use of recover_xrun in METHOD(write) and METHOD(availableFrames) in Alsa.c), in which case it doesn’t really matter.
Klipspringer revisions during the last six months include several workarounds, but only with version 4.0.10 did I fully understand the problems and got them properly eliminated.