generated from thinkode/modelRepository
initial commit and version 1.0
This commit is contained in:
85
moviepy/audio/io/AudioFileClip.py
Normal file
85
moviepy/audio/io/AudioFileClip.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Implements AudioFileClip, a class for audio clips creation using audio files."""
|
||||
|
||||
from moviepy.audio.AudioClip import AudioClip
|
||||
from moviepy.audio.io.readers import FFMPEG_AudioReader
|
||||
from moviepy.decorators import convert_path_to_string
|
||||
|
||||
|
||||
class AudioFileClip(AudioClip):
|
||||
"""
|
||||
An audio clip read from a sound file, or an array.
|
||||
The whole file is not loaded in memory. Instead, only a portion is
|
||||
read and stored in memory. this portion includes frames before
|
||||
and after the last frames read, so that it is fast to read the sound
|
||||
backward and forward.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename
|
||||
Either a soundfile name (of any extension supported by ffmpeg)
|
||||
as a string or a path-like object,
|
||||
or an array representing a sound. If the soundfile is not a .wav,
|
||||
it will be converted to .wav first, using the ``fps`` and
|
||||
``bitrate`` arguments.
|
||||
|
||||
buffersize:
|
||||
Size to load in memory (in number of frames)
|
||||
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
||||
nbytes
|
||||
Number of bits per frame of the original audio file.
|
||||
|
||||
fps
|
||||
Number of frames per second in the audio file
|
||||
|
||||
buffersize
|
||||
See Parameters.
|
||||
|
||||
Lifetime
|
||||
--------
|
||||
|
||||
Note that this creates subprocesses and locks files. If you construct one
|
||||
of these instances, you must call close() afterwards, or the subresources
|
||||
will not be cleaned up until the process ends.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code:: python
|
||||
|
||||
snd = AudioFileClip("song.wav")
|
||||
snd.close()
|
||||
"""
|
||||
|
||||
@convert_path_to_string("filename")
|
||||
def __init__(
|
||||
self, filename, decode_file=False, buffersize=200000, nbytes=2, fps=44100
|
||||
):
|
||||
AudioClip.__init__(self)
|
||||
|
||||
self.filename = filename
|
||||
self.reader = FFMPEG_AudioReader(
|
||||
filename,
|
||||
decode_file=decode_file,
|
||||
fps=fps,
|
||||
nbytes=nbytes,
|
||||
buffersize=buffersize,
|
||||
)
|
||||
self.fps = fps
|
||||
self.duration = self.reader.duration
|
||||
self.end = self.reader.duration
|
||||
self.buffersize = self.reader.buffersize
|
||||
self.filename = filename
|
||||
|
||||
self.frame_function = lambda t: self.reader.get_frame(t)
|
||||
self.nchannels = self.reader.nchannels
|
||||
|
||||
def close(self):
|
||||
"""Close the internal reader."""
|
||||
if self.reader:
|
||||
self.reader.close()
|
||||
self.reader = None
|
||||
1
moviepy/audio/io/__init__.py
Normal file
1
moviepy/audio/io/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Class and methods to read, write, preview audiofiles."""
|
||||
240
moviepy/audio/io/ffmpeg_audiowriter.py
Normal file
240
moviepy/audio/io/ffmpeg_audiowriter.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""MoviePy audio writing with ffmpeg."""
|
||||
|
||||
import subprocess as sp
|
||||
from log import log_step
|
||||
|
||||
import proglog
|
||||
|
||||
from moviepy.config import FFMPEG_BINARY
|
||||
from moviepy.decorators import requires_duration
|
||||
from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename
|
||||
|
||||
|
||||
class FFMPEG_AudioWriter:
|
||||
"""
|
||||
A class to write an AudioClip into an audio file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename
|
||||
Name of any video or audio file, like ``video.mp4`` or ``sound.wav`` etc.
|
||||
|
||||
size
|
||||
Size (width,height) in pixels of the output video.
|
||||
|
||||
fps_input
|
||||
Frames per second of the input audio (given by the AudioClip being
|
||||
written down).
|
||||
|
||||
nbytes : int, optional
|
||||
Number of bytes per sample. Default is 2 (16-bit audio).
|
||||
|
||||
nchannels : int, optional
|
||||
Number of audio channels. Default is 2 (stereo).
|
||||
|
||||
codec : str, optional
|
||||
The codec to use for the output. Default is ``libfdk_aac``.
|
||||
|
||||
bitrate:
|
||||
A string indicating the bitrate of the final video. Only
|
||||
relevant for codecs which accept a bitrate.
|
||||
|
||||
input_video : str, optional
|
||||
Path to an input video file. If provided, the audio will be muxed with this video.
|
||||
If not provided, the output will be audio-only.
|
||||
|
||||
logfile : file-like object or None, optional
|
||||
A file object where FFMPEG logs will be written. If None, logs are suppressed.
|
||||
|
||||
ffmpeg_params : list of str, optional
|
||||
Additional FFMPEG command-line parameters to customize the output.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filename,
|
||||
fps_input,
|
||||
nbytes=2,
|
||||
nchannels=2,
|
||||
codec="libfdk_aac",
|
||||
bitrate=None,
|
||||
input_video=None,
|
||||
logfile=None,
|
||||
ffmpeg_params=None,
|
||||
):
|
||||
if logfile is None:
|
||||
logfile = sp.PIPE
|
||||
self.logfile = logfile
|
||||
self.filename = filename
|
||||
self.codec = codec
|
||||
self.ext = self.filename.split(".")[-1]
|
||||
|
||||
# order is important
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error" if logfile == sp.PIPE else "info",
|
||||
"-f",
|
||||
"s%dle" % (8 * nbytes),
|
||||
"-acodec",
|
||||
"pcm_s%dle" % (8 * nbytes),
|
||||
"-ar",
|
||||
"%d" % fps_input,
|
||||
"-ac",
|
||||
"%d" % nchannels,
|
||||
"-i",
|
||||
"-",
|
||||
]
|
||||
if input_video is None:
|
||||
cmd.extend(["-vn"])
|
||||
else:
|
||||
cmd.extend(["-i", ffmpeg_escape_filename(input_video), "-vcodec", "copy"])
|
||||
|
||||
cmd.extend(["-acodec", codec] + ["-ar", "%d" % fps_input])
|
||||
cmd.extend(["-strict", "-2"]) # needed to support codec 'aac'
|
||||
if bitrate is not None:
|
||||
cmd.extend(["-ab", bitrate])
|
||||
if ffmpeg_params is not None:
|
||||
cmd.extend(ffmpeg_params)
|
||||
cmd.extend([ffmpeg_escape_filename(filename)])
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE}
|
||||
)
|
||||
|
||||
self.proc = sp.Popen(cmd, **popen_params)
|
||||
|
||||
def write_frames(self, frames_array):
|
||||
"""Send the audio frame (a chunck of ``AudioClip``) to ffmpeg for writting"""
|
||||
try:
|
||||
self.proc.stdin.write(frames_array.tobytes())
|
||||
except IOError as err:
|
||||
_, ffmpeg_error = self.proc.communicate()
|
||||
if ffmpeg_error is not None:
|
||||
ffmpeg_error = ffmpeg_error.decode()
|
||||
else:
|
||||
# The error was redirected to a logfile with `write_logfile=True`,
|
||||
# so read the error from that file instead
|
||||
self.logfile.seek(0)
|
||||
ffmpeg_error = self.logfile.read()
|
||||
|
||||
error = (
|
||||
f"{err}\n\nMoviePy error: FFMPEG encountered the following error while "
|
||||
f"writing file {self.filename}:\n\n {ffmpeg_error}"
|
||||
)
|
||||
|
||||
if "Unknown encoder" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe audio export failed because FFMPEG didn't find the "
|
||||
f"specified codec for audio encoding {self.codec}. "
|
||||
"Please install this codec or change the codec when calling "
|
||||
"write_videofile or write_audiofile.\nFor instance for mp3:\n"
|
||||
" >>> write_videofile('myvid.mp4', audio_codec='libmp3lame')"
|
||||
)
|
||||
|
||||
elif "incorrect codec parameters ?" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe audio export failed, possibly because the "
|
||||
f"codec specified for the video {self.codec} is not compatible"
|
||||
f" with the given extension {self.ext}. Please specify a "
|
||||
"valid 'codec' argument in write_audiofile or 'audio_codoc'"
|
||||
"argument in write_videofile. This would be "
|
||||
"'libmp3lame' for mp3, 'libvorbis' for ogg..."
|
||||
)
|
||||
|
||||
elif "bitrate not specified" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe audio export failed, possibly because the "
|
||||
"bitrate you specified was too high or too low for "
|
||||
"the audio codec."
|
||||
)
|
||||
|
||||
elif "Invalid encoder type" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe audio export failed because the codec "
|
||||
"or file extension you provided is not suitable for audio"
|
||||
)
|
||||
|
||||
raise IOError(error)
|
||||
|
||||
def close(self):
|
||||
"""Closes the writer, terminating the subprocess if is still alive."""
|
||||
if hasattr(self, "proc") and self.proc:
|
||||
self.proc.stdin.close()
|
||||
self.proc.stdin = None
|
||||
if self.proc.stderr is not None:
|
||||
self.proc.stderr.close()
|
||||
self.proc.stderr = None
|
||||
# If this causes deadlocks, consider terminating instead.
|
||||
self.proc.wait()
|
||||
self.proc = None
|
||||
|
||||
def __del__(self):
|
||||
# If the garbage collector comes, make sure the subprocess is terminated.
|
||||
self.close()
|
||||
|
||||
# Support the Context Manager protocol, to ensure that resources are cleaned up.
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
@requires_duration
|
||||
def ffmpeg_audiowrite(
|
||||
clip,
|
||||
filename,
|
||||
fps,
|
||||
nbytes,
|
||||
buffersize,
|
||||
codec="libvorbis",
|
||||
bitrate=None,
|
||||
write_logfile=False,
|
||||
ffmpeg_params=None,
|
||||
logger=None,
|
||||
):
|
||||
"""
|
||||
A function that wraps the FFMPEG_AudioWriter to write an AudioClip
|
||||
to a file.
|
||||
"""
|
||||
if write_logfile:
|
||||
logfile = open(filename + ".log", "w+")
|
||||
else:
|
||||
logfile = None
|
||||
# logger = proglog.default_bar_logger(logger)
|
||||
# logger(message="MoviePy - Writing audio in %s" % filename)
|
||||
writer = FFMPEG_AudioWriter(
|
||||
filename,
|
||||
fps,
|
||||
nbytes,
|
||||
clip.nchannels,
|
||||
codec=codec,
|
||||
bitrate=bitrate,
|
||||
logfile=logfile,
|
||||
ffmpeg_params=ffmpeg_params,
|
||||
)
|
||||
|
||||
total_chunks = len(list(clip.iter_chunks(chunksize=buffersize, quantize=True, nbytes=nbytes, fps=fps)))
|
||||
|
||||
old_progress = -1
|
||||
for i, chunk in enumerate(clip.iter_chunks(chunksize=buffersize, quantize=True, nbytes=nbytes, fps=fps)):
|
||||
# Calculate the progress
|
||||
progress = (i + 1) / total_chunks * 100
|
||||
int_progress = int(progress)
|
||||
# Display the progress if it has changed from the last time
|
||||
if int_progress != old_progress:
|
||||
old_progress = int_progress
|
||||
log_step("audio_extraction", old_progress, "extracting the audio from the video")
|
||||
|
||||
# Écrire le chunk audio
|
||||
writer.write_frames(chunk)
|
||||
|
||||
writer.close()
|
||||
|
||||
if write_logfile:
|
||||
logfile.close()
|
||||
# logger(message="MoviePy - Done.")
|
||||
154
moviepy/audio/io/ffplay_audiopreviewer.py
Normal file
154
moviepy/audio/io/ffplay_audiopreviewer.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""MoviePy audio writing with ffmpeg."""
|
||||
|
||||
import subprocess as sp
|
||||
|
||||
from moviepy.config import FFPLAY_BINARY
|
||||
from moviepy.decorators import requires_duration
|
||||
from moviepy.tools import cross_platform_popen_params
|
||||
|
||||
|
||||
class FFPLAY_AudioPreviewer:
|
||||
"""
|
||||
A class to preview an AudioClip.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
fps_input
|
||||
Frames per second of the input audio (given by the AudioClip being
|
||||
written down).
|
||||
|
||||
nbytes:
|
||||
Number of bytes to encode the sound: 1 for 8bit sound, 2 for
|
||||
16bit, 4 for 32bit sound. Default is 2 bytes, it's fine.
|
||||
|
||||
nchannels:
|
||||
Number of audio channels in the clip. Default to 2 channels.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fps_input,
|
||||
nbytes=2,
|
||||
nchannels=2,
|
||||
):
|
||||
# order is important
|
||||
cmd = [
|
||||
FFPLAY_BINARY,
|
||||
"-autoexit", # If you don't precise, ffplay won't stop at end
|
||||
"-nodisp", # If you don't precise a window is
|
||||
"-f",
|
||||
"s%dle" % (8 * nbytes),
|
||||
"-ar",
|
||||
"%d" % fps_input,
|
||||
"-ac",
|
||||
"%d" % nchannels,
|
||||
"-i",
|
||||
"-",
|
||||
]
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{"stdout": sp.DEVNULL, "stderr": sp.STDOUT, "stdin": sp.PIPE}
|
||||
)
|
||||
|
||||
self.proc = sp.Popen(cmd, **popen_params)
|
||||
|
||||
def write_frames(self, frames_array):
|
||||
"""Send a raw audio frame (a chunck of audio) to ffplay to be played"""
|
||||
try:
|
||||
self.proc.stdin.write(frames_array.tobytes())
|
||||
except IOError as err:
|
||||
_, ffplay_error = self.proc.communicate()
|
||||
if ffplay_error is not None:
|
||||
ffplay_error = ffplay_error.decode()
|
||||
else:
|
||||
# The error was redirected to a logfile with `write_logfile=True`,
|
||||
# so read the error from that file instead
|
||||
self.logfile.seek(0)
|
||||
ffplay_error = self.logfile.read()
|
||||
|
||||
error = (
|
||||
f"{err}\n\nMoviePy error: FFPLAY encountered the following error while "
|
||||
f":\n\n {ffplay_error}"
|
||||
)
|
||||
|
||||
raise IOError(error)
|
||||
|
||||
def close(self):
|
||||
"""Closes the writer, terminating the subprocess if is still alive."""
|
||||
if hasattr(self, "proc") and self.proc:
|
||||
self.proc.stdin.close()
|
||||
self.proc.stdin = None
|
||||
if self.proc.stderr is not None:
|
||||
self.proc.stderr.close()
|
||||
self.proc.stderr = None
|
||||
# If this causes deadlocks, consider terminating instead.
|
||||
self.proc.wait()
|
||||
self.proc = None
|
||||
|
||||
def __del__(self):
|
||||
# If the garbage collector comes, make sure the subprocess is terminated.
|
||||
self.close()
|
||||
|
||||
# Support the Context Manager protocol, to ensure that resources are cleaned up.
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
@requires_duration
|
||||
def ffplay_audiopreview(
|
||||
clip, fps=None, buffersize=2000, nbytes=2, audio_flag=None, video_flag=None
|
||||
):
|
||||
"""
|
||||
A function that wraps the FFPLAY_AudioPreviewer to preview an AudioClip
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
fps
|
||||
Frame rate of the sound. 44100 gives top quality, but may cause
|
||||
problems if your computer is not fast enough and your clip is
|
||||
complicated. If the sound jumps during the preview, lower it
|
||||
(11025 is still fine, 5000 is tolerable).
|
||||
|
||||
buffersize
|
||||
The sound is not generated all at once, but rather made by bunches
|
||||
of frames (chunks). ``buffersize`` is the size of such a chunk.
|
||||
Try varying it if you meet audio problems (but you shouldn't
|
||||
have to).
|
||||
|
||||
nbytes:
|
||||
Number of bytes to encode the sound: 1 for 8bit sound, 2 for
|
||||
16bit, 4 for 32bit sound. 2 bytes is fine.
|
||||
|
||||
audio_flag, video_flag:
|
||||
Instances of class threading events that are used to synchronize
|
||||
video and audio during ``VideoClip.preview()``.
|
||||
"""
|
||||
if not fps:
|
||||
if not clip.fps:
|
||||
fps = 44100
|
||||
else:
|
||||
fps = clip.fps
|
||||
|
||||
with FFPLAY_AudioPreviewer(fps, nbytes, clip.nchannels) as previewer:
|
||||
first_frame = True
|
||||
for chunk in clip.iter_chunks(
|
||||
chunksize=buffersize, quantize=True, nbytes=nbytes, fps=fps
|
||||
):
|
||||
# On first frame, wait for video
|
||||
if first_frame:
|
||||
first_frame = False
|
||||
|
||||
if audio_flag is not None:
|
||||
audio_flag.set() # Say to video that audio is ready
|
||||
|
||||
if video_flag is not None:
|
||||
video_flag.wait() # Wait for video to be ready
|
||||
|
||||
previewer.write_frames(chunk)
|
||||
304
moviepy/audio/io/readers.py
Normal file
304
moviepy/audio/io/readers.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""MoviePy audio reading with ffmpeg."""
|
||||
|
||||
import subprocess as sp
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
from moviepy.config import FFMPEG_BINARY
|
||||
from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename
|
||||
from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos
|
||||
|
||||
|
||||
class FFMPEG_AudioReader:
|
||||
"""A class to read the audio in either video files or audio files
|
||||
using ffmpeg. ffmpeg will read any audio and transform them into
|
||||
raw data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename
|
||||
Name of any video or audio file, like ``video.mp4`` or
|
||||
``sound.wav`` etc.
|
||||
|
||||
buffersize
|
||||
The size of the buffer to use. Should be bigger than the buffer
|
||||
used by ``write_audiofile``
|
||||
|
||||
print_infos
|
||||
Print the ffmpeg infos on the file being read (for debugging)
|
||||
|
||||
fps
|
||||
Desired frames per second in the decoded signal that will be
|
||||
received from ffmpeg
|
||||
|
||||
nbytes
|
||||
Desired number of bytes (1,2,4) in the signal that will be
|
||||
received from ffmpeg
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filename,
|
||||
buffersize,
|
||||
decode_file=False,
|
||||
print_infos=False,
|
||||
fps=44100,
|
||||
nbytes=2,
|
||||
nchannels=2,
|
||||
):
|
||||
# TODO bring FFMPEG_AudioReader more in line with FFMPEG_VideoReader
|
||||
# E.g. here self.pos is still 1-indexed.
|
||||
# (or have them inherit from a shared parent class)
|
||||
self.filename = filename
|
||||
self.nbytes = nbytes
|
||||
self.fps = fps
|
||||
self.format = "s%dle" % (8 * nbytes)
|
||||
self.codec = "pcm_s%dle" % (8 * nbytes)
|
||||
self.nchannels = nchannels
|
||||
infos = ffmpeg_parse_infos(filename, decode_file=decode_file)
|
||||
self.duration = infos["duration"]
|
||||
self.bitrate = infos["audio_bitrate"]
|
||||
self.infos = infos
|
||||
self.proc = None
|
||||
|
||||
self.n_frames = int(self.fps * self.duration)
|
||||
self.buffersize = min(self.n_frames + 1, buffersize)
|
||||
self.buffer = None
|
||||
self.buffer_startframe = 1
|
||||
self.initialize()
|
||||
self.buffer_around(1)
|
||||
|
||||
def initialize(self, start_time=0):
|
||||
"""Opens the file, creates the pipe."""
|
||||
self.close() # if any
|
||||
|
||||
if start_time != 0:
|
||||
offset = min(1, start_time)
|
||||
i_arg = [
|
||||
"-ss",
|
||||
"%.05f" % (start_time - offset),
|
||||
"-i",
|
||||
ffmpeg_escape_filename(self.filename),
|
||||
"-vn",
|
||||
"-ss",
|
||||
"%.05f" % offset,
|
||||
]
|
||||
else:
|
||||
i_arg = ["-i", ffmpeg_escape_filename(self.filename), "-vn"]
|
||||
|
||||
cmd = (
|
||||
[FFMPEG_BINARY]
|
||||
+ i_arg
|
||||
+ [
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
self.format,
|
||||
"-acodec",
|
||||
self.codec,
|
||||
"-ar",
|
||||
"%d" % self.fps,
|
||||
"-ac",
|
||||
"%d" % self.nchannels,
|
||||
"-",
|
||||
]
|
||||
)
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{
|
||||
"bufsize": self.buffersize,
|
||||
"stdout": sp.PIPE,
|
||||
"stderr": sp.PIPE,
|
||||
"stdin": sp.DEVNULL,
|
||||
}
|
||||
)
|
||||
|
||||
self.proc = sp.Popen(cmd, **popen_params)
|
||||
|
||||
self.pos = np.round(self.fps * start_time)
|
||||
|
||||
def skip_chunk(self, chunksize):
|
||||
"""Skip a chunk of audio data by reading and discarding the specified number of
|
||||
frames from the audio stream. The audio stream is read from the `proc` stdout.
|
||||
After skipping the chunk, the `pos` attribute is updated accordingly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
chunksize (int):
|
||||
The number of audio frames to skip.
|
||||
"""
|
||||
_ = self.proc.stdout.read(self.nchannels * chunksize * self.nbytes)
|
||||
self.proc.stdout.flush()
|
||||
self.pos = self.pos + chunksize
|
||||
|
||||
def read_chunk(self, chunksize):
|
||||
"""Read a chunk of audio data from the audio stream.
|
||||
|
||||
This method reads a chunk of audio data from the audio stream. The
|
||||
specified number of frames, given by `chunksize`, is read from the
|
||||
`proc` stdout. The audio data is returned as a NumPy array, where
|
||||
each row corresponds to a frame and each column corresponds to a
|
||||
channel. If there is not enough audio left to read, the remaining
|
||||
portion is padded with zeros, ensuring that the returned array has
|
||||
the desired length. The `pos` attribute is updated accordingly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
chunksize (float):
|
||||
The desired number of audio frames to read.
|
||||
|
||||
"""
|
||||
# chunksize is not being autoconverted from float to int
|
||||
chunksize = int(round(chunksize))
|
||||
s = self.proc.stdout.read(self.nchannels * chunksize * self.nbytes)
|
||||
data_type = {1: "int8", 2: "int16", 4: "int32"}[self.nbytes]
|
||||
if hasattr(np, "frombuffer"):
|
||||
result = np.frombuffer(s, dtype=data_type)
|
||||
else:
|
||||
result = np.fromstring(s, dtype=data_type)
|
||||
result = (1.0 * result / 2 ** (8 * self.nbytes - 1)).reshape(
|
||||
(int(len(result) / self.nchannels), self.nchannels)
|
||||
)
|
||||
|
||||
# Pad the read chunk with zeros when there isn't enough audio
|
||||
# left to read, so the buffer is always at full length.
|
||||
pad = np.zeros((chunksize - len(result), self.nchannels), dtype=result.dtype)
|
||||
result = np.concatenate([result, pad])
|
||||
# self.proc.stdout.flush()
|
||||
self.pos = self.pos + chunksize
|
||||
return result
|
||||
|
||||
def seek(self, pos):
|
||||
"""Read a frame at time t. Note for coders: getting an arbitrary
|
||||
frame in the video with ffmpeg can be painfully slow if some
|
||||
decoding has to be done. This function tries to avoid fectching
|
||||
arbitrary frames whenever possible, by moving between adjacent
|
||||
frames.
|
||||
"""
|
||||
if (pos < self.pos) or (pos > (self.pos + 1000000)):
|
||||
t = 1.0 * pos / self.fps
|
||||
self.initialize(t)
|
||||
elif pos > self.pos:
|
||||
self.skip_chunk(pos - self.pos)
|
||||
# last case standing: pos = current pos
|
||||
self.pos = pos
|
||||
|
||||
def get_frame(self, tt):
|
||||
"""Retrieve the audio frame(s) corresponding to the given timestamp(s).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tt (float or numpy.ndarray):
|
||||
The timestamp(s) at which to retrieve the audio frame(s).
|
||||
If `tt` is a single float value, the frame corresponding to that
|
||||
timestamp is returned. If `tt` is a NumPy array of timestamps, an
|
||||
array of frames corresponding to each timestamp is returned.
|
||||
"""
|
||||
if isinstance(tt, np.ndarray):
|
||||
# lazy implementation, but should not cause problems in
|
||||
# 99.99 % of the cases
|
||||
|
||||
# elements of t that are actually in the range of the
|
||||
# audio file.
|
||||
in_time = (tt >= 0) & (tt < self.duration)
|
||||
|
||||
# Check that the requested time is in the valid range
|
||||
if not in_time.any():
|
||||
raise IOError(
|
||||
"Error in file %s, " % (self.filename)
|
||||
+ "Accessing time t=%.02f-%.02f seconds, " % (tt[0], tt[-1])
|
||||
+ "with clip duration=%f seconds, " % self.duration
|
||||
)
|
||||
|
||||
# The np.round in the next line is super-important.
|
||||
# Removing it results in artifacts in the noise.
|
||||
frames = np.round((self.fps * tt)).astype(int)[in_time]
|
||||
fr_min, fr_max = frames.min(), frames.max()
|
||||
|
||||
# if min and max frames don't fit the buffer, it results in IndexError
|
||||
# we avoid that by recursively calling this function on smaller length
|
||||
# and concatenate the results:w
|
||||
max_frame_threshold = fr_min + self.buffersize // 2
|
||||
threshold_idx = np.searchsorted(frames, max_frame_threshold, side="right")
|
||||
if threshold_idx != len(frames):
|
||||
in_time_head = in_time[0:threshold_idx]
|
||||
in_time_tail = in_time[threshold_idx:]
|
||||
return np.concatenate(
|
||||
[self.get_frame(in_time_head), self.get_frame(in_time_tail)]
|
||||
)
|
||||
|
||||
if not (0 <= (fr_min - self.buffer_startframe) < len(self.buffer)):
|
||||
self.buffer_around(fr_min)
|
||||
elif not (0 <= (fr_max - self.buffer_startframe) < len(self.buffer)):
|
||||
self.buffer_around(fr_max)
|
||||
|
||||
try:
|
||||
result = np.zeros((len(tt), self.nchannels))
|
||||
indices = frames - self.buffer_startframe
|
||||
result[in_time] = self.buffer[indices]
|
||||
return result
|
||||
|
||||
except IndexError as error:
|
||||
warnings.warn(
|
||||
"Error in file %s, " % (self.filename)
|
||||
+ "At time t=%.02f-%.02f seconds, " % (tt[0], tt[-1])
|
||||
+ "indices wanted: %d-%d, " % (indices.min(), indices.max())
|
||||
+ "but len(buffer)=%d\n" % (len(self.buffer))
|
||||
+ str(error),
|
||||
UserWarning,
|
||||
)
|
||||
|
||||
# repeat the last frame instead
|
||||
indices[indices >= len(self.buffer)] = len(self.buffer) - 1
|
||||
result[in_time] = self.buffer[indices]
|
||||
return result
|
||||
|
||||
else:
|
||||
ind = int(self.fps * tt)
|
||||
if ind < 0 or ind > self.n_frames: # out of time: return 0
|
||||
return np.zeros(self.nchannels)
|
||||
|
||||
if not (0 <= (ind - self.buffer_startframe) < len(self.buffer)):
|
||||
# out of the buffer: recenter the buffer
|
||||
self.buffer_around(ind)
|
||||
|
||||
# read the frame in the buffer
|
||||
return self.buffer[ind - self.buffer_startframe]
|
||||
|
||||
def buffer_around(self, frame_number):
|
||||
"""Fill the buffer with frames, centered on frame_number if possible."""
|
||||
# start-frame for the buffer
|
||||
new_bufferstart = max(0, frame_number - self.buffersize // 2)
|
||||
|
||||
if self.buffer is not None:
|
||||
current_f_end = self.buffer_startframe + self.buffersize
|
||||
if new_bufferstart < current_f_end < new_bufferstart + self.buffersize:
|
||||
# We already have part of what must be read
|
||||
conserved = current_f_end - new_bufferstart
|
||||
chunksize = self.buffersize - conserved
|
||||
array = self.read_chunk(chunksize)
|
||||
self.buffer = np.vstack([self.buffer[-conserved:], array])
|
||||
else:
|
||||
self.seek(new_bufferstart)
|
||||
self.buffer = self.read_chunk(self.buffersize)
|
||||
else:
|
||||
self.seek(new_bufferstart)
|
||||
self.buffer = self.read_chunk(self.buffersize)
|
||||
|
||||
self.buffer_startframe = new_bufferstart
|
||||
|
||||
def close(self):
|
||||
"""Closes the reader, terminating the subprocess if is still alive."""
|
||||
if self.proc:
|
||||
if self.proc.poll() is None:
|
||||
self.proc.terminate()
|
||||
self.proc.stdout.close()
|
||||
self.proc.stderr.close()
|
||||
self.proc.wait()
|
||||
self.proc = None
|
||||
|
||||
def __del__(self):
|
||||
# If the garbage collector comes, make sure the subprocess is terminated.
|
||||
self.close()
|
||||
Reference in New Issue
Block a user