Skip to content

Instantly share code, notes, and snippets.

@ednisley
Created March 16, 2017 11:14
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ednisley/73fa3375cd626848b39468f0f8575821 to your computer and use it in GitHub Desktop.
Python source code: Raspberry Pi streaming radio player with OLED display
from evdev import InputDevice,ecodes,KeyEvent
import subprocess32 as subp
import select
import re
import sys
import time
import logging
import logging.handlers
import os.path
import argparse as args
import textwrap
from luma.oled.device import sh1106
from luma.core.serial import spi
from luma.core.render import canvas
from PIL import ImageFont
# URL must be last entry in command line list
Media = {'KEY_KP7' : ['Classical',False,['mplayer','-playlist','http://stream2137.init7.net/listen.pls']],
'KEY_KP8' : ['Jazz',False,['mplayer','-playlist','http://stream2138.init7.net/listen.pls']],
'KEY_KP9' : ['WMHT',False,['mplayer','http://wmht.streamguys1.com/wmht1']],
'KEY_KP4' : ['Classic 1k',True,['mplayer','-playlist','http://listen.radionomy.com/1000classicalhits.m3u']],
'KEY_KP5' : ['Love',True,['mplayer','-playlist','/home/pi/Playlists/LoveRadio.m3u']],
'KEY_KP6' : ['WAMC',False,['mplayer','-playlist','http://playerservices.streamtheworld.com/pls/WAMCFM.pls']],
'KEY_KP1' : ['60s',True,['mplayer','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u']],
'KEY_KP2' : ['50-70s',True,['mplayer','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u']],
'KEY_KP3' : ['Soft Rock',True,['mplayer','-playlist','http://listen.radionomy.com/softrockradio.m3u']],
'KEY_KP0' : ['Zen',False,['mplayer','http://iradio.iceca.st:80/zenradio']],
'KEY_KPDOT' : ['Ambient',False,['mplayer','http://185.32.125.42:7331/maschinengeist.org.mp3']],
'KEY_KPMINUS' : ['Relaxation',True,['mplayer','-playlist','/home/pi/Playlists/Frequences-relaxation.m3u']],
'KEY_KPPLUS' : ['Plenitude',True,['mplayer','-playlist','/home/pi/Playlists/Radio-PLENITUDE.m3u']]
}
# these keycode will be fed directly into mplayer
Controls = {'KEY_KPSLASH' : '//////',
'KEY_KPASTERISK' : '******',
'KEY_VOLUMEUP' : '*',
'KEY_VOLUMEDOWN' : '/'
}
# stream title keywords that trigger muting
MuteStrings = ['TargetSpot', # common Radionomy insert
'Intro of','Jingle','*bumper*', # Radio-PLENITUDE
'[Unknown]','Advert:','+++','---','SRR','Srr', # softrockradio
'PEACE LK1','PEACE J1'] # Frequences-relaxation
# Set up default configuration
CurrentKC = 'KEY_KP7' # default stream source
MuteDelay = 6.5 # delay before non-music track; varies with buffering
UnMuteDelay = 9.0 # delay after non-music track
MixerChannel = 'PCM' # default amixer output control
MixerVol = '30' # mixer gain
RestartDelay = 10 # delay after stream failure
Contrast = 255 # OLED brightness setting
# Set up command line parsing
cmdline = args.ArgumentParser(description='Streaming Radio Player',epilog='KE4ZNU - http://softsolder.com')
cmdline.add_argument('Loc',help='Location: BR1 BR2 ...',default='any',nargs='?')
args = cmdline.parse_args()
# Set up logging
LogFN = '/home/pi/Streamer.log'
LogFmt = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
LogHandler = logging.handlers.RotatingFileHandler(LogFN,backupCount=9)
LogHandler.setFormatter(LogFmt)
logger = logging.getLogger('StreamLog')
logger.addHandler(LogHandler)
logger.setLevel(logging.INFO)
# Tweak config based on where we are
Location = vars(args)['Loc'].upper()
logger.info('Player setup for: ' + Location)
if Location == 'BR1':
CurrentKC = 'KEY_KPDOT'
MixerVol = '5'
Contrast = 1
elif Location == 'BR2':
MuteDelay = 4.5
UnMuteDelay = 8.5
MixerVol = '5'
Contrast = 1
elif Location == 'LR':
MixerVol = '40'
CurrentKC = 'KEY_KPPLUS'
# 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)
# if volume control knob exists, then set up its events
VolumeDevice = '/dev/input/volume'
vp = select.poll()
if os.path.exists(VolumeDevice):
logger.info('Volume control device: %s',VolumeDevice)
v = InputDevice(VolumeDevice)
v.grab()
vp.register(v.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR)
# set up file for mplayer output tracing
lw = open('/home/pi/mp.log','w') # mplayer piped output
# set the mixer output low enough that the initial audio won't wake the dead
subp.call(['amixer','-q','sset',MixerChannel,MixerVol])
if Media[CurrentKC][1]:
subp.call(['amixer','-q','sset',MixerChannel,'mute'])
Muted = True # squelch anything before valid track name
logger.info('Audio muted at startup')
else:
subp.call(['amixer','-q','sset',MixerChannel,'unmute'])
Muted = False # allow early audio
logger.info('Audio unmuted at startup')
# Set up OLED display
font1 = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',14)
font2 = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',11)
wrapper = textwrap.TextWrapper(width=128//font2.getsize('n')[0])
StatLine = 0
DataLine = 17 # allow one line for weird ascenders and accent marks
LineSpace = 16
serial = spi(device=0,port=0)
device = sh1106(serial)
device.contrast(Contrast)
def ShowStatus(L1=None,L2=None,L3='None'):
with canvas(device) as screen:
screen.text((1,StatLine),Media[CurrentKC][0][0:11],
font=font1,fill='white')
screen.text((127-(4*font1.getsize('M')[0] + 2),StatLine),'Mute' if Muted else ' ',
font=font1,fill='white')
screen.text((1,DataLine),L1,
font=font2,fill='white')
screen.text((1,DataLine + 1*LineSpace),L2,
font=font2,fill='white')
screen.text((1,DataLine + 2*LineSpace),L3,
font=font2,fill='white')
ShowStatus('Startup in ' + Location,
'Mixer: ' + MixerChannel + ' = ' + MixerVol,
'Contrast: ' + str(Contrast))
# Start the player with the default stream, set up for polling
logger.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)
#--------------------
#--- Play the streams
while True:
# pluck next line from mplayer and decode it
if [] != pp.poll(10):
text = p.stdout.readline()
if 'Error: ' in text: # something horrible went wrong
lw.write(text)
lw.flush()
logger.info('Unsolvable problem! ' + text)
logger.info('Exiting')
LogHandler.doRollover()
logging.shutdown()
sys.exit('Exit streamer on mplayer error --' + text)
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
# logger.info('Raw split line: %s', trkinfo)
for ln in trkinfo:
if 'StreamTitle' in ln: # this part probably contains the track name
NeedMute = False # assume a listenable track
trkhit = re.search(r"StreamTitle='(.*)'",ln) # extract title if possible
if trkhit: # regex returned valid result?
TrackName = trkhit.group(1) # get string between two quotes
else:
logger.info('Regex failed for line: [' + ln + ']')
TrackName = 'Invalid StreamTitle format!'
logger.info('Track name: [%s]', TrackName)
if Media[CurrentKC][1] and ( (len(TrackName) == 0) or any(m in TrackName for m in MuteStrings) ) :
NeedMute = True
if NeedMute:
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
logger.info('Track muted')
else:
if Muted:
if Media[CurrentKC][1]:
time.sleep(UnMuteDelay) # another brute-force timing assumption
subp.call(['amixer','-q','sset',MixerChannel,'unmute'])
Muted = False
logger.info('Track unmuted')
if TrackName:
info = wrapper.wrap(TrackName)
ShowStatus(info[0],
info[1] if len(info) > 1 else '',
info[2] if len(info) > 2 else '')
else:
ShowStatus('No track info','','')
elif 'Exiting.' in text: # mplayer just imploded
lw.write(text)
lw.flush()
logger.info('EOF or stream cutoff: [' + text + ']')
ShowStatus('Killing dead Mplayer','','')
pp.unregister(p.stdout.fileno())
p.terminate() # p.kill()
p.wait()
logger.info('Discarding keys')
while [] != kp.poll(0):
kev = k.read
time.sleep(RestartDelay)
logger.info('Restarting mplayer')
ShowStatus('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)
logger.info(' ... running')
ShowStatus('Mplayer 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
if kc in Controls:
try:
p.stdin.write(Controls[kc])
except Exception as e:
logger.info('Error sending volume, restarting player: ' + str(e))
try:
pp.unregister(p.stdout.fileno())
except Exception as e:
logger.info('Cannot unregister stdout: ' + str(e))
ShowStatus('Vol error','Restarting',' Mplayer')
time.sleep(RestartDelay)
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)
logger.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
if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
if True:
logger.info('Shutting down')
LogHandler.doRollover()
logging.shutdown()
p.kill()
q = subp.call(['sudo','shutdown','-P','now'])
q.wait()
time.sleep(5)
else:
logger.info('Exiting from main')
LogHandler.doRollover()
logging.shutdown()
sys.exit('Exit on command')
break
if KeyEvent(e).keystate != KeyEvent.key_down: # now OK to discard key up & hold
continue
if kc == 'KEY_KPENTER': # toggle muted state
if Muted:
logger.info('Forcing unmute')
subp.call(['amixer','-q','sset',MixerChannel,'unmute'])
Muted = False
else:
logger.info('Forcing mute')
subp.call(['amixer','-q','sset',MixerChannel,'mute'])
Muted = True
continue
if kc in Controls:
logger.info('Control: ' + kc)
try:
p.stdin.write(Controls[kc])
except Exception as e:
logger.info('Error sending controls, restarting player: ' + str(e))
ShowStatus('Ctl error','Restarting',' Mplayer')
try:
pp.unregister(p.stdout.fileno())
except Exception as e:
logger.info('Cannot unregister stdout: ' + str(e))
p.terminate() # p.kill()
p.wait()
time.sleep(RestartDelay)
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)
logger.info(' ... running')
ShowStatus('Mplayer',' running','')
if kc in Media:
logger.info('Switching stream: ' + Media[kc][0] + ' -> ' + Media[kc][-1][-1])
oldname = Media[CurrentKC][0]
CurrentKC = kc
ShowStatus('Switching from',oldname,'Halt Mplayer')
try:
pp.unregister(p.stdout.fileno())
except Exception as e:
logger.info('Cannot unregister stdout: ' + str(e))
try:
p.communicate(input='q')
except Exception as e:
logger.info('Mplayer already dead? ' + str(e))
try:
p.terminate() # p.kill()
p.wait()
except Exception as e:
logger.info('Trouble with terminate or wait: ' + str(e))
if Media[CurrentKC][1]:
subp.call(['amixer','-q','sset',MixerChannel,'mute'])
Muted = True
logger.info('Audio muted for restart')
else:
subp.call(['amixer','-q','sset',MixerChannel,'unmute'])
Muted = False
logger.info('Audio unmuted for restart')
time.sleep(RestartDelay)
logger.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)
logger.info(' ... running')
ShowStatus('Started Mplayer','','')
@ednisley
Copy link
Author

More details on my blog at http://wp.me/poZKh-6Au

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment