Spaces:
Runtime error
Runtime error
File size: 17,960 Bytes
d82cf6a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 |
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.drivers.base.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
def save(self, filename, file=None, encoder=None):
"""Save this Source to a file.
:Parameters:
`filename` : str
Used to set the file format, and to open the output file
if `file` is unspecified.
`file` : file-like object or None
File to write audio data to.
`encoder` : MediaEncoder or None
If unspecified, all encoders matching the filename extension
are tried. If all fail, the exception from the first one
attempted is raised.
"""
if encoder:
return encoder.enccode(self, filename, file)
else:
import pyglet.media.codecs
return pyglet.media.codecs.registry.encode(self, filename, file)
# 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`.
"""
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, [])
|