# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

'''Linux ALSA audio implementation.
'''

__docformat__ = 'restructuredtext'
__version__ = '$Id: __init__.py 2084 2008-05-27 12:42:19Z Alex.Holkner $'

import ctypes

import pyglet
from pyglet.media import AudioPlayer, Listener, MediaException

from pyglet.media.drivers.alsa import asound

alsa_debug = None
if pyglet.options['debug_media']:
    alsa_debug = 'alsa.log'

class ALSAException(MediaException):
    pass

if alsa_debug:
    def check(err):
        if err < 0:
            raise ALSAException(asound.snd_strerror(err))
        return err
else:
    check = lambda v: v

class ALSAAudioPlayer(AudioPlayer):
    _device_name = 'default'
    _buffer_time = 0.3
    _min_write_bytes = 10000

    def __init__(self, audio_format):
        super(ALSAAudioPlayer, self).__init__(audio_format)

        format = {
            8:  asound.SND_PCM_FORMAT_U8,
            16: asound.SND_PCM_FORMAT_S16,
            24: asound.SND_PCM_FORMAT_S24,  # probably won't work
            32: asound.SND_PCM_FORMAT_S32
        }.get(audio_format.sample_size)
        if format is None:
            raise ALSAException('Unsupported audio format.')

        self.pcm = ctypes.POINTER(asound.snd_pcm_t)()
        self.hwparams = ctypes.POINTER(asound.snd_pcm_hw_params_t)()
        self.swparams = ctypes.POINTER(asound.snd_pcm_sw_params_t)()

        check(asound.snd_pcm_open(ctypes.byref(self.pcm),
                                  self._device_name,
                                  asound.SND_PCM_STREAM_PLAYBACK,
                                  asound.SND_PCM_NONBLOCK))
        check(asound.snd_pcm_hw_params_malloc(ctypes.byref(self.hwparams)))
        check(asound.snd_pcm_sw_params_malloc(ctypes.byref(self.swparams)))
        check(asound.snd_pcm_hw_params_any(self.pcm, self.hwparams))

        check(asound.snd_pcm_hw_params_set_access(self.pcm, self.hwparams,
            asound.SND_PCM_ACCESS_RW_INTERLEAVED))
        check(asound.snd_pcm_hw_params_set_format(self.pcm, self.hwparams,
            format))
        check(asound.snd_pcm_hw_params_set_channels(self.pcm, self.hwparams,
            audio_format.channels))

        rate = ctypes.c_uint(audio_format.sample_rate)
        dir = ctypes.c_int(0)
        check(asound.snd_pcm_hw_params_set_rate_near(self.pcm, self.hwparams,
            rate, dir))
        # Note actual sample rate is in rate.value.  Ignored for now because
        # difference is negligible.

        buffer_size = int(self._buffer_time * audio_format.sample_rate)
        bs = asound.snd_pcm_uframes_t(buffer_size)
        check(asound.snd_pcm_hw_params_set_buffer_size_near(self.pcm,
            self.hwparams, bs))
        # Actual buffer size is in bs.value.  Ignored for now.

        check(asound.snd_pcm_hw_params(self.pcm, self.hwparams))

        if alsa_debug:
            asound.snd_output_printf(debug_output,
                'New device: %s\n' % self._device_name)
            check(asound.snd_pcm_dump(self.pcm, debug_output))

        self.can_pause = asound.snd_pcm_hw_params_can_pause(self.hwparams)

        # List of (alsatime, timestamp)
        self._timestamps = []
        self._stop_alsatime = None
        self._eos_count = 0
        self._playing = False

    def __del__(self):
        try:
            check(asound.snd_pcm_close(self.pcm))
        except:
            pass

    def get_write_size(self):
        state = asound.snd_pcm_state(self.pcm)
        if state == asound.SND_PCM_STATE_PAUSED:
            return 0

        avail = max(0, asound.snd_pcm_avail_update(self.pcm))
        bytes = avail * self.audio_format.bytes_per_sample
        if bytes < self._min_write_bytes:
            return 0
        return bytes

    def write(self, audio_data):
        samples = audio_data.length // self.audio_format.bytes_per_sample
        samples_out = asound.snd_pcm_writei(self.pcm, audio_data.data,
                                            samples)
        if samples_out < 0:
            if samples_out == -11: # EAGAIN
                return
            elif samples_out == -32: # EPIPE (xrun)
                check(asound.snd_pcm_prepare(self.pcm))
                return
            else:
                raise ALSAException(asound.snd_strerror(samples_out))

        delay = asound.snd_pcm_sframes_t()
        check(asound.snd_pcm_delay(self.pcm, delay))
        alsatime = self._get_asound_time() + \
            delay.value / float(self.audio_format.sample_rate)

        self._timestamps.append((alsatime, audio_data.timestamp))

        audio_data.consume(samples_out * self.audio_format.bytes_per_sample,
                           self.audio_format)

    def write_eos(self):
        if self._timestamps:
            self._timestamps.append((None, None))

    def write_end(self):
        pass

    def play(self):
        if self._playing:
            return

        state = asound.snd_pcm_state(self.pcm)
        if self.can_pause and state == asound.SND_PCM_STATE_PAUSED:
            check(asound.snd_pcm_pause(self.pcm, 0))
        elif state not in (asound.SND_PCM_STATE_RUNNING,
                           asound.SND_PCM_STATE_PREPARED):
            check(asound.snd_pcm_prepare(self.pcm))
        self._playing = True

        if self._stop_alsatime is not None:
            diff = self._get_asound_time() - self._stop_alsatime
            self._timestamps = [(a + diff, t) for a, t in self._timestamps]
            self._stop_alsatime = None

    def stop(self):
        if not self._playing:
            return

        if self.can_pause and self._playing:
            check(asound.snd_pcm_pause(self.pcm, 1))
            self._stop_alsatime = self._get_asound_time()
        else:
            # Hardware can't pause, so we'll just drop everything that's
            # been buffered.  Improvement could be to rebuffer data that
            # wasn't played.
            self.clear()
        self._playing = False

    def clear(self):
        check(asound.snd_pcm_drop(self.pcm))
        self._stop_alsatime = None
        self._timestamps = []

    def _get_asound_time(self):
        status = ctypes.POINTER(asound.snd_pcm_status_t)()
        timestamp = asound.snd_timestamp_t()

        check(asound.snd_pcm_status_malloc(ctypes.byref(status)))
        check(asound.snd_pcm_status(self.pcm, status))
        asound.snd_pcm_status_get_tstamp(status, ctypes.byref(timestamp))
        asound.snd_pcm_status_free(status)
        return timestamp.tv_sec + timestamp.tv_usec * 0.000001

    def pump(self):
        underrun = False

        if self._stop_alsatime is not None:
            return underrun

        # Check that ALSA's still playing
        if self._playing:
            state = asound.snd_pcm_state(self.pcm)
            if state not in (asound.SND_PCM_STATE_RUNNING,
                             asound.SND_PCM_STATE_PREPARED):
                # Underrun!
                check(asound.snd_pcm_prepare(self.pcm))
                underrun = True

        alsatime = self._get_asound_time()
        try:
            while self._timestamps[0][0] < alsatime:
                self._timestamps.pop(0)
                while self._timestamps[0][0] is None:
                    self._eos_count += 1
                    self._timestamps.pop(0)
        except IndexError:
            pass

        return underrun

    def get_time(self):
        if self._stop_alsatime is None:
            alsatime = self._get_asound_time()
        else:
            alsatime = self._stop_alsatime

        if not self._timestamps:
            self._playing = False
            return 0.0

        alsats, ts = self._timestamps[0]
        t =  alsatime - alsats + ts
        return t

    def clear_eos(self):
        if self._eos_count > 0:
            self._eos_count -= 1
            return True
        return False

class ALSAListener(Listener):
    def _set_volume(self, volume):
        # TODO master volume
        self._volume = volume

    # All other properties are silently ignored.

    def _set_position(self, position):
        self._position = position

    def _set_velocity(self, velocity):
        self._velocity = velocity

    def _set_forward_orientation(self, orientation):
        self._forward_orientation = orientation

    def _set_up_orientation(self, orientation):
        self._up_orientation = orientation

    def _set_doppler_factor(self, factor):
        self._doppler_factor = factor

    def _set_speed_of_sound(self, speed_of_sound):
        self._speed_of_sound = speed_of_sound

def driver_init():
    global debug_output
    debug_output = ctypes.POINTER(asound.snd_output_t)()
    if alsa_debug:
        check(asound.snd_output_stdio_open(ctypes.byref(debug_output),
                                           alsa_debug,
                                           'w'))

driver_listener = ALSAListener()
driver_audio_player_class = ALSAAudioPlayer
