| #! /usr/bin/env python | |
| """audiopy -- a program to control the Solaris audio device. | |
| Contact: Barry Warsaw | |
| Email: bwarsaw@python.org | |
| Version: %(__version__)s | |
| When no arguments are given, this pops up a graphical window which lets you | |
| choose the audio input and output devices, and set the output volume. | |
| This program can be driven via the command line, and when done so, no window | |
| pops up. Most options have the general form: | |
| --device[={0,1}] | |
| -d[={0,1}] | |
| Set the I/O device. With no value, it toggles the specified device. | |
| With a value, 0 turns the device off and 1 turns the device on. | |
| The list of devices and their short options are: | |
| (input) | |
| microphone -- m | |
| linein -- i | |
| cd -- c | |
| (output) | |
| headphones -- p | |
| speaker -- s | |
| lineout -- o | |
| Other options are: | |
| --gain volume | |
| -g volume | |
| Sets the output gain to the specified volume, which must be an integer | |
| in the range [%(MIN_GAIN)s..%(MAX_GAIN)s] | |
| --version | |
| -v | |
| Print the version number and exit. | |
| --help | |
| -h | |
| Print this message and exit. | |
| """ | |
| import sys | |
| import os | |
| import errno | |
| import sunaudiodev | |
| from SUNAUDIODEV import * | |
| # Milliseconds between interrupt checks | |
| KEEPALIVE_TIMER = 500 | |
| __version__ = '1.1' | |
| class MainWindow: | |
| def __init__(self, device): | |
| from Tkinter import * | |
| self.__helpwin = None | |
| self.__devctl = device | |
| info = device.getinfo() | |
| # | |
| self.__tkroot = tkroot = Tk(className='Audiopy') | |
| tkroot.withdraw() | |
| # create the menubar | |
| menubar = Menu(tkroot) | |
| filemenu = Menu(menubar, tearoff=0) | |
| filemenu.add_command(label='Quit', | |
| command=self.__quit, | |
| accelerator='Alt-Q', | |
| underline=0) | |
| helpmenu = Menu(menubar, name='help', tearoff=0) | |
| helpmenu.add_command(label='About Audiopy...', | |
| command=self.__popup_about, | |
| underline=0) | |
| helpmenu.add_command(label='Help...', | |
| command=self.__popup_using, | |
| underline=0) | |
| menubar.add_cascade(label='File', | |
| menu=filemenu, | |
| underline=0) | |
| menubar.add_cascade(label='Help', | |
| menu=helpmenu, | |
| underline=0) | |
| # now create the top level window | |
| root = self.__root = Toplevel(tkroot, class_='Audiopy', menu=menubar) | |
| root.protocol('WM_DELETE_WINDOW', self.__quit) | |
| root.title('audiopy ' + __version__) | |
| root.iconname('audiopy ' + __version__) | |
| root.tk.createtimerhandler(KEEPALIVE_TIMER, self.__keepalive) | |
| # | |
| buttons = [] | |
| # | |
| # where does input come from? | |
| frame = Frame(root, bd=1, relief=RAISED) | |
| frame.grid(row=1, column=0, sticky='NSEW') | |
| label = Label(frame, text='Input From:') | |
| label.grid(row=0, column=0, sticky=E) | |
| self.__inputvar = IntVar() | |
| ## | |
| btn = Radiobutton(frame, | |
| text='None', | |
| variable=self.__inputvar, | |
| value=0, | |
| command=self.__pushtodev, | |
| underline=0) | |
| btn.grid(row=0, column=1, sticky=W) | |
| root.bind('<Alt-n>', self.__none) | |
| root.bind('<Alt-N>', self.__none) | |
| if not info.i_avail_ports & MICROPHONE: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| ## | |
| btn = Radiobutton(frame, | |
| text='Microphone', | |
| variable=self.__inputvar, | |
| value=MICROPHONE, | |
| command=self.__pushtodev, | |
| underline=0) | |
| btn.grid(row=1, column=1, sticky=W) | |
| root.bind('<Alt-m>', self.__mic) | |
| root.bind('<Alt-M>', self.__mic) | |
| if not info.i_avail_ports & MICROPHONE: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| ## | |
| btn = Radiobutton(frame, | |
| text='Line In', | |
| variable=self.__inputvar, | |
| value=LINE_IN, | |
| command=self.__pushtodev, | |
| underline=5) | |
| btn.grid(row=2, column=1, sticky=W) | |
| root.bind('<Alt-i>', self.__linein) | |
| root.bind('<Alt-I>', self.__linein) | |
| if not info.i_avail_ports & LINE_IN: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| ## if SUNAUDIODEV was built on an older version of Solaris, the CD | |
| ## input device won't exist | |
| try: | |
| btn = Radiobutton(frame, | |
| text='CD', | |
| variable=self.__inputvar, | |
| value=CD, | |
| command=self.__pushtodev, | |
| underline=0) | |
| btn.grid(row=3, column=1, sticky=W) | |
| root.bind('<Alt-c>', self.__cd) | |
| root.bind('<Alt-C>', self.__cd) | |
| if not info.i_avail_ports & CD: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| except NameError: | |
| pass | |
| # | |
| # where does output go to? | |
| frame = Frame(root, bd=1, relief=RAISED) | |
| frame.grid(row=2, column=0, sticky='NSEW') | |
| label = Label(frame, text='Output To:') | |
| label.grid(row=0, column=0, sticky=E) | |
| self.__spkvar = IntVar() | |
| btn = Checkbutton(frame, | |
| text='Speaker', | |
| variable=self.__spkvar, | |
| onvalue=SPEAKER, | |
| command=self.__pushtodev, | |
| underline=0) | |
| btn.grid(row=0, column=1, sticky=W) | |
| root.bind('<Alt-s>', self.__speaker) | |
| root.bind('<Alt-S>', self.__speaker) | |
| if not info.o_avail_ports & SPEAKER: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| ## | |
| self.__headvar = IntVar() | |
| btn = Checkbutton(frame, | |
| text='Headphones', | |
| variable=self.__headvar, | |
| onvalue=HEADPHONE, | |
| command=self.__pushtodev, | |
| underline=4) | |
| btn.grid(row=1, column=1, sticky=W) | |
| root.bind('<Alt-p>', self.__headphones) | |
| root.bind('<Alt-P>', self.__headphones) | |
| if not info.o_avail_ports & HEADPHONE: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| ## | |
| self.__linevar = IntVar() | |
| btn = Checkbutton(frame, | |
| variable=self.__linevar, | |
| onvalue=LINE_OUT, | |
| text='Line Out', | |
| command=self.__pushtodev, | |
| underline=0) | |
| btn.grid(row=2, column=1, sticky=W) | |
| root.bind('<Alt-l>', self.__lineout) | |
| root.bind('<Alt-L>', self.__lineout) | |
| if not info.o_avail_ports & LINE_OUT: | |
| btn.configure(state=DISABLED) | |
| buttons.append(btn) | |
| # | |
| # Fix up widths | |
| widest = 0 | |
| for b in buttons: | |
| width = b['width'] | |
| if width > widest: | |
| widest = width | |
| for b in buttons: | |
| b.configure(width=widest) | |
| # root bindings | |
| root.bind('<Alt-q>', self.__quit) | |
| root.bind('<Alt-Q>', self.__quit) | |
| # | |
| # Volume | |
| frame = Frame(root, bd=1, relief=RAISED) | |
| frame.grid(row=3, column=0, sticky='NSEW') | |
| label = Label(frame, text='Output Volume:') | |
| label.grid(row=0, column=0, sticky=W) | |
| self.__scalevar = IntVar() | |
| self.__scale = Scale(frame, | |
| orient=HORIZONTAL, | |
| from_=MIN_GAIN, | |
| to=MAX_GAIN, | |
| length=200, | |
| variable=self.__scalevar, | |
| command=self.__volume) | |
| self.__scale.grid(row=1, column=0, sticky=EW) | |
| # | |
| # do we need to poll for changes? | |
| self.__needtopoll = 1 | |
| try: | |
| fd = self.__devctl.fileno() | |
| self.__needtopoll = 0 | |
| except AttributeError: | |
| pass | |
| else: | |
| import fcntl | |
| import signal | |
| import STROPTS | |
| # set up the signal handler | |
| signal.signal(signal.SIGPOLL, self.__update) | |
| fcntl.ioctl(fd, STROPTS.I_SETSIG, STROPTS.S_MSG) | |
| self.__update() | |
| def __quit(self, event=None): | |
| self.__devctl.close() | |
| self.__root.quit() | |
| def __popup_about(self, event=None): | |
| import tkMessageBox | |
| tkMessageBox.showinfo('About Audiopy ' + __version__, | |
| '''\ | |
| Audiopy %s | |
| Control the Solaris audio device | |
| For information | |
| Contact: Barry A. Warsaw | |
| Email: bwarsaw@python.org''' % __version__) | |
| def __popup_using(self, event=None): | |
| if not self.__helpwin: | |
| self.__helpwin = Helpwin(self.__tkroot, self.__quit) | |
| self.__helpwin.deiconify() | |
| def __keepalive(self): | |
| # Exercise the Python interpreter regularly so keyboard interrupts get | |
| # through. | |
| self.__tkroot.tk.createtimerhandler(KEEPALIVE_TIMER, self.__keepalive) | |
| if self.__needtopoll: | |
| self.__update() | |
| def __update(self, num=None, frame=None): | |
| # It's possible (although I have never seen it) to get an interrupted | |
| # system call during the getinfo() call. If so, and we're polling, | |
| # don't sweat it because we'll come around again later. Otherwise, | |
| # we'll give it a couple of tries and then give up until next time. | |
| tries = 0 | |
| while 1: | |
| try: | |
| info = self.__devctl.getinfo() | |
| break | |
| except sunaudiodev.error: | |
| if self.__needtopoll or tries > 3: | |
| return | |
| tries = tries + 1 | |
| # input | |
| self.__inputvar.set(info.i_port) | |
| # output | |
| self.__spkvar.set(info.o_port & SPEAKER) | |
| self.__headvar.set(info.o_port & HEADPHONE) | |
| self.__linevar.set(info.o_port & LINE_OUT) | |
| # volume | |
| self.__scalevar.set(info.o_gain) | |
| def __pushtodev(self, event=None): | |
| info = self.__devctl.getinfo() | |
| info.o_port = self.__spkvar.get() + \ | |
| self.__headvar.get() + \ | |
| self.__linevar.get() | |
| info.i_port = self.__inputvar.get() | |
| info.o_gain = self.__scalevar.get() | |
| try: | |
| self.__devctl.setinfo(info) | |
| except sunaudiodev.error, msg: | |
| # TBD: what to do? it's probably temporary. | |
| pass | |
| def __getset(self, var, onvalue): | |
| if var.get() == onvalue: | |
| var.set(0) | |
| else: | |
| var.set(onvalue) | |
| self.__pushtodev() | |
| def __none(self, event=None): | |
| self.__inputvar.set(0) | |
| self.__pushtodev() | |
| def __mic(self, event=None): | |
| self.__getset(self.__inputvar, MICROPHONE) | |
| def __linein(self, event=None): | |
| self.__getset(self.__inputvar, LINE_IN) | |
| def __cd(self, event=None): | |
| self.__getset(self.__inputvar, CD) | |
| def __speaker(self, event=None): | |
| self.__getset(self.__spkvar, SPEAKER) | |
| def __headphones(self, event=None): | |
| self.__getset(self.__headvar, HEADPHONE) | |
| def __lineout(self, event=None): | |
| self.__getset(self.__linevar, LINE_OUT) | |
| def __volume(self, event=None): | |
| self.__pushtodev() | |
| def start(self): | |
| self.__keepalive() | |
| self.__tkroot.mainloop() | |
| class Helpwin: | |
| def __init__(self, master, quitfunc): | |
| from Tkinter import * | |
| self.__root = root = Toplevel(master, class_='Audiopy') | |
| root.protocol('WM_DELETE_WINDOW', self.__withdraw) | |
| root.title('Audiopy Help Window') | |
| root.iconname('Audiopy Help Window') | |
| root.bind('<Alt-q>', quitfunc) | |
| root.bind('<Alt-Q>', quitfunc) | |
| root.bind('<Alt-w>', self.__withdraw) | |
| root.bind('<Alt-W>', self.__withdraw) | |
| # more elaborate help is available in the README file | |
| readmefile = os.path.join(sys.path[0], 'README') | |
| try: | |
| fp = None | |
| try: | |
| fp = open(readmefile) | |
| contents = fp.read() | |
| # wax the last page, it contains Emacs cruft | |
| i = contents.rfind('\f') | |
| if i > 0: | |
| contents = contents[:i].rstrip() | |
| finally: | |
| if fp: | |
| fp.close() | |
| except IOError: | |
| sys.stderr.write("Couldn't open audiopy's README, " | |
| 'using docstring instead.\n') | |
| contents = __doc__ % globals() | |
| self.__text = text = Text(root, relief=SUNKEN, | |
| width=80, height=24) | |
| text.insert(0.0, contents) | |
| scrollbar = Scrollbar(root) | |
| scrollbar.pack(fill=Y, side=RIGHT) | |
| text.pack(fill=BOTH, expand=YES) | |
| text.configure(yscrollcommand=(scrollbar, 'set')) | |
| scrollbar.configure(command=(text, 'yview')) | |
| def __withdraw(self, event=None): | |
| self.__root.withdraw() | |
| def deiconify(self): | |
| self.__root.deiconify() | |
| def usage(code, msg=''): | |
| print __doc__ % globals() | |
| if msg: | |
| print msg | |
| sys.exit(code) | |
| def main(): | |
| # | |
| # Open up the audio control device and query for the current output | |
| # device | |
| device = sunaudiodev.open('control') | |
| if len(sys.argv) == 1: | |
| # GUI | |
| w = MainWindow(device) | |
| try: | |
| w.start() | |
| except KeyboardInterrupt: | |
| pass | |
| return | |
| # spec: LONG OPT, SHORT OPT, 0=input,1=output, MASK | |
| options = [('--microphone', '-m', 0, MICROPHONE), | |
| ('--linein', '-i', 0, LINE_IN), | |
| ('--headphones', '-p', 1, HEADPHONE), | |
| ('--speaker', '-s', 1, SPEAKER), | |
| ('--lineout', '-o', 1, LINE_OUT), | |
| ] | |
| # See the comment above about `CD' | |
| try: | |
| options.append(('--cd', '-c', 0, CD)) | |
| except NameError: | |
| pass | |
| info = device.getinfo() | |
| # first get the existing values | |
| i = 0 | |
| while i < len(sys.argv)-1: | |
| i = i + 1 | |
| arg = sys.argv[i] | |
| if arg in ('-h', '--help'): | |
| usage(0) | |
| # does not return | |
| elif arg in ('-g', '--gain'): | |
| gainspec = '<missing>' | |
| try: | |
| gainspec = sys.argv[i+1] | |
| gain = int(gainspec) | |
| except (ValueError, IndexError): | |
| usage(1, 'Bad gain specification: ' + gainspec) | |
| info.o_gain = gain | |
| i = i + 1 | |
| continue | |
| elif arg in ('-v', '--version'): | |
| print '''\ | |
| audiopy -- a program to control the Solaris audio device. | |
| Contact: Barry Warsaw | |
| Email: bwarsaw@python.org | |
| Version: %s''' % __version__ | |
| sys.exit(0) | |
| for long, short, io, mask in options: | |
| if arg in (long, short): | |
| # toggle the option | |
| if io == 0: | |
| info.i_port = info.i_port ^ mask | |
| else: | |
| info.o_port = info.o_port ^ mask | |
| break | |
| val = None | |
| try: | |
| if arg[:len(long)+1] == long+'=': | |
| val = int(arg[len(long)+1:]) | |
| elif arg[:len(short)+1] == short+'=': | |
| val = int(arg[len(short)+1:]) | |
| except ValueError: | |
| usage(1, msg='Invalid option: ' + arg) | |
| # does not return | |
| if val == 0: | |
| if io == 0: | |
| info.i_port = info.i_port & ~mask | |
| else: | |
| info.o_port = info.o_port & ~mask | |
| break | |
| elif val == 1: | |
| if io == 0: | |
| info.i_port = info.i_port | mask | |
| else: | |
| info.o_port = info.o_port | mask | |
| break | |
| # else keep trying next option | |
| else: | |
| usage(1, msg='Invalid option: ' + arg) | |
| # now set the values | |
| try: | |
| device.setinfo(info) | |
| except sunaudiodev.error, (code, msg): | |
| if code <> errno.EINVAL: | |
| raise | |
| device.close() | |
| if __name__ == '__main__': | |
| main() |