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: Mostly Viable Product

    The latest version of my simpleminded streaming radio player includes:

    • More durable parsing for track titles with embedded quotes and semicolons
    • Muting during empty / non-music Radionomy tracks
    • The Dutchess County E911 service

    Audionomy’s empty / non-music tracks include a remarkable number of mis-encoded MP3 sections triggering decoding problems; those problems don’t occur during music tracks. Some tracks come through as advertisements, which would be mostly OK apart from the garbled / high-volume gibberish, but on the whole they’re un-listenable:

    ICY Info: StreamTitle='';
    A:1271.0 (21:10.9) of 0.0 (00.0)  4.0% 44%
    [mp3float @ 0x7623e080]overread, skip -7 enddists: -5 -5
    [mp3float @ 0x7623e080]overread, skip -9 enddists: -6 -6
    A:1271.2 (21:11.2) of 0.0 (00.0)  4.0% 45%
    [mp3float @ 0x7623e080]overread, skip -7 enddists: -5 -5
    A:1309.1 (21:49.1) of 0.0 (00.0)  4.0% 42%
    ICY Info: StreamTitle='Targetspot - TargetSpot';
    A:1316.4 (21:56.4) of 0.0 (00.0)  4.0% 40%
    [mp3float @ 0x7623e080]overread, skip -5 enddists: -4 -4
    [mp3float @ 0x7623e080]overread, skip -5 enddists: -2 -2
    

    Muting happens in the mixer, because that seems easier than messing with mplayer in mid-flight. Rather than attempt to control the muted state with specific timeouts, I just un-mute after a new track title arrives; that has no effect if it’s already un-muted. The delays depend on the buffer fill level and avoid the worst of the gibberish.

    The player still falls over dead / jams solid on occasion, generally because the incoming data has stopped streaming or delivered severe encoding problems. Other than that, it runs pretty much all day, every day, on at least one of the Raspberry Pi streamers.

    Still no track display. Mostly, we still don’t miss it.

    The Python source code as a GitHub Gist:

    from evdev import InputDevice,ecodes,KeyEvent
    import subprocess32 as subprocess
    import select
    import re
    import sys
    import time
    Media = {'KEY_KP7' : ['Classical',['mplayer','-playlist','http://stream2137.init7.net/listen.pls'%5D%5D,
    'KEY_KP8' : ['Jazz',['mplayer','-playlist','http://stream2138.init7.net/listen.pls'%5D%5D,
    'KEY_KP9' : ['WMHT',['mplayer','http://live.str3am.com:2070/wmht1'%5D%5D,
    'KEY_KP4' : ['Classic 1000',['mplayer','-playlist','http://listen.radionomy.com/1000classicalhits.m3u'%5D%5D,
    'KEY_KP5' : ['DCNY 911',['mplayer','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u'%5D%5D,
    'KEY_KP6' : ['WAMC',['mplayer','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D%5D,
    'KEY_KP1' : ['60s',['mplayer','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u'%5D%5D,
    'KEY_KP2' : ['50-70s',['mplayer','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u'%5D%5D,
    'KEY_KP3' : ['Soft Rock',['mplayer','-playlist','http://listen.radionomy.com/softrockradio.m3u'%5D%5D,
    'KEY_KP0' : ['Zen',['mplayer','-playlist','http://listen.radionomy.com/zen-for-you.m3u'%5D%5D
    }
    CurrentKC = 'KEY_KP7'
    Controls = {'KEY_KPSLASH' : '//////',
    'KEY_KPASTERISK' : '******',
    'KEY_KPENTER' : ' ',
    'KEY_KPMINUS' : '<',
    'KEY_KPPLUS' : '>',
    'KEY_VOLUMEUP' : '*',
    'KEY_VOLUMEDOWN' : '/'
    }
    MuteDelay = 5.5 # delay before non-music track; varies with buffering
    UnMuteDelay = 7.3 # delay after non-music track
    MixerChannel = 'PCM' # which amixer thing to use
    # 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 files for mplayer pipes
    lw = open('/tmp/mp.log','w') # mplayer piped output
    lr = open('/tmp/mp.log','r') # … reading that output
    # set the mixer output low enough that the initial stream won't wake the dead
    subprocess.call(['amixer','sset',MixerChannel,'10'])
    # Start the player with the default stream
    print 'Starting mplayer on',Media[CurrentKC][0],' -> ',Media[CurrentKC][-1][-1]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    #——————–
    #— Play the streams
    while True:
    # pluck next line from mplayer and decode it
    text = lr.readline()
    if 'ICY Info: ' in text: # these lines may contain track names
    trkinfo = text.split(';') # also splits at semicolon embedded in track name
    for ln in trkinfo:
    if 'StreamTitle' in ln: # this part contains a track name
    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
    if ('' == TrackName or 'TargetSpot' in TrackName) and 'radionomy' in Media[CurrentKC][-1][-1]:
    print ' … muting empty Radionomy track'
    time.sleep(MuteDelay)
    subprocess.call(['amixer','-q','sset',MixerChannel,'mute'])
    else:
    print ' … unmuting'
    time.sleep(UnMuteDelay)
    subprocess.call(['amixer','-q','sset',MixerChannel,'unmute'])
    else:
    print ' … semicolon in track name: ', ln
    else:
    print ' … quotes in track name: ', ln
    elif 'Exiting…' in text:
    print 'Got EOF / stream cutoff'
    print ' … killing dead mplayer'
    p.kill()
    print ' … flushing pipes'
    lw.truncate(0)
    print ' … discarding keys'
    while [] != kp.poll(0):
    kev = k.read
    print ' … restarting mplayer: ',Media[CurrentKC][0]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    continue
    # accept pending events from volume control knob
    if [] != vp.poll(10):
    vev = v.read()
    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]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    # accept pending events from keypad
    if [] != kp.poll(10):
    kev = k.read()
    for e in kev:
    if e.type == ecodes.EV_KEY:
    kc = KeyEvent(e).keycode
    if kc == 'KEY_NUMLOCK':
    continue
    # print 'Got: ',kc
    if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
    if True:
    print 'Backspace = shutdown!'
    p = subprocess.call(['sudo','shutdown','-HP','now'])
    else:
    print 'BS = bail from main, ssh to restart!'
    sys.exit(0)
    if KeyEvent(e).keystate != KeyEvent.key_down:
    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]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    if kc in Media:
    print 'Switching stream to ',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'
    p.kill()
    print ' … flushing pipes'
    lw.truncate(0)
    print ' … restarting player: ',Media[CurrentKC][0]
    p = subprocess.Popen(Media[CurrentKC][-1],
    stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT)
    print ' … running'
    print 'Out of loop!'
    view raw Streamer.py hosted with ❤ by GitHub