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
numpyarrays. - Write batches of frames from
numpyarrays to video or audio files, even compressed - Uses
static_ffmpegto 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]
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
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
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.
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.
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)
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
Pipemode of the current object (default PipeMode.UNK_MODE)
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.
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.
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.
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.
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.
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.
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
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
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).
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).
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.
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.
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
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
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)
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
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.
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).
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).
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.
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.
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)
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
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.
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.
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
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
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.
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.
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)
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
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.
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.
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.
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.
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.
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.
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
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
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).
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).
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.
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.
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 ()
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
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)
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
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.
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).
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).
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.
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.
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)
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
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.
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.
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.
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.
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.
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.
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)
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.
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.
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.
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.