#!/usr/bin/env python # Paulo Roma - simple tkinter interface for fmtools, # with lirc and recording support. # Date: 23/12/2009 # The radio is turned off on exit. import os, sys, string, pickle, math import datetime, time, signal from threading import Thread from subprocess import Popen, PIPE try: from tkinter import * # python3 except ImportError: try: from mtTkinter import * except ImportError: from Tkinter import * print ( "mtTkinter not found: http://tkinter.unpythonic.net/wiki/mtTkinter" ) print ( "Remote control will not work!!" ) try: import pylirc use_lirc = True except ImportError: use_lirc = False print ( "pylirc not found: http://pylirc.mccabe.nu/" ) print ( "Remote control will not work!!" ) try: import pynotify if pynotify.init("tkradio"): use_notify = True else: use_notify = False print ( "pynotify module initialization failed" ) except: use_notify = False print ( "notify-python not found: http://www.galago-project.org/downloads.php" ) # These are stations in the Rio de Janeiro area. # Customize for your own locale. They can be set # in file ~/.fmrc or ~/.radiostations: # station name, frequency and volume. stations = [["Globo", "89.3 100"], ["MPB", "90.3 100"], ["CBN", "92.5 100"], ["Band News", "94.9 100"], ["Paradiso", "95.7 100"], ["Tupy", "96.5 100"], ["Beat98 FM", "98.1 100"], ["MEC", "98.9 100"], ["JB FM", "99.7 100"], ["O Dia", "100.5 100"], ["Transamerica", "101.3 100"], ["Mix", "102.1 100"], ["Oi", "102.9 100"], ["Nativa", "103.7 100"]] radbut = stations[4][1] # default radio frequency volume = 100 # initial volume [0,100] state = False # keep track of mutting/unmutting blocking = 0 # lirc blocking control tid = 0 # recorder thread id lid = 0 # loopback process id fmrec = None # recorder thread variable irrec = None # lirc thread variable # external programs used MIXER = "/usr/bin/amixer" # alsa-utils PIDOF = "/sbin/pidof" # sysvinit-tools PS = "/bin/ps" # procps GREP = "/bin/grep" # grep FM = "/usr/bin/fm" # fmtools OGG = "/usr/bin/oggenc" # vorbis-tools RPNG = "/usr/share/pixmaps/radio.png" RGIF = "/usr/share/fmtools/radio.gif" #CHANNEL = "PCM" CHANNEL = "Master" class IRRec(Thread): """Class for interacting with lirc.""" def __init__ (self, lirc_handle): """Constructor.""" Thread.__init__ (self) self.lirc_handle = lirc_handle self.__on = True def stop ( self ): """Kills this thread.""" self.__on = False def run(self): """Run the thread code.""" code = {"config" : ""} while ( self.__on ): # Delay... time.sleep(1) s = pylirc.nextcode(1) while ( s ): for code in s: if ( code["config"] == "next" ): next() elif ( code["config"] == "previous" ): previous() elif ( code["config"] == "off" ): mute() elif ( code["config"] == "on" ): radio ("on") elif ( code["config"] == "volup" ): volup () elif ( code["config"] == "voldown" ): voldown() elif ( code["config"] in "0123456789" ): time.sleep(1) b=pylirc.nextcode() if ( b and b[0] in "0123456789" ): code["config"] += b[0] setStation ( int (code["config"]) ) elif ( code["config"] == "rec" ): rec_on() elif ( code["config"] == "stop" ): rec_off() elif ( code["config"] == "loop" ): loopon() elif ( code["config"] == "quit" ): fini() else: # Print all the configs... print ( "Command: %s, Repeat: %d" % (code["config"], code["repeat"]) ) if (not blocking): s = pylirc.nextcode(1) else: s = [] # if we get here, the thread is over, so clean up lirc pylirc.exit() class FMRec(Thread): """Class for controlling the recording process.""" def __init__ (self): """Constructor.""" Thread.__init__ (self) self.__pid = 0 # arecord process id self.__on = True # for implementing a thread stop, # which python does not have def __del__ (self): """Destructor. Stops the recording, before quitting.""" self.stop() def stop ( self ): """Stops the recording, by killing the recorder process.""" global tid tid = 0 if ( self.__pid ): os.kill ( self.__pid, signal.SIGTERM ) self.__pid = 0 self.__on = False def run(self): """Start the thread.""" while (self.__on): if ( not self.__pid ): data = str(datetime.date.today()) hora = list(time.localtime(time.time())) hora = str(hora[3])+":"+str(hora[4])+":"+str(hora[5]) rec_file = '/tmp/tkradio-'+fmstations[cur_station][0]+"-"+data+"-"+hora+'.ogg' if use_notify: n = pynotify.Notification("tkradio recording on file:", rec_file, RPNG) n.show() ogge_param [-1] = rec_file p1 = Popen(brec_param, stdout=PIPE) p2 = Popen(ogge_param, stdin=p1.stdout) self.__pid = p1.pid time.sleep(1.0) # Suspend execution for the given number of seconds # if we get here, the thread is finished self.stop () def start_irrec (): """Start the IRRec thread if lircd is running.""" global irrec lircid = getpid ( 'lircd' ) if ( lircid ): # is lirc running? # handle lirc events path = os.environ.get("HOME") fname = path+"/.fmlircrc" if ( not os.path.exists (fname) ): fname = "/usr/share/fmtools/fmlircrc" lirc_handle = pylirc.init("tkradio", fname, blocking) if (lirc_handle): if ( use_notify ): n = pynotify.Notification("tkradio", "Successfully opened lirc. Handle is "+str(lirc_handle), RPNG) n.set_timeout(2000) n.show() irrec = IRRec(lirc_handle) irrec.start() def set_rec_type(): """Set recording based on alsa or pulseaudio.""" global REC # program for recording global PLAY # program for playing global arec_param # recording parameters global apla_param # playing parameters global brec_param # recording parameters for encoding global ogge_param # encoding parameters pulseaudio = getpid ( 'pulseaudio' ) if ( pulseaudio ): # is pulseaudio running? REC = "/usr/bin/parec" # pulseaudio-utils PLAY = "/usr/bin/pacat" # pulseaudio-utils arec_param = [REC] brec_param = [REC] apla_param = [PLAY] ogge_param = [OGG, '-', '-r', '-Q', '-o', ""] else: REC = "/usr/bin/arecord" # alsa-utils PLAY = "/usr/bin/aplay" # alsa-utils arec_param = [REC, '-D', 'default', '-d', '0', '-f', 'cd'] brec_param = [REC, '-D', 'default', '-d', '0', '-f', 'cd', '-'] apla_param = [PLAY, '-f', 'cd', '-D', 'default'] ogge_param = [OGG, '-', '-Q', '-o', ""] return pulseaudio def radio ( cmd ): """Send the given command to the radio.""" os.system(FM + " " + cmd) def setCurStation ( frequency ): """Update the current station.""" global cur_station ind = 0 for st in fmstations: if ( st[1] == frequency ): cur_station = ind break ind += 1 def setstation(): """Set the station chosen via Radio Button.""" freq = station.get() changeStation ( freq ) setCurStation ( freq ) def setStation(ind): """Set the station to ind.""" if ( ind >= 0 and ind < ns ): freq = fmstations[ind][1] changeStation ( freq ) setCurStation ( freq ) def changeStation ( st ): """Set the station to the given station.""" radio ( st ) freq.delete(0, END) freq.insert(0,st.split()[0]) station.set ( st ) def fini(): """Quit the radio.""" radio ("off") # kill all threads if ( fmrec ): fmrec.stop() if ( irrec ): irrec.stop() if ( lid ): os.kill ( lid, signal.SIGTERM ) os._exit (0) def mute(): """Mute/Unmute the radio.""" global state if ( not state ): radio ("off") state = True btmute.set ( "On" ) btm.config(state=ACTIVE) else: radio ("on") state = False btmute.set ( "Off" ) btm.config(state=NORMAL) def setVolume ( v ): os.system(MIXER + " -q -c 0 set " + CHANNEL + " " + str(v) + "%") def getVolume ( ): vol = os.popen (MIXER + " -c 0 get " + CHANNEL + " | " + GREP + " -E \"%\"").readline() i = str.find (vol,"%") j = str.find (vol,"[",0,i) return int(vol[j+1:i]) def on_move(value=0): """Use slider position to set the volume.""" setVolume ( scale.get() ) def volup (): """Increase the volume.""" v = scale.get() + 5 if ( v > 100 ): v = 100 scale.set ( v ) setVolume ( v ) def voldown(): """Decrease the volume.""" v = scale.get() - 5 if ( v < 0 ): v = 0 scale.set ( v ) setVolume ( v ) def enter (): "Enter a new frequency.""" f = freq.get()+" "+ str(volume) changeStation (f) setCurStation (f) def readStations ( ): """Read the preset station file.""" path = os.environ.get("HOME") fname = path+"/.radiostations" if ( not os.path.exists (fname) ): fname = path+"/.fmrc" lst = [] if ( os.path.exists (fname) ): textf = open(fname, 'r') for line in textf: l=line.split(None) st = [l[0].replace("_"," "),l[1]+" "+l[2]] lst.append ( st ) textf.close() return lst def next (): "Go to the next station.""" global cur_station cur_station = (cur_station + 1) % ns changeStation ( fmstations[cur_station][1] ) def previous (): "Go to the previous station.""" global cur_station cur_station = (cur_station - 1) % ns changeStation ( fmstations[cur_station][1] ) def trigger (): """Create a thread for recording.""" global tid, fmrec if ( not tid ): fmrec = FMRec () fmrec.start() tid = 1 def loop(): """Route the capture sources on the sound card back in as PCM audio.""" global lid if ( loopvar.get() == "ON" ): if ( not lid ): p1 = Popen(arec_param, stdout=PIPE) p2 = Popen(apla_param, stdin=p1.stdout) lid = p1.pid if ( use_notify ): n = pynotify.Notification("tkradio", "Software Loop Back activated", RPNG) n.set_timeout(2000) n.show() else: if ( lid ): os.kill ( lid, signal.SIGTERM ) lid = 0 def loopon(): """Toggle the loop variable.""" if ( loopvar.get() == "ON" ): loopvar.set ("OFF") else: loopvar.set ("ON") loop() def rec(): """Record the current station.""" if ( recvar.get() == "ON" ): rec_on() else: rec_off() def rec_on(): """Turn the recorder on.""" recvar.set ("ON") trigger() def rec_off(): """Turn the recorder off.""" recvar.set ("OFF") if ( fmrec ): fmrec.stop() def mouse_wheel(event): """Respond to mouse wheel events.""" if event.num == 5 or event.delta == -120: voldown () if event.num == 4 or event.delta == 120: volup () def str2num(datum): """A conversion function that "guesses" the best conversion.""" try: return int(datum) except: try: return float(datum) except: return datum def getpid(proc): """Return the ID of the given process.""" aid = os.popen ( PIDOF + ' ' + proc ).readline() aid = aid.replace('\n','') return str2num(aid) class radioState: """Holds the state of the radio (used for persistency).""" def __init__ ( self, intial_station ): self.volume = getVolume() self.loop = "OFF" self.mute = False self.station = intial_station self.pos = "" def __str__ (self): return " Volume = %s\n Loop = %s\n Mute = %d\n Station = %s\n Pos = %s\n" % \ ( self.volume, self.loop, self.mute, self.station, self.pos ) def main (argv=None): """Main program.""" global scale # volume scale global state # toggle mute/umute global station # variable for the station radio buttons global btmute # variable for the text in the mute button global btm # mute button global freq # variable for manually entering a frequency global fmstations # preset fm stations global cur_station # current station global ns # number of preset fm stations global recvar # variable for setting record on/off global loopvar # variable for setting loopback on/off global lid # loopback process id def cleanup(): savedState.volume = scale.get() savedState.loop = loopvar.get() savedState.mute = state savedState.station = station.get() savedState.pos = mw.geometry() pf = open(statfile,'wb') pickle.dump ( savedState, pf ) pf.close() # print ( savedState ) raise SystemExit if argv is None: argv = sys.argv pyversion = str.split(sys.version)[0] print ( "Python Version: %s" % pyversion ) # check whether tkradio is already running stat = os.popen (PS + " aux | " + GREP + " -E \"python(" + pyversion[0:3] + ")? " + argv[0] + "\"").readline() cid = os.getpid() if ( stat ): pid = stat.split()[1] if ( cid != int(pid) ): sys.exit ( "%s is already running: pid = %s" %(argv[0], pid) ) path = os.environ.get("HOME") statfile = path + '/.tkradio' if (sys.hexversion > 0x03000000): statfile += '3' if ( not os.path.exists (statfile) ): savedState = radioState(radbut) else: pf = open(statfile,'rb') savedState = pickle.load(pf) pf.close() mw = Tk() # do not resize the radio mw.resizable(False,False) station = StringVar() station.set (savedState.station) btmute = StringVar() state = not savedState.mute btmute.set ( "OFF" ) top = Frame(); top.pack() bbt = Frame(); bbt.pack() bot = Frame(); bot.pack() mw.title ("tkradio") fmstations = readStations ( ) if ( not fmstations ): fmstations = stations ns = len ( fmstations ) cur_station = -1 # sets the recording type: alsa or pulse if ( set_rec_type() ): Label(top, text = 'pulse: '+CHANNEL).pack() else: Label(top, text = 'alsa: '+CHANNEL).pack() # make tuner buttons for st in fmstations: Radiobutton(bot,text=st[0],value=st[1],variable=station,command=setstation).pack(anchor=W) scale = Scale(top, from_=0, to=100, orient=HORIZONTAL, command=on_move, bd=0, sliderlength=10, width=5, showvalue=0) scale.pack(side='top') scale.set(savedState.volume) # the current radio frequency Button(bbt,text="<", command = previous).pack(side="left",anchor=E) Button(bbt,text="Enter", command = enter).pack(side="left") Button(bbt,text=">", command = next).pack(side="left",anchor=W) freq=Entry(top,font="Arial 24",width=5,justify=CENTER) freq.insert(0,station.get()) freq.pack(side="bottom") recvar = StringVar() # creates a checkbutton for the recording state loopvar = StringVar() # creates a checkbutton for the loopback recvar.set ( "OFF" ) aid = getpid ( str.rsplit(REC,'/',1)[1] ) if ( aid ): # is the loop back already on? loopvar.set ( "ON" ) lid = aid else: loopvar.set ( savedState.loop ) loop () # create quit and mute buttons Button(top,text="Exit", command = cleanup).pack(side="right") btm=Button(top,text="Off", command = mute, textvariable = btmute) btm.pack(side="left") Checkbutton (top, text="Rec", variable=recvar, onvalue="ON", offvalue="OFF", command=rec).pack(side="top",anchor=W) Checkbutton (top, text="Loop", variable=loopvar, onvalue="ON", offvalue="OFF", command=loop).pack(side="right") # mouse whell control mw.bind("<Button-4>", mouse_wheel) mw.bind("<Button-5>", mouse_wheel) # turn the radio on setstation() mute() # set an icon for the window icon_img = PhotoImage(file=RGIF) mw.tk.call('wm', 'iconphoto', mw._w, icon_img) # start the lirc thread if ( use_lirc ): start_irrec() mw.protocol("WM_DELETE_WINDOW", cleanup) if ( savedState.pos ): mw.geometry(savedState.pos) mw.mainloop() if __name__=="__main__": try: sys.exit(main()) except (KeyboardInterrupt,SystemExit): fini()