text: # ----------------------------------------------------------------------------
  # pyglet
  # Copyright (c) 2006-2008 Alex Holkner
  # Copyright (c) 2008-2020 pyglet contributors
  # 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.
  # ----------------------------------------------------------------------------
  
  import io
  
  from pyglet.media.exceptions import MediaException, CannotSeekException
  
  
  class AudioFormat:
      """Audio details.
  
      An instance of this class is provided by sources with audio tracks.  You
      should not modify the fields, as they are used internally to describe the
      format of data provided by the source.
  
      Args:
          channels (int): The number of channels: 1 for mono or 2 for stereo
              (pyglet does not yet support surround-sound sources).
          sample_size (int): Bits per sample; only 8 or 16 are supported.
          sample_rate (int): Samples per second (in Hertz).
      """
  
      def __init__(self, channels, sample_size, sample_rate):
          self.channels = channels
          self.sample_size = sample_size
          self.sample_rate = sample_rate
  
          # Convenience
          self.bytes_per_sample = (sample_size >> 3) * channels
          self.bytes_per_second = self.bytes_per_sample * sample_rate
  
      def __eq__(self, other):
          if other is None:
              return False
          return (self.channels == other.channels and
                  self.sample_size == other.sample_size and
                  self.sample_rate == other.sample_rate)
  
      def __ne__(self, other):
          return not self.__eq__(other)
  
      def __repr__(self):
          return '%s(channels=%d, sample_size=%d, sample_rate=%d)' % (
              self.__class__.__name__, self.channels, self.sample_size,
              self.sample_rate)
  
  
  class VideoFormat:
      """Video details.
  
      An instance of this class is provided by sources with a video stream. You
      should not modify the fields.
  
      Note that the sample aspect has no relation to the aspect ratio of the
      video image.  For example, a video image of 640x480 with sample aspect 2.0
      should be displayed at 1280x480.  It is the responsibility of the
      application to perform this scaling.
  
      Args:
          width (int): Width of video image, in pixels.
          height (int): Height of video image, in pixels.
          sample_aspect (float): Aspect ratio (width over height) of a single
              video pixel.
          frame_rate (float): Frame rate (frames per second) of the video.
  
              .. versionadded:: 1.2
      """
  
      def __init__(self, width, height, sample_aspect=1.0):
          self.width = width
          self.height = height
          self.sample_aspect = sample_aspect
          self.frame_rate = None
  
      def __eq__(self, other):
          if isinstance(other, VideoFormat):
              return (self.width == other.width and
                      self.height == other.height and
                      self.sample_aspect == other.sample_aspect and
                      self.frame_rate == other.frame_rate)
          return False
  
  
  class AudioData:
      """A single packet of audio data.
  
      This class is used internally by pyglet.
  
      Args:
          data (str or ctypes array or pointer): Sample data.
          length (int): Size of sample data, in bytes.
          timestamp (float): Time of the first sample, in seconds.
          duration (float): Total data duration, in seconds.
          events (List[:class:`pyglet.media.events.MediaEvent`]): List of events
              contained within this packet. Events are timestamped relative to
              this audio packet.
      """
  
      __slots__ = 'data', 'length', 'timestamp', 'duration', 'events'
  
      def __init__(self, data, length, timestamp, duration, events):
          self.data = data
          self.length = length
          self.timestamp = timestamp
          self.duration = duration
          self.events = events
  
      def __eq__(self, other):
          if isinstance(other, AudioData):
              return (self.data == other.data and
                      self.length == other.length and
                      self.timestamp == other.timestamp and
                      self.duration == other.duration and
                      self.events == other.events)
          return False
  
      def consume(self, num_bytes, audio_format):
          """Remove some data from the beginning of the packet.
  
          All events are cleared.
  
          Args:
              num_bytes (int): The number of bytes to consume from the packet.
              audio_format (:class:`.AudioFormat`): The packet audio format.
          """
          self.events = ()
          if num_bytes >= self.length:
              self.data = None
              self.length = 0
              self.timestamp += self.duration
              self.duration = 0.
              return
          elif num_bytes == 0:
              return
  
          self.data = self.data[num_bytes:]
          self.length -= num_bytes
          self.duration -= num_bytes / audio_format.bytes_per_second
          self.timestamp += num_bytes / audio_format.bytes_per_second
  
      def get_string_data(self):
          """Return data as a bytestring.
  
          Returns:
              bytes: Data as a (byte)string.
          """
          if self.data is None:
              return b''
  
          return memoryview(self.data).tobytes()[:self.length]
  
  
  class SourceInfo:
      """Source metadata information.
  
      Fields are the empty string or zero if the information is not available.
  
      Args:
          title (str): Title
          author (str): Author
          copyright (str): Copyright statement
          comment (str): Comment
          album (str): Album name
          year (int): Year
          track (int): Track number
          genre (str): Genre
  
      .. versionadded:: 1.2
      """
  
      title = ''
      author = ''
      copyright = ''
      comment = ''
      album = ''
      year = 0
      track = 0
      genre = ''
  
  
  class Source:
      """An audio and/or video source.
  
      Args:
          audio_format (:class:`.AudioFormat`): Format of the audio in this
              source, or ``None`` if the source is silent.
          video_format (:class:`.VideoFormat`): Format of the video in this
              source, or ``None`` if there is no video.
          info (:class:`.SourceInfo`): Source metadata such as title, artist,
              etc; or ``None`` if the` information is not available.
  
              .. versionadded:: 1.2
  
      Attributes:
          is_player_source (bool): Determine if this source is a player
              current source.
  
              Check on a :py:class:`~pyglet.media.player.Player` if this source
              is the current source.
      """
  
      _duration = None
      _players = []  # List of players when calling Source.play
  
      audio_format = None
      video_format = None
      info = None
      is_player_source = False
  
      @property
      def duration(self):
          """float: The length of the source, in seconds.
  
          Not all source durations can be determined; in this case the value
          is ``None``.
  
          Read-only.
          """
          return self._duration
  
      def play(self):
          """Play the source.
  
          This is a convenience method which creates a Player for
          this source and plays it immediately.
  
          Returns:
              :class:`.Player`
          """
          from pyglet.media.player import Player  # XXX Nasty circular dependency
          player = Player()
          player.queue(self)
          player.play()
          Source._players.append(player)
  
          def _on_player_eos():
              Source._players.remove(player)
              # There is a closure on player. To get the refcount to 0,
              # we need to delete this function.
              player.on_player_eos = None
  
          player.on_player_eos = _on_player_eos
          return player
  
      def get_animation(self):
          """
          Import all video frames into memory.
  
          An empty animation will be returned if the source has no video.
          Otherwise, the animation will contain all unplayed video frames (the
          entire source, if it has not been queued on a player). After creating
          the animation, the source will be at EOS (end of stream).
  
          This method is unsuitable for videos running longer than a
          few seconds.
  
          .. versionadded:: 1.1
  
          Returns:
              :class:`pyglet.image.Animation`
          """
          from pyglet.image import Animation, AnimationFrame
          if not self.video_format:
              # XXX: This causes an assertion in the constructor of Animation
              return Animation([])
          else:
              frames = []
              last_ts = 0
              next_ts = self.get_next_video_timestamp()
              while next_ts is not None:
                  image = self.get_next_video_frame()
                  if image is not None:
                      delay = next_ts - last_ts
                      frames.append(AnimationFrame(image, delay))
                      last_ts = next_ts
                  next_ts = self.get_next_video_timestamp()
              return Animation(frames)
  
      def get_next_video_timestamp(self):
          """Get the timestamp of the next video frame.
  
          .. versionadded:: 1.1
  
          Returns:
              float: The next timestamp, or ``None`` if there are no more video
              frames.
          """
          pass
  
      def get_next_video_frame(self):
          """Get the next video frame.
  
          .. versionadded:: 1.1
  
          Returns:
              :class:`pyglet.image.AbstractImage`: The next video frame image,
              or ``None`` if the video frame could not be decoded or there are
              no more video frames.
          """
          pass
  
      # Internal methods that Player calls on the source:
  
      def seek(self, timestamp):
          """Seek to given timestamp.
  
          Args:
              timestamp (float): Time where to seek in the source. The
                  ``timestamp`` will be clamped to the duration of the source.
          """
          raise CannotSeekException()
  
      def get_queue_source(self):
          """Return the ``Source`` to be used as the queue source for a player.
  
          Default implementation returns self.
          """
          return self
  
      def get_audio_data(self, num_bytes, compensation_time=0.0):
          """Get next packet of audio data.
  
          Args:
              num_bytes (int): Maximum number of bytes of data to return.
              compensation_time (float): Time in sec to compensate due to a
                  difference between the master clock and the audio clock.
  
          Returns:
              :class:`.AudioData`: Next packet of audio data, or ``None`` if
              there is no (more) data.
          """
          return None
  
  
  class StreamingSource(Source):
      """A source that is decoded as it is being played.
  
      The source can only be played once at a time on any
      :class:`~pyglet.media.player.Player`.
      """
  
      @property
      def is_queued(self):
          """
          bool: Determine if this source is a player current source.
  
          Check on a :py:class:`~pyglet.media.player.Player` if this source
          is the current source.
  
          :deprecated: Use :attr:`is_player_source` instead.
          """
          return self.is_player_source
  
      def get_queue_source(self):
          """Return the ``Source`` to be used as the source for a player.
  
          Default implementation returns self.
  
          Returns:
              :class:`.Source`
          """
          if self.is_player_source:
              raise MediaException('This source is already queued on a player.')
          self.is_player_source = True
          return self
  
      def delete(self):
          """Release the resources held by this StreamingSource."""
          pass
  
  
  class StaticSource(Source):
      """A source that has been completely decoded in memory.
  
      This source can be queued onto multiple players any number of times.
  
      Construct a :py:class:`~pyglet.media.StaticSource` for the data in
      ``source``.
  
      Args:
          source (Source):  The source to read and decode audio and video data
              from.
      """
  
      def __init__(self, source):
          source = source.get_queue_source()
          if source.video_format:
              raise NotImplementedError('Static sources not supported for video.')
  
          self.audio_format = source.audio_format
          if not self.audio_format:
              self._data = None
              self._duration = 0.
              return
  
          # Arbitrary: number of bytes to request at a time.
          buffer_size = 1 << 20  # 1 MB
  
          # Naive implementation.  Driver-specific implementations may override
          # to load static audio data into device (or at least driver) memory.
          data = io.BytesIO()
          while True:
              audio_data = source.get_audio_data(buffer_size)
              if not audio_data:
                  break
              data.write(audio_data.get_string_data())
          self._data = data.getvalue()
  
          self._duration = len(self._data) / self.audio_format.bytes_per_second
  
      def get_queue_source(self):
          if self._data is not None:
              return StaticMemorySource(self._data, self.audio_format)
  
      def get_audio_data(self, num_bytes, compensation_time=0.0):
          """The StaticSource does not provide audio data.
  
          When the StaticSource is queued on a
          :class:`~pyglet.media.player.Player`, it creates a
          :class:`.StaticMemorySource` containing its internal audio data and
          audio format.
  
          Raises:
              RuntimeError
          """
          raise RuntimeError('StaticSource cannot be queued.')
  
  
  class StaticMemorySource(StaticSource):
      """
      Helper class for default implementation of :class:`.StaticSource`.
  
      Do not use directly. This class is used internally by pyglet.
  
      Args:
          data (AudioData): The audio data.
          audio_format (AudioFormat): The audio format.
      """
  
      def __init__(self, data, audio_format):
          """Construct a memory source over the given data buffer."""
          self._file = io.BytesIO(data)
          self._max_offset = len(data)
          self.audio_format = audio_format
          self._duration = len(data) / float(audio_format.bytes_per_second)
  
      def seek(self, timestamp):
          """Seek to given timestamp.
  
          Args:
              timestamp (float): Time where to seek in the source.
          """
          offset = int(timestamp * self.audio_format.bytes_per_second)
  
          # Align to sample
          if self.audio_format.bytes_per_sample == 2:
              offset &= 0xfffffffe
          elif self.audio_format.bytes_per_sample == 4:
              offset &= 0xfffffffc
  
          self._file.seek(offset)
  
      def get_audio_data(self, num_bytes, compensation_time=0.0):
          """Get next packet of audio data.
  
          Args:
              num_bytes (int): Maximum number of bytes of data to return.
              compensation_time (float): Not used in this class.
  
          Returns:
              :class:`.AudioData`: Next packet of audio data, or ``None`` if
              there is no (more) data.
          """
          offset = self._file.tell()
          timestamp = float(offset) / self.audio_format.bytes_per_second
  
          # Align to sample size
          if self.audio_format.bytes_per_sample == 2:
              num_bytes &= 0xfffffffe
          elif self.audio_format.bytes_per_sample == 4:
              num_bytes &= 0xfffffffc
  
          data = self._file.read(num_bytes)
          if not len(data):
              return None
  
          duration = float(len(data)) / self.audio_format.bytes_per_second
          return AudioData(data, len(data), timestamp, duration, [])
  
  
  class SourceGroup:
      """Group of like sources to allow gapless playback.
  
      Seamlessly read data from a group of sources to allow for
      gapless playback. All sources must share the same audio format.
      The first source added sets the format.
      """
  
      def __init__(self):
          self.audio_format = None
          self.video_format = None
          self.duration = 0.0
          self._timestamp_offset = 0.0
          self._dequeued_durations = []
          self._sources = []
  
      def seek(self, time):
          if self._sources:
              self._sources[0].seek(time)
  
      def add(self, source):
          self.audio_format = self.audio_format or source.audio_format
          source = source.get_queue_source()
          assert (source.audio_format == self.audio_format), "Sources must share the same audio format."
          self._sources.append(source)
          self.duration += source.duration
  
      def has_next(self):
          return len(self._sources) > 1
  
      def get_queue_source(self):
          return self
  
      def _advance(self):
          if self._sources:
              self._timestamp_offset += self._sources[0].duration
              self._dequeued_durations.insert(0, self._sources[0].duration)
              old_source = self._sources.pop(0)
              self.duration -= old_source.duration
  
              if isinstance(old_source, StreamingSource):
                  old_source.delete()
                  del old_source
  
      def get_audio_data(self, num_bytes, compensation_time=0.0):
          """Get next audio packet.
  
          :Parameters:
              `num_bytes` : int
                  Hint for preferred size of audio packet; may be ignored.
  
          :rtype: `AudioData`
          :return: Audio data, or None if there is no more data.
          """
  
          if not self._sources:
              return None
  
          buffer = b""
          duration = 0.0
          timestamp = 0.0
  
          while len(buffer) < num_bytes and self._sources:
              audiodata = self._sources[0].get_audio_data(num_bytes)
              if audiodata:
                  buffer += audiodata.data
                  duration += audiodata.duration
                  timestamp += self._timestamp_offset
              else:
                  self._advance()
  
          return AudioData(buffer, len(buffer), timestamp, duration, [])
  