#! /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()
