Blob Blame History Raw
#!/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()