simple-ffmpeg-batch-io 1.0.0__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.
- simple_ffmpeg_batch_io/AudioIO.py +824 -0
- simple_ffmpeg_batch_io/FrameContainer.py +81 -0
- simple_ffmpeg_batch_io/FrameCounter.py +182 -0
- simple_ffmpeg_batch_io/PipeMode.py +26 -0
- simple_ffmpeg_batch_io/VideoIO.py +803 -0
- simple_ffmpeg_batch_io/__init__.py +23 -0
- simple_ffmpeg_batch_io/cli.py +27 -0
- simple_ffmpeg_batch_io-1.0.0.dist-info/METADATA +382 -0
- simple_ffmpeg_batch_io-1.0.0.dist-info/RECORD +12 -0
- simple_ffmpeg_batch_io-1.0.0.dist-info/WHEEL +4 -0
- simple_ffmpeg_batch_io-1.0.0.dist-info/entry_points.txt +3 -0
- simple_ffmpeg_batch_io-1.0.0.dist-info/licenses/LICENSE +198 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read/write audio frames or batches of audio frames from (compressed) file, including video file with audio stream(s), using FFmpeg backend.
|
|
3
|
+
|
|
4
|
+
This module defines the main `AudioIO` class used to open audio streams,
|
|
5
|
+
read audio frames or batches of frames, and write processed outputs.
|
|
6
|
+
|
|
7
|
+
Authors
|
|
8
|
+
-------
|
|
9
|
+
Dominique Vaufreydaz (inspired from original C++ code: https://github.com/Vaufreyd/ReadWriteVideosWithOpenCV)
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__authors__ = ("Dominique Vaufreydaz")
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import subprocess as sp
|
|
17
|
+
import re
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Union
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from .FrameCounter import FrameCounter
|
|
24
|
+
from .FrameContainer import FrameContainer
|
|
25
|
+
from .PipeMode import PipeMode
|
|
26
|
+
|
|
27
|
+
# init static_ffmpeg at import time, first time it will download ffmpeg executables
|
|
28
|
+
import static_ffmpeg
|
|
29
|
+
static_ffmpeg.add_paths()
|
|
30
|
+
|
|
31
|
+
class AudioIO:
|
|
32
|
+
# "static" variables to ffmpeg, ffprobe executables
|
|
33
|
+
audioProgram, paramProgram = static_ffmpeg.run.get_or_fetch_platform_executables_else_raise()
|
|
34
|
+
|
|
35
|
+
class AudioIOException(Exception):
|
|
36
|
+
"""
|
|
37
|
+
Dedicated exception class for AudioIO class.
|
|
38
|
+
"""
|
|
39
|
+
def __init__(self, message="Error while reading/writing video occurs"):
|
|
40
|
+
self.message = message
|
|
41
|
+
super().__init__(self.message)
|
|
42
|
+
|
|
43
|
+
class AudioFormat(Enum):
|
|
44
|
+
"""
|
|
45
|
+
Enum class for supported input video type: 32-bit float is the only supported type for the moment.
|
|
46
|
+
"""
|
|
47
|
+
PCM32LE = 'pcm_f32le' # default format (unique mode for the moment)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def reader(cls, filename, **kwargs):
|
|
51
|
+
"""
|
|
52
|
+
Create and open an AudioIO object in reader mode
|
|
53
|
+
|
|
54
|
+
See ``AudioIO.open`` for the full list of accepted parameters.
|
|
55
|
+
"""
|
|
56
|
+
reader = cls()
|
|
57
|
+
reader.open(filename, **kwargs)
|
|
58
|
+
return reader
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def writer(cls, filename, **kwargs):
|
|
62
|
+
"""
|
|
63
|
+
Create and open an AudioIO object in writer mode
|
|
64
|
+
|
|
65
|
+
See ``AudioIO.create`` for the full list of accepted parameters.
|
|
66
|
+
"""
|
|
67
|
+
writer = cls()
|
|
68
|
+
writer.create(filename, **kwargs)
|
|
69
|
+
return writer
|
|
70
|
+
|
|
71
|
+
# To use with context manager "with AudioIO.reader(...) as f:' for instance
|
|
72
|
+
def __enter__(self):
|
|
73
|
+
"""
|
|
74
|
+
Method call at initialisation of a context manager like "with AudioIO.reader/writer(...) as f:' for instance
|
|
75
|
+
"""
|
|
76
|
+
# simply return myself
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
80
|
+
"""
|
|
81
|
+
Method call when existing of a context manager like "with AudioIO.reader/writer(...) as f:' for instance
|
|
82
|
+
"""
|
|
83
|
+
# close AudioIO
|
|
84
|
+
self.close()
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def get_time_in_sec(filename, *, debug=False, logLevel=16):
|
|
89
|
+
"""
|
|
90
|
+
Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
filename : str or path.
|
|
95
|
+
Raw audio waveform as a 1D array.
|
|
96
|
+
|
|
97
|
+
debug : bool (default False).
|
|
98
|
+
Show debug info.
|
|
99
|
+
|
|
100
|
+
log_level: int (default 16).
|
|
101
|
+
Log level to pass to the underlying ffmpeg/ffprobe command.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
----------
|
|
105
|
+
float
|
|
106
|
+
Length in seconds of video file (including milliseconds as decimal part with 3 decimals)
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
cmd = [AudioIO.paramProgram, # ffprobe
|
|
110
|
+
'-hide_banner',
|
|
111
|
+
'-loglevel', str(logLevel),
|
|
112
|
+
'-show_entries', 'format=duration',
|
|
113
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
114
|
+
filename
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if debug == True:
|
|
118
|
+
print(' '.join(cmd))
|
|
119
|
+
|
|
120
|
+
# call ffprobe and get params in one single line
|
|
121
|
+
lpipe = sp.Popen(cmd, stdout=sp.PIPE)
|
|
122
|
+
output = lpipe.stdout.readlines()
|
|
123
|
+
lpipe.terminate()
|
|
124
|
+
# transform Bytes output to one single string
|
|
125
|
+
output = ''.join( [element.decode('utf-8') for element in output])
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
return float(output)
|
|
129
|
+
except (ValueError, TypeError):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def get_params(filename, *, debug=False, logLevel=16):
|
|
134
|
+
"""
|
|
135
|
+
Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
filename : str or path.
|
|
140
|
+
Raw audio waveform as a 1D array.
|
|
141
|
+
|
|
142
|
+
debug : bool (default (False).
|
|
143
|
+
Show debug info.
|
|
144
|
+
|
|
145
|
+
log_level: int (default 16).
|
|
146
|
+
Log level to pass to the underlying ffmpeg/ffprobe command.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
----------
|
|
150
|
+
tuple
|
|
151
|
+
Tuple containing (channels,sample_rate) of the file
|
|
152
|
+
"""
|
|
153
|
+
cmd = [AudioIO.paramProgram, # ffprobe
|
|
154
|
+
'-hide_banner',
|
|
155
|
+
'-loglevel', str(logLevel),
|
|
156
|
+
'-show_entries', 'stream=channels,sample_rate',
|
|
157
|
+
filename
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
if debug == True:
|
|
161
|
+
print(' '.join(cmd))
|
|
162
|
+
|
|
163
|
+
# call ffprobe and get params in one single line
|
|
164
|
+
lpipe = sp.Popen(cmd, stdout=sp.PIPE)
|
|
165
|
+
output = lpipe.stdout.readlines()
|
|
166
|
+
lpipe.terminate()
|
|
167
|
+
# transform Bytes output to one single string
|
|
168
|
+
output = ''.join( [element.decode('utf-8') for element in output])
|
|
169
|
+
|
|
170
|
+
pattern_sample_rate = r'sample_rate=(\d+)'
|
|
171
|
+
pattern_channels = r'channels=(\d+)'
|
|
172
|
+
|
|
173
|
+
# Search for values in the ffprobe output
|
|
174
|
+
match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE)
|
|
175
|
+
match_channels = re.search(pattern_channels, output, flags=re.MULTILINE)
|
|
176
|
+
|
|
177
|
+
# Extraction des valeurs
|
|
178
|
+
if match_sample_rate:
|
|
179
|
+
sample_rate = int(match_sample_rate.group(1))
|
|
180
|
+
else:
|
|
181
|
+
raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'")
|
|
182
|
+
|
|
183
|
+
if match_channels:
|
|
184
|
+
channels = int(match_channels.group(1))
|
|
185
|
+
else:
|
|
186
|
+
raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'")
|
|
187
|
+
|
|
188
|
+
return (channels,sample_rate)
|
|
189
|
+
|
|
190
|
+
# Attributes
|
|
191
|
+
mode: PipeMode
|
|
192
|
+
""" Pipemode of the current object (default PipeMode.UNK_MODE)"""
|
|
193
|
+
|
|
194
|
+
loglevel: int
|
|
195
|
+
""" loglevel of the underlying ffmpeg backend for this object (default 16)"""
|
|
196
|
+
|
|
197
|
+
debugModel: bool
|
|
198
|
+
""" debutMode flag for this object (print debut info, default False)"""
|
|
199
|
+
|
|
200
|
+
channels: int
|
|
201
|
+
""" Number of channels of images (default -1) """
|
|
202
|
+
|
|
203
|
+
sample_rate: int
|
|
204
|
+
""" sample_rate of images (default -1) """
|
|
205
|
+
|
|
206
|
+
plannar: bool
|
|
207
|
+
""" Read/write data as plannar, i.e. not interleaved (default True) """
|
|
208
|
+
|
|
209
|
+
pipe: sp.Popen
|
|
210
|
+
""" pipe object to ffmpeg/ffprobe (default None)"""
|
|
211
|
+
|
|
212
|
+
frame_size: int
|
|
213
|
+
""" Weight in bytes of one image (default -1)"""
|
|
214
|
+
|
|
215
|
+
filename: str
|
|
216
|
+
""" Filename of the file (default None)"""
|
|
217
|
+
|
|
218
|
+
frame_counter: FrameCounter
|
|
219
|
+
""" `Framecounter` object to count ellapsed time (default None)"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, *, logLevel = 16, debugMode = False):
|
|
222
|
+
"""
|
|
223
|
+
Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
log_level: int (default 16)
|
|
228
|
+
Log level to pass to the underlying ffmpeg/ffprobe command.
|
|
229
|
+
|
|
230
|
+
debugMode: bool (default (False)
|
|
231
|
+
Show debug info. while processing video
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
self.mode = PipeMode.UNK_MODE
|
|
235
|
+
self.logLevel = logLevel
|
|
236
|
+
self.debugMode = debugMode
|
|
237
|
+
|
|
238
|
+
# Call init() method
|
|
239
|
+
self.init()
|
|
240
|
+
|
|
241
|
+
def init(self):
|
|
242
|
+
"""
|
|
243
|
+
Init or reinit a VideoIO object.
|
|
244
|
+
"""
|
|
245
|
+
self.channels = -1
|
|
246
|
+
self.sample_rate = -1
|
|
247
|
+
self.plannar = True
|
|
248
|
+
self.pipe = None
|
|
249
|
+
self.frame_size = -1
|
|
250
|
+
self.filename = None
|
|
251
|
+
self.frame_counter = None
|
|
252
|
+
|
|
253
|
+
_repr_exclude = {"pipe"}
|
|
254
|
+
""" List of excluded attribute for string conversion. """
|
|
255
|
+
|
|
256
|
+
# converting the object to a string representation
|
|
257
|
+
def __repr__(self):
|
|
258
|
+
"""
|
|
259
|
+
Convert object (excluding attributes in _repr_exclude) to string representation.
|
|
260
|
+
"""
|
|
261
|
+
attrs = ", ".join(
|
|
262
|
+
f"{k}={v!r}"
|
|
263
|
+
for k, v in self.__dict__.items()
|
|
264
|
+
if k not in self._repr_exclude
|
|
265
|
+
)
|
|
266
|
+
return f"{self.__class__.__name__}({attrs})"
|
|
267
|
+
|
|
268
|
+
__str__ = __repr__
|
|
269
|
+
""" String representation """
|
|
270
|
+
|
|
271
|
+
def get_elapsed_time_as_str(self) -> str:
|
|
272
|
+
"""
|
|
273
|
+
Method to get elapsed time (float value represented) as str.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
----------
|
|
277
|
+
str or None
|
|
278
|
+
Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
|
|
279
|
+
None if no frame counter are available.
|
|
280
|
+
"""
|
|
281
|
+
if self.frame_counter is None:
|
|
282
|
+
return None
|
|
283
|
+
return self.frame_counter.get_elapsed_time_as_str()
|
|
284
|
+
|
|
285
|
+
def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
|
|
286
|
+
"""
|
|
287
|
+
Method to get elapsed time (hour format) as str.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
----------
|
|
291
|
+
str or None
|
|
292
|
+
Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
|
|
293
|
+
None if no frame counter are available.
|
|
294
|
+
"""
|
|
295
|
+
if self.frame_counter is None:
|
|
296
|
+
return None
|
|
297
|
+
return self.frame_counter.get_formated_elapsed_time_as_str()
|
|
298
|
+
|
|
299
|
+
def get_elapsed_time(self) -> float:
|
|
300
|
+
"""
|
|
301
|
+
Method to get elapsed time as float value rounded to 3 decimals.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
----------
|
|
305
|
+
float or None
|
|
306
|
+
Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
|
|
307
|
+
None if no frame counter are available.
|
|
308
|
+
"""
|
|
309
|
+
if self.frame_counter is None:
|
|
310
|
+
return None
|
|
311
|
+
return self.frame_counter.get_elapsed_time()
|
|
312
|
+
|
|
313
|
+
def is_opened(self) -> bool:
|
|
314
|
+
"""
|
|
315
|
+
Method to get status of the underlying pipe to ffmpeg.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
----------
|
|
319
|
+
bool
|
|
320
|
+
True if pipe is opened (reading or writing mode), False if not.
|
|
321
|
+
"""
|
|
322
|
+
# is the pip opened?
|
|
323
|
+
if self.pipe is not None and self.pipe.poll() is None:
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
def close(self):
|
|
329
|
+
"""
|
|
330
|
+
Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.
|
|
331
|
+
"""
|
|
332
|
+
if self.pipe is not None:
|
|
333
|
+
if self.mode == PipeMode.WRITE_MODE:
|
|
334
|
+
# killing will make ffmpeg not finish properly the job, close the pipe
|
|
335
|
+
# to let it know that no more data are comming
|
|
336
|
+
self.pipe.stdin.close()
|
|
337
|
+
else: # self.mode == PipeMode.READ_MODE
|
|
338
|
+
# in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
|
|
339
|
+
self.pipe.kill()
|
|
340
|
+
|
|
341
|
+
# wait for subprocess to end
|
|
342
|
+
self.pipe.wait()
|
|
343
|
+
|
|
344
|
+
# reinit object for later use
|
|
345
|
+
self.init()
|
|
346
|
+
|
|
347
|
+
def create( self, filename, sample_rate, channels, *, writeOverExistingFile = False,
|
|
348
|
+
outputEncoding = AudioFormat.PCM32LE, encodingParams = None, plannar = True ):
|
|
349
|
+
"""
|
|
350
|
+
Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create
|
|
351
|
+
on a AudioIO will close any former open video.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
filename: str or path
|
|
356
|
+
filename of path to the file (mp4, avi, ...)
|
|
357
|
+
|
|
358
|
+
sample_rate: int
|
|
359
|
+
If defined as a positive value, sample_rates of the output file will be set to this value.
|
|
360
|
+
|
|
361
|
+
channels: int
|
|
362
|
+
If defined as a positive value, number of channels of output file will be set to this value.
|
|
363
|
+
|
|
364
|
+
fps:
|
|
365
|
+
If defined as a positive value, fps of input video will be set to this value.
|
|
366
|
+
|
|
367
|
+
outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE)
|
|
368
|
+
Define audio format for samples. Possible value is AudioFormat.PCM32LE.
|
|
369
|
+
|
|
370
|
+
encodingParams: str optional (default None)
|
|
371
|
+
Parameter to pass to ffmpeg to encode video like audio filters.
|
|
372
|
+
|
|
373
|
+
plannar : bool optionnal (default True)
|
|
374
|
+
Input data to write are grouped by channel if True, interleaved instead.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
----------
|
|
378
|
+
bool
|
|
379
|
+
Was the creation successfull
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
# Close if already opened
|
|
383
|
+
self.close()
|
|
384
|
+
|
|
385
|
+
# Set geometry/fps of the video stream from params
|
|
386
|
+
self.sample_rate = int(sample_rate)
|
|
387
|
+
self.channels = int(channels)
|
|
388
|
+
self.plannar = plannar
|
|
389
|
+
|
|
390
|
+
# Check params
|
|
391
|
+
if self.sample_rate <= 0 or self.channels <= 0:
|
|
392
|
+
raise self.AudioIOException("Bad parameters: sample_rate={}, channels={}".format(self.sample_rate,self.channels))
|
|
393
|
+
|
|
394
|
+
# To write audio, we do not need to know in advance frame size, we will write x values of n bytes
|
|
395
|
+
self.frame_size = None
|
|
396
|
+
|
|
397
|
+
# Video params are set, open the video
|
|
398
|
+
cmd = [self.audioProgram] # ffmpeg
|
|
399
|
+
|
|
400
|
+
if writeOverExistingFile == True:
|
|
401
|
+
cmd.extend(['-y'])
|
|
402
|
+
|
|
403
|
+
cmd.extend(['-hide_banner',
|
|
404
|
+
'-nostats',
|
|
405
|
+
'-loglevel', str(self.logLevel),
|
|
406
|
+
'-f', 'f32le', '-acodec', outputEncoding.value, # input expected coding
|
|
407
|
+
'-ar', f"{self.sample_rate}",
|
|
408
|
+
'-ac', f"{self.channels}",
|
|
409
|
+
'-i', '-'])
|
|
410
|
+
|
|
411
|
+
if encodingParams is not None:
|
|
412
|
+
cmd.extend(encodingParams.split())
|
|
413
|
+
|
|
414
|
+
# remove video
|
|
415
|
+
cmd.extend( ['-vn', filename ] )
|
|
416
|
+
|
|
417
|
+
if self.debugMode == True:
|
|
418
|
+
print( ' '.join(cmd), file=sys.stderr )
|
|
419
|
+
|
|
420
|
+
# store filename and set mode
|
|
421
|
+
self.filename = filename
|
|
422
|
+
self.mode = PipeMode.WRITE_MODE
|
|
423
|
+
|
|
424
|
+
# try call ffmpeg and write frames directly to pipe
|
|
425
|
+
try:
|
|
426
|
+
self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
|
|
427
|
+
self.frame_counter = FrameCounter(self.sample_rate)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
# if pipe failed, reinit object and raise exception
|
|
430
|
+
self.init()
|
|
431
|
+
raise
|
|
432
|
+
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
def open( self, filename, *, sample_rate = -1, channels = -1, inputEncoding = AudioFormat.PCM32LE,
|
|
436
|
+
decodingParams = None, frame_size = 1.0, plannar = True, start_time = 0.0 ):
|
|
437
|
+
"""
|
|
438
|
+
Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open
|
|
439
|
+
on a AudioIO will close any former open file.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
filename: str or path
|
|
444
|
+
filename of path to the file (mp4, avi, ...)
|
|
445
|
+
|
|
446
|
+
sample_rate: int optional (default -1)
|
|
447
|
+
If defined as a positive value, sample rate of the input audio will be converted to this value.
|
|
448
|
+
|
|
449
|
+
channels: int optional (default -1)
|
|
450
|
+
If defined as a positive value, number of channels of the input audio will converted to this value.
|
|
451
|
+
|
|
452
|
+
inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE)
|
|
453
|
+
Define audio format for samples. Possible value is AudioFormat.PCM32LE.
|
|
454
|
+
|
|
455
|
+
decodingParams: str optional (default None)
|
|
456
|
+
Parameter to pass to ffmpeg to decode video like audio filters.
|
|
457
|
+
|
|
458
|
+
plannar: bool optionnal (default True)
|
|
459
|
+
Group audio samples per channel if True. Else, samples are interleaved.
|
|
460
|
+
|
|
461
|
+
frame_size: int or float (default 1.0)
|
|
462
|
+
If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples.
|
|
463
|
+
if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms.
|
|
464
|
+
Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate)
|
|
465
|
+
|
|
466
|
+
start_time: float optional (default 0.0)
|
|
467
|
+
Define the reading start time. If not set, reading at beginning of the file.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
----------
|
|
471
|
+
bool
|
|
472
|
+
Was the opening successfull
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
# Close if already opened
|
|
476
|
+
self.close()
|
|
477
|
+
|
|
478
|
+
# Force conversion of parameters
|
|
479
|
+
channels = int(channels)
|
|
480
|
+
sample_rate = float(sample_rate)
|
|
481
|
+
|
|
482
|
+
self.plannar = plannar
|
|
483
|
+
|
|
484
|
+
# get parameters from file if needed:
|
|
485
|
+
if sample_rate <= 0 or channels <= 0:
|
|
486
|
+
self.channels, self.sample_rate = self.getAudioParams(filename)
|
|
487
|
+
|
|
488
|
+
# check if parameters ask to overide video parameters
|
|
489
|
+
if channels > 0:
|
|
490
|
+
self.channels = channels
|
|
491
|
+
if sample_rate > 0:
|
|
492
|
+
self.sample_rate = sample_rate
|
|
493
|
+
|
|
494
|
+
# check parameters
|
|
495
|
+
|
|
496
|
+
if isinstance(frame_size,float):
|
|
497
|
+
# time in seconds
|
|
498
|
+
self.frame_size = int(frame_size*self.sample_rate)
|
|
499
|
+
elif isinstance(frame_size,int):
|
|
500
|
+
# number of samples
|
|
501
|
+
self.frame_size = frame_size
|
|
502
|
+
else:
|
|
503
|
+
# to do
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
# Video params are set, open the video
|
|
507
|
+
cmd = [self.audioProgram, # ffmpeg
|
|
508
|
+
'-hide_banner',
|
|
509
|
+
'-nostats',
|
|
510
|
+
'-loglevel', str(self.logLevel)]
|
|
511
|
+
|
|
512
|
+
if decodingParams is not None:
|
|
513
|
+
cmd.extend([decodingParams.split()])
|
|
514
|
+
|
|
515
|
+
if start_time < 0.0:
|
|
516
|
+
pass
|
|
517
|
+
elif start_time > 0.0:
|
|
518
|
+
cmd.extend(["-ss", f"{start_time}"])
|
|
519
|
+
|
|
520
|
+
cmd.extend( ['-i', filename,
|
|
521
|
+
'-f', 'f32le', '-acodec', inputEncoding.value, # input expected coding
|
|
522
|
+
'-ar', f"{self.sample_rate}",
|
|
523
|
+
'-ac', f"{self.channels}",
|
|
524
|
+
'-' # output to stdout
|
|
525
|
+
]
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
if self.debugMode == True:
|
|
529
|
+
print( ' '.join(cmd) )
|
|
530
|
+
|
|
531
|
+
# store filename and set mode to READ_MODE
|
|
532
|
+
self.filename = filename
|
|
533
|
+
self.mode = PipeMode.READ_MODE
|
|
534
|
+
|
|
535
|
+
# try to call ffmpeg to get frames directly from pipe
|
|
536
|
+
try:
|
|
537
|
+
self.pipe = sp.Popen(cmd, stdout=sp.PIPE)
|
|
538
|
+
self.frame_counter = FrameCounter(self.sample_rate)
|
|
539
|
+
if start_time > 0.0:
|
|
540
|
+
self.frame_counter += start_time # adding with float means adding time
|
|
541
|
+
except Exception as e:
|
|
542
|
+
# if pipe failed, reinit object and raise exception
|
|
543
|
+
self.init()
|
|
544
|
+
raise
|
|
545
|
+
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
def read_frame(self, with_timestamps = False):
|
|
549
|
+
"""
|
|
550
|
+
Read next frame from the audio file
|
|
551
|
+
|
|
552
|
+
Parameters
|
|
553
|
+
----------
|
|
554
|
+
with_timestamps: bool optional (default False)
|
|
555
|
+
If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s)
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
----------
|
|
559
|
+
nparray or FrameContainer
|
|
560
|
+
A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame
|
|
561
|
+
of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False.
|
|
562
|
+
if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and
|
|
563
|
+
the associated timestamp in ``FrameContainer.timestamps`` as an array (one element).
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
if self.pipe is None:
|
|
567
|
+
raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram))
|
|
568
|
+
# - pipe is in write mode
|
|
569
|
+
if self.mode != PipeMode.READ_MODE:
|
|
570
|
+
raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
|
|
571
|
+
|
|
572
|
+
if with_timestamps:
|
|
573
|
+
# get elapsed time in video, it is time of next frame(s)
|
|
574
|
+
current_elapsed_time = self.get_elapsed_time()
|
|
575
|
+
|
|
576
|
+
# read rgb image from pipe
|
|
577
|
+
toread = self.frame_size*4
|
|
578
|
+
buffer = self.pipe.stdout.read(toread)
|
|
579
|
+
if len(buffer) != toread:
|
|
580
|
+
# not considered as an error, no more frame, no exception
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
# get numpy UINT8 array from buffer
|
|
584
|
+
audio = np.frombuffer(buffer, dtype = np.float32).reshape(self.frame_size, self.channels)
|
|
585
|
+
|
|
586
|
+
# make it plannar (or not)
|
|
587
|
+
if self.plannar:
|
|
588
|
+
#transpose it
|
|
589
|
+
audio = audio.T
|
|
590
|
+
|
|
591
|
+
# increase frame_counter
|
|
592
|
+
self.frame_counter.frame_count += (self.frame_size * self.channels)
|
|
593
|
+
|
|
594
|
+
# say to gc that this buffer is no longer needed
|
|
595
|
+
del buffer
|
|
596
|
+
|
|
597
|
+
if with_timestamps:
|
|
598
|
+
return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time)
|
|
599
|
+
|
|
600
|
+
return audio
|
|
601
|
+
|
|
602
|
+
def read_batch(self, numberOfFrames, with_timestamps = False):
|
|
603
|
+
"""
|
|
604
|
+
Read next batch of audio from the file
|
|
605
|
+
|
|
606
|
+
Parameters
|
|
607
|
+
----------
|
|
608
|
+
number_of_frames: int
|
|
609
|
+
Number of desired images within the batch. The last batch from the file may have less images.
|
|
610
|
+
|
|
611
|
+
with_timestamps: bool optional (default False)
|
|
612
|
+
If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
|
|
613
|
+
|
|
614
|
+
Returns
|
|
615
|
+
----------
|
|
616
|
+
nparray or FrameContainer
|
|
617
|
+
A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch
|
|
618
|
+
of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False.
|
|
619
|
+
if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and
|
|
620
|
+
the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame).
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
if self.pipe is None:
|
|
624
|
+
raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram))
|
|
625
|
+
# - pipe is in write mode
|
|
626
|
+
if self.mode != PipeMode.READ_MODE:
|
|
627
|
+
raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
|
|
628
|
+
|
|
629
|
+
if with_timestamps:
|
|
630
|
+
# get elapsed time in video, it is time of next frame(s)
|
|
631
|
+
current_elapsed_time = self.get_elapsed_time()
|
|
632
|
+
|
|
633
|
+
# try to read complete batch
|
|
634
|
+
toread = self.frame_size*4*self.channels*numberOfFrames
|
|
635
|
+
buffer = self.pipe.stdout.read(toread)
|
|
636
|
+
|
|
637
|
+
# check if we have at least 1 Frame
|
|
638
|
+
if len(buffer) < toread:
|
|
639
|
+
# not considered as an error, no more frame, no exception
|
|
640
|
+
return None
|
|
641
|
+
|
|
642
|
+
# compute actual number of Frames
|
|
643
|
+
actualNbFrames = len(buffer)//(self.frame_size*4*self.channels)
|
|
644
|
+
|
|
645
|
+
# get and reshape batch from buffer
|
|
646
|
+
batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,))
|
|
647
|
+
|
|
648
|
+
if self.plannar:
|
|
649
|
+
batch = batch.transpose(0, 2, 1)
|
|
650
|
+
|
|
651
|
+
# increase frame_counter
|
|
652
|
+
self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels)
|
|
653
|
+
|
|
654
|
+
# say to gc that this buffer is no longer needed
|
|
655
|
+
del buffer
|
|
656
|
+
|
|
657
|
+
if with_timestamps:
|
|
658
|
+
return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time)
|
|
659
|
+
|
|
660
|
+
return batch
|
|
661
|
+
|
|
662
|
+
def write_frame(self, audio) -> bool:
|
|
663
|
+
"""
|
|
664
|
+
Write an audio frame to the file
|
|
665
|
+
|
|
666
|
+
Parameters
|
|
667
|
+
----------
|
|
668
|
+
audio: nparray
|
|
669
|
+
The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).
|
|
670
|
+
|
|
671
|
+
Returns
|
|
672
|
+
----------
|
|
673
|
+
bool
|
|
674
|
+
Writing was successful or not.
|
|
675
|
+
"""
|
|
676
|
+
# Check params
|
|
677
|
+
# - pipe exists
|
|
678
|
+
if self.pipe is None:
|
|
679
|
+
raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
|
|
680
|
+
# - pipe is in write mode
|
|
681
|
+
if self.mode != PipeMode.WRITE_MODE:
|
|
682
|
+
raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
|
|
683
|
+
# - shape of image is fine, thus we have pixels for a full compatible frame
|
|
684
|
+
if audio.shape[0] != self.channels:
|
|
685
|
+
raise self.AudioIOException("Wong audio shape: {} expected ({},{}).".format(audio.shape,self.channels,self.frame_size))
|
|
686
|
+
# - type of data is Float32
|
|
687
|
+
if audio.dtype != np.float32:
|
|
688
|
+
raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype))
|
|
689
|
+
|
|
690
|
+
# array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar
|
|
691
|
+
if not self.plannar:
|
|
692
|
+
audio = audio.reshape(-1)
|
|
693
|
+
|
|
694
|
+
print( audio.shape )
|
|
695
|
+
|
|
696
|
+
# garantee to have a C continuous array
|
|
697
|
+
if not audio.flags['C_CONTIGUOUS']:
|
|
698
|
+
a = np.ascontiguousarray(a)
|
|
699
|
+
|
|
700
|
+
# write frame
|
|
701
|
+
buffer = audio.tobytes()
|
|
702
|
+
if self.pipe.stdin.write( buffer ) < len(buffer):
|
|
703
|
+
print( f"Error writing frame to {self.filename}" )
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
# increase frame_counter
|
|
707
|
+
self.frame_counter.frame_count += (self.frame_size * self.channels)
|
|
708
|
+
|
|
709
|
+
# say to gc that this buffer is no longer needed
|
|
710
|
+
del buffer
|
|
711
|
+
|
|
712
|
+
return True
|
|
713
|
+
|
|
714
|
+
def write_batch(self, batch):
|
|
715
|
+
"""
|
|
716
|
+
Write a batch of audio frame to the file
|
|
717
|
+
|
|
718
|
+
Parameters
|
|
719
|
+
----------
|
|
720
|
+
batch: nparray
|
|
721
|
+
The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.
|
|
722
|
+
|
|
723
|
+
Returns
|
|
724
|
+
----------
|
|
725
|
+
bool
|
|
726
|
+
Writing was successful or not.
|
|
727
|
+
"""
|
|
728
|
+
# Check params
|
|
729
|
+
# - pipe exists
|
|
730
|
+
if self.pipe is None:
|
|
731
|
+
raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
|
|
732
|
+
# - pipe is in write mode
|
|
733
|
+
if self.mode != PipeMode.WRITE_MODE:
|
|
734
|
+
raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
|
|
735
|
+
# batch is 3D (n, channels, nb samples)
|
|
736
|
+
if batch.ndim !=3:
|
|
737
|
+
raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
|
|
738
|
+
# - shape of images in batch is fine
|
|
739
|
+
if batch.shape[2] != self.channels:
|
|
740
|
+
raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape))
|
|
741
|
+
|
|
742
|
+
# array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
|
|
743
|
+
# reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
|
|
744
|
+
if not self.plannar:
|
|
745
|
+
# goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
|
|
746
|
+
batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
|
|
747
|
+
batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
|
|
748
|
+
|
|
749
|
+
# garantee to have a C continuous array
|
|
750
|
+
if not batch.flags['C_CONTIGUOUS']:
|
|
751
|
+
batch = np.ascontiguousarray(batch)
|
|
752
|
+
|
|
753
|
+
# write frame
|
|
754
|
+
buffer = batch.tobytes()
|
|
755
|
+
if self.pipe.stdin.write( buffer ) < len(buffer):
|
|
756
|
+
# say to gc that this buffer is no longer needed
|
|
757
|
+
del buffer
|
|
758
|
+
raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
|
|
759
|
+
|
|
760
|
+
# increase frame_counter
|
|
761
|
+
self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float
|
|
762
|
+
|
|
763
|
+
# say to gc that this buffer is no longer needed
|
|
764
|
+
del buffer
|
|
765
|
+
|
|
766
|
+
return True
|
|
767
|
+
|
|
768
|
+
def iter_frames(self, with_timestamps = False):
|
|
769
|
+
"""
|
|
770
|
+
Method to iterate on audio frames using AudioIO obj.
|
|
771
|
+
for audio_frame in obj.iter_frames():
|
|
772
|
+
....
|
|
773
|
+
|
|
774
|
+
Parameters
|
|
775
|
+
----------
|
|
776
|
+
with_timestamps: bool optional (default False)
|
|
777
|
+
If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames
|
|
778
|
+
|
|
779
|
+
Returns
|
|
780
|
+
----------
|
|
781
|
+
nparray or FrameContainer
|
|
782
|
+
A batch of images of shape ()
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
if self.mode == PipeMode.READ_MODE:
|
|
787
|
+
while self.isOpened():
|
|
788
|
+
frame = self.readFrame(with_timestamps)
|
|
789
|
+
if frame is not None:
|
|
790
|
+
yield frame
|
|
791
|
+
finally:
|
|
792
|
+
self.close()
|
|
793
|
+
|
|
794
|
+
def iter_batches(self, batch_size : int, with_timestamps = False ):
|
|
795
|
+
"""
|
|
796
|
+
Method to iterate on batch ofaudio frames using VideoIO obj.
|
|
797
|
+
for audio_batch in obj.iter_batches():
|
|
798
|
+
....
|
|
799
|
+
|
|
800
|
+
Parameters
|
|
801
|
+
----------
|
|
802
|
+
with_timestamps: bool optional (default False)
|
|
803
|
+
If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
|
|
804
|
+
"""
|
|
805
|
+
try:
|
|
806
|
+
if self.mode == PipeMode.READ_MODE:
|
|
807
|
+
while self.isOpened():
|
|
808
|
+
batch = self.readBatch(batch_size, with_timestamps)
|
|
809
|
+
if batch is not None:
|
|
810
|
+
yield batch
|
|
811
|
+
finally:
|
|
812
|
+
self.close()
|
|
813
|
+
|
|
814
|
+
# function aliases to be compliant with original C++ version
|
|
815
|
+
getAudioTimeInSec = get_time_in_sec
|
|
816
|
+
getAudioParams = get_params
|
|
817
|
+
get_audio_time_in_sec = get_time_in_sec
|
|
818
|
+
get_audio_params = get_params
|
|
819
|
+
isOpened = is_opened
|
|
820
|
+
readFrame = read_frame
|
|
821
|
+
readBatch = read_batch
|
|
822
|
+
writeFrame = write_frame
|
|
823
|
+
writeBatch = write_batch
|
|
824
|
+
|