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.
@@ -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
+