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