Speaker pHAT 'ALSA write failed (unrecoverable): File descriptor in bad state' error when running pygame

Have recently hooked up the speaker pHAT and enjoying it a lot! However, I’m having a problem with the hat ‘disconnecting’ after some time when using the pygame audio mixer. I have a python3 script that loops every 0.1 seconds and I want it running constantly. However, I frequently get underrun errors: ‘ALSA lib pcm.c:7843:(snd_pcm_recover) underrun occurred’ (1-4 pop up every minute) which disrupt the audio slightly. And eventually this one pops up: ‘ALSA write failed (unrecoverable): File descriptor in bad state’ which makes the hat stop working with the python script, requiring me to restart the script. CPU doesn’t usually exceed 10% when running the script and I’ve tried increasing the buffersize for both the pygame mixer and in asound.conf.

Has anyone else encountered this/have ideas for a fix? Also happy to hear suggestions for an alternative audio library that will play mp3 (ideally stream internet radio also), pause, change volume, seek. Spent a long time trying to set Pyglet up last night only to discover that it will only play mp3 with AVBin, which seems to be unavailable for the Pi.

My goto is VLC, which @RogueM and I put together a set of scripts and installers for: https://github.com/pimoroni/phat-beat/tree/master/projects/vlc-radio

Granted, it’s aimed at pHAT BEAT, but it should be easy enough to tweak it, or just not use the phatbeatd part at all.

What’s your Python script doing? Could you paste it here? Use three backticks for formatting, like so:

```
your code here
```

Thanks gadgetoid, I’ll have a look at your VLC package. The script is basically meant to be a radio alarm with some additional functions. Apologies it’s quite long, probably the only significant parts are the play() function and the main loop, but wanted to include everything just in case.

#!/usr/bin/env python3

from pygame import mixer
import RPi.GPIO as GPIO
import os
from gtts import gTTS
from datetime import datetime
import feedparser
import time
import random
import praw
import sn3218

dir_path = os.path.dirname(os.path.realpath(__file__))

#initialise audio#
mixer.pre_init(22050, -16, 2, 4096) #frequency, size, channels, buffersize
mixer.init()

#setup buttons#
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(26, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(16, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(6, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)

#setup clock#
currenttime = -1

#setup alarms#
alarm1 = 1000
alarm2  = 2300
musicalarm = True
newsalarm = True
radioalarm = True

#radio settings
bbc2 = "http://www.listenlive.eu/bbcradio2.m3u &"
bbc4 = "http://www.listenlive.eu/bbcradio4.m3u &"
bbc4extra = "http://www.listenlive.eu/bbcradio4extra.m3u &"
bbc3 = "http://www.listenlive.eu/bbcradio3.m3u &"
classicrock = "http://network.absoluteradio.co.uk/core/audio/mp3/live.pls?service=acbb &"
jazzfm = "http://www.listenlive.eu/jazzfmuk.m3u &"


stations = [bbc2, bbc3, bbc4, bbc4extra, classicrock, jazzfm]
stationslen = len(stations)
currentstationnum = 2
radioplaying = False

#authorise Reddit#
submissionsgot = []
reddit = praw.Reddit(client_id = "XXXXXXX", client_secret = "XXXXXXX", password = "XXXXXX", username = "XXXXXX", user_agent = "XXXXXXX")
subreddit = reddit.subreddit('jokes')

#menu variable: 1 = main, 2 = settings, 3 = alarm mode, 4 = set radio,
#5 = radio menu, 6 = play menu, 7 = alarm settings, 8 = story play, 9 = music play, 10 = set volume#
menu = 1

#get current day#
if datetime.today().weekday() == 0:
    today = "Monday"      
if datetime.today().weekday() == 1:
    today = "Tuesday"
if datetime.today().weekday() == 2:
    today = "Wednesday"
if datetime.today().weekday() == 3:
    today = "Thursday"
if datetime.today().weekday() == 4:
    today = "Friday"
if datetime.today().weekday() == 5:
    today = "Saturday"
if datetime.today().weekday() == 6:
    today = "Sunday"


def play(filename):

    mixer.init()
    mixer.music.load(dir_path + "/" + filename)
    mixer.music.play()
    

def speak(text, filename):

    tts = gTTS(text=text, lang='en')
    tts.save(dir_path + "/" + str(filename) + ".mp3")
    play(str(filename) + ".mp3")

def joke():

    play("sounds/dial.mp3")       
    
    global submissionsgot
    
    subreddit = reddit.subreddit('jokes')
    
    for submission in subreddit.hot(limit=99):
        subid = submission.id
        title = submission.title
        text = submission.selftext
        joke = submission.title + submission.selftext
        subid = submission.id
        if subid in submissionsgot:
            continue
        elif len(text) >= 50:
            continue
        elif "black" in joke or "wife" in joke or "blonde" in joke or "jew" in joke:
            continue
        speak(title, "joketitle")
        while mixer.music.get_busy():
            time.sleep(0.1)
            continue
        speak(text, "joketext")
        while mixer.music.get_busy():
            time.sleep(0.1)
            continue
        submissionsgot.append(subid)
        break                   

    if random.randint(1,5) == 1:
       time.sleep(2)
       play("laugh.mp3")

def newsfeed(rssurl, source, feedlimit):

   quitnews = False
   parse = feedparser.parse(rssurl)
   speak("news from " + source, 'feedintro')
   print (source)
   while mixer.music.get_busy():
       time.sleep(0.1)
       continue
   for idx,  post in enumerate(parse.entries):
      if quitnews == True:
         break
      if idx >= feedlimit:
         break
      feed = post.title + " : " + post.description + "\n"
      print(feed)
      speak(feed, "post1_" + str(idx))
      while mixer.music.get_busy():
         button4 = GPIO.input(5)
         time.sleep(0.1)
         if button4 == False:
            print("button 4 pressed")
            mixer.music.stop()
            quitnews = True
         else:
            continue

def weather():

    print ("weather")
    if datetime.today().weekday() == 0:
        today = "Monday"      
    if datetime.today().weekday() == 1:
        today = "Tuesday"
    if datetime.today().weekday() == 2:
        today = "Wednesday"
    if datetime.today().weekday() == 3:
        today = "Thursday"
    if datetime.today().weekday() == 4:
        today = "Friday"
    if datetime.today().weekday() == 5:
        today = "Saturday"
    if datetime.today().weekday() == 6:
        today = "Sunday"


    parse = feedparser.parse('http://open.live.bbc.co.uk/weather/feeds/en/2652618/3dayforecast.rss')

    for idx,  post in enumerate(parse.entries):
    
        if today in post.title:    
            weather = post.title + "\n"
            speak("today is" + weather, "weather")
            while mixer.music.get_busy():
                time.sleep(0.1)
                continue
        else:
            continue
        
      
def playrandomstory():

    stories = os.listdir(dir_path + "/stories")
    storylistlen = len(stories)
    randomstory = stories[random.randrange(0,storylistlen)]
    play("stories/" + randomstory)

def playrandomtrack():

    playlist = os.listdir(dir_path + "/music")
    playlistlen = len(playlist)
    randomtrack = playlist[random.randrange(0,playlistlen)]
    play("music/" + randomtrack)
    print("play track")


def playdayofweek():

    global today

    if today == "Monday":
        play("dayofweek/02 Monday, Monday.mp3")
    elif today == "Friday":
        play("dayofweek/15 Friday I'm in Love.mp3")
    elif today == "Sunday":
        play("dayofweek/18 Sunday Girl.mp3")
    while mixer.music.get_busy():
        time.sleep(0.1)
        continue
        
#Default booleans
running = True
pressed = False
lightson = True

#play startup sound
play("sounds/smsalert.mp3")

#main loop
while running == True:
    
    now = datetime.now()

    button1 = GPIO.input(26)
    button2 = GPIO.input(16)
    button3 = GPIO.input(6)
    button4 = GPIO.input(5)

    b1 = 0
    b2 = 0
    b3 = 0
    b4 = 0

    currentstationurl = stations[currentstationnum]

#checks if currenttime matches the actual time and runs alarm ifcurrent time matches an alarm
    if currenttime != (now.hour * 100) + now.minute:
        currenttime = (now.hour * 100) + now.minute
        print (currenttime)

        if currenttime == alarm1:
            print ("alarm")
            if musicalarm == True:
                playrandomtrack()
                
            while mixer.music.get_busy():
                button3 = GPIO.input(6)
                if button3 == False:
                    print ("press")
                    mixer.music.stop()
                time.sleep(0.1)
                continue

            weather()

            if newsalarm == True:
                newsfeed('http://www.aljazeera.com/xml/rss/all.xml', 'Al Jazeera', 5)
                newsfeed('http://feeds.bbci.co.uk/news/world/rss.xml', 'the BBC', 5)

            if radioalarm == True:
                os.system("mplayer -playlist " + bbc4)
                radioplaying = True
                menu = 5

            if currenttime == alarm2:
                pass
            
#now checks for button presses
    while button1 == False:
        b1 += 1
        button1 = GPIO.input(26)
        time.sleep(0.1)

    while button2  == False:

        b2 += 1
        button2 = GPIO.input(16)
        time.sleep(0.1)

    while button3 == False:

        b3 += 1
        button3 = GPIO.input(6)
        time.sleep(0.1)

    while button4 == False:

        b4 += 1
        button4 = GPIO.input(5)
        time.sleep(0.1)
        
#press button 1
    if 1 <= b1 <= 14:

        print ("button 1 pressed")

        if menu == 1 and pressed == False:
            play("playmenu.mp3")
            menu = 6
            time.sleep(1)
            pressed = True
            
        elif menu == 2 and pressed == False:
            menu = 7
            play("alarmsettings.mp3")
            time.sleep(1)
            pressed = True
            
        elif menu == 3:
            if radioalarm  == True:
                play("radiooff.mp3")
                radioalarm = False
                print("radio off")
                time.sleep(1)
            else:
                play("radioon.mp3")
                radioalarm = True
                print ("radio on")
                time.sleep(1)

        elif menu == 5 and pressed == False:
            os.system("sudo killall mplayer")
            if currentstationnum == stationslen-1:
                currentstationnum = stationslen-1
            else:
                currentstationnum -= 1
            currentstationurl = stations[currentstationnum]
            os.system("mplayer -playlist " + currentstationurl)
            time.sleep(1)
            pressed = True

        elif menu == 6 and pressed == False:
            try:
                joke()
            except:
                print("error")

        elif menu == 7 and pressed == False:
            play("setalarm.mp3")
            alarminput = input("set alarm --> ")
            speak("alarm set to" + alarminput, "alarmtime") 
            alarm1 = float(alarminput[:2] + "." + alarminput[2:])

        elif menu == 8:
            volume = mixer.music.get_volume()
            if volume >= 0.1:
                mixer.music.set_volume(volume - 0.1)
                print (mixer.music.get_volume())

        elif menu == 10:
            mixer.music.set_volume(0.2)
            play("low.mp3")
                        
#press button 2
    if 1 <= b2 <= 14:
        
        print ("button 2 pressed")

        if menu == 1:
            speak("the time is " + str(now.hour) + " " + str(now.minute), "time")
       
        if menu == 2:
            play("setvolume.mp3")
            menu = 10
        
    #alarm settings menu: set alarm mode
        if menu == 7 and pressed == False:
            play("setalarmmode.mp3")
            menu = 3
            time.sleep(1)
            pressed = True
        
    #alarm mode menu: set news alarm 
        if menu == 3:
            if newsalarm  == True:
                newsalarm = False
                play("newsoff.mp3")
                print("news off")
                time.sleep(1)
            else:
                newsalarm = True
                play("newson.mp3")
                print ("news on")
                time.sleep(1)

        if menu == 5 and pressed == False:
            os.system("sudo killall mplayer")
            if currentstationnum == stationslen-1:
                currentstationnum = 0
            else:
                currentstationnum += 1
            currentstationurl = stations[currentstationnum]
            os.system("mplayer -playlist " + currentstationurl)
            time.sleep(1)
            pressed = True

        if menu == 6:
            playrandomstory()
            menu = 8

        if menu == 8:
            playrandomstory()

        if menu == 10:
            mixer.music.set_volume(0.5)
            play("medium.mp3")

#hold button 2
    if b2 >= 15:
        
        print ("button 2 held")

        if menu == 8:
            print("change pos")
            mixer.music.set_pos(-5)
            
#press button 3
    if 1 <= b3 <= 14:

        print ("button 3 pressed")
        
        if menu == 1:
            if mixer.get_busy():
                mixer.music.stop()
            elif radioplaying == True:
                os.system("sudo killall mplayer")
                radioplaying = False
            else:
                os.system("mplayer -playlist " + currentstationurl)
                radioplaying = True
                menu = 5
                
        elif menu == 2:
            if lightson == True:
                play("lightsoff.mp3")
                sn3218.disable()
                lightson = False
            else:
                play("lightson.mp3")
                sn3218.enable()
                lightson = True
            pressed = True
            
        elif menu == 3:#alarmtype
            if musicalarm  == True:
                musicalarm = False
                play("musicoff.mp3")
                print("music alarm off")
                time.sleep(1)
            else:
                musicalarm = True
                play("musicon.mp3")
                print ("music alarm on")
                time.sleep(1)

        elif menu == 4 and pressed == False:
            play("bbc3.mp3")
            currentstationurl = bbc3
            time.sleep(1)

        elif menu == 5:
            os.system("sudo killall mplayer")
            radioplaying = False
            menu = 1

        elif menu == 6:
            newsfeed('http://www.aljazeera.com/xml/rss/all.xml', 'Al Jazeera', 5)
            newsfeed('http://feeds.bbci.co.uk/news/world/rss.xml', 'the BBC', 5)


        elif menu == 7:
            play("setradio.mp3")
            menu = 4

        elif menu == 8:
            if mixer.get_init() != None:
                mixer.music.stop()
                menu = 6

        elif menu == 10:
            mixer.music.set_volume(0.99)
            play("high.mp3")
                    

#press button 4
    if 1 <= b4 <= 14:

        print ("button 4 pressed")
        
        if menu == 1:
            play("settings.mp3")
            menu = 2
            pressed = True

        elif menu == 2 and pressed == False:
            play("mainmenu.mp3")
            menu = 1
            pressed = True
        
        elif menu == 3:
            play("alarmsettings.mp3")
            menu = 7
            pressed = True

        elif menu == 4:
            play("alarmsettings.mp3")
            menu = 7
            pressed = True

        elif menu == 6:
            if mixer.music.get_busy() == True:
                mixer.music.stop()
            else:
                play("mainmenu.mp3")
                menu = 1
                pressed = True

        elif menu == 7:
            play("settings.mp3")
            menu = 2
            pressed = True

        elif menu == 8:
            volume = mixer.music.get_volume()
            if volume <= 0.9:
                mixer.music.set_volume(volume + 0.1)

        elif menu == 10:
            play("settings.mp3")
            menu = 2
            pressed = True

#hold button 4
    if b4 >= 15:

        print ("button 4 held")
        
        if menu == 2 or menu == 3 or menu == 4 or menu == 7 or menu == 10:
            play("mainmenu.mp3")
            menu = 1
        
    pressed = False

#pause to give the cpu a break
    time.sleep(0.1)
  

I’ll have to set it up and give it a go. Looks pretty comprehensive!

For starters I don’t think there’s any need to call mixer.init() more than once. That could potentially cause problems after multiple files are played.

Haha yes, I’ve tried to incorporate all of my limited programming knowledge into this script.

Yes, I was experimenting with the idea of initialising and closing the mixer every time audio is played. Though interestingly even if the mixer is initialised only once I get the ‘using phat speaker’ message twice - perhaps this is part of the problem? I actually had a go with running a script that does pretty much nothing except pygame.mixer.init() followed by a while loop and had the same issue with ALSA errors.

Have started experimenting with vlc.py with some success and it looks like a viable alternative, though not as beginner-friendly as pygame.

Final note, you’re probably already aware but the ALSA error messages only come up when the script is run in the terminal.

Many thanks for your help!