The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

The New Hotness

  • Raspberry Pi Streaming Radio Player: Improved Pipe Handling

    My Raspberry Pi-based streaming radio player generally worked fine, except sometimes the keypad / volume control knob would stop responding after switching streams. This being an erratic thing, the error had to be a timing problem in otherwise correct code and, after spending Quality Time with the Python subprocess and select doc, I decided I was abusing mplayer’s stdin and stdout pipes.

    This iteration registers mplayer’s stdout pipe as Yet Another select.poll() Polling Object, so that the main loop can respond whenever a complete line arrives. Starting mplayer in quiet mode reduces the tonnage of stdout text, at the cost of losing the streaming status that I really couldn’t do anything with, and eliminates the occasional stalls when mplayer (apparently) dies in the middle of a line.

    The code kills and restarts mplayer whenever it detects an EOF or stream cutoff. That works most of the time, but a persistent server or network failure can still send the code into a sulk. Manually selecting a different stream (after we eventually notice the silence) generally sets things right, mainly by whacking mplayer upside the head; it’s good enough.

    It seems I inadvertently invented streaming ad suppression by muting (most of) the tracks that produced weird audio effects. Given that the “radio stations” still get paid for sending ads to me, I’m not actually cheating anybody out of their revenue: I’ve just automated our trips to the volume control knob. The audio goes silent for a few seconds (or, sheesh, a few minutes) , blatting a second or two of ad noise around the gap to remind us of what we’re missing; given the prevalence of National Forest Service PSAs, the audio ad market must be a horrific wasteland.

    The Python source code as a GitHub Gist:

    from evdev import InputDevice,ecodes,KeyEvent
    import subprocess32 as subp
    import select
    import re
    import sys
    import time
    import logging
    Media = {'KEY_KP7' : ['Classical',False,['mplayer','–quiet','-playlist','http://stream2137.init7.net/listen.pls'%5D%5D,
    'KEY_KP8' : ['Jazz',False,['mplayer','–quiet','-playlist','http://stream2138.init7.net/listen.pls'%5D%5D,
    'KEY_KP9' : ['WMHT',False,['mplayer','–quiet','http://live.str3am.com:2070/wmht1'%5D%5D,
    'KEY_KP4' : ['Classic 1000',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/1000classicalhits.m3u'%5D%5D,
    'KEY_KP5' : ['DCNY 911',False,['mplayer','–quiet','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u'%5D%5D,
    'KEY_KP6' : ['WAMC',False,['mplayer','–quiet','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D%5D,
    'KEY_KP1' : ['60s',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u'%5D%5D,
    'KEY_KP2' : ['50-70s',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u'%5D%5D,
    'KEY_KP3' : ['Soft Rock',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/softrockradio.m3u'%5D%5D,
    'KEY_KP0' : ['Zen',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/zen-for-you.m3u'%5D%5D
    }
    CurrentKC = 'KEY_KP3'
    Controls = {'KEY_KPSLASH' : '//////',
    'KEY_KPASTERISK' : '******',
    'KEY_KPENTER' : ' ',
    'KEY_KPMINUS' : '<',
    'KEY_KPPLUS' : '>',
    'KEY_VOLUMEUP' : '*',
    'KEY_VOLUMEDOWN' : '/'
    }
    MuteStrings = ["TargetSpot","[Unknown]","Advert:","+++","—","SRR","Srr","ZEN FOR"]
    MuteDelay = 8.0 # delay before non-music track; varies with buffering
    UnMuteDelay = 7.5 # delay after non-music track
    Muted = False # keep track of muted state
    MixerChannel = 'PCM' # which amixer thing to use
    logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s',filename='/tmp/Streamer.log',level=logging.INFO)
    logger = logging.getLogger()
    # set up event inputs and polling objects
    # This requires some udev magic to create the symlinks
    k = InputDevice('/dev/input/keypad')
    k.grab()
    kp = select.poll()
    kp.register(k.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    v = InputDevice('/dev/input/volume')
    v.grab()
    vp = select.poll()
    vp.register(v.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    # set up file for output tracing
    lw = open('/tmp/mp.log','w') # mplayer piped output
    # set the mixer output low enough that the initial stream won't wake the dead
    subp.call(['amixer','sset',MixerChannel,'10'])
    # Start the player with the default stream, set up for polling
    print 'Starting mplayer on',Media[CurrentKC][0],' -> ',Media[CurrentKC][-1][-1]
    logging.info('Starting mplayer on %s -> %s',Media[CurrentKC][0],Media[CurrentKC][-1][-1])
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp = select.poll() # this may be valid for other invocations, but is not pretty
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    #——————–
    #— Play the streams
    while True:
    # pluck next line from mplayer and decode it
    if [] != pp.poll(10):
    text = p.stdout.readline()
    if 'ICY Info: ' in text: # these lines may contain track names
    lw.write(text)
    lw.flush()
    trkinfo = text.split(';') # also splits at semicolon embedded in track name
    # logging.info('Raw split line: %s', trkinfo)
    for ln in trkinfo:
    if 'StreamTitle' in ln: # this part contains a track name
    NeedMute = False # assume a listenable track
    if 2 == ln.count("'"): # exactly two single quotes = probably none embedded in track name
    trkhit = re.search(r"StreamTitle='(.*)'",ln)
    if trkhit: # true for valid search results
    TrackName = trkhit.group(1) # the string between the two quotes
    print 'Track name: ', TrackName
    logging.info('Track name: [%s]', TrackName)
    if Media[CurrentKC][1] and ( (len(TrackName) == 0) or any(m in TrackName for m in MuteStrings) ) :
    NeedMute = True
    else:
    print ' … semicolon in track name: ', ln
    logging.info('Semicolon in track name: [' + ln + ']')
    else:
    print ' … quotes in track name: ', ln
    logging.info('Quotes in track name: [' + ln + ']')
    if NeedMute:
    print ' … muting:',
    if Media[CurrentKC][1] and not Muted:
    time.sleep(MuteDelay) # brute-force assumption about buffer leadtime
    subp.call(['amixer','-q','sset',MixerChannel,'mute'])
    Muted = True
    print 'done'
    logging.info('Track muted')
    else:
    print ' … unmuting:',
    if Muted:
    if Media[CurrentKC][1]:
    time.sleep(UnMuteDelay) # another brute-force timing assumption
    Muted = False
    subp.call(['amixer','-q','sset',MixerChannel,'unmute'])
    print 'done'
    logging.info('Track unmuted')
    elif 'Exiting.' in text: # mplayer just imploded
    lw.write(text)
    lw.flush()
    print 'Got EOF / stream cutoff'
    logging.info('EOF or stream cutoff')
    print ' … killing dead mplayer'
    pp.unregister(p.stdout.fileno())
    p.terminate() # p.kill()
    p.wait()
    # print ' … flushing pipes'
    # lw.truncate(0)
    print ' … discarding keys'
    while [] != kp.poll(0):
    kev = k.read
    print ' … restarting mplayer: ',Media[CurrentKC][0]
    logging.info('Restarting mplayer')
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    # accept pending events from volume control knob
    if [] != vp.poll(10):
    vev = v.read()
    lw.write('Volume')
    lw.flush()
    for e in vev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    # print 'Volume kc: ',kc
    if kc in Controls:
    print 'Vol Control: ',kc
    try:
    p.stdin.write(Controls[kc])
    except Exception as e:
    print "Can't send control: ",e
    print ' … restarting player: ',Media[CurrentKC][0]
    logging.info('Error sending volume, restarting player')
    pp.unregister(p.stdout.fileno())
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    # accept pending events from keypad
    if [] != kp.poll(10):
    kev = k.read()
    lw.write("Keypad")
    lw.flush()
    for e in kev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    if kc == 'KEY_NUMLOCK': # discard these, as we don't care
    continue
    # print 'Got: ',kc
    if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
    if True:
    print 'Backspace = shutdown!'
    p.kill()
    logging.shutdown()
    q = subp.call(['sudo','shutdown','-P','now'])
    q.wait()
    time.sleep(5)
    print "Oddly, we did not die…"
    else:
    print 'BS = bail from main!'
    logging.shutdown()
    sys.exit(0)
    break
    if KeyEvent(e).keystate != KeyEvent.key_down: # discard key up & other rubbish
    continue
    if kc in Controls:
    print 'Control:', kc
    try:
    p.stdin.write(Controls[kc])
    except Exception as e:
    print "Can't send control: ",e
    print ' … restarting player: ',Media[CurrentKC][0]
    logging.info('Error sending controls, restarting player')
    pp.unregister(p.stdout.fileno())
    p.terminate() # p.kill()
    p.wait()
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    if kc in Media:
    print 'Switching stream to ',Media[kc][0],' -> ',Media[kc][-1][-1]
    logging.info('Switching stream: ' + Media[kc][0] + ' -> ' + Media[kc][-1][-1])
    CurrentKC = kc
    print ' … halting player'
    try:
    p.communicate(input='q')
    except Exception as e:
    print 'Perhaps mplayer died?',e
    print ' … killing it for sure'
    pp.unregister(p.stdout.fileno())
    p.terminate() # p.kill()
    p.wait()
    # print ' … flushing pipes'
    # lw.truncate(0)
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subp.Popen(Media[CurrentKC][-1],
    stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT)
    pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
    print ' … running'
    logging.info(' … running')
    print 'Out of loop!'
    logging.shutdown()
    view raw Streamer.py hosted with ❤ by GitHub