Home | History | Annotate | Download | only in audiopy
      1 #! /usr/bin/env python
      2 
      3 """audiopy -- a program to control the Solaris audio device.
      4 
      5 Contact: Barry Warsaw
      6 Email:   bwarsaw (at] python.org
      7 Version: %(__version__)s
      8 
      9 When no arguments are given, this pops up a graphical window which lets you
     10 choose the audio input and output devices, and set the output volume.
     11 
     12 This program can be driven via the command line, and when done so, no window
     13 pops up.  Most options have the general form:
     14 
     15     --device[={0,1}]
     16     -d[={0,1}]
     17         Set the I/O device.  With no value, it toggles the specified device.
     18         With a value, 0 turns the device off and 1 turns the device on.
     19 
     20 The list of devices and their short options are:
     21 
     22  (input)
     23     microphone  -- m
     24     linein      -- i
     25     cd          -- c
     26 
     27  (output)
     28     headphones  -- p
     29     speaker     -- s
     30     lineout     -- o
     31 
     32 Other options are:
     33 
     34     --gain volume
     35     -g volume
     36         Sets the output gain to the specified volume, which must be an integer
     37         in the range [%(MIN_GAIN)s..%(MAX_GAIN)s]
     38 
     39     --version
     40     -v
     41         Print the version number and exit.
     42 
     43     --help
     44     -h
     45         Print this message and exit.
     46 """
     47 
     48 import sys
     49 import os
     50 import errno
     51 import sunaudiodev
     52 from SUNAUDIODEV import *
     53 
     54 # Milliseconds between interrupt checks
     55 KEEPALIVE_TIMER = 500
     56 
     57 __version__ = '1.1'
     58 
     59 
     60 
     62 class MainWindow:
     63     def __init__(self, device):
     64         from Tkinter import *
     65         self.__helpwin = None
     66         self.__devctl = device
     67         info = device.getinfo()
     68         #
     69         self.__tkroot = tkroot = Tk(className='Audiopy')
     70         tkroot.withdraw()
     71         # create the menubar
     72         menubar = Menu(tkroot)
     73         filemenu = Menu(menubar, tearoff=0)
     74         filemenu.add_command(label='Quit',
     75                              command=self.__quit,
     76                              accelerator='Alt-Q',
     77                              underline=0)
     78         helpmenu = Menu(menubar, name='help', tearoff=0)
     79         helpmenu.add_command(label='About Audiopy...',
     80                              command=self.__popup_about,
     81                              underline=0)
     82         helpmenu.add_command(label='Help...',
     83                              command=self.__popup_using,
     84                              underline=0)
     85         menubar.add_cascade(label='File',
     86                             menu=filemenu,
     87                             underline=0)
     88         menubar.add_cascade(label='Help',
     89                             menu=helpmenu,
     90                             underline=0)
     91         # now create the top level window
     92         root = self.__root = Toplevel(tkroot, class_='Audiopy', menu=menubar)
     93         root.protocol('WM_DELETE_WINDOW', self.__quit)
     94         root.title('audiopy ' + __version__)
     95         root.iconname('audiopy ' + __version__)
     96         root.tk.createtimerhandler(KEEPALIVE_TIMER, self.__keepalive)
     97         #
     98         buttons = []
     99         #
    100         # where does input come from?
    101         frame = Frame(root, bd=1, relief=RAISED)
    102         frame.grid(row=1, column=0, sticky='NSEW')
    103         label = Label(frame, text='Input From:')
    104         label.grid(row=0, column=0, sticky=E)
    105         self.__inputvar = IntVar()
    106         ##
    107         btn = Radiobutton(frame,
    108                           text='None',
    109                           variable=self.__inputvar,
    110                           value=0,
    111                           command=self.__pushtodev,
    112                           underline=0)
    113         btn.grid(row=0, column=1, sticky=W)
    114         root.bind('<Alt-n>', self.__none)
    115         root.bind('<Alt-N>', self.__none)
    116         if not info.i_avail_ports & MICROPHONE:
    117             btn.configure(state=DISABLED)
    118         buttons.append(btn)
    119         ##
    120         btn = Radiobutton(frame,
    121                           text='Microphone',
    122                           variable=self.__inputvar,
    123                           value=MICROPHONE,
    124                           command=self.__pushtodev,
    125                           underline=0)
    126         btn.grid(row=1, column=1, sticky=W)
    127         root.bind('<Alt-m>', self.__mic)
    128         root.bind('<Alt-M>', self.__mic)
    129         if not info.i_avail_ports & MICROPHONE:
    130             btn.configure(state=DISABLED)
    131         buttons.append(btn)
    132         ##
    133         btn = Radiobutton(frame,
    134                           text='Line In',
    135                           variable=self.__inputvar,
    136                           value=LINE_IN,
    137                           command=self.__pushtodev,
    138                           underline=5)
    139         btn.grid(row=2, column=1, sticky=W)
    140         root.bind('<Alt-i>', self.__linein)
    141         root.bind('<Alt-I>', self.__linein)
    142         if not info.i_avail_ports & LINE_IN:
    143             btn.configure(state=DISABLED)
    144         buttons.append(btn)
    145         ## if SUNAUDIODEV was built on an older version of Solaris, the CD
    146         ## input device won't exist
    147         try:
    148             btn = Radiobutton(frame,
    149                               text='CD',
    150                               variable=self.__inputvar,
    151                               value=CD,
    152                               command=self.__pushtodev,
    153                               underline=0)
    154             btn.grid(row=3, column=1, sticky=W)
    155             root.bind('<Alt-c>', self.__cd)
    156             root.bind('<Alt-C>', self.__cd)
    157             if not info.i_avail_ports & CD:
    158                 btn.configure(state=DISABLED)
    159             buttons.append(btn)
    160         except NameError:
    161             pass
    162         #
    163         # where does output go to?
    164         frame = Frame(root, bd=1, relief=RAISED)
    165         frame.grid(row=2, column=0, sticky='NSEW')
    166         label = Label(frame, text='Output To:')
    167         label.grid(row=0, column=0, sticky=E)
    168         self.__spkvar = IntVar()
    169         btn = Checkbutton(frame,
    170                           text='Speaker',
    171                           variable=self.__spkvar,
    172                           onvalue=SPEAKER,
    173                           command=self.__pushtodev,
    174                           underline=0)
    175         btn.grid(row=0, column=1, sticky=W)
    176         root.bind('<Alt-s>', self.__speaker)
    177         root.bind('<Alt-S>', self.__speaker)
    178         if not info.o_avail_ports & SPEAKER:
    179             btn.configure(state=DISABLED)
    180         buttons.append(btn)
    181         ##
    182         self.__headvar = IntVar()
    183         btn = Checkbutton(frame,
    184                           text='Headphones',
    185                           variable=self.__headvar,
    186                           onvalue=HEADPHONE,
    187                           command=self.__pushtodev,
    188                           underline=4)
    189         btn.grid(row=1, column=1, sticky=W)
    190         root.bind('<Alt-p>', self.__headphones)
    191         root.bind('<Alt-P>', self.__headphones)
    192         if not info.o_avail_ports & HEADPHONE:
    193             btn.configure(state=DISABLED)
    194         buttons.append(btn)
    195         ##
    196         self.__linevar = IntVar()
    197         btn = Checkbutton(frame,
    198                           variable=self.__linevar,
    199                           onvalue=LINE_OUT,
    200                           text='Line Out',
    201                           command=self.__pushtodev,
    202                           underline=0)
    203         btn.grid(row=2, column=1, sticky=W)
    204         root.bind('<Alt-l>', self.__lineout)
    205         root.bind('<Alt-L>', self.__lineout)
    206         if not info.o_avail_ports & LINE_OUT:
    207             btn.configure(state=DISABLED)
    208         buttons.append(btn)
    209         #
    210         # Fix up widths
    211         widest = 0
    212         for b in buttons:
    213             width = b['width']
    214             if width > widest:
    215                 widest = width
    216         for b in buttons:
    217             b.configure(width=widest)
    218         # root bindings
    219         root.bind('<Alt-q>', self.__quit)
    220         root.bind('<Alt-Q>', self.__quit)
    221         #
    222         # Volume
    223         frame = Frame(root, bd=1, relief=RAISED)
    224         frame.grid(row=3, column=0, sticky='NSEW')
    225         label = Label(frame, text='Output Volume:')
    226         label.grid(row=0, column=0, sticky=W)
    227         self.__scalevar = IntVar()
    228         self.__scale = Scale(frame,
    229                              orient=HORIZONTAL,
    230                              from_=MIN_GAIN,
    231                              to=MAX_GAIN,
    232                              length=200,
    233                              variable=self.__scalevar,
    234                              command=self.__volume)
    235         self.__scale.grid(row=1, column=0, sticky=EW)
    236         #
    237         # do we need to poll for changes?
    238         self.__needtopoll = 1
    239         try:
    240             fd = self.__devctl.fileno()
    241             self.__needtopoll = 0
    242         except AttributeError:
    243             pass
    244         else:
    245             import fcntl
    246             import signal
    247             import STROPTS
    248             # set up the signal handler
    249             signal.signal(signal.SIGPOLL, self.__update)
    250             fcntl.ioctl(fd, STROPTS.I_SETSIG, STROPTS.S_MSG)
    251             self.__update()
    252         
    253     def __quit(self, event=None):
    254         self.__devctl.close()
    255         self.__root.quit()
    256 
    257     def __popup_about(self, event=None):
    258         import tkMessageBox
    259         tkMessageBox.showinfo('About Audiopy ' + __version__,
    260                               '''\
    261 Audiopy %s
    262 Control the Solaris audio device
    263 
    264 For information
    265 Contact: Barry A. Warsaw
    266 Email:   bwarsaw (at] python.org''' % __version__)
    267 
    268     def __popup_using(self, event=None):
    269         if not self.__helpwin:
    270             self.__helpwin = Helpwin(self.__tkroot, self.__quit)
    271         self.__helpwin.deiconify()
    272             
    273 
    274     def __keepalive(self):
    275         # Exercise the Python interpreter regularly so keyboard interrupts get
    276         # through.
    277         self.__tkroot.tk.createtimerhandler(KEEPALIVE_TIMER, self.__keepalive)
    278         if self.__needtopoll:
    279             self.__update()
    280 
    281     def __update(self, num=None, frame=None):
    282         # It's possible (although I have never seen it) to get an interrupted
    283         # system call during the getinfo() call.  If so, and we're polling,
    284         # don't sweat it because we'll come around again later.  Otherwise,
    285         # we'll give it a couple of tries and then give up until next time.
    286         tries = 0
    287         while 1:
    288             try:
    289                 info = self.__devctl.getinfo()
    290                 break
    291             except sunaudiodev.error:
    292                 if self.__needtopoll or tries > 3:
    293                     return
    294                 tries = tries + 1
    295         # input
    296         self.__inputvar.set(info.i_port)
    297         # output
    298         self.__spkvar.set(info.o_port & SPEAKER)
    299         self.__headvar.set(info.o_port & HEADPHONE)
    300         self.__linevar.set(info.o_port & LINE_OUT)
    301         # volume
    302         self.__scalevar.set(info.o_gain)
    303 
    304     def __pushtodev(self, event=None):
    305         info = self.__devctl.getinfo()
    306         info.o_port = self.__spkvar.get() + \
    307                       self.__headvar.get() + \
    308                       self.__linevar.get()
    309         info.i_port = self.__inputvar.get()
    310         info.o_gain = self.__scalevar.get()
    311         try:
    312             self.__devctl.setinfo(info)
    313         except sunaudiodev.error, msg:
    314             # TBD: what to do?  it's probably temporary.
    315             pass
    316 
    317     def __getset(self, var, onvalue):
    318         if var.get() == onvalue:
    319             var.set(0)
    320         else:
    321             var.set(onvalue)
    322         self.__pushtodev()
    323 
    324     def __none(self, event=None):
    325         self.__inputvar.set(0)
    326         self.__pushtodev()
    327 
    328     def __mic(self, event=None):
    329         self.__getset(self.__inputvar, MICROPHONE)
    330 
    331     def __linein(self, event=None):
    332         self.__getset(self.__inputvar, LINE_IN)
    333 
    334     def __cd(self, event=None):
    335         self.__getset(self.__inputvar, CD)
    336 
    337     def __speaker(self, event=None):
    338         self.__getset(self.__spkvar, SPEAKER)
    339 
    340     def __headphones(self, event=None):
    341         self.__getset(self.__headvar, HEADPHONE)
    342 
    343     def __lineout(self, event=None):
    344         self.__getset(self.__linevar, LINE_OUT)
    345 
    346     def __volume(self, event=None):
    347         self.__pushtodev()
    348 
    349     def start(self):
    350         self.__keepalive()
    351         self.__tkroot.mainloop()
    352 
    353 
    354 
    356 class Helpwin:
    357     def __init__(self, master, quitfunc):
    358         from Tkinter import *
    359         self.__root = root = Toplevel(master, class_='Audiopy')
    360         root.protocol('WM_DELETE_WINDOW', self.__withdraw)
    361         root.title('Audiopy Help Window')
    362         root.iconname('Audiopy Help Window')
    363         root.bind('<Alt-q>', quitfunc)
    364         root.bind('<Alt-Q>', quitfunc)
    365         root.bind('<Alt-w>', self.__withdraw)
    366         root.bind('<Alt-W>', self.__withdraw)
    367 
    368         # more elaborate help is available in the README file
    369         readmefile = os.path.join(sys.path[0], 'README')
    370         try:
    371             fp = None
    372             try:
    373                 fp = open(readmefile)
    374                 contents = fp.read()
    375                 # wax the last page, it contains Emacs cruft
    376                 i = contents.rfind('\f')
    377                 if i > 0:
    378                     contents = contents[:i].rstrip()
    379             finally:
    380                 if fp:
    381                     fp.close()
    382         except IOError:
    383             sys.stderr.write("Couldn't open audiopy's README, "
    384                              'using docstring instead.\n')
    385             contents = __doc__ % globals()
    386 
    387         self.__text = text = Text(root, relief=SUNKEN,
    388                                   width=80, height=24)
    389         text.insert(0.0, contents)
    390         scrollbar = Scrollbar(root)
    391         scrollbar.pack(fill=Y, side=RIGHT)
    392         text.pack(fill=BOTH, expand=YES)
    393         text.configure(yscrollcommand=(scrollbar, 'set'))
    394         scrollbar.configure(command=(text, 'yview'))
    395 
    396     def __withdraw(self, event=None):
    397         self.__root.withdraw()
    398 
    399     def deiconify(self):
    400         self.__root.deiconify()
    401 
    402 
    403 
    404 
    406 def usage(code, msg=''):
    407     print __doc__ % globals()
    408     if msg:
    409         print msg
    410     sys.exit(code)
    411 
    412 
    413 def main():
    414     #
    415     # Open up the audio control device and query for the current output
    416     # device
    417     device = sunaudiodev.open('control')
    418 
    419     if len(sys.argv) == 1:
    420         # GUI
    421         w = MainWindow(device)
    422         try:
    423             w.start()
    424         except KeyboardInterrupt:
    425             pass
    426         return
    427 
    428     # spec:    LONG OPT, SHORT OPT, 0=input,1=output, MASK
    429     options = [('--microphone', '-m', 0, MICROPHONE),
    430                ('--linein',     '-i', 0, LINE_IN),
    431                ('--headphones', '-p', 1, HEADPHONE),
    432                ('--speaker',    '-s', 1, SPEAKER),
    433                ('--lineout',    '-o', 1, LINE_OUT),
    434                ]
    435     # See the comment above about `CD'
    436     try:
    437         options.append(('--cd',         '-c', 0, CD))
    438     except NameError:
    439         pass
    440 
    441     info = device.getinfo()
    442     # first get the existing values
    443     i = 0
    444     while i < len(sys.argv)-1:
    445         i = i + 1
    446         arg = sys.argv[i]
    447         if arg in ('-h', '--help'):
    448             usage(0)
    449             # does not return
    450         elif arg in ('-g', '--gain'):
    451             gainspec = '<missing>'
    452             try:
    453                 gainspec = sys.argv[i+1]
    454                 gain = int(gainspec)
    455             except (ValueError, IndexError):
    456                 usage(1, 'Bad gain specification: ' + gainspec)
    457             info.o_gain = gain
    458             i = i + 1
    459             continue
    460         elif arg in ('-v', '--version'):
    461             print '''\
    462 audiopy -- a program to control the Solaris audio device.
    463 Contact: Barry Warsaw
    464 Email:   bwarsaw (at] python.org
    465 Version: %s''' % __version__            
    466             sys.exit(0)
    467         for long, short, io, mask in options:
    468             if arg in (long, short):
    469                 # toggle the option
    470                 if io == 0: 
    471                     info.i_port = info.i_port ^ mask
    472                 else:
    473                     info.o_port = info.o_port ^ mask
    474                 break
    475             val = None
    476             try:
    477                 if arg[:len(long)+1] == long+'=':
    478                     val = int(arg[len(long)+1:])
    479                 elif arg[:len(short)+1] == short+'=':
    480                     val = int(arg[len(short)+1:])
    481             except ValueError:
    482                 usage(1, msg='Invalid option: ' + arg)
    483                 # does not return
    484             if val == 0:
    485                 if io == 0:
    486                     info.i_port = info.i_port & ~mask
    487                 else:
    488                     info.o_port = info.o_port & ~mask
    489                 break
    490             elif val == 1:
    491                 if io == 0:
    492                     info.i_port = info.i_port | mask
    493                 else:
    494                     info.o_port = info.o_port | mask
    495                 break
    496             # else keep trying next option
    497         else:
    498             usage(1, msg='Invalid option: ' + arg)
    499     # now set the values
    500     try:
    501         device.setinfo(info)
    502     except sunaudiodev.error, (code, msg):
    503         if code <> errno.EINVAL:
    504             raise
    505     device.close()
    506             
    507 
    508 
    510 if __name__ == '__main__':
    511     main()
    512