#!/usr/bin/python

# tz_audio_player.py
#       --copyright--                   Copyright 2010 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       February 10, 2010       bar
#       February 11, 2010       bar     more verbose control and fix is_silent() to see the output buffer
#       February 13, 2010       bar     don't sleep at file-wrap time
#       February 14, 2010       bar     yet another printout
#       February 24, 2010       bar     set_volume()
#       March 6, 2010           bar     give /dev/dsp a half second to be non-busy
#       March 7, 2010           bar     don't restart over-ridden, non-repeat sounds
#                                       show unspec'd exceptions
#                                       put a stray print statement inside an if_verbose
#       March 8, 2010           bar     put a nice continue in an exception sitation while feeding sample to the device
#                                       apparently the test version that didn't reset the device when a clip overrides another got checked in to svn. maybe. somewhere.
#       November 29, 2011       bar     pyflake cleanup
#       May 27, 2012            bar     doxygen namespace
#       August 12, 2012         bar     put name in thread
#       May 28, 2014            bar     put thread id in threads
#       --eodstamps--
##      \file
#       \namespace              tzpython.tz_audio_player
#
#
#       Audio player.
#
#


import  array
import  Queue
import  sys
import  threading
import  time
import  wave

# import  fcntl
# import  struct

try                 :
    import  ossaudiodev
except  ImportError :
    ossaudiodev     = None

import  tz_wave
import  tzlib



def is_able_to_play() :
    return(ossaudiodev != None)



class   a_clip(object) :
    def __init__(me, channels = 2, sample_rate = 44100, sample_width = 2, samples = "", name = None, repeat = False) :
        me.channels     = channels          or 2
        me.sample_rate  = sample_rate       or 44100
        me.sample_width = sample_width      or 2
        me.samples      = samples           or ""
        me.name         = name
        me.repeat       = repeat            or False
        me.sil_smp      = ((me.sample_width == 1) and 128) or 0     # if 1 byte samples, assume unsigned

    def is_same_type(me, ome) :
        return((me.channels == ome.channels) and (me.sample_rate == ome.sample_rate) and (me.sample_width == ome.sample_width))

    #   a_clip


def read_clip_from_wave_file(fn, repeat = False) :
    ( wavs, sr, sw )    = tz_wave.read_wave(fn)
    ch          = len(wavs) or 1
    # print "srsw", fn, sr, sw, ch, len(wavs[0]), wave._array_fmts[sw]
    if  ch  > 1     :
        wav = []
        for w in zip(*wavs):
            wav    += w                             # note that this '+=' is not 'append'
        pass
    else    :
        wav = [ s + 128 for s in wavs[0]    ]
    ws      = array.array(wave._array_fmts[sw], wav)
    ws      = ws.tostring()

    return(a_clip(ch, sr, sw, ws, fn, repeat = repeat))




class   a_playable_clip(object) :                                   # what gets queued for a_player
    def __init__(me, clip, repeat = False, quit = False) :
        me.clip     = clip
        if  (repeat == None) and me.clip :
            repeat  = me.clip.repeat
        me.repeat   = repeat
        me.quit     = quit
        me.si       = 0                                             # how many samples of clip we've put in audio buffer
    #   a_playable_clip



class   a_player(threading.Thread) :

    def __init__(me, verbose = 0, *args, **kwargs) :
        kwargs['name']  = kwargs.get('name', "tz_audio_player.a_player")
        super(a_player, me).__init__(args = args, kwargs = kwargs)

        me.verbose  = verbose or 0

        me.tid      = None

        me.a        = None

        me.stack    = []

        me.q        = Queue.Queue()

        me.setDaemon(True)


    def _show_exception(me) :
        if  me.verbose :
            tzlib.print_exception()
            print >> sys.stderr, "Ignored in tz_audio_player"
        pass


    def _shhhh(me)  :
        a           = me.a
        if  a       :
            try     :
                a.reset()
            except  ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                me._show_exception()
            except          :
                if  me.verbose :
                    tzlib.print_exception()
                raise
            pass
        pass


    def _close(me) :
        me.cr       = None          # forget what type of samples we're playing in any case

        ( a, me.a ) = ( me.a, None )
        if  a :
            try     :
                a.close()
            except  ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                me._show_exception()
            except          :
                if  me.verbose :
                    tzlib.print_exception()
                raise
            pass
        pass


    def _start_playing(me, clip) :
        if  ossaudiodev :
            if  me.a    :
                return(True)

            t                   = tzlib.elapsed_time()
            while   True        :
                nt              = tzlib.elapsed_time()

                try             :
                    me.a        = ossaudiodev.open("w")

                    # ii   =  fcntl.ioctl(me.a, ossaudiodev.SNDCTL_DSP_SETFRAGMENT, array.array('I', [ 0x7fff000f ]), 1)      # does not affect bufsize on spring

                    me.a.setfmt(((clip.sample_width == 1) and ossaudiodev.AFMT_U8) or ossaudiodev.AFMT_S16_LE)
                    me.a.channels(clip.channels)
                    me.a.speed(   clip.sample_rate)

                    # print "buffsize", me.a.bufsize()

                    return(True)

                except  ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                    me._show_exception()
                    me._close()

                except          :
                    try         :
                        me._close()
                    except      :
                        pass
                    if  me.verbose :
                        tzlib.print_exception()
                    raise

                if  nt - t > 0.5 :
                    break
                time.sleep(0.05)
            pass

        return(False)


    def run(me)     :
        me.tid      = tzlib.get_tid()
        if  me.verbose :
            print "tz_audio_player threadid %d" % ( tzlib.get_tid() )

        while True  :
            rc      = None
            try     :
                while True  :
                    if  me.a    :
                        cr      = me.q.get_nowait()
                    else        :
                        cr      = me.q.get(True)

                    rc          = cr

                    if  (not cr.clip) or cr.quit :
                        ccr     = (len(me.stack) and me.stack[-1]) or None
                        if  (me.verbose > 1) and ccr and ccr.clip :
                            print "tz_audio_player stopping %s" % str(ccr.clip.name)

                        if  len(me.stack)   :
                            me.stack.pop()

                        if  not cr.repeat   :
                            me.stack        = []

                        me._shhhh()
                        me._close()
                        if  cr.quit         :
                            if  me.verbose > 1 :
                                print "tz_audio_player closing from %s" % str((ccr and ccr.clip and ccr.clip.name) or None)

                            break

                        if  not len(me.stack) :
                            if  me.verbose > 1 :
                                print "tz_audio_player resetting from %s" % str((ccr and ccr.clip and ccr.clip.name) or None)
                            pass
                        else        :
                            cr      = me.stack[-1]
                            me._start_playing(cr.clip)
                            me.cr   = cr
                        pass
                    elif me.a and me.cr and me.cr.clip and (not cr.clip.is_same_type(me.cr.clip)) :
                        raise ValueError("Clip not same format as clip playing [%s] [%s]" % ( str(cr.clip.name), str(me.cr.clip.name) ) )
                    else                :
                        me._shhhh()
                        me._close()
                        if  me._start_playing(cr.clip) :
                            me.cr       = cr
                            me.cr.si    = 0
                            while len(me.stack) :
                                ocr     = me.stack[-1]
                                if  ocr.repeat  :
                                    ocr.si      = 0         # start the overridden repeat sound at the beginning, if there was one (should start where it left off - or whatever)
                                    break
                                me.stack.pop()              # but, if the overridden sound is not repeat, forget it.
                            me.stack.append(me.cr)
                            if me.verbose > 1   :
                                print "tz_audio_player playing %srepeating %s" % ( (((not me.cr.repeat) and "non-") or ""), str(me.cr.clip.name), )
                            pass
                        pass
                    pass
                pass
            except Queue.Empty :
                pass

            if  me.a        :
                me.cr       = me.stack[-1]
                cr          = me.cr
                c           = cr.clip

                bc          = 0
                fr          = me.a.obuffree()
                ei          = cr.si + (fr * c.channels * c.sample_width)
                try         :
                    bc      = me.a.write(c.samples[cr.si : ei])
                    if  me.verbose > 3 :
                        print "tz_audio_player sounding %u-%u=%u %s" % ( ei, cr.si, ei - cr.si, str(cr.clip.name) )
                    pass
                except  ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                    me._show_exception()
                    me.stack    = []
                    me._close()
                    continue
                except          :
                    me._close()
                    raise
                if  (me.verbose > 3) and (not cr.si) :
                    print "tz_audio_player sounded %u of %u %s" % ( bc, ei - cr.si, str(cr.clip.name) )

                fl          = bc / c.channels / c.sample_width
                if  fr >= me.a.bufsize() / 2 :
                    rc      = cr                                    # don't sleep
                    if  me.verbose > 2 :
                        print "tz_audio_player filled %u of %u in %u %s" % ( fl, fr, me.a.bufsize(), str(cr.clip.name) )
                    pass

                cr.si      += fl
                if  cr.si  >= len(c.samples) :
                    rc      = cr                                    # don't sleep
                    cr.si   = 0
                    if  not cr.repeat  :
                        if  len(me.stack) :
                            me.stack.pop()

                        if  not len(me.stack) :
                            me._close()                             # !!!! blocking - letting the sound finish - or does it just preclude re-opening again quickly?
                            if  me.verbose > 1 :
                                print "tz_audio_player ended %s"  % str(cr.clip.name)
                            pass
                        else        :
                            me.cr   = me.stack[-1]
                            cr      = me.cr
                            if  me.verbose > 1 :
                                print "tz_audio_player restarting at %u %s"  % ( cr.si, str(cr.clip.name) )
                            pass
                        pass
                    elif me.verbose > 1 :
                        print "tz_audio_player repeating %s"  % str(cr.clip.name)
                    pass

                if  not rc  :
                    time.sleep(0.05)
                pass
            pass
        pass


    def is_silent(me) :
        a   = me.a
        return((not me.stack) and ((not a) or (a.obuffree() == a.bufsize())))


    def play(me, clip, repeat = None) :
        if  repeat == None :
            repeat  = clip.repeat or False
        me.q.put(a_playable_clip(clip, repeat = repeat))


    def stop_and_restore_previous(me) :
        me.q.put(a_playable_clip(None, repeat = True))


    def be_quiet(me) :
        me.q.put(a_playable_clip(None, repeat = False))


    def set_volume(me, percent = None) :
        retval  = [ -1, -1 ]
        try :
            percent[0]
        except TypeError :
            percent = [ percent, percent ]
        try :
            percent[1]
        except TypeError :
            percent.append(percent[0])


        if  (percent[0] != None) or (percent[1] != None) :
            try :
                mx  = ossaudiodev.openmixer()
                try :
                    retval  = mx.get(ossaudiodev.SOUND_MIXER_VOLUME)

                    try :
                        if (percent[0] == None) or (not (0 <= percent[0] <= 100)) :
                            percent[0]  = retval[0]
                        if (percent[1] == None) or (not (0 <= percent[1] <= 100)) :
                            percent[1]  = retval[1]
                        mx.set(ossaudiodev.SOUND_MIXER_VOLUME,  percent)                    # these just don't unmute. In fact, the whole volume control is weird - maybe with the toolbar widget getting in way?
                        mx.set(ossaudiodev.SOUND_MIXER_SPEAKER, percent)
                        mx.set(ossaudiodev.SOUND_MIXER_PCM,     percent)
                        mx.set(ossaudiodev.SOUND_MIXER_OGAIN,   percent)
                        mx.close()
                    except  ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                        me._show_exception()
                        mx.close()
                    pass
                except  ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                        me._show_exception()
                        mx.close()
                pass
            except      ( ossaudiodev.OSSAudioError, IOError, OSError ) :
                        me._show_exception()
            pass

        return(retval)



    def close(me) :
        me.q.put(a_playable_clip(None, repeat = False, quit = True))
        try :
            me.join(1.0)
        except RuntimeError :
            pass
        pass


    def is_able_to_play(me) :
        global is_able_to_play
        return(is_able_to_play())


    #   a_player


#
#
#       Test code
#
#
if __name__ == '__main__' :

    import  copy

    import  TZCommandLineAtFile
    import  TZKeyReady


    program_name    = sys.argv.pop(0)

    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)

    if  not is_able_to_play() :
        print "Only runs with OSS - Unix systems!"
    else :
        me              = a_player(verbose = 1)
        me.start()

        volume          = me.set_volume()

        n               = 0
        while len(sys.argv) :
            fn          = sys.argv.pop(0)

            clip        = read_clip_from_wave_file(fn, repeat = not ((n and 1) or 0) )

            me.play(clip)

            n          += 1


        while True :

            k   = TZKeyReady.key_ready()
            if  k :
                kl  = k.lower()

                if  kl == '\x1b' :
                    break
                if  kl == 'q' :
                    break

                if  kl == 's' :
                    me.be_quiet()

                if  kl == 'm' :
                    me.stop_and_restore_previous()

                if  kl == 'r' :
                    me.play(copy.deepcopy(clip))

                if  kl == 'u' :
                    print me.set_volume(100)

                if  kl == 'd' :
                    print me.set_volume(0)

                pass

            time.sleep(0.1)

        me.set_volume(volume)

        me.close()

    pass


#
#
# eof
