simple_ffmpeg_batch_io

Reading and writing image and audio batches from/to video and audio files using an FFmpeg backend, with a simple Python API built on top of numpy.

Features

  • Read batches of audio and video frames from video and audio files into numpy arrays.
  • Write batches of frames from numpy arrays to video or audio files, even compressed
  • Uses static_ffmpeg to provide FFmpeg binaries in a portable way. Simple-ffmpeg-batch-io provide ffmpeg and ffprobe as commands within the virtual environment
  • Designed for machine learning and audio/video generation pipelines.

Installation of last version

pip install simple-ffmpeg-batch-io

Examples

Handling video files (i.e. images from video files)

Read video file at its own frame rate and frame shape

# Open it with VideoIO old way (C++ style)
inputVideo = VideoIO()
inputVideo.open(video_filename) # video file is a str or a path

# or open it a more pythonish way
inputVideo = VideoIO.reader(video_filename) # video file is a str or a path

# Here, one can read inputVideo.width, inputVideo.height, inputVideo.fps
print(inputVideo.width, inputVideo.height, inputVideo.fps)

# read one frame
frame = inputVideo.read_frame() # Here frame is a numpy arrays of shape (Width,height,channels). Channel is 3 as we support only 3 channels for the moment.

# Process video frame by frame
for frame in inputVideo.iter_frames():
    # Process frame. Here frame is a numpy arrays of shape (Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
    process( frame )

# or process video using batches
n = 10
for batch in inputVideo.iter_batches(n):
    # Process batch. Here batch is a numpy arrays of shape (n,Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
    process( batch )

inputVideo.close()

# Read video frame by frame with associated timestamps using with context, no need to close after end of context, close is aotomùatically called.
with VideoIO.reader(video_filename) as inputVideo:
    for frame in inputVideo.iter_frames(with_timestamps = True):
        # Here frame is encapsulated within simple-ffmpeg-batch-io.FrameContainer object
        process( frame.data )    # numpy array of shape (Width,height,channels)
        print( frame.timestamps ) # python list of the timestamp associated to the frame (video here, but same for audio), here only one element as one frame is used

# OR
# Read video batch by batch with associated timestamps using with context, no need to close after end of context, close is aotomùatically called.
n = 10
with VideoIO.reader(video_filename) as inputVideo:
    for batch in inputVideo.iter_batches(n, with_timestamps = True):
        # Here batch is encapsulated within simple-ffmpeg-batch-io.FrameContainer object
        process( batch.data )    # numpy array of shape (n,Width,height,channels)
        print( batch.timestamps ) # python list of timestamps associated to each frame (video here, but same for audio), here only one element as one frame is used

Read video file with more parameters

from simple_ffmpeg_batch_io import VideoIO

# Read video changing width, height, and fps
inputVideo = VideoIO.reader(video_file, width=100, height=100, fps=1.0)

# Read modifying only some of them
inputVideo = VideoIO.reader(video_file, width=100, fps=2.0)

Read video and write video file with dedicated ffmpeg parameters

from simple_ffmpeg_batch_io import VideoIO

# open file using filter:
# resizing to width=320, adapting height to keep aspect ratio while keep height pair (for some codec like H264)
# pixelising it to 5x5 pixels
# important note: one must not use decodingParams for scalling. Indeed, VideoIO class use filter to scale video, use width/height parameters
with VideoIO.reader("camera_4_short.mp4", width=320, height=320, decodingParams="-vf pixelize=w=5:h=5") as inputVideo,
     VideoIO.writer("camera_4_short_pix.avi", width=inputVideo.width, height=inputVideo.height, fps=inputVideo.fps ) as outputVideo:  # possible to add encodingParams to add filters, ...
    # iter over batch of 10s and write it to the output file
    batch_size = int( 10*inputVideo.fps )
    for batch in inputVideo.iter_batches(batch_size):
        outputVideo.write_batch(batch)

Handling audio from video or audio files

Read audio from audio file

# Read audio converting it to one channel, 16000 Hz, frame size of 1s as parameter is a float, start reading file at 2.0s
# default mode is plannar, i.e. samples are not interleaved, they are separated by channel after reading
# frame_size for subsequent call to read_frame, iter_frames, read_batch or iter_batches is 1.0s (16000 samples) as the value is a float, thus times in seconds.
inputAudio = AudioIO.reader(audio_filename, 16000, 1, frame_size = 1.0, start_time = 2.0 )

# If the frame_size value is an int, frame_size is considered as a number of samples, for instance to have 0.5 seconds at 16 Khz (8000 samples for each frame)
inputAudio = AudioIO.reader(audio_filename, 16000, 1, frame_size = 8000, start_time = 2.0 )


# 

Static utility functions

VideoIO

from simple_ffmpeg_batch_io import VideoIO

# get (width, height, fps) of a video using a static function
print( VideoIO.get_params(video_filename) )

# get length of an audio stream as float (seconds.milliseconds)
print( VideoIO.get_time_in_sec(video_filename) )

AudioIO

from simple_ffmpeg_batch_io import AudioIO

# get (channels,sample_rate) of a stream using astatic function
print( AudioIO.get_params(filename) )

# get length of video stream as float (seconds.milliseconds)
print( AudioIO.get_time_in_sec(filename) )

Submodules

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

Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode

Parameters

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

debugMode: bool (default (False) Show debug info. while processing video

@classmethod
def reader(cls, filename, **kwargs):
50    @classmethod
51    def reader(cls, filename, **kwargs):
52        """
53        Create and open a VideoIO object in reader mode (read a video file)
54
55        See `VideoIO.open` for the full list
56        of accepted parameters.
57        """
58        reader = cls()
59        reader.open(filename,**kwargs)
60        return reader

Create and open a VideoIO object in reader mode (read a video file)

See VideoIO.open for the full list of accepted parameters.

@classmethod
def writer(cls, filename, width, height, fps, **kwargs):
62    @classmethod
63    def writer(cls, filename, width, height, fps, **kwargs):
64        """
65        Create and open a VideoIO object in writer mode (write a video file)
66
67        See `VideoIO.create` for the full list
68        of accepted parameters.
69        """
70        writer = cls()
71        writer.create(filename, width, height, fps, **kwargs)
72        return writer

Create and open a VideoIO object in writer mode (write a video file)

See VideoIO.create for the full list of accepted parameters.

@staticmethod
def get_time_in_sec(filename, *, debug=False, logLevel=16):
 90    @staticmethod
 91    def get_time_in_sec(filename, *, debug=False, logLevel=16):
 92        """
 93        Static method to get length of a video file in seconds including milliseconds as decimal part.
 94
 95        Parameters
 96        ----------
 97        filename : str or path
 98            Video file name.
 99
100        debug : bool (default False)
101            Show debug info.
102
103        log_level: int (default 16)
104            Log level to pass to the underlying ffmpeg/ffprobe command.
105        
106        Returns
107        ----------
108        float
109            Length in seconds of video file (including milliseconds as decimal part)
110        """
111        
112        cmd = [VideoIO.paramProgram, # ffprobe
113                    '-hide_banner',
114                    '-loglevel', str(logLevel),
115                    '-show_entries', 'format=duration',
116                    '-of', 'default=noprint_wrappers=1:nokey=1',
117                    filename
118                    ]
119
120        if debug == True:
121            print(' '.join(cmd))
122
123        # call ffprobe and get params in one single line
124        lpipe = sp.Popen(cmd, stdout=sp.PIPE)
125        output = lpipe.stdout.readlines()
126        lpipe.terminate()
127        # transform Bytes output to one single string
128        output = ''.join( [element.decode('utf-8') for element in output])
129
130        try:
131            return float(output)
132        except (ValueError, TypeError):
133            return None

Static method to get length of a video file in seconds including milliseconds as decimal part.

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part)

@staticmethod
def get_params(filename, *, debug=False, logLevel=16):
135    @staticmethod
136    def get_params(filename, *, debug=False, logLevel=16):
137        """
138        Static method to get params (width, height, fps) from a video file.
139
140        Parameters
141        ----------
142        filename: str or path
143            Video filename.
144
145        debug: bool (default (False)
146            Show debug info.
147
148        log_level: int (default 16)
149            Log level to pass to the underlying ffmpeg/ffprobe command.
150
151        Returns
152        ----------
153        tuple
154            Tuple containing (width, height, fps) of the video
155        """
156        cmd = [VideoIO.paramProgram, # ffprobe
157                    '-hide_banner',
158                    '-loglevel', str(logLevel),
159                    '-show_entries', 'stream=width,height,r_frame_rate',
160                    filename
161                    ]
162
163        if debug == True:
164            print(' '.join(cmd))
165
166        # call ffprobe and get params in one single line
167        lpipe = sp.Popen(cmd, stdout=sp.PIPE)
168        output = lpipe.stdout.readlines()
169        lpipe.terminate()
170        # transform Bytes output to one single string
171        output = ''.join( [element.decode('utf-8') for element in output])
172
173        pattern_width = r'width=(\d+)'
174        pattern_height = r'height=(\d+)'
175        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
176
177        # Search for values in the ffprobe output
178        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
179        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
180        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
181
182        # Extraction des valeurs
183        if match_width:
184            width = int(match_width.group(1))
185        else:
186            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
187
188        if match_height:
189            height = int(match_height.group(1))
190        else:
191            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
192
193        if match_fps:
194            numerator = float(match_fps.group(1))
195            denominator = float(match_fps.group(2))
196            fps = numerator / denominator
197        else:
198            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
199
200        return (width, height, fps)

Static method to get params (width, height, fps) from a video file.

Parameters

filename: str or path Video filename.

debug: bool (default (False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (width, height, fps) of the video

mode: simple_ffmpeg_batch_io.PipeMode.PipeMode

Pipemode of the current object (default PipeMode.UNK_MODE)

loglevel: int

loglevel of the underlying ffmpeg backend for this object (default 16)

debugModel: bool

debutMode flag for this object (print debut info, default False)

width: int

width of images (default -1)

height: int

height of images (default -1)

fps: float

fps of video (default -1.0)

pipe: subprocess.Popen

pipe object to ffmpeg/ffprobe (default None)

shape: tuple

Shape of images (default (None, None, None))

imageSize: int

Weight in bytes of one image (default -1)

filename: str

Filename of the video file (default None)

frame_counter: FrameCounter

Framecounter object to count ellapsed time (default None)

logLevel
debugMode
def init(self):
256    def init(self):
257        """
258        Init or reinit a VideoIO object.
259        """
260        self.width  = -1
261        self.height = -1
262        self.fps = -1.0
263        self.pipe = None
264        self.shape = (None, None, None)
265        self.imageSize = -1
266        self.filename = None
267        self.frame_counter = None

Init or reinit a VideoIO object.

def get_elapsed_time_as_str(self) -> str:
287    def get_elapsed_time_as_str(self) -> str:
288        """
289        Method to get elapsed time (float value) as str from `frame_counter` attribute.
290
291        Returns
292        ----------
293        str or None
294            Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
295            None if no frame counter are available.
296        """
297        if self.frame_counter is None:
298            return None
299        return self.frame_counter.get_elapsed_time_as_str()

Method to get elapsed time (float value) as str from frame_counter attribute.

Returns

str or None Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_formated_elapsed_time_as_str(self, show_ms=True) -> str:
301    def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
302        """
303        Method to get elapsed time (hour format) as str from `frame_counter` attribute.
304
305        Returns
306        ----------
307        str or None
308            Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
309            None if no frame counter are available.
310        """
311        if self.frame_counter is None:
312            return None
313        return self.frame_counter.get_formated_elapsed_time_as_str()

Method to get elapsed time (hour format) as str from frame_counter attribute.

Returns

str or None Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_elapsed_time(self) -> float:
315    def get_elapsed_time(self) -> float:
316        """
317        Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute.
318
319        Returns
320        ----------
321        float or None
322            Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
323            None if no frame counter are available.
324        """
325        if self.frame_counter is None:
326            return None
327        return self.frame_counter.get_elapsed_time()

Method to get elapsed time as float value rounded to 3 decimals (millisecond) from frame_counter attribute.

Returns

float or None Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def is_opened(self) -> bool:
329    def is_opened(self) -> bool:
330        """
331        Method to get status of the underlying pipe to ffmpeg.
332
333        Returns
334        ----------
335        bool
336            True if pipe is opened (reading or writing mode), False if not.
337        """
338        # is the pipe opened?
339        if self.pipe is not None and self.pipe.poll() is None:
340            return True
341
342        return False

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def close(self) -> None:
344    def close(self) -> None:
345        """
346        Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.
347        """
348        if self.pipe is not None:
349            if self.mode == PipeMode.WRITE_MODE:
350                # killing will make ffmpeg not finish properly the job, close the pipe
351                # to let it know that no more data are comming
352                self.pipe.stdin.close()
353            else: # self.mode == PipeMode.READ_MODE
354                # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
355                self.pipe.kill()
356
357            # wait for subprocess to end
358            self.pipe.wait()
359
360        # reinit object for later use
361        self.init()

Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.

def create( self, width, height, fps, *, writeOverExistingFile=False, inputEncoding=<PixelFormat.GBR24: 'bgr24'>, encodingParams=None) -> bool:
363    def create(self, width, height, fps, *, writeOverExistingFile = False,
364                     inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool:
365        """
366        Method to create a video using parametrized access through ffmpeg. Importante note: calling create
367        on a VideoIO will close any former open video.
368
369        Parameters
370        ----------
371        filename: str or path
372            filename of path to the file (mp4, avi, ...)
373
374        width: int
375            If defined as a positive value, width of output images will be set to this value.
376
377        height: int
378            If defined as a positive value, height of output images will be set to this value.
379
380        fps:
381            If defined as a positive value, fps of output video will be set to this value.
382
383        inputEncoding: PixelFormat optional (default PixelFormat.BGR24)
384            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
385
386        encodingParams: str optional (default None)
387            Parameter to pass to ffmpeg to encode video like video filters.
388
389        Returns
390        ----------
391        bool
392            Was the creation successfull
393        """
394
395        # Close if already opened
396        self.close()
397
398        # Set geometry/fps of the video stream from params
399        self.width = int(width)
400        self.height = int(height)
401        self.fps = float(fps)
402
403        # Check params
404        if self.width <= 0 or self.height <= 0 or self.fps <= 0.0:
405            raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps))
406
407        # Params are ok, set shape and image size
408        self.shape     = (self.height,self.width,3)
409        self.imageSize = self.height * self.width * 3
410
411        # Video params are set, open the video
412        cmd = [self.videoProgram] # ffmpeg
413
414        if writeOverExistingFile == True:
415            cmd.extend(['-y'])
416
417        cmd.extend(['-hide_banner',
418            '-nostats',
419            '-loglevel', str(self.logLevel),
420            '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value,
421            '-video_size', f"{self.width}x{self.height}",
422            '-r', "{:.3f}".format(self.fps),
423            '-i', '-'])
424
425        if encodingParams is not None:
426            cmd.extend(encodingParams.split())
427
428        cmd.extend( ['-an', filename ] )
429
430        if self.debugMode == True:
431            print( ' '.join(cmd), file=sys.stderr )
432
433        # store filename and set mode
434        self.filename = filename
435        self.mode = PipeMode.WRITE_MODE
436
437        # try call ffmpeg and write frames directly to pipe
438        try:
439            self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
440            self.frame_counter = FrameCounter(self.fps)
441        except Exception as e:
442            # if pipe failed, reinit object and raise exception
443            self.init()
444            raise
445
446        return True

Method to create a video using parametrized access through ffmpeg. Importante note: calling create on a VideoIO will close any former open video.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

width: int If defined as a positive value, width of output images will be set to this value.

height: int If defined as a positive value, height of output images will be set to this value.

fps: If defined as a positive value, fps of output video will be set to this value.

inputEncoding: PixelFormat optional (default PixelFormat.BGR24) Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.

encodingParams: str optional (default None) Parameter to pass to ffmpeg to encode video like video filters.

Returns

bool Was the creation successfull

def open( self, filename, *, width=-1, height=-1, fps=-1.0, outputEncoding=<PixelFormat.GBR24: 'bgr24'>, decodingParams=None, start_time=0.0) -> bool:
448    def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24,
449                    decodingParams = None, start_time = 0.0 ) -> bool:
450        """
451        Method to read video using parametrized access through ffmpeg. Importante note: calling open
452        on a VideoIO will close any former open video.
453
454        Parameters
455        ----------
456        filename: str or path
457            filename of path to the file (mp4, avi, ...)
458
459        width: int optional (default -1)
460            If defined as a positive value, width of input images will be converted to this value.
461
462        height: int optional (default -1)
463            If defined as a positive value, height of input images will be converted to this value.
464
465        fps: float optional (default -1.0)
466            If defined as a positive value, fps of input video will be converted to this value.
467
468        outputEncoding: PixelFormat optional (default PixelFormat.BGR24)
469            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
470
471        decodingParams: str optional (default None)
472            Parameter to pass to ffmpeg to decode video like filters.
473
474        start_time: float optional (default 0.0)
475            Define the reading start time. If not set, reading at beginning of the video.
476
477        Returns
478        ----------
479        bool
480            Was the opening successfull
481        """
482
483        # Close if already opened
484        self.close()
485
486        # Force conversion of parameters
487        width = int(width)
488        height = int(height)
489        fps = float(fps)
490
491        # get parameters from video
492        self.width, self.height, self.fps = self.getVideoParams(filename)
493
494        # check if parameters ask to overide video parameters
495        # TODO: add support for negative value (automatic preservation of aspect ratio)
496        if width > 0:
497            self.width = width
498        if height > 0:
499            self.height = height
500        if fps > 0.0:
501            self.fps = fps
502
503        # Params are ok, set shape and image size
504        self.shape = (self.height,self.width,3)
505        self.imageSize = self.height * self.width * 3
506
507        # Video params are set, open the video
508        cmd = [self.videoProgram, # ffmpeg
509                    '-hide_banner',
510                    '-nostats',
511                    '-loglevel', str(self.logLevel)]
512
513        if start_time < 0.0:
514            pass
515        elif start_time > 0.0:
516            cmd.extend(["-ss", f"{start_time}"])    # set start time if any
517
518        cmd.extend( ['-i', filename] )
519
520        video_filters = '' # empty
521        if decodingParams is not None:
522            decodingParams = decodingParams.split()
523            # walk over decodingParams for specific params
524            i = 0
525            while i < len(decodingParams):
526                if decodingParams[i] == '-vf':
527                    decodingParams.pop(i)  # remove '-vf'
528                    if i < len(decodingParams):
529                        video_filters += ','+decodingParams.pop(i)  # remove parameters from list too
530                    # to do : add support to other option like -y
531                else:
532                    i += 1
533        else:
534            decodingParams = []
535
536        cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters
537                    *(decodingParams),
538                    '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding
539                    '-an', # no audio
540                     '-r', f"{self.fps}",
541                     '-' # output to stdout
542                    ] )
543
544        if self.debugMode == True:
545            print( ' '.join(cmd) )
546
547        # store filename and set mode to READ_MODE
548        self.filename = filename
549        self.mode = PipeMode.READ_MODE
550
551        # try to call ffmpeg to get frames directly from pipe
552        try:
553            self.pipe = sp.Popen(cmd, stdout=sp.PIPE)
554            self.frame_counter = FrameCounter(self.fps)
555            if start_time > 0.0:
556                self.frame_counter += start_time # adding with float means adding time
557        except Exception as e:
558            # if pipe failed, reinit object and raise exception
559            self.init()
560            raise
561
562        return True

Method to read video using parametrized access through ffmpeg. Importante note: calling open on a VideoIO will close any former open video.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

width: int optional (default -1) If defined as a positive value, width of input images will be converted to this value.

height: int optional (default -1) If defined as a positive value, height of input images will be converted to this value.

fps: float optional (default -1.0) If defined as a positive value, fps of input video will be converted to this value.

outputEncoding: PixelFormat optional (default PixelFormat.BGR24) Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.

decodingParams: str optional (default None) Parameter to pass to ffmpeg to decode video like filters.

start_time: float optional (default 0.0) Define the reading start time. If not set, reading at beginning of the video.

Returns

bool Was the opening successfull

def read_frame(self, with_timestamps=False):
564    def read_frame(self, with_timestamps = False):
565        """
566        Read next frame from the video
567
568        Parameters
569        ----------
570        with_timestamps: bool optional (default False)
571            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
572
573        Returns
574        ----------
575        nparray or FrameContainer
576            An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and
577            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
578        """
579
580        if self.pipe is None:
581            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
582        # - pipe is in write mode
583        if self.mode != PipeMode.READ_MODE:
584            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
585
586        if with_timestamps:
587            # get elapsed time in video, it is time of next frame(s)
588            current_elapsed_time = self.get_elapsed_time()
589
590        # read rgb image from pipe
591        buffer = self.pipe.stdout.read(self.imageSize)
592        if len(buffer) != self.imageSize:
593            # not considered as an error, no more frame, no exception
594            return None
595
596        # get numpy UINT8 array from buffer
597        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
598
599        # increase frame_counter
600        self.frame_counter.frame_count += 1
601
602        # say to gc that this buffer is no longer needed
603        del buffer
604
605        if with_timestamps:
606            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
607
608        return rgbImage

Read next frame from the video

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for one frame).

def read_batch(unknown):
610    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[nb.array, FrameC]:
611        """
612        Read next batch of images from the video
613
614        Parameters
615        ----------
616        number_of_frames: int
617            Number of desired images within the batch. The last batch of the video may have less images.
618            
619        with_timestamps: bool optional (default False)
620            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
621
622        Returns
623        ----------
624        nparray or FrameContainer
625            A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and
626            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
627        """
628
629        if self.pipe is None:
630            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
631        # - pipe is in write mode
632        if self.mode != PipeMode.READ_MODE:
633            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
634
635        if with_timestamps:
636            # get elapsed time in video, it is time of next frame(s)
637            current_elapsed_time = self.get_elapsed_time()
638
639        # try to read complete batch
640        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
641
642        # check if we have at least 1 Frame
643        if len(buffer) < self.imageSize:
644            # not considered as an error, no more frame, no exception
645            return None
646
647        # compute actual number of Frames
648        actualNbFrames = len(buffer)//self.imageSize
649
650        # get and reshape batch from buffer
651        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
652
653        # increase frame_counter
654        self.frame_counter.frame_count += actualNbFrames
655        
656        # say to gc that this buffer is no longer needed
657        del buffer
658
659        if with_timestamps:
660            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
661
662        return batch

Read next batch of images from the video

Parameters

number_of_frames: int Number of desired images within the batch. The last batch of the video may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in FrameContainer.data and the associated timestamps in FrameContainer.timestamps as an array (one element for each frame).

def write_frame(self, image) -> bool:
664    def write_frame(self, image) -> bool:
665        """
666        Write an image to the video
667
668        Parameters
669        ----------
670        image: nparray
671            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
672
673        Returns
674        ----------
675        bool
676            Writing was successful or not.
677        """
678        
679        # Check params
680        # - pipe exists
681        if self.pipe is None:
682            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
683        # - pipe is in write mode
684        if self.mode != PipeMode.WRITE_MODE:
685            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
686        # - shape of image is fine, thus we have pixels for a full compatible frame
687        if image.shape != self.shape:
688            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
689        # - type of data is UINT8
690        if image.dtype != np.uint8:
691            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
692
693        # write frame
694        buffer = image.tobytes()
695        if self.pipe.stdin.write( buffer ) < self.imageSize:
696            print( "Error writing frame to" )
697            return False
698
699        # increase frame_counter
700        self.frame_counter.frame_count += 1
701
702        # say to gc that this buffer is no longer needed 
703        del buffer
704
705        return True

Write an image to the video

Parameters

image: nparray The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

def write_batch(self, batch) -> bool:
707    def write_batch(self, batch) -> bool:
708        """
709        Write a batch of images to the video
710
711        Parameters
712        ----------
713        batch: nparray
714            A batch of images to write to the video file in the PixelFormat provided when create was called.
715
716        Returns
717        ----------
718        bool
719            Writing was successful or not.
720        """
721
722        # Check params
723        # - pipe exists
724        if self.pipe is None:
725            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
726        # - pipe is in write mode
727        if self.mode != PipeMode.WRITE_MODE:
728            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
729        # - shape of images in batch is fine
730        if batch.shape[-3:] != self.shape:
731            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
732        # - we have the right amount of pixels for the full batch
733        if batch.size != (batch.shape[0]*self.imageSize):
734            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
735
736        # write frame
737        buffer = batch.tobytes()
738        if self.pipe.stdin.write( buffer ) < batch.size:
739            # say to gc that this buffer is no longer needed
740            del buffer
741            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
742
743        # increase frame_counter
744        self.frame_counter.frame_count += batch.shape[0]       
745            
746        # say to gc that this buffer is no longer needed
747        del buffer
748
749        return True

Write a batch of images to the video

Parameters

batch: nparray A batch of images to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

def iter_frames(self, with_timestamps=False):
751    def iter_frames(self, with_timestamps = False):
752        """
753        Method to iterate on video frames using VideoIO obj.
754        for frame in obj.iter_frames():
755            ....
756
757        Parameters
758        ----------
759        with_timestamps: bool optional (default False)
760            If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames
761        """
762
763        try:
764            if self.mode == PipeMode.READ_MODE:
765                while self.isOpened():
766                    frame = self.readFrame(with_timestamps)
767                    if frame is not None:
768                        yield frame
769        finally:
770            self.close()

Method to iterate on video frames using VideoIO obj. for frame in obj.iter_frames(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames

def iter_batches(self, batch_size: int, with_timestamps=False):
772    def iter_batches(self, batch_size : int, with_timestamps = False):
773        """
774        Method to iterate on batch of frames using VideoIO obj.
775        for image_batch in obj.iter_batches():
776            ....
777
778        Parameters
779        ----------
780        with_timestamps: bool optional (default False)
781            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
782        """
783        try:
784            if self.mode == PipeMode.READ_MODE:
785                while self.isOpened():
786                    batch = self.readBatch(batch_size, with_timestamps)
787                    if batch is not None:
788                        yield batch
789        finally:
790            self.close()

Method to iterate on batch of frames using VideoIO obj. for image_batch in obj.iter_batches(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

@staticmethod
def getVideoTimeInSec(filename, *, debug=False, logLevel=16):
 90    @staticmethod
 91    def get_time_in_sec(filename, *, debug=False, logLevel=16):
 92        """
 93        Static method to get length of a video file in seconds including milliseconds as decimal part.
 94
 95        Parameters
 96        ----------
 97        filename : str or path
 98            Video file name.
 99
100        debug : bool (default False)
101            Show debug info.
102
103        log_level: int (default 16)
104            Log level to pass to the underlying ffmpeg/ffprobe command.
105        
106        Returns
107        ----------
108        float
109            Length in seconds of video file (including milliseconds as decimal part)
110        """
111        
112        cmd = [VideoIO.paramProgram, # ffprobe
113                    '-hide_banner',
114                    '-loglevel', str(logLevel),
115                    '-show_entries', 'format=duration',
116                    '-of', 'default=noprint_wrappers=1:nokey=1',
117                    filename
118                    ]
119
120        if debug == True:
121            print(' '.join(cmd))
122
123        # call ffprobe and get params in one single line
124        lpipe = sp.Popen(cmd, stdout=sp.PIPE)
125        output = lpipe.stdout.readlines()
126        lpipe.terminate()
127        # transform Bytes output to one single string
128        output = ''.join( [element.decode('utf-8') for element in output])
129
130        try:
131            return float(output)
132        except (ValueError, TypeError):
133            return None

Static method to get length of a video file in seconds including milliseconds as decimal part.

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part)

@staticmethod
def getVideoParams(filename, *, debug=False, logLevel=16):
135    @staticmethod
136    def get_params(filename, *, debug=False, logLevel=16):
137        """
138        Static method to get params (width, height, fps) from a video file.
139
140        Parameters
141        ----------
142        filename: str or path
143            Video filename.
144
145        debug: bool (default (False)
146            Show debug info.
147
148        log_level: int (default 16)
149            Log level to pass to the underlying ffmpeg/ffprobe command.
150
151        Returns
152        ----------
153        tuple
154            Tuple containing (width, height, fps) of the video
155        """
156        cmd = [VideoIO.paramProgram, # ffprobe
157                    '-hide_banner',
158                    '-loglevel', str(logLevel),
159                    '-show_entries', 'stream=width,height,r_frame_rate',
160                    filename
161                    ]
162
163        if debug == True:
164            print(' '.join(cmd))
165
166        # call ffprobe and get params in one single line
167        lpipe = sp.Popen(cmd, stdout=sp.PIPE)
168        output = lpipe.stdout.readlines()
169        lpipe.terminate()
170        # transform Bytes output to one single string
171        output = ''.join( [element.decode('utf-8') for element in output])
172
173        pattern_width = r'width=(\d+)'
174        pattern_height = r'height=(\d+)'
175        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
176
177        # Search for values in the ffprobe output
178        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
179        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
180        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
181
182        # Extraction des valeurs
183        if match_width:
184            width = int(match_width.group(1))
185        else:
186            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
187
188        if match_height:
189            height = int(match_height.group(1))
190        else:
191            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
192
193        if match_fps:
194            numerator = float(match_fps.group(1))
195            denominator = float(match_fps.group(2))
196            fps = numerator / denominator
197        else:
198            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
199
200        return (width, height, fps)

Static method to get params (width, height, fps) from a video file.

Parameters

filename: str or path Video filename.

debug: bool (default (False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (width, height, fps) of the video

def isOpened(self) -> bool:
329    def is_opened(self) -> bool:
330        """
331        Method to get status of the underlying pipe to ffmpeg.
332
333        Returns
334        ----------
335        bool
336            True if pipe is opened (reading or writing mode), False if not.
337        """
338        # is the pipe opened?
339        if self.pipe is not None and self.pipe.poll() is None:
340            return True
341
342        return False

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def readFrame(self, with_timestamps=False):
564    def read_frame(self, with_timestamps = False):
565        """
566        Read next frame from the video
567
568        Parameters
569        ----------
570        with_timestamps: bool optional (default False)
571            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
572
573        Returns
574        ----------
575        nparray or FrameContainer
576            An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and
577            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
578        """
579
580        if self.pipe is None:
581            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
582        # - pipe is in write mode
583        if self.mode != PipeMode.READ_MODE:
584            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
585
586        if with_timestamps:
587            # get elapsed time in video, it is time of next frame(s)
588            current_elapsed_time = self.get_elapsed_time()
589
590        # read rgb image from pipe
591        buffer = self.pipe.stdout.read(self.imageSize)
592        if len(buffer) != self.imageSize:
593            # not considered as an error, no more frame, no exception
594            return None
595
596        # get numpy UINT8 array from buffer
597        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
598
599        # increase frame_counter
600        self.frame_counter.frame_count += 1
601
602        # say to gc that this buffer is no longer needed
603        del buffer
604
605        if with_timestamps:
606            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
607
608        return rgbImage

Read next frame from the video

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for one frame).

def readBatch(unknown):
610    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[nb.array, FrameC]:
611        """
612        Read next batch of images from the video
613
614        Parameters
615        ----------
616        number_of_frames: int
617            Number of desired images within the batch. The last batch of the video may have less images.
618            
619        with_timestamps: bool optional (default False)
620            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
621
622        Returns
623        ----------
624        nparray or FrameContainer
625            A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and
626            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
627        """
628
629        if self.pipe is None:
630            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
631        # - pipe is in write mode
632        if self.mode != PipeMode.READ_MODE:
633            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
634
635        if with_timestamps:
636            # get elapsed time in video, it is time of next frame(s)
637            current_elapsed_time = self.get_elapsed_time()
638
639        # try to read complete batch
640        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
641
642        # check if we have at least 1 Frame
643        if len(buffer) < self.imageSize:
644            # not considered as an error, no more frame, no exception
645            return None
646
647        # compute actual number of Frames
648        actualNbFrames = len(buffer)//self.imageSize
649
650        # get and reshape batch from buffer
651        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
652
653        # increase frame_counter
654        self.frame_counter.frame_count += actualNbFrames
655        
656        # say to gc that this buffer is no longer needed
657        del buffer
658
659        if with_timestamps:
660            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
661
662        return batch

Read next batch of images from the video

Parameters

number_of_frames: int Number of desired images within the batch. The last batch of the video may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in FrameContainer.data and the associated timestamps in FrameContainer.timestamps as an array (one element for each frame).

def writeFrame(self, image) -> bool:
664    def write_frame(self, image) -> bool:
665        """
666        Write an image to the video
667
668        Parameters
669        ----------
670        image: nparray
671            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
672
673        Returns
674        ----------
675        bool
676            Writing was successful or not.
677        """
678        
679        # Check params
680        # - pipe exists
681        if self.pipe is None:
682            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
683        # - pipe is in write mode
684        if self.mode != PipeMode.WRITE_MODE:
685            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
686        # - shape of image is fine, thus we have pixels for a full compatible frame
687        if image.shape != self.shape:
688            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
689        # - type of data is UINT8
690        if image.dtype != np.uint8:
691            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
692
693        # write frame
694        buffer = image.tobytes()
695        if self.pipe.stdin.write( buffer ) < self.imageSize:
696            print( "Error writing frame to" )
697            return False
698
699        # increase frame_counter
700        self.frame_counter.frame_count += 1
701
702        # say to gc that this buffer is no longer needed 
703        del buffer
704
705        return True

Write an image to the video

Parameters

image: nparray The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

def writeBatch(self, batch) -> bool:
707    def write_batch(self, batch) -> bool:
708        """
709        Write a batch of images to the video
710
711        Parameters
712        ----------
713        batch: nparray
714            A batch of images to write to the video file in the PixelFormat provided when create was called.
715
716        Returns
717        ----------
718        bool
719            Writing was successful or not.
720        """
721
722        # Check params
723        # - pipe exists
724        if self.pipe is None:
725            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
726        # - pipe is in write mode
727        if self.mode != PipeMode.WRITE_MODE:
728            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
729        # - shape of images in batch is fine
730        if batch.shape[-3:] != self.shape:
731            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
732        # - we have the right amount of pixels for the full batch
733        if batch.size != (batch.shape[0]*self.imageSize):
734            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
735
736        # write frame
737        buffer = batch.tobytes()
738        if self.pipe.stdin.write( buffer ) < batch.size:
739            # say to gc that this buffer is no longer needed
740            del buffer
741            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
742
743        # increase frame_counter
744        self.frame_counter.frame_count += batch.shape[0]       
745            
746        # say to gc that this buffer is no longer needed
747        del buffer
748
749        return True

Write a batch of images to the video

Parameters

batch: nparray A batch of images to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

@staticmethod
def get_video_time_in_sec(filename, *, debug=False, logLevel=16):
 90    @staticmethod
 91    def get_time_in_sec(filename, *, debug=False, logLevel=16):
 92        """
 93        Static method to get length of a video file in seconds including milliseconds as decimal part.
 94
 95        Parameters
 96        ----------
 97        filename : str or path
 98            Video file name.
 99
100        debug : bool (default False)
101            Show debug info.
102
103        log_level: int (default 16)
104            Log level to pass to the underlying ffmpeg/ffprobe command.
105        
106        Returns
107        ----------
108        float
109            Length in seconds of video file (including milliseconds as decimal part)
110        """
111        
112        cmd = [VideoIO.paramProgram, # ffprobe
113                    '-hide_banner',
114                    '-loglevel', str(logLevel),
115                    '-show_entries', 'format=duration',
116                    '-of', 'default=noprint_wrappers=1:nokey=1',
117                    filename
118                    ]
119
120        if debug == True:
121            print(' '.join(cmd))
122
123        # call ffprobe and get params in one single line
124        lpipe = sp.Popen(cmd, stdout=sp.PIPE)
125        output = lpipe.stdout.readlines()
126        lpipe.terminate()
127        # transform Bytes output to one single string
128        output = ''.join( [element.decode('utf-8') for element in output])
129
130        try:
131            return float(output)
132        except (ValueError, TypeError):
133            return None

Static method to get length of a video file in seconds including milliseconds as decimal part.

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part)

@staticmethod
def get_video_params(filename, *, debug=False, logLevel=16):
135    @staticmethod
136    def get_params(filename, *, debug=False, logLevel=16):
137        """
138        Static method to get params (width, height, fps) from a video file.
139
140        Parameters
141        ----------
142        filename: str or path
143            Video filename.
144
145        debug: bool (default (False)
146            Show debug info.
147
148        log_level: int (default 16)
149            Log level to pass to the underlying ffmpeg/ffprobe command.
150
151        Returns
152        ----------
153        tuple
154            Tuple containing (width, height, fps) of the video
155        """
156        cmd = [VideoIO.paramProgram, # ffprobe
157                    '-hide_banner',
158                    '-loglevel', str(logLevel),
159                    '-show_entries', 'stream=width,height,r_frame_rate',
160                    filename
161                    ]
162
163        if debug == True:
164            print(' '.join(cmd))
165
166        # call ffprobe and get params in one single line
167        lpipe = sp.Popen(cmd, stdout=sp.PIPE)
168        output = lpipe.stdout.readlines()
169        lpipe.terminate()
170        # transform Bytes output to one single string
171        output = ''.join( [element.decode('utf-8') for element in output])
172
173        pattern_width = r'width=(\d+)'
174        pattern_height = r'height=(\d+)'
175        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
176
177        # Search for values in the ffprobe output
178        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
179        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
180        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
181
182        # Extraction des valeurs
183        if match_width:
184            width = int(match_width.group(1))
185        else:
186            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
187
188        if match_height:
189            height = int(match_height.group(1))
190        else:
191            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
192
193        if match_fps:
194            numerator = float(match_fps.group(1))
195            denominator = float(match_fps.group(2))
196            fps = numerator / denominator
197        else:
198            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
199
200        return (width, height, fps)

Static method to get params (width, height, fps) from a video file.

Parameters

filename: str or path Video filename.

debug: bool (default (False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (width, height, fps) of the video

videoProgram = 'C:\\Users\\vaufreyd\\miniconda3\\envs\\CompilePackage\\Lib\\site-packages\\static_ffmpeg\\bin\\win32\\ffmpeg.exe'
paramProgram = 'C:\\Users\\vaufreyd\\miniconda3\\envs\\CompilePackage\\Lib\\site-packages\\static_ffmpeg\\bin\\win32\\ffprobe.exe'
class VideoIO.VideoIOException(builtins.Exception):
35    class VideoIOException(Exception):
36        """
37        Dedicated exception class for VideoIO class.
38        """
39        def __init__(self, message="Error while reading/writing video occurs"):
40            self.message = message
41            super().__init__(self.message)

Dedicated exception class for VideoIO class.

VideoIO.VideoIOException(message='Error while reading/writing video occurs')
39        def __init__(self, message="Error while reading/writing video occurs"):
40            self.message = message
41            super().__init__(self.message)
message
class VideoIO.PixelFormat(enum.Enum):
43    class PixelFormat(Enum):
44        """
45        Enum class for supported input video type: GBR 24 bits or RGB 24 bis.
46        """
47        GBR24 = 'bgr24' # default format
48        RGB24 = 'rgb24'

Enum class for supported input video type: GBR 24 bits or RGB 24 bis.

GBR24 = <PixelFormat.GBR24: 'bgr24'>
RGB24 = <PixelFormat.RGB24: 'rgb24'>
class AudioIO:
 31class 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        frameSize: 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.frameSize = -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.frameSize = 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.frameSize) as defined in the reader/open call if self.plannar is True. A frame
561            of shape (self.channels*self.frameSize) 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.frameSize) as defined in the reader/open call if self.plannar is True. A batch
618            of shape (n, self.channels*self.frameSize) 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,self.frameSize) if plannar is True else (self.channels*self.frameSize).
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        # garantee to have a C continuous array
695        if not audio.flags['C_CONTIGUOUS']:
696            a = np.ascontiguousarray(a) 
697
698        # write frame
699        buffer = audio.tobytes()
700        if self.pipe.stdin.write( buffer ) < len(buffer):
701            print( f"Error writing frame to {self.filename}" )
702            return False
703
704        # increase frame_counter
705        self.frame_counter.frame_count += (self.frame_size * self.channels)
706
707        # say to gc that this buffer is no longer needed 
708        del buffer
709
710        return True
711
712    def write_batch(self, batch):
713        """
714        Write a batch of audio frame to the file
715
716        Parameters
717        ----------
718        batch: nparray
719            The batch of audio frames to write to the video file of shape (n,self.channels,self.frameSize) if plannar is True else (n,self.channels*self.frameSize) of interleaved audio data.
720
721        Returns
722        ----------
723        bool
724            Writing was successful or not.
725        """
726        # Check params
727        # - pipe exists
728        if self.pipe is None:
729            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
730        # - pipe is in write mode
731        if self.mode != PipeMode.WRITE_MODE:
732            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
733        # batch is 3D (n, channels, nb samples)
734        if batch.ndim !=3:
735            raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
736        # - shape of images in batch is fine
737        if batch.shape[1] != self.channels:
738            raise self.AudioIOException("Wrong audio channels in batch: {} expected {}.".format(batch.shape[2], self.channels))
739
740        # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
741        # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
742        if not self.plannar:
743            # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
744            batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
745            batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
746
747        # garantee to have a C continuous array
748        if not batch.flags['C_CONTIGUOUS']:
749            batch = np.ascontiguousarray(batch)
750
751        # write frame
752        buffer = batch.tobytes()
753        if self.pipe.stdin.write( buffer ) < len(buffer):
754            # say to gc that this buffer is no longer needed
755            del buffer
756            raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
757
758        # increase frame_counter
759        self.frame_counter.frame_count += (batch.shape[0] * self.frame_size * self.channels)
760              
761        # say to gc that this buffer is no longer needed
762        del buffer
763
764        return True
765
766    def iter_frames(self, with_timestamps = False):
767        """
768        Method to iterate on audio frames using AudioIO obj.
769        for audio_frame in obj.iter_frames():
770            ....
771
772        Parameters
773        ----------
774        with_timestamps: bool optional (default False)
775            If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames
776
777        Returns
778        ----------
779        nparray or FrameContainer
780            A batch of images of shape ()
781        """
782
783        try:
784            if self.mode == PipeMode.READ_MODE:
785                while self.isOpened():
786                    frame = self.readFrame(with_timestamps)
787                    if frame is not None:
788                        yield frame
789        finally:
790            self.close()
791
792    def iter_batches(self, batch_size : int, with_timestamps = False ):
793        """
794        Method to iterate on batch ofaudio  frames using VideoIO obj.
795        for audio_batch in obj.iter_batches():
796            ....
797
798        Parameters
799        ----------
800        with_timestamps: bool optional (default False)
801            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
802        """
803        try:
804            if self.mode == PipeMode.READ_MODE:
805                while self.isOpened():
806                    batch = self.readBatch(batch_size, with_timestamps)
807                    if batch is not None:
808                        yield batch
809        finally:
810            self.close()
811
812    # function aliases to be compliant with original C++ version
813    getAudioTimeInSec = get_time_in_sec
814    getAudioParams = get_params
815    isOpened = is_opened
816    readFrame = read_frame
817    readBatch = read_batch
818    writeFrame = write_frame
819    writeBatch = write_batch
820    get_audio_time_in_sec = get_time_in_sec
821    get_audio_params = get_params
AudioIO(*, logLevel=16, debugMode=False)
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()

Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode

Parameters

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

debugMode: bool (default (False) Show debug info. while processing video

@classmethod
def reader(cls, filename, **kwargs):
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

Create and open an AudioIO object in reader mode

See AudioIO.open for the full list of accepted parameters.

@classmethod
def writer(cls, filename, **kwargs):
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

Create and open an AudioIO object in writer mode

See AudioIO.create for the full list of accepted parameters.

@staticmethod
def get_time_in_sec(filename, *, debug=False, logLevel=16):
 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

Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part with 3 decimals)

@staticmethod
def get_params(filename, *, debug=False, logLevel=16):
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        frameSize: 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)"""

Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default (False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (channels,sample_rate) of the file

mode
logLevel
debugMode
def init(self):
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.frameSize = -1
250        self.filename = None
251        self.frame_counter = None

Init or reinit a VideoIO object.

def get_elapsed_time_as_str(self) -> str:
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()

Method to get elapsed time (float value represented) as str.

Returns

str or None Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_formated_elapsed_time_as_str(self, show_ms=True) -> str:
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()

Method to get elapsed time (hour format) as str.

Returns

str or None Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_elapsed_time(self) -> float:
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()

Method to get elapsed time as float value rounded to 3 decimals.

Returns

float or None Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def is_opened(self) -> bool:
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

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def close(self):
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()

Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.

def create( self, filename, sample_rate, channels, *, writeOverExistingFile=False, outputEncoding=<AudioFormat.PCM32LE: 'pcm_f32le'>, encodingParams=None, plannar=True):
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.frameSize = 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

Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create on a AudioIO will close any former open video.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

sample_rate: int If defined as a positive value, sample_rates of the output file will be set to this value.

channels: int If defined as a positive value, number of channels of output file will be set to this value.

fps: If defined as a positive value, fps of input video will be set to this value.

outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) Define audio format for samples. Possible value is AudioFormat.PCM32LE.

encodingParams: str optional (default None) Parameter to pass to ffmpeg to encode video like audio filters.

plannar : bool optionnal (default True) Input data to write are grouped by channel if True, interleaved instead.

Returns

bool Was the creation successfull

def open( self, filename, *, sample_rate=-1, channels=-1, inputEncoding=<AudioFormat.PCM32LE: 'pcm_f32le'>, decodingParams=None, frame_size=1.0, plannar=True, start_time=0.0):
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

Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open on a AudioIO will close any former open file.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

sample_rate: int optional (default -1) If defined as a positive value, sample rate of the input audio will be converted to this value.

channels: int optional (default -1) If defined as a positive value, number of channels of the input audio will converted to this value.

inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) Define audio format for samples. Possible value is AudioFormat.PCM32LE.

decodingParams: str optional (default None) Parameter to pass to ffmpeg to decode video like audio filters.

plannar: bool optionnal (default True) Group audio samples per channel if True. Else, samples are interleaved.

frame_size: int or float (default 1.0) If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples. 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. Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate)

start_time: float optional (default 0.0) Define the reading start time. If not set, reading at beginning of the file.

Returns

bool Was the opening successfull

def read_frame(self, with_timestamps=False):
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.frameSize) as defined in the reader/open call if self.plannar is True. A frame
561            of shape (self.channels*self.frameSize) 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

Read next frame from the audio file

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the audio and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer A frame of shape (self.channels,self.frameSize) as defined in the reader/open call if self.plannar is True. A frame of shape (self.channels*self.frameSize) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio data in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element).

def read_batch(self, numberOfFrames, with_timestamps=False):
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.frameSize) as defined in the reader/open call if self.plannar is True. A batch
618            of shape (n, self.channels*self.frameSize) 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

Read next batch of audio from the file

Parameters

number_of_frames: int Number of desired images within the batch. The last batch from the file may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of shape (n, self.channels,self.frameSize) as defined in the reader/open call if self.plannar is True. A batch of shape (n, self.channels*self.frameSize) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio batch in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for each audio frame).

def write_frame(self, audio) -> bool:
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,self.frameSize) if plannar is True else (self.channels*self.frameSize).
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        # garantee to have a C continuous array
695        if not audio.flags['C_CONTIGUOUS']:
696            a = np.ascontiguousarray(a) 
697
698        # write frame
699        buffer = audio.tobytes()
700        if self.pipe.stdin.write( buffer ) < len(buffer):
701            print( f"Error writing frame to {self.filename}" )
702            return False
703
704        # increase frame_counter
705        self.frame_counter.frame_count += (self.frame_size * self.channels)
706
707        # say to gc that this buffer is no longer needed 
708        del buffer
709
710        return True

Write an audio frame to the file

Parameters

audio: nparray The audio frame to write to the video file of shape (self.channels,self.frameSize) if plannar is True else (self.channels*self.frameSize).

Returns

bool Writing was successful or not.

def write_batch(self, batch):
712    def write_batch(self, batch):
713        """
714        Write a batch of audio frame to the file
715
716        Parameters
717        ----------
718        batch: nparray
719            The batch of audio frames to write to the video file of shape (n,self.channels,self.frameSize) if plannar is True else (n,self.channels*self.frameSize) of interleaved audio data.
720
721        Returns
722        ----------
723        bool
724            Writing was successful or not.
725        """
726        # Check params
727        # - pipe exists
728        if self.pipe is None:
729            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
730        # - pipe is in write mode
731        if self.mode != PipeMode.WRITE_MODE:
732            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
733        # batch is 3D (n, channels, nb samples)
734        if batch.ndim !=3:
735            raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
736        # - shape of images in batch is fine
737        if batch.shape[1] != self.channels:
738            raise self.AudioIOException("Wrong audio channels in batch: {} expected {}.".format(batch.shape[2], self.channels))
739
740        # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
741        # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
742        if not self.plannar:
743            # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
744            batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
745            batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
746
747        # garantee to have a C continuous array
748        if not batch.flags['C_CONTIGUOUS']:
749            batch = np.ascontiguousarray(batch)
750
751        # write frame
752        buffer = batch.tobytes()
753        if self.pipe.stdin.write( buffer ) < len(buffer):
754            # say to gc that this buffer is no longer needed
755            del buffer
756            raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
757
758        # increase frame_counter
759        self.frame_counter.frame_count += (batch.shape[0] * self.frame_size * self.channels)
760              
761        # say to gc that this buffer is no longer needed
762        del buffer
763
764        return True

Write a batch of audio frame to the file

Parameters

batch: nparray The batch of audio frames to write to the video file of shape (n,self.channels,self.frameSize) if plannar is True else (n,self.channels*self.frameSize) of interleaved audio data.

Returns

bool Writing was successful or not.

def iter_frames(self, with_timestamps=False):
766    def iter_frames(self, with_timestamps = False):
767        """
768        Method to iterate on audio frames using AudioIO obj.
769        for audio_frame in obj.iter_frames():
770            ....
771
772        Parameters
773        ----------
774        with_timestamps: bool optional (default False)
775            If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames
776
777        Returns
778        ----------
779        nparray or FrameContainer
780            A batch of images of shape ()
781        """
782
783        try:
784            if self.mode == PipeMode.READ_MODE:
785                while self.isOpened():
786                    frame = self.readFrame(with_timestamps)
787                    if frame is not None:
788                        yield frame
789        finally:
790            self.close()

Method to iterate on audio frames using AudioIO obj. for audio_frame in obj.iter_frames(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of images of shape ()

def iter_batches(self, batch_size: int, with_timestamps=False):
792    def iter_batches(self, batch_size : int, with_timestamps = False ):
793        """
794        Method to iterate on batch ofaudio  frames using VideoIO obj.
795        for audio_batch in obj.iter_batches():
796            ....
797
798        Parameters
799        ----------
800        with_timestamps: bool optional (default False)
801            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
802        """
803        try:
804            if self.mode == PipeMode.READ_MODE:
805                while self.isOpened():
806                    batch = self.readBatch(batch_size, with_timestamps)
807                    if batch is not None:
808                        yield batch
809        finally:
810            self.close()

Method to iterate on batch ofaudio frames using VideoIO obj. for audio_batch in obj.iter_batches(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

@staticmethod
def getAudioTimeInSec(filename, *, debug=False, logLevel=16):
 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

Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part with 3 decimals)

@staticmethod
def getAudioParams(filename, *, debug=False, logLevel=16):
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        frameSize: 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)"""

Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default (False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (channels,sample_rate) of the file

def isOpened(self) -> bool:
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

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def readFrame(self, with_timestamps=False):
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.frameSize) as defined in the reader/open call if self.plannar is True. A frame
561            of shape (self.channels*self.frameSize) 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

Read next frame from the audio file

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the audio and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer A frame of shape (self.channels,self.frameSize) as defined in the reader/open call if self.plannar is True. A frame of shape (self.channels*self.frameSize) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio data in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element).

def readBatch(self, numberOfFrames, with_timestamps=False):
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.frameSize) as defined in the reader/open call if self.plannar is True. A batch
618            of shape (n, self.channels*self.frameSize) 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

Read next batch of audio from the file

Parameters

number_of_frames: int Number of desired images within the batch. The last batch from the file may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of shape (n, self.channels,self.frameSize) as defined in the reader/open call if self.plannar is True. A batch of shape (n, self.channels*self.frameSize) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio batch in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for each audio frame).

def writeFrame(self, audio) -> bool:
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,self.frameSize) if plannar is True else (self.channels*self.frameSize).
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        # garantee to have a C continuous array
695        if not audio.flags['C_CONTIGUOUS']:
696            a = np.ascontiguousarray(a) 
697
698        # write frame
699        buffer = audio.tobytes()
700        if self.pipe.stdin.write( buffer ) < len(buffer):
701            print( f"Error writing frame to {self.filename}" )
702            return False
703
704        # increase frame_counter
705        self.frame_counter.frame_count += (self.frame_size * self.channels)
706
707        # say to gc that this buffer is no longer needed 
708        del buffer
709
710        return True

Write an audio frame to the file

Parameters

audio: nparray The audio frame to write to the video file of shape (self.channels,self.frameSize) if plannar is True else (self.channels*self.frameSize).

Returns

bool Writing was successful or not.

def writeBatch(self, batch):
712    def write_batch(self, batch):
713        """
714        Write a batch of audio frame to the file
715
716        Parameters
717        ----------
718        batch: nparray
719            The batch of audio frames to write to the video file of shape (n,self.channels,self.frameSize) if plannar is True else (n,self.channels*self.frameSize) of interleaved audio data.
720
721        Returns
722        ----------
723        bool
724            Writing was successful or not.
725        """
726        # Check params
727        # - pipe exists
728        if self.pipe is None:
729            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
730        # - pipe is in write mode
731        if self.mode != PipeMode.WRITE_MODE:
732            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
733        # batch is 3D (n, channels, nb samples)
734        if batch.ndim !=3:
735            raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
736        # - shape of images in batch is fine
737        if batch.shape[1] != self.channels:
738            raise self.AudioIOException("Wrong audio channels in batch: {} expected {}.".format(batch.shape[2], self.channels))
739
740        # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
741        # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
742        if not self.plannar:
743            # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
744            batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
745            batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
746
747        # garantee to have a C continuous array
748        if not batch.flags['C_CONTIGUOUS']:
749            batch = np.ascontiguousarray(batch)
750
751        # write frame
752        buffer = batch.tobytes()
753        if self.pipe.stdin.write( buffer ) < len(buffer):
754            # say to gc that this buffer is no longer needed
755            del buffer
756            raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
757
758        # increase frame_counter
759        self.frame_counter.frame_count += (batch.shape[0] * self.frame_size * self.channels)
760              
761        # say to gc that this buffer is no longer needed
762        del buffer
763
764        return True

Write a batch of audio frame to the file

Parameters

batch: nparray The batch of audio frames to write to the video file of shape (n,self.channels,self.frameSize) if plannar is True else (n,self.channels*self.frameSize) of interleaved audio data.

Returns

bool Writing was successful or not.

@staticmethod
def get_audio_time_in_sec(filename, *, debug=False, logLevel=16):
 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

Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part with 3 decimals)

@staticmethod
def get_audio_params(filename, *, debug=False, logLevel=16):
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        frameSize: 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)"""

Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default (False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (channels,sample_rate) of the file

audioProgram = 'C:\\Users\\vaufreyd\\miniconda3\\envs\\CompilePackage\\Lib\\site-packages\\static_ffmpeg\\bin\\win32\\ffmpeg.exe'
paramProgram = 'C:\\Users\\vaufreyd\\miniconda3\\envs\\CompilePackage\\Lib\\site-packages\\static_ffmpeg\\bin\\win32\\ffprobe.exe'
class AudioIO.AudioIOException(builtins.Exception):
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)

Dedicated exception class for AudioIO class.

AudioIO.AudioIOException(message='Error while reading/writing video occurs')
39        def __init__(self, message="Error while reading/writing video occurs"):
40            self.message = message
41            super().__init__(self.message)
message
class AudioIO.AudioFormat(enum.Enum):
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)

Enum class for supported input video type: 32-bit float is the only supported type for the moment.

PCM32LE = <AudioFormat.PCM32LE: 'pcm_f32le'>
class FrameCounter:
 16class FrameCounter:
 17    """
 18    Create a ``FrameCounter`` to follow elapsed time in audio/video file in read or write mode. Static utility functions allow to format elapsed time.
 19    """
 20
 21    class FrameCounterException(Exception):
 22        """
 23        Dedicated exception class for FrameCounter class.
 24        """
 25        def __init__(self, message="Error while setting FrameCounter parameters."):
 26            self.message = message
 27            super().__init__(self.message)
 28
 29    _fps: float # (private)
 30    """ Fps of current stream """
 31
 32    _frame_count: int # (private)
 33    """ Frame count in the current stream """
 34
 35    def __init__(self, fps: Union[int, float]):
 36        """
 37        Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
 38
 39        Parameters
 40        ----------
 41        fps: int or float.
 42            Frames per second of the associated stream.
 43        """
 44        # check init fps value
 45        self._fps = float(fps)
 46        if self._fps <= 0.0:
 47            raise FrameCounterException("fps must be > 0.0.")
 48
 49        # 2 modes
 50        self._frame_count = 0       # at 00:00:00.000
 51
 52    # support +=
 53    def __iadd__(self, other: Union[int, float]):
 54        """
 55        Support += operator for FrameCounter. 
 56
 57        Parameters
 58        ----------
 59        other: int or float.
 60            If other is a float, add the number of frame to add 'other' seconds (thus other * self._fps samples).
 61            If other is an int, add the value as a number of samples in the stream.
 62        """        
 63        if isinstance(other,float):
 64            # float means adding time
 65            self._frame_count += int(other * self._fps) # number of second * Nb of elements per seconds
 66        else:
 67            # for int, add number of element
 68            self._frame_count += other
 69        return self
 70    
 71    @property
 72    def frame_count(self):
 73        """
 74        Property to get underlying self._frame_count. Idea is to control setter to valid setting values.
 75        """   
 76        return self._frame_count
 77
 78    @frame_count.setter
 79    def frame_count(self, value: int):
 80        """
 81        Setter for underlying self._frame_count controlling setting value.
 82        """   
 83        if value < 0:
 84            raise FrameCounterException("frame_count must be >= 0")
 85        self._frame_count = value
 86
 87    @property
 88    def fps(self):
 89        """
 90        Property to get underlying self._fps.
 91        """
 92        return self._fps
 93
 94    @staticmethod
 95    def format_time(nb_frames: int, fps: float, show_ms : bool = True, show_days : bool = False) -> str:
 96        """
 97        Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds,
 98        show_days to show days instead of cummulative hour count.
 99
100        Parameters
101        ----------
102        nb_frames: int.
103            Number of samples already present in the stream.
104            
105        fps: float.
106            Fps of the associated stream.
107            
108        show_ms: bool.
109            Flag to say if we want to show milliseconds in the output str.
110            
111        show_days: bool.
112            Flag to say if we want to show says instead of cumulative hours in the output str.
113
114        Returns
115        -------
116            str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
117            or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
118        """  
119        # exact time in seconds (float)
120        exact_seconds = nb_frames / fps
121
122        # integer part for days/hours/minutes/seconds
123        total_seconds = int(exact_seconds)
124
125        # milliseconds = decimal part * 1000
126        millis = int(round((exact_seconds - total_seconds) * 1000))
127
128        # handle the case where rounding results in 1000 ms
129        if millis == 1000:
130            millis = 0
131            total_seconds += 1
132
133        if show_days:
134            # compute number of days, hours, minutes, seconds
135            days, mod = divmod(total_seconds, 24 * 3600)
136            hours, mod = divmod(mod, 3600)
137            minutes, seconds = divmod(mod, 60)
138
139            if days > 0:
140                days = f"{days} days(s) "
141            else:
142                days = ""
143        else:
144            days = "" # no day as show_days = False
145            hours, mod = divmod(total_seconds, 3600)
146            minutes, seconds = divmod(mod, 60)
147
148        if show_ms == True:
149            millis = f".{millis:03d}"
150        else:
151            millis = ""
152
153        return f"{days}{hours:02d}:{minutes:02d}:{seconds:02d}{millis}"
154
155    def get_elapsed_time_as_str(self) -> str:
156        """
157        Get elapsed time as string representing a float value rounded to 3 decimals.
158
159        Returns
160        -------
161            str representing corresponding time in float format rounded to 3 decimals.
162        """
163        return f"{float(self._frame_count)/self._fps:.3f}"
164
165    def get_formated_elapsed_time_as_str(self, show_ms : bool = True, show_days : bool = False) -> str:
166        """
167        Get elapsed time as string representing time with different mode (see ``FrameCounter.format_time`` for parameter explanation).
168        Returns
169        -------
170            str representing corresponding time in float format rounded to 3 decimals.
171        """
172        # frame count to time correction is done in format_time
173        return FrameCounter.format_time(self._frame_count, self._fps, show_ms, show_days)
174
175    def get_elapsed_time(self) -> float:
176        """
177        Get elapsed time as float value rounded to 3 decimals.
178
179        Returns
180        -------
181            str representing corresponding time in float format rounded to 3 decimals.
182        """
183        return round(float(self._frame_count)/self._fps,3)

Create a FrameCounter to follow elapsed time in audio/video file in read or write mode. Static utility functions allow to format elapsed time.

FrameCounter(fps: int | float)
35    def __init__(self, fps: Union[int, float]):
36        """
37        Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
38
39        Parameters
40        ----------
41        fps: int or float.
42            Frames per second of the associated stream.
43        """
44        # check init fps value
45        self._fps = float(fps)
46        if self._fps <= 0.0:
47            raise FrameCounterException("fps must be > 0.0.")
48
49        # 2 modes
50        self._frame_count = 0       # at 00:00:00.000

Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode

Parameters

fps: int or float. Frames per second of the associated stream.

frame_count
71    @property
72    def frame_count(self):
73        """
74        Property to get underlying self._frame_count. Idea is to control setter to valid setting values.
75        """   
76        return self._frame_count

Property to get underlying self._frame_count. Idea is to control setter to valid setting values.

fps
87    @property
88    def fps(self):
89        """
90        Property to get underlying self._fps.
91        """
92        return self._fps

Property to get underlying self._fps.

@staticmethod
def format_time( nb_frames: int, fps: float, show_ms: bool = True, show_days: bool = False) -> str:
 94    @staticmethod
 95    def format_time(nb_frames: int, fps: float, show_ms : bool = True, show_days : bool = False) -> str:
 96        """
 97        Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds,
 98        show_days to show days instead of cummulative hour count.
 99
100        Parameters
101        ----------
102        nb_frames: int.
103            Number of samples already present in the stream.
104            
105        fps: float.
106            Fps of the associated stream.
107            
108        show_ms: bool.
109            Flag to say if we want to show milliseconds in the output str.
110            
111        show_days: bool.
112            Flag to say if we want to show says instead of cumulative hours in the output str.
113
114        Returns
115        -------
116            str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
117            or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
118        """  
119        # exact time in seconds (float)
120        exact_seconds = nb_frames / fps
121
122        # integer part for days/hours/minutes/seconds
123        total_seconds = int(exact_seconds)
124
125        # milliseconds = decimal part * 1000
126        millis = int(round((exact_seconds - total_seconds) * 1000))
127
128        # handle the case where rounding results in 1000 ms
129        if millis == 1000:
130            millis = 0
131            total_seconds += 1
132
133        if show_days:
134            # compute number of days, hours, minutes, seconds
135            days, mod = divmod(total_seconds, 24 * 3600)
136            hours, mod = divmod(mod, 3600)
137            minutes, seconds = divmod(mod, 60)
138
139            if days > 0:
140                days = f"{days} days(s) "
141            else:
142                days = ""
143        else:
144            days = "" # no day as show_days = False
145            hours, mod = divmod(total_seconds, 3600)
146            minutes, seconds = divmod(mod, 60)
147
148        if show_ms == True:
149            millis = f".{millis:03d}"
150        else:
151            millis = ""
152
153        return f"{days}{hours:02d}:{minutes:02d}:{seconds:02d}{millis}"

Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds, show_days to show days instead of cummulative hour count.

Parameters

nb_frames: int. Number of samples already present in the stream.

fps: float. Fps of the associated stream.

show_ms: bool. Flag to say if we want to show milliseconds in the output str.

show_days: bool. Flag to say if we want to show says instead of cumulative hours in the output str.

Returns

str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
def get_elapsed_time_as_str(self) -> str:
155    def get_elapsed_time_as_str(self) -> str:
156        """
157        Get elapsed time as string representing a float value rounded to 3 decimals.
158
159        Returns
160        -------
161            str representing corresponding time in float format rounded to 3 decimals.
162        """
163        return f"{float(self._frame_count)/self._fps:.3f}"

Get elapsed time as string representing a float value rounded to 3 decimals.

Returns

str representing corresponding time in float format rounded to 3 decimals.
def get_formated_elapsed_time_as_str(self, show_ms: bool = True, show_days: bool = False) -> str:
165    def get_formated_elapsed_time_as_str(self, show_ms : bool = True, show_days : bool = False) -> str:
166        """
167        Get elapsed time as string representing time with different mode (see ``FrameCounter.format_time`` for parameter explanation).
168        Returns
169        -------
170            str representing corresponding time in float format rounded to 3 decimals.
171        """
172        # frame count to time correction is done in format_time
173        return FrameCounter.format_time(self._frame_count, self._fps, show_ms, show_days)

Get elapsed time as string representing time with different mode (see FrameCounter.format_time for parameter explanation).

Returns

str representing corresponding time in float format rounded to 3 decimals.
def get_elapsed_time(self) -> float:
175    def get_elapsed_time(self) -> float:
176        """
177        Get elapsed time as float value rounded to 3 decimals.
178
179        Returns
180        -------
181            str representing corresponding time in float format rounded to 3 decimals.
182        """
183        return round(float(self._frame_count)/self._fps,3)

Get elapsed time as float value rounded to 3 decimals.

Returns

str representing corresponding time in float format rounded to 3 decimals.
class FrameCounter.FrameCounterException(builtins.Exception):
21    class FrameCounterException(Exception):
22        """
23        Dedicated exception class for FrameCounter class.
24        """
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)

Dedicated exception class for FrameCounter class.

FrameCounter.FrameCounterException(message='Error while setting FrameCounter parameters.')
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)
message