1# ----------------------------------------------------------------------------
2# pyglet
3# Copyright (c) 2006-2008 Alex Holkner
4# Copyright (c) 2008-2021 pyglet contributors
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10#
11#  * Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13#  * Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in
15#    the documentation and/or other materials provided with the
16#    distribution.
17#  * Neither the name of pyglet nor the names of its
18#    contributors may be used to endorse or promote products
19#    derived from this software without specific prior written
20#    permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33# POSSIBILITY OF SUCH DAMAGE.
34# ----------------------------------------------------------------------------
35"""High-level sound and video player."""
36
37import time
38from collections import deque
39
40import pyglet
41from pyglet.media import buffered_logger as bl
42from pyglet.media.drivers import get_audio_driver
43from pyglet.media.codecs.base import Source, SourceGroup
44
45_debug = pyglet.options['debug_media']
46
47
48class PlaybackTimer:
49    """Playback Timer.
50
51    This is a simple timer object which tracks the time elapsed. It can be
52    paused and reset.
53    """
54
55    def __init__(self):
56        """Initialize the timer with time 0."""
57        self._time = 0.0
58        self._systime = None
59
60    def start(self):
61        """Start the timer."""
62        self._systime = time.time()
63
64    def pause(self):
65        """Pause the timer."""
66        self._time = self.get_time()
67        self._systime = None
68
69    def reset(self):
70        """Reset the timer to 0."""
71        self._time = 0.0
72        if self._systime is not None:
73            self._systime = time.time()
74
75    def get_time(self):
76        """Get the elapsed time."""
77        if self._systime is None:
78            now = self._time
79        else:
80            now = time.time() - self._systime + self._time
81        return now
82
83    def set_time(self, value):
84        """
85        Manually set the elapsed time.
86
87        Args:
88            value (float): the new elapsed time value
89        """
90        self.reset()
91        self._time = value
92
93
94class _PlayerProperty:
95    """Descriptor for Player attributes to forward to the AudioPlayer.
96
97    We want the Player to have attributes like volume, pitch, etc. These are
98    actually implemented by the AudioPlayer. So this descriptor will forward
99    an assignement to one of the attributes to the AudioPlayer. For example
100    `player.volume = 0.5` will call `player._audio_player.set_volume(0.5)`.
101
102    The Player class has default values at the class level which are retrieved
103    if not found on the instance.
104    """
105
106    def __init__(self, attribute, doc=None):
107        self.attribute = attribute
108        self.__doc__ = doc or ''
109
110    def __get__(self, obj, objtype=None):
111        if obj is None:
112            return self
113        if '_' + self.attribute in obj.__dict__:
114            return obj.__dict__['_' + self.attribute]
115        return getattr(objtype, '_' + self.attribute)
116
117    def __set__(self, obj, value):
118        obj.__dict__['_' + self.attribute] = value
119        if obj._audio_player:
120            getattr(obj._audio_player, 'set_' + self.attribute)(value)
121
122
123class Player(pyglet.event.EventDispatcher):
124    """High-level sound and video player."""
125
126    # Spacialisation attributes, preserved between audio players
127    _volume = 1.0
128    _min_distance = 1.0
129    _max_distance = 100000000.
130
131    _position = (0, 0, 0)
132    _pitch = 1.0
133
134    _cone_orientation = (0, 0, 1)
135    _cone_inner_angle = 360.
136    _cone_outer_angle = 360.
137    _cone_outer_gain = 1.
138
139    def __init__(self):
140        """Initialize the Player with a MasterClock."""
141        self._source = None
142        self._playlists = deque()
143        self._audio_player = None
144
145        self._texture = None
146        # Desired play state (not an indication of actual state).
147        self._playing = False
148
149        self._timer = PlaybackTimer()
150        #: Loop the current source indefinitely or until
151        #: :meth:`~Player.next_source` is called. Defaults to ``False``.
152        #:
153        #: :type: bool
154        #:
155        #: .. versionadded:: 1.4
156        self.loop = False
157
158    def __del__(self):
159        """Release the Player resources."""
160        self.delete()
161
162    def queue(self, source):
163        """
164        Queue the source on this player.
165
166        If the player has no source, the player will start to play immediately
167        or pause depending on its :attr:`.playing` attribute.
168
169        Args:
170            source (Source or Iterable[Source]): The source to queue.
171        """
172        if isinstance(source, (Source, SourceGroup)):
173            source = _one_item_playlist(source)
174        else:
175            try:
176                source = iter(source)
177            except TypeError:
178                raise TypeError("source must be either a Source or an iterable."
179                                " Received type {0}".format(type(source)))
180        self._playlists.append(source)
181
182        if self.source is None:
183            source = next(self._playlists[0])
184            self._source = source.get_queue_source()
185
186        self._set_playing(self._playing)
187
188    def _set_playing(self, playing):
189        # stopping = self._playing and not playing
190        # starting = not self._playing and playing
191
192        self._playing = playing
193        source = self.source
194
195        if playing and source:
196            if source.audio_format:
197                if self._audio_player is None:
198                    self._create_audio_player()
199                if self._audio_player:
200                    # We succesfully created an audio player
201                    self._audio_player.prefill_audio()
202
203            if bl.logger is not None:
204                bl.logger.init_wall_time()
205                bl.logger.log("p.P._sp", 0.0)
206
207            if source.video_format:
208                if not self._texture:
209                    self._create_texture()
210
211            if self._audio_player:
212                self._audio_player.play()
213            if source.video_format:
214                pyglet.clock.schedule_once(self.update_texture, 0)
215            # For audio synchronization tests, the following will
216            # add a delay to de-synchronize the audio.
217            # Negative number means audio runs ahead.
218            # self._mclock._systime += -0.3
219            self._timer.start()
220            if self._audio_player is None and source.video_format is None:
221                pyglet.clock.schedule_once(lambda dt: self.dispatch_event("on_eos"), source.duration)
222
223
224        else:
225            if self._audio_player:
226                self._audio_player.stop()
227
228            pyglet.clock.unschedule(self.update_texture)
229            self._timer.pause()
230
231    @property
232    def playing(self):
233        """
234        bool: Read-only. Determine if the player state is playing.
235
236        The *playing* property is irrespective of whether or not there is
237        actually a source to play. If *playing* is ``True`` and a source is
238        queued, it will begin to play immediately. If *playing* is ``False``,
239        it is implied that the player is paused. There is no other possible
240        state.
241        """
242        return self._playing
243
244    def play(self):
245        """Begin playing the current source.
246
247        This has no effect if the player is already playing.
248        """
249        self._set_playing(True)
250
251    def pause(self):
252        """Pause playback of the current source.
253
254        This has no effect if the player is already paused.
255        """
256        self._set_playing(False)
257
258    def delete(self):
259        """Release the resources acquired by this player.
260
261        The internal audio player and the texture will be deleted.
262        """
263        if self._source:
264            self.source.is_player_source = False
265        if self._audio_player:
266            self._audio_player.delete()
267            self._audio_player = None
268        if self._texture:
269            self._texture = None
270
271    def next_source(self):
272        """Move immediately to the next source in the current playlist.
273
274        If the playlist is empty, discard it and check if another playlist
275        is queued. There may be a gap in playback while the audio buffer
276        is refilled.
277        """
278        was_playing = self._playing
279        self.pause()
280        self._timer.reset()
281
282        if self._source:
283            # Reset source to the beginning
284            self.seek(0.0)
285            self.source.is_player_source = False
286
287        playlists = self._playlists
288        if not playlists:
289            return
290
291        try:
292            new_source = next(playlists[0])
293        except StopIteration:
294            self._playlists.popleft()
295            if not self._playlists:
296                new_source = None
297            else:
298                # Could someone queue an iterator which is empty??
299                new_source = next(self._playlists[0])
300
301        if new_source is None:
302            self._source = None
303            self.delete()
304            self.dispatch_event('on_player_eos')
305        else:
306            old_audio_format = self._source.audio_format
307            old_video_format = self._source.video_format
308            self._source = new_source.get_queue_source()
309
310            if self._audio_player:
311                if old_audio_format == self._source.audio_format:
312                    self._audio_player.clear()
313                    self._audio_player.source = self._source
314                else:
315                    self._audio_player.delete()
316                    self._audio_player = None
317            if old_video_format != self._source.video_format:
318                self._texture = None
319                pyglet.clock.unschedule(self.update_texture)
320
321            self._set_playing(was_playing)
322            self.dispatch_event('on_player_next_source')
323
324    def seek(self, timestamp):
325        """
326        Seek for playback to the indicated timestamp on the current source.
327
328        Timestamp is expressed in seconds. If the timestamp is outside the
329        duration of the source, it will be clamped to the end.
330
331        Args:
332            timestamp (float): The time where to seek in the source, clamped to the
333                beginning and end of the source.
334        """
335        playing = self._playing
336        if playing:
337            self.pause()
338        if not self.source:
339            return
340
341        if bl.logger is not None:
342            bl.logger.log("p.P.sk", timestamp)
343
344        timestamp = max(timestamp, 0)
345
346        self._timer.set_time(timestamp)
347        self._source.seek(timestamp)
348        if self._audio_player:
349            # XXX: According to docstring in AbstractAudioPlayer this cannot
350            # be called when the player is not stopped
351            self._audio_player.clear()
352        if self.source.video_format:
353            self.update_texture()
354            pyglet.clock.unschedule(self.update_texture)
355        self._set_playing(playing)
356
357    def _create_audio_player(self):
358        assert not self._audio_player
359        assert self.source
360
361        source = self.source
362        audio_driver = get_audio_driver()
363        if audio_driver is None:
364            # Failed to find a valid audio driver
365            return
366
367        self._audio_player = audio_driver.create_audio_player(source, self)
368
369        # Set the audio player attributes
370        for attr in ('volume', 'min_distance', 'max_distance', 'position',
371                     'pitch', 'cone_orientation', 'cone_inner_angle',
372                     'cone_outer_angle', 'cone_outer_gain'):
373            value = getattr(self, attr)
374            setattr(self, attr, value)
375
376    @property
377    def source(self):
378        """Source: Read-only. The current :class:`Source`, or ``None``."""
379        return self._source
380
381    @property
382    def time(self):
383        """
384        float: Read-only. Current playback time of the current source.
385
386        The playback time is a float expressed in seconds, with 0.0 being the
387        beginning of the media. The playback time returned represents the
388        player master clock time which is used to synchronize both the audio
389        and the video.
390        """
391        return self._timer.get_time()
392
393    def _create_texture(self):
394        video_format = self.source.video_format
395        self._texture = pyglet.image.Texture.create(
396            video_format.width, video_format.height, rectangle=True)
397        self._texture = self._texture.get_transform(flip_y=True)
398        # After flipping the texture along the y axis, the anchor_y is set
399        # to the top of the image. We want to keep it at the bottom.
400        self._texture.anchor_y = 0
401        return self._texture
402
403    @property
404    def texture(self):
405        """
406        :class:`pyglet.image.Texture`: Get the texture for the current video frame.
407
408        You should call this method every time you display a frame of video,
409        as multiple textures might be used. The return value will be None if
410        there is no video in the current source.
411        """
412        return self._texture
413
414    def get_texture(self):
415        """
416        Get the texture for the current video frame.
417
418        You should call this method every time you display a frame of video,
419        as multiple textures might be used. The return value will be None if
420        there is no video in the current source.
421
422        Returns:
423            :class:`pyglet.image.Texture`
424
425        .. deprecated:: 1.4
426                Use :attr:`~texture` instead
427        """
428        return self.texture
429
430    def seek_next_frame(self):
431        """Step forwards one video frame in the current source."""
432        time = self.source.get_next_video_timestamp()
433        if time is None:
434            return
435        self.seek(time)
436
437    def update_texture(self, dt=None):
438        """Manually update the texture from the current source.
439
440        This happens automatically, so you shouldn't need to call this method.
441
442        Args:
443            dt (float): The time elapsed since the last call to
444                ``update_texture``.
445        """
446        # self.pr.disable()
447        # if dt > 0.05:
448        #     print("update_texture dt:", dt)
449        #     import pstats
450        #     ps = pstats.Stats(self.pr).sort_stats("cumulative")
451        #     ps.print_stats()
452        source = self.source
453        time = self.time
454        if bl.logger is not None:
455            bl.logger.log(
456                "p.P.ut.1.0", dt, time,
457                self._audio_player.get_time() if self._audio_player else 0,
458                bl.logger.rebased_wall_time()
459            )
460
461        frame_rate = source.video_format.frame_rate
462        frame_duration = 1 / frame_rate
463        ts = source.get_next_video_timestamp()
464        # Allow up to frame_duration difference
465        while ts is not None and ts + frame_duration < time:
466            source.get_next_video_frame()  # Discard frame
467            if bl.logger is not None:
468                bl.logger.log("p.P.ut.1.5", ts)
469            ts = source.get_next_video_timestamp()
470
471        if bl.logger is not None:
472            bl.logger.log("p.P.ut.1.6", ts)
473
474        if ts is None:
475            # No more video frames to show. End of video stream.
476            if bl.logger is not None:
477                bl.logger.log("p.P.ut.1.7", frame_duration)
478
479            pyglet.clock.schedule_once(self._video_finished, 0)
480            return
481
482        image = source.get_next_video_frame()
483        if image is not None:
484            if self._texture is None:
485                self._create_texture()
486            self._texture.blit_into(image, 0, 0, 0)
487        elif bl.logger is not None:
488            bl.logger.log("p.P.ut.1.8")
489
490        ts = source.get_next_video_timestamp()
491        if ts is None:
492            delay = frame_duration
493        else:
494            delay = ts - time
495
496        delay = max(0.0, delay)
497        if bl.logger is not None:
498            bl.logger.log("p.P.ut.1.9", delay, ts)
499        pyglet.clock.schedule_once(self.update_texture, delay)
500        # self.pr.enable()
501
502    def _video_finished(self, dt):
503        if self._audio_player is None:
504            self.dispatch_event("on_eos")
505
506    volume = _PlayerProperty('volume', doc="""
507    The volume level of sound playback.
508
509    The nominal level is 1.0, and 0.0 is silence.
510
511    The volume level is affected by the distance from the listener (if
512    positioned).
513    """)
514    min_distance = _PlayerProperty('min_distance', doc="""
515    The distance beyond which the sound volume drops by half, and within
516    which no attenuation is applied.
517
518    The minimum distance controls how quickly a sound is attenuated as it
519    moves away from the listener. The gain is clamped at the nominal value
520    within the min distance. By default the value is 1.0.
521
522    The unit defaults to meters, but can be modified with the listener
523    properties. """)
524    max_distance = _PlayerProperty('max_distance', doc="""
525    The distance at which no further attenuation is applied.
526
527    When the distance from the listener to the player is greater than this
528    value, attenuation is calculated as if the distance were value. By
529    default the maximum distance is infinity.
530
531    The unit defaults to meters, but can be modified with the listener
532    properties.
533    """)
534    position = _PlayerProperty('position', doc="""
535    The position of the sound in 3D space.
536
537    The position is given as a tuple of floats (x, y, z). The unit
538    defaults to meters, but can be modified with the listener properties.
539    """)
540    pitch = _PlayerProperty('pitch', doc="""
541    The pitch shift to apply to the sound.
542
543    The nominal pitch is 1.0. A pitch of 2.0 will sound one octave higher,
544    and play twice as fast. A pitch of 0.5 will sound one octave lower, and
545    play twice as slow. A pitch of 0.0 is not permitted.
546    """)
547    cone_orientation = _PlayerProperty('cone_orientation', doc="""
548    The direction of the sound in 3D space.
549
550    The direction is specified as a tuple of floats (x, y, z), and has no
551    unit. The default direction is (0, 0, -1). Directional effects are only
552    noticeable if the other cone properties are changed from their default
553    values.
554    """)
555    cone_inner_angle = _PlayerProperty('cone_inner_angle', doc="""
556    The interior angle of the inner cone.
557
558    The angle is given in degrees, and defaults to 360. When the listener
559    is positioned within the volume defined by the inner cone, the sound is
560    played at normal gain (see :attr:`volume`).
561    """)
562    cone_outer_angle = _PlayerProperty('cone_outer_angle', doc="""
563    The interior angle of the outer cone.
564
565    The angle is given in degrees, and defaults to 360. When the listener
566    is positioned within the volume defined by the outer cone, but outside
567    the volume defined by the inner cone, the gain applied is a smooth
568    interpolation between :attr:`volume` and :attr:`cone_outer_gain`.
569    """)
570    cone_outer_gain = _PlayerProperty('cone_outer_gain', doc="""
571    The gain applied outside the cone.
572
573    When the listener is positioned outside the volume defined by the outer
574    cone, this gain is applied instead of :attr:`volume`.
575    """)
576
577    # Events
578
579    def on_player_eos(self):
580        """The player ran out of sources. The playlist is empty.
581
582        :event:
583        """
584        if _debug:
585            print('Player.on_player_eos')
586
587    def on_eos(self):
588        """The current source ran out of data.
589
590        The default behaviour is to advance to the next source in the
591        playlist if the :attr:`.loop` attribute is set to ``False``.
592        If :attr:`.loop` attribute is set to ``True``, the current source
593        will start to play again until :meth:`next_source` is called or
594        :attr:`.loop` is set to ``False``.
595
596        :event:
597        """
598        if _debug:
599            print('Player.on_eos')
600        if bl.logger is not None:
601            bl.logger.log("p.P.oe")
602            bl.logger.close()
603
604        if self.loop:
605            was_playing = self._playing
606            self.pause()
607            self._timer.reset()
608
609            if self.source:
610                # Reset source to the beginning
611                self.seek(0.0)
612            if self._audio_player:
613                self._audio_player.clear()
614            self._set_playing(was_playing)
615
616        else:
617            self.next_source()
618
619    def on_player_next_source(self):
620        """The player starts to play the next queued source in the playlist.
621
622        This is a useful event for adjusting the window size to the new
623        source :class:`VideoFormat` for example.
624
625        :event:
626        """
627        pass
628
629    def on_driver_reset(self):
630        """The audio driver has been reset, by default this will kill the current audio player and create a new one,
631        and requeue the buffers. Any buffers that may have been queued in a player will be resubmitted.  It will
632        continue from from the last buffers submitted, not played and may cause sync issues if using video.
633
634        :event:
635        """
636        if self._audio_player:
637            self._audio_player.on_driver_reset()
638
639            # Voice has been changed, will need to reset all options on the voice.
640            for attr in ('volume', 'min_distance', 'max_distance', 'position',
641                         'pitch', 'cone_orientation', 'cone_inner_angle',
642                         'cone_outer_angle', 'cone_outer_gain'):
643                value = getattr(self, attr)
644                setattr(self, attr, value)
645
646            if self._playing:
647                self._audio_player.play()
648
649
650Player.register_event_type('on_eos')
651Player.register_event_type('on_player_eos')
652Player.register_event_type('on_player_next_source')
653Player.register_event_type('on_driver_reset')
654
655
656def _one_item_playlist(source):
657    yield source
658
659
660class PlayerGroup:
661    """Group of players that can be played and paused simultaneously.
662
663    Create a player group for the given list of players.
664
665    All players in the group must currently not belong to any other group.
666
667    Args:
668        players (List[Player]): List of :class:`.Player` s in this group.
669    """
670
671    def __init__(self, players):
672        """Initialize the PlayerGroup with the players."""
673        self.players = list(players)
674
675    def play(self):
676        """Begin playing all players in the group simultaneously."""
677        audio_players = [p._audio_player
678                         for p in self.players if p._audio_player]
679        if audio_players:
680            audio_players[0]._play_group(audio_players)
681        for player in self.players:
682            player.play()
683
684    def pause(self):
685        """Pause all players in the group simultaneously."""
686        audio_players = [p._audio_player
687                         for p in self.players if p._audio_player]
688        if audio_players:
689            audio_players[0]._stop_group(audio_players)
690        for player in self.players:
691            player.pause()
692