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