MusicPlayerDaemon / MPD

Music Player Daemon
https://www.musicpd.org/
GNU General Public License v2.0
2.18k stars 352 forks source link

ALSA config causes "Broken pipe" playback errors over time #1874

Closed peesock closed 6 months ago

peesock commented 1 year ago

Bug report

Describe the bug

Maybe this is a skill issue, but my alsa pcm device for mpd has a special config that mixes audio between mpd playback and a loopback device, letting me record my mpv audio if needed.

Playing a long playlist will eventually cause an alsa underrun followed by the snd_pcm_writei() failed: Broken pipe error and playback goes quiet until i pause and play the music again. Setting higher alsa buffer sizes delays the error, but it still happens.

The "multi" alsa plugin that makes this happen seems to be the cause, as using any pcm device without multi in its chain works perfectly fine, even with lower buffer sizes (2048 vs 8192).

Another thing to note is i also get alsa underruns on other programs using the same multi plugin setup, and no underruns without it. That's what makes me think i'm configuring alsa incorrectly, but mpd's reaction to it isn't good.

Expected Behavior

Play music forever and ever

Actual Behavior

Play music and occasionally go mute for a while or until pause+play

Version

Music Player Daemon 0.23.13 (0.23.13)
Copyright 2003-2007 Warren Dukes <warren.dukes@gmail.com>
Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>
This is free software; see the source for copying conditions.  There is NO
warranty; not even MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Database plugins:
 simple proxy upnp

Storage plugins:
 local udisks nfs curl

Neighbor plugins:
 upnp udisks

Decoders plugins:
 [mad] mp3 mp2
 [mpg123] mp3
 [vorbis] ogg oga
 [oggflac] ogg oga
 [flac] flac
 [opus] opus ogg oga
 [sndfile] wav aiff aif au snd paf iff svx sf voc w64 pvf xi htk caf sd2
 [audiofile] wav au aiff aif
 [dsdiff] dff
 [dsf] dsf
 [hybrid_dsd] m4a
 [faad] aac
 [mpcdec] mpc
 [wavpack] wv
 [openmpt] mptm mod s3m xm it 669 amf ams c67 dbm digi dmf dsm dtm far imf ice j2b m15 mdl med mms mt2 mtm nst okt plm psm pt36 ptm sfx sfx2 st26 stk stm stp ult wow gdm mo3 oxm umx xpk ppm mmcmp
 [modplug] 669 amf ams dbm dfm dsm far it med mdl mod mtm mt2 okt s3m stm ult umx xm
 [mikmod] amf dsm far gdm imf it med mod mtm s3m stm stx ult uni xm
 [sidplay] sid mus str prg P00
 [wildmidi] mid
 [fluidsynth] mid
 [gme] ay gbs gym hes kss nsf nsfe rsn sap spc vgm vgz
 [ffmpeg] 16sv 3g2 3gp 4xm 8svx aa3 aac ac3 adx afc aif aifc aiff al alaw amr anim apc ape asf atrac au aud avi avm2 avs bap bfi c93 cak cin cmv cpk daud dct divx dts dv dvd dxa eac3 film flac flc fli fll flx flv g726 gsm gxf iss m1v m2v m2t m2ts m4a m4b m4v mad mj2 mjpeg mjpg mka mkv mlp mm mmf mov mp+ mp1 mp2 mp3 mp4 mpc mpeg mpg mpga mpp mpu mve mvi mxf nc nsv nut nuv oga ogm ogv ogx oma ogg omg opus psp pva qcp qt r3d ra ram rl2 rm rmvb roq rpl rvc shn smk snd sol son spx str swf tak tgi tgq tgv thp ts tsp tta xa xvid uv uv2 vb vid vob voc vp6 vmd wav webm wma wmv wsaud wsvga wv wve rtp:// rtsp:// rtsps://
 [pcm]

Filters:
 libsamplerate soxr

Tag plugins:
 id3tag

Output plugins:
 shout null fifo pipe alsa ao oss openal solaris pipewire pulse jack httpd snapcast recorder

Encoder plugins:
 null vorbis opus lame twolame wave flac

Archive plugins:
 [bz2] bz2
 [zzip] zip
 [iso] iso

Input plugins:
 file io_uring archive alsa qobuz curl ffmpeg nfs mms cdio_paranoia

Playlist plugins:
 extm3u m3u pls xspf asx rss soundcloud flac cue embcue

Protocols:
 file:// alsa:// cdda:// ftp:// ftps:// gopher:// hls+http:// hls+https:// http:// https:// mms:// mmsh:// mmst:// mmsu:// nfs:// qobuz:// rtmp:// rtmpe:// rtmps:// rtmpt:// rtmpte:// rtmpts:// rtp:// rtsp:// rtsps:// scp:// sftp:// smb:// srtp://

Other features:
 avahi dbus udisks epoll icu inotify ipv6 tcp un

Configuration

music_directory     "~/music"
playlist_directory      "~/.local/share/mpd/playlists"

log_file            "~/.local/share/mpd/log"

pid_file            "~/.local/share/mpd/pid"

state_file          "~/.local/share/mpd/state"
sticker_file            "~/.local/share/mpd/sticker.sql"

user                "user"
bind_to_address     "127.0.0.1"
bind_to_address     "~/.local/share/mpd/socket"
port                "6600"
log_level           "notice"
auto_update "yes"

follow_outside_symlinks "yes"
follow_inside_symlinks      "yes"

database {
       plugin "simple"
       path "~/.local/share/mpd/db"
       cache_directory "~/.local/share/mpd/cache"
}

input {
        plugin "curl"
}

audio_output {
    type        "alsa"
    name        "desktop (alsa)"
    device      "mpd"   # optional
    mixer_type      "hardware"  # optional
    mixer_device    "hw:Loopback"   # optional
    mixer_control   "MPD"       # optional
    format  "48000:16:2"
    # mixer_index   "0"     # optional
}

audio_output {
    type    "fifo"
    name    "fifo file"
    path    "/tmp/mpd.fifo"
    format  "48000:16:2"
}

ALSA config

pcm.mpd {
    type asym
        playback.pcm "mpd2chan"
        capture.pcm "mpdsnooper"
# playback.pcm "plug:pcm.mpdrouter"
# capture.pcm "plug:pcm.mpdsnooper"
        hint {
            show on
                description "for playing from and recording MPD."
        }
}
# remove other 2 channels; they are redundant
pcm.mpd2chan {
    type plug
        slave.pcm {
            type route
                slave.pcm "mpdrouter"
                slave.channels 4
                ttable.0.0 1
                ttable.1.1 1
        }
    slave.channels 2
}
# mix audio between both devices
pcm.mpdrouter {
    type route;
    slave.pcm "mpdquader";
    ttable.0.0 1;
    ttable.1.1 1;
    ttable.0.2 1;
    ttable.1.3 1;
}
pcm.mpdquader {
    type multi
        slaves.a.pcm "mpdmixer"
        slaves.a.channels 2
# slaves.b.pcm "mpdfifo"
        slaves.b.pcm "mpdsoftvol"
        slaves.b.channels 2
        bindings.0 { slave a; channel 0; }
    bindings.1 { slave a; channel 1; }
    bindings.2 { slave b; channel 0; }
    bindings.3 { slave b; channel 1; }
}

pcm.mpdsoftvol {
    type softvol
        slave {
            pcm speakermixer
        }
    control {
        name "MPD"
            card Loopback
    }
}

pcm.mpdmixer {
    type dmix 
        ipc_key 1113
        slave {
            pcm "hw:Loopback,0,1"
                rate 48000
                format S16_LE
                channels 2
                period_size 1024
                buffer_size 2048
# buffer_size 8192
        }
}
pcm.mpdsnooper {
    type dsnoop 
        ipc_key 2224
        slave {
            pcm "hw:Loopback,1,1"
                format S16_LE
                channels 2
                period_size 1024
                buffer_size 2048
# buffer_size 8192
        }
}

Log

alsa_output: opened mpd2chan type=PLUG
alsa_output: buffer: size=2048..2048 time=42666..42667
alsa_output: period: size=1024..1024 time=21333..21334
alsa_output: default period_time = buffer_time/4 = 42666/4 = 10666
alsa_output: format=S16_LE (Signed 16 bit Little Endian)
alsa_output: buffer_size=2048 period_size=1024
output: opened "desktop (alsa)" (alsa) audio_format=48000:16:2
libsamplerate: setting samplerate conversion ratio to 1.1
output: converting in=44100:16:2 -> f=44100:16:2 -> out=48000:16:2
output: opened "fifo file" (fifo) audio_format=48000:16:2
libsamplerate: setting samplerate conversion ratio to 1.1
output: converting in=44100:16:2 -> f=44100:16:2 -> out=48000:16:2
inotify: watching music directory
decoder_thread: probing plugin flac
decoder: audio_format=44100:16:2, seekable=true
alsa_output: Underrun on ALSA device "mpd"
output: Failed to play on "desktop (alsa)" (alsa): snd_pcm_writei() failed: Broken pipe
borine commented 11 months ago

The "multi" alsa plugin that makes this happen seems to be the cause

The multi plugin was originally designed to be used with just one soundcard. That card provides a single clock for all the devices of the multi, so they naturally all run at exactly the same rate. When you use devices from different sound cards within a multi plugin, then they each have their own clock and so inevitably there is some very small rate drift between them. This causes one device to run down its buffer more quickly than the other.

The ALSA device seen by MPD is the multi plugin itself, and this runs at the rate set by the clock of its "master" child device. If the other child is faster than the master then it will eventually underrun because MPD is supplying samples at the rate set by the master child. There is nothing that MPD can do to work around this, it is a (design) feature of the ALSA multi plugin.

One thing you could try is to use the "speakermixer" device as the master child (you do not give the definition of "pcm.speakermixer"). The easiest way to do this is to add the key "master" to you multi definition:

pcm.mpdquader {
    type multi
        master 1
        slaves.a.pcm "mpdmixer"
        slaves.a.channels 2
# slaves.b.pcm "mpdfifo"
        slaves.b.pcm "mpdsoftvol"
        slaves.b.channels 2
        bindings.0 { slave a; channel 0; }
    bindings.1 { slave a; channel 1; }
    bindings.2 { slave b; channel 0; }
    bindings.3 { slave b; channel 1; }
}

That will make "mpdsoftvol" the master. (by default the master child is the first one given in the multi definition). Change to master 0 to make "mpdmixer" the master again.

This is not guaranteed to work. The master child not only supplies the clock, but also takes precedence when setting the hardware parameters so you may find other effects as well as the relative timing of the two devices.

Note also that the ALSA "rate" plugin can have an even greater effect on relative timing than the hardware device clock. PCM frame transfers happen at time intervals determined by the period time; but a transfer can only move a whole number of frames. If a rate conversion results in a fractional number of frames for the period size, then the rate plugin will round the period size down to the nearest whole number. This makes the child prone to underruns. The solution is to always choose a period time that is a whole number of frames at both the parent device rate and the child device rate. For example, if converting from 48000 to 44100, then choose a period time that is a multiple of 10ms: 10ms is 480 frames at 48000 and 441 frames at 44100.

output: Failed to play on "desktop (alsa)" (alsa): snd_pcm_writei() failed: Broken pipe

ALSA PCM functions return error code EPIPE to indicate underrun or overrun. Within the function AlsaOutput::DrainInternal() MPD does not attempt to recover from ALSA errors, so if an underrun occurs when MPD is draining its internal buffer then MPD stops the player. @MaxKellermann perhaps MPD should use AlsaInputStream::Recover() here to prevent recoverable errors from stopping playback?

peesock commented 11 months ago

"speakermixer" is the dmix plugin to my speakers:

pcm.speakermixer {
    type dmix
        ipc_key 1112
        slave {
            pcm "hw:PCH,0"
            rate 48000
            format S16_LE
            channels 2
            period_size 1024
# buffer_size 2048
            buffer_size 4096
        }
}

Setting that to the master child seems to work perfectly from short tests on both my MPD alsa device and my normal alsa device, which makes sense in hindsight (setting a loopback as the master doesn't make sense). This might also fix my long-running audio/video sync issues when recording with ffmpeg.

Thanks for the help. Should this be closed, or is mpd's stopped playback issue still an issue?

peesock commented 11 months ago

Update: I played around a lot and noticed that sometimes when opening pcm.mpd (and also my normal alsa device, which has the same config), it explodes and gives the same broken pipe error. On clients that don't immediately give up on errors, it seems to cause worse and worse xruns over time. But if it doesn't error at first, it seems to play forever. Edit: it does not play forever. It also constantly fails with aplay, but my normal alsa device is fine with aplay, so there must be some difference...

Increasing the number of periods from 2 to 3 or 4 eliminates the error when opening the device, but the pipe will eventually break over time. Setting lower period sizes (256) with a period of 3 or 4 shows mpd clearly increasing cpu usage over time to 100% of a core, before dropping back down and repeating.

I also tried combining alsaloop from alsa-utils with the loopback device and dmix to avoid using the multi plugin completely, but always resulted in a hundred xruns and failure. Audio is very hard.

borine commented 11 months ago

Audio is very hard

Using low-level tools such as alsa-lib and alsaloop to achieve complex systems is hard. You need a deep understanding of what each ALSA plugin and utility does, and what their limitations are. This is not the proper place to get into such discussions.

Audio is much easier if you used audio tools that are designed to solve the 2 main problems that you are struggling with: synchronizing 2 soundcards and mixing audio streams from 2 or more applications into both those devices. Pipewire/Wireplumber and Pulseaudio do these things for free. You may have reached the point where your time is better spent learning to use use one of those rather than (the rather sparsely documented) ALSA.