yta-video-opengl 0.0.22__py3-none-any.whl → 0.0.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yta_video_opengl/editor.py +333 -0
- yta_video_opengl/nodes/__init__.py +32 -28
- yta_video_opengl/nodes/audio/__init__.py +164 -55
- yta_video_opengl/nodes/video/__init__.py +27 -1
- yta_video_opengl/nodes/video/{opengl.py → opengl/__init__.py} +8 -4
- yta_video_opengl/nodes/video/opengl/experimental.py +760 -0
- yta_video_opengl/tests.py +236 -358
- yta_video_opengl/utils.py +9 -421
- {yta_video_opengl-0.0.22.dist-info → yta_video_opengl-0.0.24.dist-info}/METADATA +2 -6
- yta_video_opengl-0.0.24.dist-info/RECORD +13 -0
- yta_video_opengl/audio.py +0 -219
- yta_video_opengl/classes.py +0 -1276
- yta_video_opengl/complete/__init__.py +0 -0
- yta_video_opengl/complete/frame_combinator.py +0 -204
- yta_video_opengl/complete/frame_generator.py +0 -319
- yta_video_opengl/complete/frame_wrapper.py +0 -135
- yta_video_opengl/complete/timeline.py +0 -571
- yta_video_opengl/complete/track/__init__.py +0 -500
- yta_video_opengl/complete/track/media/__init__.py +0 -222
- yta_video_opengl/complete/track/parts.py +0 -267
- yta_video_opengl/complete/track/utils.py +0 -78
- yta_video_opengl/media.py +0 -347
- yta_video_opengl/reader/__init__.py +0 -710
- yta_video_opengl/reader/cache/__init__.py +0 -253
- yta_video_opengl/reader/cache/audio.py +0 -195
- yta_video_opengl/reader/cache/utils.py +0 -48
- yta_video_opengl/reader/cache/video.py +0 -113
- yta_video_opengl/t.py +0 -233
- yta_video_opengl/video.py +0 -277
- yta_video_opengl/writer.py +0 -278
- yta_video_opengl-0.0.22.dist-info/RECORD +0 -31
- {yta_video_opengl-0.0.22.dist-info → yta_video_opengl-0.0.24.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.22.dist-info → yta_video_opengl-0.0.24.dist-info}/WHEEL +0 -0
@@ -1,571 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
When we are reading from a source, the reader
|
3
|
-
has its own time base and properties. When we
|
4
|
-
are writing, the writer has different time
|
5
|
-
base and properties. We need to adjust our
|
6
|
-
writer to be able to write, because the videos
|
7
|
-
we read can be different, and the video we are
|
8
|
-
writing is defined by us. The 'time_base' is
|
9
|
-
an important property or will make ffmpeg
|
10
|
-
become crazy and deny packets (that means no
|
11
|
-
video written).
|
12
|
-
"""
|
13
|
-
from yta_video_opengl.complete.track import VideoTrack, AudioTrack
|
14
|
-
from yta_video_opengl.video import Video
|
15
|
-
from yta_video_opengl.t import get_ts, fps_to_time_base, T
|
16
|
-
from yta_video_opengl.complete.frame_wrapper import AudioFrameWrapped
|
17
|
-
from yta_video_opengl.complete.frame_combinator import AudioFrameCombinator
|
18
|
-
from yta_video_opengl.writer import VideoWriter
|
19
|
-
from yta_validation.parameter import ParameterValidator
|
20
|
-
from yta_validation import PythonValidator
|
21
|
-
from av.video.frame import VideoFrame
|
22
|
-
from av.audio.frame import AudioFrame
|
23
|
-
from quicktions import Fraction
|
24
|
-
from functools import reduce
|
25
|
-
from typing import Union
|
26
|
-
|
27
|
-
import numpy as np
|
28
|
-
|
29
|
-
|
30
|
-
class Timeline:
|
31
|
-
"""
|
32
|
-
Class to represent all the tracks that
|
33
|
-
exist on the project and to handle the
|
34
|
-
combination of all their frames.
|
35
|
-
"""
|
36
|
-
|
37
|
-
@property
|
38
|
-
def end(
|
39
|
-
self
|
40
|
-
) -> Fraction:
|
41
|
-
"""
|
42
|
-
The end of the last video of the track
|
43
|
-
that lasts longer. This is the last time
|
44
|
-
moment that has to be rendered.
|
45
|
-
"""
|
46
|
-
return max(
|
47
|
-
track.end
|
48
|
-
for track in self.tracks
|
49
|
-
)
|
50
|
-
|
51
|
-
@property
|
52
|
-
def tracks(
|
53
|
-
self
|
54
|
-
) -> list[Union['AudioTrack', 'VideoTrack']]:
|
55
|
-
"""
|
56
|
-
All the tracks we have but ordered by
|
57
|
-
their indexes, from lower index (highest
|
58
|
-
priority) to highest index (lowest
|
59
|
-
priority).
|
60
|
-
"""
|
61
|
-
return sorted(self._tracks, key = lambda track: track.index)
|
62
|
-
|
63
|
-
@property
|
64
|
-
def video_tracks(
|
65
|
-
self
|
66
|
-
) -> list['VideoTrack']:
|
67
|
-
"""
|
68
|
-
All the video tracks we have but ordered
|
69
|
-
by their indexes, from lower index
|
70
|
-
(highest priority) to highest index
|
71
|
-
(lowest priority).
|
72
|
-
"""
|
73
|
-
return [
|
74
|
-
track
|
75
|
-
for track in self.tracks
|
76
|
-
if PythonValidator.is_instance_of(track, 'VideoTrack')
|
77
|
-
]
|
78
|
-
|
79
|
-
@property
|
80
|
-
def audio_tracks(
|
81
|
-
self
|
82
|
-
) -> list['AudioTrack']:
|
83
|
-
"""
|
84
|
-
All the audio tracks we have but ordered
|
85
|
-
by their indexes, from lower index
|
86
|
-
(highest priority) to highest index
|
87
|
-
(lowest priority).
|
88
|
-
"""
|
89
|
-
return [
|
90
|
-
track
|
91
|
-
for track in self.tracks
|
92
|
-
if PythonValidator.is_instance_of(track, 'AudioTrack')
|
93
|
-
]
|
94
|
-
|
95
|
-
@property
|
96
|
-
def number_of_tracks(
|
97
|
-
self
|
98
|
-
) -> int:
|
99
|
-
"""
|
100
|
-
The number of tracks we have in the
|
101
|
-
timeline.
|
102
|
-
"""
|
103
|
-
return len(self.tracks)
|
104
|
-
|
105
|
-
@property
|
106
|
-
def number_of_video_tracks(
|
107
|
-
self
|
108
|
-
) -> int:
|
109
|
-
"""
|
110
|
-
The number of video tracks we have in the
|
111
|
-
timeline.
|
112
|
-
"""
|
113
|
-
return len(self.video_tracks)
|
114
|
-
|
115
|
-
@property
|
116
|
-
def number_of_audio_tracks(
|
117
|
-
self
|
118
|
-
) -> int:
|
119
|
-
"""
|
120
|
-
The number of audio tracks we have in the
|
121
|
-
timeline.
|
122
|
-
"""
|
123
|
-
return len(self.audio_tracks)
|
124
|
-
|
125
|
-
def __init__(
|
126
|
-
self,
|
127
|
-
size: tuple[int, int] = (1_920, 1_080),
|
128
|
-
fps: Union[int, float, Fraction] = 60.0,
|
129
|
-
audio_fps: Union[int, Fraction] = 44_100.0, # 48_000.0 for aac
|
130
|
-
# TODO: I don't like this name
|
131
|
-
# TODO: Where does this come from (?)
|
132
|
-
audio_samples_per_frame: int = 1024,
|
133
|
-
video_codec: str = 'h264',
|
134
|
-
video_pixel_format: str = 'yuv420p',
|
135
|
-
audio_codec: str = 'aac',
|
136
|
-
# TODO: What about this below (?)
|
137
|
-
# audio_layout = 'stereo',
|
138
|
-
# audio_format = 'fltp'
|
139
|
-
):
|
140
|
-
# TODO: By now I'm having only video
|
141
|
-
# tracks
|
142
|
-
self._tracks: list[VideoTrack] = []
|
143
|
-
"""
|
144
|
-
All the video tracks we are handling.
|
145
|
-
"""
|
146
|
-
|
147
|
-
self.size: tuple[int, int] = size
|
148
|
-
"""
|
149
|
-
The size that the final video must have.
|
150
|
-
"""
|
151
|
-
self.fps: Union[int, float, Fraction] = fps
|
152
|
-
"""
|
153
|
-
The fps of the output video.
|
154
|
-
"""
|
155
|
-
self.audio_fps: Union[int, Fraction] = audio_fps
|
156
|
-
"""
|
157
|
-
The fps of the output audio.
|
158
|
-
"""
|
159
|
-
self.audio_samples_per_frame: int = audio_samples_per_frame
|
160
|
-
"""
|
161
|
-
The audio samples each audio frame must
|
162
|
-
have.
|
163
|
-
"""
|
164
|
-
self.video_codec: str = video_codec
|
165
|
-
"""
|
166
|
-
The video codec for the video exported.
|
167
|
-
"""
|
168
|
-
self.video_pixel_format: str = video_pixel_format
|
169
|
-
"""
|
170
|
-
The pixel format for the video exported.
|
171
|
-
"""
|
172
|
-
self.audio_codec: str = audio_codec
|
173
|
-
"""
|
174
|
-
The audio codec for the audio exported.
|
175
|
-
"""
|
176
|
-
|
177
|
-
# We will have 2 video tracks by now
|
178
|
-
self.add_video_track().add_video_track()
|
179
|
-
|
180
|
-
def _add_track(
|
181
|
-
self,
|
182
|
-
index: Union[int, None] = None,
|
183
|
-
is_audio: bool = False
|
184
|
-
) -> 'Timeline':
|
185
|
-
"""
|
186
|
-
Add a new track to the timeline that will
|
187
|
-
be placed in the last position (highest
|
188
|
-
index, lowest priority).
|
189
|
-
|
190
|
-
It will be a video track unless you send
|
191
|
-
the 'is_audio' parameter as True.
|
192
|
-
"""
|
193
|
-
number_of_tracks = (
|
194
|
-
self.number_of_audio_tracks
|
195
|
-
if is_audio else
|
196
|
-
self.number_of_video_tracks
|
197
|
-
)
|
198
|
-
|
199
|
-
tracks = (
|
200
|
-
self.audio_tracks
|
201
|
-
if is_audio else
|
202
|
-
self.video_tracks
|
203
|
-
)
|
204
|
-
|
205
|
-
index = (
|
206
|
-
index
|
207
|
-
if (
|
208
|
-
index is not None and
|
209
|
-
index <= number_of_tracks
|
210
|
-
) else
|
211
|
-
number_of_tracks
|
212
|
-
)
|
213
|
-
|
214
|
-
# We need to change the index of the
|
215
|
-
# affected tracks (the ones that are
|
216
|
-
# in that index and after it)
|
217
|
-
if index < number_of_tracks:
|
218
|
-
for track in tracks:
|
219
|
-
if track.index >= index:
|
220
|
-
track.index += 1
|
221
|
-
|
222
|
-
track = (
|
223
|
-
AudioTrack(
|
224
|
-
index = index,
|
225
|
-
fps = self.fps,
|
226
|
-
audio_fps = self.audio_fps,
|
227
|
-
audio_samples_per_frame = self.audio_samples_per_frame,
|
228
|
-
# TODO: Where do we obtain this from (?)
|
229
|
-
audio_layout = 'stereo',
|
230
|
-
audio_format = 'fltp'
|
231
|
-
)
|
232
|
-
if is_audio else
|
233
|
-
VideoTrack(
|
234
|
-
index = index,
|
235
|
-
size = self.size,
|
236
|
-
fps = self.fps,
|
237
|
-
audio_fps = self.audio_fps,
|
238
|
-
audio_samples_per_frame = self.audio_samples_per_frame,
|
239
|
-
# TODO: Where do we obtain this from (?)
|
240
|
-
audio_layout = 'stereo',
|
241
|
-
audio_format = 'fltp'
|
242
|
-
)
|
243
|
-
)
|
244
|
-
|
245
|
-
self._tracks.append(track)
|
246
|
-
|
247
|
-
return self
|
248
|
-
|
249
|
-
def add_video_track(
|
250
|
-
self,
|
251
|
-
index: Union[int, None] = None
|
252
|
-
) -> 'Timeline':
|
253
|
-
"""
|
254
|
-
Add a new video track to the timeline, that
|
255
|
-
will be placed in the last position (highest
|
256
|
-
index, lowest priority).
|
257
|
-
"""
|
258
|
-
return self._add_track(
|
259
|
-
index = index,
|
260
|
-
is_audio = False
|
261
|
-
)
|
262
|
-
|
263
|
-
def add_audio_track(
|
264
|
-
self,
|
265
|
-
index: Union[int, None] = None
|
266
|
-
) -> 'Timeline':
|
267
|
-
"""
|
268
|
-
Add a new audio track to the timeline, that
|
269
|
-
will be placed in the last position (highest
|
270
|
-
index, lowest priority).
|
271
|
-
"""
|
272
|
-
return self._add_track(
|
273
|
-
index = index,
|
274
|
-
is_audio = True
|
275
|
-
)
|
276
|
-
|
277
|
-
# TODO: Create a 'remove_track'
|
278
|
-
|
279
|
-
def add_video(
|
280
|
-
self,
|
281
|
-
video: Video,
|
282
|
-
t: Union[int, float, Fraction, None] = None,
|
283
|
-
track_index: int = 0
|
284
|
-
) -> 'Timeline':
|
285
|
-
"""
|
286
|
-
Add the provided 'video' to the timeline,
|
287
|
-
starting at the provided 't' time moment.
|
288
|
-
|
289
|
-
TODO: The 'do_use_second_track' parameter
|
290
|
-
is temporary.
|
291
|
-
"""
|
292
|
-
ParameterValidator.validate_mandatory_number_between('track_index', track_index, 0, self.number_of_tracks)
|
293
|
-
|
294
|
-
if track_index >= self.number_of_video_tracks:
|
295
|
-
raise Exception(f'The "track_index" {str(track_index)} provided does not exist in this timeline.')
|
296
|
-
|
297
|
-
# TODO: This should be, maybe, looking for
|
298
|
-
# tracks by using the index property, not
|
299
|
-
# as array index, but by now it is like
|
300
|
-
# this as it is not very robust yet
|
301
|
-
self.video_tracks[track_index].add_media(video, t)
|
302
|
-
|
303
|
-
return self
|
304
|
-
|
305
|
-
# TODO: Create a 'remove_video'
|
306
|
-
# TODO: Create a 'add_audio'
|
307
|
-
# TODO: Create a 'remove_audio'
|
308
|
-
|
309
|
-
def get_frame_at(
|
310
|
-
self,
|
311
|
-
t: Union[int, float, Fraction]
|
312
|
-
) -> 'VideoFrame':
|
313
|
-
"""
|
314
|
-
Get all the frames that are played at the
|
315
|
-
't' time provided, but combined in one.
|
316
|
-
"""
|
317
|
-
frames = list(
|
318
|
-
track.get_frame_at(t)
|
319
|
-
for track in self.video_tracks
|
320
|
-
)
|
321
|
-
# TODO: Combinate frames, we force them to
|
322
|
-
# rgb24 to obtain them with the same shape,
|
323
|
-
# but maybe we have to change this because
|
324
|
-
# we also need to handle alphas
|
325
|
-
|
326
|
-
"""
|
327
|
-
We need to ignore the frames that are tagged
|
328
|
-
as coming from an empty part, so we can have:
|
329
|
-
|
330
|
-
1. Only empty frames
|
331
|
-
-> Black background, keep one
|
332
|
-
2. Empty frames but other frames:
|
333
|
-
-> Skip all empty frames and apply
|
334
|
-
track orders
|
335
|
-
"""
|
336
|
-
|
337
|
-
output_frame = frames[0]._frame.to_ndarray(format = 'rgb24')
|
338
|
-
|
339
|
-
for frame in frames:
|
340
|
-
# We just need the first non-empty frame,
|
341
|
-
# that must be from the track with the
|
342
|
-
# bigger priority
|
343
|
-
# TODO: I assume, by now, that the frames
|
344
|
-
# come in order (bigger priority first)
|
345
|
-
if not frame.is_from_empty_part:
|
346
|
-
# TODO: By now I'm just returning the first
|
347
|
-
# one but we will need to check the alpha
|
348
|
-
# layer to combine if possible
|
349
|
-
output_frame = frame._frame.to_ndarray(format = 'rgb24')
|
350
|
-
break
|
351
|
-
|
352
|
-
# # TODO: This code below is to combine the
|
353
|
-
# # frames but merging all of them, that is
|
354
|
-
# # unexpected in a video editor but we have
|
355
|
-
# # the way to do it
|
356
|
-
# from yta_video_opengl.complete.frame_combinator import VideoFrameCombinator
|
357
|
-
# # TODO: What about the 'format' (?)
|
358
|
-
# output_frame = VideoFrameCombinator.blend_add(output_frame, frame.to_ndarray(format = 'rgb24'))
|
359
|
-
|
360
|
-
# TODO: How to build this VideoFrame correctly
|
361
|
-
# and what about the 'format' (?)
|
362
|
-
# We don't handle pts here, just the image
|
363
|
-
return VideoFrame.from_ndarray(output_frame, format = 'rgb24')
|
364
|
-
|
365
|
-
def get_audio_frames_at(
|
366
|
-
self,
|
367
|
-
t: float
|
368
|
-
):
|
369
|
-
audio_frames: list[AudioFrameWrapped] = []
|
370
|
-
"""
|
371
|
-
Matrix in which the rows are the different
|
372
|
-
tracks we have, and the column includes all
|
373
|
-
the audio frames for this 't' time moment
|
374
|
-
for the track of that row. We can have more
|
375
|
-
than one frame per column per row (track)
|
376
|
-
but we need a single frame to combine all
|
377
|
-
the tracks.
|
378
|
-
"""
|
379
|
-
# TODO: What if the different audio streams
|
380
|
-
# have also different fps (?)
|
381
|
-
# We use both tracks because videos and
|
382
|
-
# audio tracks have both audios
|
383
|
-
for track in self.tracks:
|
384
|
-
# TODO: Make this work properly
|
385
|
-
audio_frames.append(list(track.get_audio_frames_at(t)))
|
386
|
-
|
387
|
-
# TODO: I am receiving empty array here []
|
388
|
-
# that doesn't include any frame in a specific
|
389
|
-
# track that contains a video, why (?)
|
390
|
-
print(audio_frames)
|
391
|
-
|
392
|
-
# We need only 1 single audio frame per column
|
393
|
-
collapsed_frames = [
|
394
|
-
concatenate_audio_frames(frames)
|
395
|
-
for frames in audio_frames
|
396
|
-
]
|
397
|
-
|
398
|
-
# TODO: What about the lenghts and those
|
399
|
-
# things? They should be ok because they are
|
400
|
-
# based on our output but I'm not completely
|
401
|
-
# sure here..
|
402
|
-
print(collapsed_frames)
|
403
|
-
|
404
|
-
# We keep only the non-silent frames because
|
405
|
-
# we will sum them after and keeping them
|
406
|
-
# will change the results.
|
407
|
-
non_empty_collapsed_frames = [
|
408
|
-
frame._frame
|
409
|
-
for frame in collapsed_frames
|
410
|
-
if not frame.is_from_empty_part
|
411
|
-
]
|
412
|
-
|
413
|
-
if len(non_empty_collapsed_frames) == 0:
|
414
|
-
# If they were all silent, just keep one
|
415
|
-
non_empty_collapsed_frames = [collapsed_frames[0]._frame]
|
416
|
-
|
417
|
-
# Now, mix column by column (track by track)
|
418
|
-
# TODO: I do this to have an iterator, but
|
419
|
-
# maybe we need more than one single audio
|
420
|
-
# frame because of the size at the original
|
421
|
-
# video or something...
|
422
|
-
frames = [
|
423
|
-
AudioFrameCombinator.sum_tracks_frames(non_empty_collapsed_frames, self.audio_fps)
|
424
|
-
]
|
425
|
-
|
426
|
-
for audio_frame in frames:
|
427
|
-
yield audio_frame
|
428
|
-
|
429
|
-
def render(
|
430
|
-
self,
|
431
|
-
output_filename: str = 'test_files/output_render.mp4',
|
432
|
-
start: Union[int, float, Fraction] = 0.0,
|
433
|
-
end: Union[int, float, Fraction, None] = None,
|
434
|
-
) -> 'Timeline':
|
435
|
-
"""
|
436
|
-
Render the time range in between the given
|
437
|
-
'start' and 'end' and store the result with
|
438
|
-
the also provided 'fillename'.
|
439
|
-
|
440
|
-
If no 'start' and 'end' provided, the whole
|
441
|
-
project will be rendered.
|
442
|
-
"""
|
443
|
-
ParameterValidator.validate_mandatory_string('output_filename', output_filename, do_accept_empty = False)
|
444
|
-
ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
445
|
-
ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
|
446
|
-
|
447
|
-
end = (
|
448
|
-
self.end
|
449
|
-
if end is None else
|
450
|
-
end
|
451
|
-
)
|
452
|
-
|
453
|
-
# Limit 'end' a bit...
|
454
|
-
if end >= 300:
|
455
|
-
raise Exception('More than 5 minutes not supported yet.')
|
456
|
-
|
457
|
-
if start >= end:
|
458
|
-
raise Exception('The provided "start" cannot be greater or equal to the "end" provided.')
|
459
|
-
|
460
|
-
writer = VideoWriter(output_filename)
|
461
|
-
|
462
|
-
# TODO: This has to be dynamic according to the
|
463
|
-
# video we are writing (?)
|
464
|
-
writer.set_video_stream(
|
465
|
-
codec_name = self.video_codec,
|
466
|
-
fps = self.fps,
|
467
|
-
size = self.size,
|
468
|
-
pixel_format = self.video_pixel_format
|
469
|
-
)
|
470
|
-
|
471
|
-
writer.set_audio_stream(
|
472
|
-
codec_name = self.audio_codec,
|
473
|
-
fps = self.audio_fps
|
474
|
-
)
|
475
|
-
|
476
|
-
time_base = fps_to_time_base(self.fps)
|
477
|
-
audio_time_base = fps_to_time_base(self.audio_fps)
|
478
|
-
|
479
|
-
audio_pts = 0
|
480
|
-
for t in get_ts(start, end, self.fps):
|
481
|
-
frame = self.get_frame_at(t)
|
482
|
-
|
483
|
-
print(f'Getting t:{str(float(t))}')
|
484
|
-
|
485
|
-
# We need to adjust our output elements to be
|
486
|
-
# consecutive and with the right values
|
487
|
-
# TODO: We are using int() for fps but its float...
|
488
|
-
frame.time_base = time_base
|
489
|
-
frame.pts = T(t, time_base).truncated_pts
|
490
|
-
|
491
|
-
writer.mux_video_frame(
|
492
|
-
frame = frame
|
493
|
-
)
|
494
|
-
|
495
|
-
for audio_frame in self.get_audio_frames_at(t):
|
496
|
-
# We need to adjust our output elements to be
|
497
|
-
# consecutive and with the right values
|
498
|
-
# TODO: We are using int() for fps but its float...
|
499
|
-
audio_frame.time_base = audio_time_base
|
500
|
-
audio_frame.pts = audio_pts
|
501
|
-
|
502
|
-
# We increment for the next iteration
|
503
|
-
audio_pts += audio_frame.samples
|
504
|
-
|
505
|
-
writer.mux_audio_frame(audio_frame)
|
506
|
-
|
507
|
-
writer.mux_video_frame(None)
|
508
|
-
writer.mux_audio_frame(None)
|
509
|
-
writer.output.close()
|
510
|
-
|
511
|
-
# TODO: Refactor and move please
|
512
|
-
# TODO: This has to work for AudioFrame
|
513
|
-
# also, but I need it working for Wrapped
|
514
|
-
def concatenate_audio_frames(
|
515
|
-
frames: list[AudioFrameWrapped]
|
516
|
-
) -> AudioFrameWrapped:
|
517
|
-
"""
|
518
|
-
Concatenate all the given 'frames' in one
|
519
|
-
single audio frame and return it.
|
520
|
-
|
521
|
-
The audio frames must have the same layout
|
522
|
-
and sample rate.
|
523
|
-
"""
|
524
|
-
if not frames:
|
525
|
-
# TODO: This should not happen
|
526
|
-
return None
|
527
|
-
|
528
|
-
if len(frames) == 1:
|
529
|
-
return frames[0]
|
530
|
-
|
531
|
-
# We need to preserve the metadata
|
532
|
-
is_from_empty_part = all(
|
533
|
-
frame.is_from_empty_part
|
534
|
-
for frame in frames
|
535
|
-
)
|
536
|
-
metadata = reduce(lambda key_values, frame: {**key_values, **frame.metadata}, frames, {})
|
537
|
-
|
538
|
-
sample_rate = frames[0]._frame.sample_rate
|
539
|
-
layout = frames[0]._frame.layout.name
|
540
|
-
|
541
|
-
arrays = []
|
542
|
-
# TODO: What about 'metadata' (?)
|
543
|
-
for frame in frames:
|
544
|
-
if (
|
545
|
-
frame._frame.sample_rate != sample_rate or
|
546
|
-
frame._frame.layout.name != layout
|
547
|
-
):
|
548
|
-
raise ValueError("Los frames deben tener mismo sample_rate y layout")
|
549
|
-
|
550
|
-
# arr = frame.to_ndarray() # (channels, samples)
|
551
|
-
# if arr.dtype == np.int16:
|
552
|
-
# arr = arr.astype(np.float32) / 32768.0
|
553
|
-
# elif arr.dtype != np.float32:
|
554
|
-
# arr = arr.astype(np.float32)
|
555
|
-
|
556
|
-
arrays.append(frame._frame.to_ndarray())
|
557
|
-
|
558
|
-
combined = np.concatenate(arrays, axis = 1)
|
559
|
-
|
560
|
-
out = AudioFrame.from_ndarray(
|
561
|
-
array = combined,
|
562
|
-
format = frames[0].format,
|
563
|
-
layout = layout
|
564
|
-
)
|
565
|
-
out.sample_rate = sample_rate
|
566
|
-
|
567
|
-
return AudioFrameWrapped(
|
568
|
-
frame = out,
|
569
|
-
metadata = metadata,
|
570
|
-
is_from_empty_part = is_from_empty_part
|
571
|
-
)
|