|
|
@ -66,6 +66,54 @@ class AudioSegment(object):
|
|
|
|
samples, sample_rate = soundfile.read(file, dtype='float32')
|
|
|
|
samples, sample_rate = soundfile.read(file, dtype='float32')
|
|
|
|
return cls(samples, sample_rate)
|
|
|
|
return cls(samples, sample_rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def slice_from_file(cls, file, start=None, end=None):
|
|
|
|
|
|
|
|
"""Loads a small section of an audio without having to load
|
|
|
|
|
|
|
|
the entire file into the memory which can be incredibly wasteful.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:param file: Input audio filepath or file object.
|
|
|
|
|
|
|
|
:type file: basestring|file
|
|
|
|
|
|
|
|
:param start: Start time in seconds. If start is negative, it wraps
|
|
|
|
|
|
|
|
around from the end. If not provided, this function
|
|
|
|
|
|
|
|
reads from the very beginning.
|
|
|
|
|
|
|
|
:type start: float
|
|
|
|
|
|
|
|
:param end: End time in seconds. If end is negative, it wraps around
|
|
|
|
|
|
|
|
from the end. If not provided, the default behvaior is
|
|
|
|
|
|
|
|
to read to the end of the file.
|
|
|
|
|
|
|
|
:type end: float
|
|
|
|
|
|
|
|
:return: AudioSegment instance of the specified slice of the input
|
|
|
|
|
|
|
|
audio file.
|
|
|
|
|
|
|
|
:rtype: AudioSegment
|
|
|
|
|
|
|
|
:raise ValueError: If start or end is incorrectly set, e.g. out of
|
|
|
|
|
|
|
|
bounds in time.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
sndfile = soundfile.SoundFile(file)
|
|
|
|
|
|
|
|
sample_rate = sndfile.samplerate
|
|
|
|
|
|
|
|
duration = float(len(sndfile)) / sample_rate
|
|
|
|
|
|
|
|
start = 0. if start is None else start
|
|
|
|
|
|
|
|
end = 0. if end is None else end
|
|
|
|
|
|
|
|
if start < 0.0:
|
|
|
|
|
|
|
|
start += duration
|
|
|
|
|
|
|
|
if end < 0.0:
|
|
|
|
|
|
|
|
end += duration
|
|
|
|
|
|
|
|
if start < 0.0:
|
|
|
|
|
|
|
|
raise ValueError("The slice start position (%f s) is out of "
|
|
|
|
|
|
|
|
"bounds." % start)
|
|
|
|
|
|
|
|
if end < 0.0:
|
|
|
|
|
|
|
|
raise ValueError("The slice end position (%f s) is out of bounds." %
|
|
|
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
if start > end:
|
|
|
|
|
|
|
|
raise ValueError("The slice start position (%f s) is later than "
|
|
|
|
|
|
|
|
"the slice end position (%f s)." % (start, end))
|
|
|
|
|
|
|
|
if end > duration:
|
|
|
|
|
|
|
|
raise ValueError("The slice end position (%f s) is out of bounds "
|
|
|
|
|
|
|
|
"(> %f s)" % (end, duration))
|
|
|
|
|
|
|
|
start_frame = int(start * sample_rate)
|
|
|
|
|
|
|
|
end_frame = int(end * sample_rate)
|
|
|
|
|
|
|
|
sndfile.seek(start_frame)
|
|
|
|
|
|
|
|
data = sndfile.read(frames=end_frame - start_frame, dtype='float32')
|
|
|
|
|
|
|
|
return cls(data, sample_rate)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@classmethod
|
|
|
|
def from_bytes(cls, bytes):
|
|
|
|
def from_bytes(cls, bytes):
|
|
|
|
"""Create audio segment from a byte string containing audio samples.
|
|
|
|
"""Create audio segment from a byte string containing audio samples.
|
|
|
@ -105,6 +153,20 @@ class AudioSegment(object):
|
|
|
|
samples = np.concatenate([seg.samples for seg in segments])
|
|
|
|
samples = np.concatenate([seg.samples for seg in segments])
|
|
|
|
return cls(samples, sample_rate)
|
|
|
|
return cls(samples, sample_rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def make_silence(cls, duration, sample_rate):
|
|
|
|
|
|
|
|
"""Creates a silent audio segment of the given duration and sample rate.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:param duration: Length of silence in seconds.
|
|
|
|
|
|
|
|
:type duration: float
|
|
|
|
|
|
|
|
:param sample_rate: Sample rate.
|
|
|
|
|
|
|
|
:type sample_rate: float
|
|
|
|
|
|
|
|
:return: Silent AudioSegment instance of the given duration.
|
|
|
|
|
|
|
|
:rtype: AudioSegment
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
samples = np.zeros(int(duration * sample_rate))
|
|
|
|
|
|
|
|
return cls(samples, sample_rate)
|
|
|
|
|
|
|
|
|
|
|
|
def to_wav_file(self, filepath, dtype='float32'):
|
|
|
|
def to_wav_file(self, filepath, dtype='float32'):
|
|
|
|
"""Save audio segment to disk as wav file.
|
|
|
|
"""Save audio segment to disk as wav file.
|
|
|
|
|
|
|
|
|
|
|
@ -130,68 +192,6 @@ class AudioSegment(object):
|
|
|
|
format='WAV',
|
|
|
|
format='WAV',
|
|
|
|
subtype=subtype_map[dtype])
|
|
|
|
subtype=subtype_map[dtype])
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def slice_from_file(cls, file, start=None, end=None):
|
|
|
|
|
|
|
|
"""Loads a small section of an audio without having to load
|
|
|
|
|
|
|
|
the entire file into the memory which can be incredibly wasteful.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:param file: Input audio filepath or file object.
|
|
|
|
|
|
|
|
:type file: basestring|file
|
|
|
|
|
|
|
|
:param start: Start time in seconds. If start is negative, it wraps
|
|
|
|
|
|
|
|
around from the end. If not provided, this function
|
|
|
|
|
|
|
|
reads from the very beginning.
|
|
|
|
|
|
|
|
:type start: float
|
|
|
|
|
|
|
|
:param end: End time in seconds. If end is negative, it wraps around
|
|
|
|
|
|
|
|
from the end. If not provided, the default behvaior is
|
|
|
|
|
|
|
|
to read to the end of the file.
|
|
|
|
|
|
|
|
:type end: float
|
|
|
|
|
|
|
|
:return: AudioSegment instance of the specified slice of the input
|
|
|
|
|
|
|
|
audio file.
|
|
|
|
|
|
|
|
:rtype: AudioSegment
|
|
|
|
|
|
|
|
:raise ValueError: If start or end is incorrectly set, e.g. out of
|
|
|
|
|
|
|
|
bounds in time.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
sndfile = soundfile.SoundFile(file)
|
|
|
|
|
|
|
|
sample_rate = sndfile.samplerate
|
|
|
|
|
|
|
|
duration = float(len(sndfile)) / sample_rate
|
|
|
|
|
|
|
|
start = 0. if start is None else start
|
|
|
|
|
|
|
|
end = 0. if end is None else end
|
|
|
|
|
|
|
|
if start < 0.0:
|
|
|
|
|
|
|
|
start += duration
|
|
|
|
|
|
|
|
if end < 0.0:
|
|
|
|
|
|
|
|
end += duration
|
|
|
|
|
|
|
|
if start < 0.0:
|
|
|
|
|
|
|
|
raise ValueError("The slice start position (%f s) is out of "
|
|
|
|
|
|
|
|
"bounds." % start)
|
|
|
|
|
|
|
|
if end < 0.0:
|
|
|
|
|
|
|
|
raise ValueError("The slice end position (%f s) is out of bounds." %
|
|
|
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
if start > end:
|
|
|
|
|
|
|
|
raise ValueError("The slice start position (%f s) is later than "
|
|
|
|
|
|
|
|
"the slice end position (%f s)." % (start, end))
|
|
|
|
|
|
|
|
if end > duration:
|
|
|
|
|
|
|
|
raise ValueError("The slice end position (%f s) is out of bounds "
|
|
|
|
|
|
|
|
"(> %f s)" % (end, duration))
|
|
|
|
|
|
|
|
start_frame = int(start * sample_rate)
|
|
|
|
|
|
|
|
end_frame = int(end * sample_rate)
|
|
|
|
|
|
|
|
sndfile.seek(start_frame)
|
|
|
|
|
|
|
|
data = sndfile.read(frames=end_frame - start_frame, dtype='float32')
|
|
|
|
|
|
|
|
return cls(data, sample_rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def make_silence(cls, duration, sample_rate):
|
|
|
|
|
|
|
|
"""Creates a silent audio segment of the given duration and sample rate.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:param duration: Length of silence in seconds.
|
|
|
|
|
|
|
|
:type duration: float
|
|
|
|
|
|
|
|
:param sample_rate: Sample rate.
|
|
|
|
|
|
|
|
:type sample_rate: float
|
|
|
|
|
|
|
|
:return: Silent AudioSegment instance of the given duration.
|
|
|
|
|
|
|
|
:rtype: AudioSegment
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
samples = np.zeros(int(duration * sample_rate))
|
|
|
|
|
|
|
|
return cls(samples, sample_rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def superimpose(self, other):
|
|
|
|
def superimpose(self, other):
|
|
|
|
"""Add samples from another segment to those of this segment
|
|
|
|
"""Add samples from another segment to those of this segment
|
|
|
|
(sample-wise addition, not segment concatenation).
|
|
|
|
(sample-wise addition, not segment concatenation).
|
|
|
@ -225,7 +225,7 @@ class AudioSegment(object):
|
|
|
|
samples = self._convert_samples_from_float32(self._samples, dtype)
|
|
|
|
samples = self._convert_samples_from_float32(self._samples, dtype)
|
|
|
|
return samples.tostring()
|
|
|
|
return samples.tostring()
|
|
|
|
|
|
|
|
|
|
|
|
def apply_gain(self, gain):
|
|
|
|
def gain_db(self, gain):
|
|
|
|
"""Apply gain in decibels to samples.
|
|
|
|
"""Apply gain in decibels to samples.
|
|
|
|
|
|
|
|
|
|
|
|
Note that this is an in-place transformation.
|
|
|
|
Note that this is an in-place transformation.
|
|
|
@ -278,7 +278,7 @@ class AudioSegment(object):
|
|
|
|
"Unable to normalize segment to %f dB because the "
|
|
|
|
"Unable to normalize segment to %f dB because the "
|
|
|
|
"the probable gain have exceeds max_gain_db (%f dB)" %
|
|
|
|
"the probable gain have exceeds max_gain_db (%f dB)" %
|
|
|
|
(target_db, max_gain_db))
|
|
|
|
(target_db, max_gain_db))
|
|
|
|
self.apply_gain(min(max_gain_db, target_db - self.rms_db))
|
|
|
|
self.gain_db(min(max_gain_db, target_db - self.rms_db))
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_online_bayesian(self,
|
|
|
|
def normalize_online_bayesian(self,
|
|
|
|
target_db,
|
|
|
|
target_db,
|
|
|
@ -319,7 +319,7 @@ class AudioSegment(object):
|
|
|
|
rms_estimate_db = 10 * np.log10(mean_squared_estimate)
|
|
|
|
rms_estimate_db = 10 * np.log10(mean_squared_estimate)
|
|
|
|
# Compute required time-varying gain.
|
|
|
|
# Compute required time-varying gain.
|
|
|
|
gain_db = target_db - rms_estimate_db
|
|
|
|
gain_db = target_db - rms_estimate_db
|
|
|
|
self.apply_gain(gain_db)
|
|
|
|
self.gain_db(gain_db)
|
|
|
|
|
|
|
|
|
|
|
|
def resample(self, target_sample_rate, quality='sinc_medium'):
|
|
|
|
def resample(self, target_sample_rate, quality='sinc_medium'):
|
|
|
|
"""Resample the audio to a target sample rate.
|
|
|
|
"""Resample the audio to a target sample rate.
|
|
|
@ -366,6 +366,31 @@ class AudioSegment(object):
|
|
|
|
raise ValueError("Unknown value for the sides %s" % sides)
|
|
|
|
raise ValueError("Unknown value for the sides %s" % sides)
|
|
|
|
self._samples = padded._samples
|
|
|
|
self._samples = padded._samples
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def shift(self, shift_ms):
|
|
|
|
|
|
|
|
"""Shift the audio in time. If `shift_ms` is positive, shift with time
|
|
|
|
|
|
|
|
advance; if negative, shift with time delay. Silence are padded to
|
|
|
|
|
|
|
|
keep the duration unchanged.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note that this is an in-place transformation.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:param shift_ms: Shift time in millseconds. If positive, shift with
|
|
|
|
|
|
|
|
time advance; if negative; shift with time delay.
|
|
|
|
|
|
|
|
:type shift_ms: float
|
|
|
|
|
|
|
|
:raises ValueError: If shift_ms is longer than audio duration.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if abs(shift_ms) / 1000.0 > self.duration:
|
|
|
|
|
|
|
|
raise ValueError("Absolute value of shift_ms should be smaller "
|
|
|
|
|
|
|
|
"than audio duration.")
|
|
|
|
|
|
|
|
shift_samples = int(shift_ms * self._sample_rate / 1000)
|
|
|
|
|
|
|
|
if shift_samples > 0:
|
|
|
|
|
|
|
|
# time advance
|
|
|
|
|
|
|
|
self._samples[:-shift_samples] = self._samples[shift_samples:]
|
|
|
|
|
|
|
|
self._samples[-shift_samples:] = 0
|
|
|
|
|
|
|
|
elif shift_samples < 0:
|
|
|
|
|
|
|
|
# time delay
|
|
|
|
|
|
|
|
self._samples[-shift_samples:] = self._samples[:shift_samples]
|
|
|
|
|
|
|
|
self._samples[:-shift_samples] = 0
|
|
|
|
|
|
|
|
|
|
|
|
def subsegment(self, start_sec=None, end_sec=None):
|
|
|
|
def subsegment(self, start_sec=None, end_sec=None):
|
|
|
|
"""Cut the AudioSegment between given boundaries.
|
|
|
|
"""Cut the AudioSegment between given boundaries.
|
|
|
|
|
|
|
|
|
|
|
@ -505,7 +530,7 @@ class AudioSegment(object):
|
|
|
|
noise_gain_db = min(self.rms_db - noise.rms_db - snr_dB, max_gain_db)
|
|
|
|
noise_gain_db = min(self.rms_db - noise.rms_db - snr_dB, max_gain_db)
|
|
|
|
noise_new = copy.deepcopy(noise)
|
|
|
|
noise_new = copy.deepcopy(noise)
|
|
|
|
noise_new.random_subsegment(self.duration, rng=rng)
|
|
|
|
noise_new.random_subsegment(self.duration, rng=rng)
|
|
|
|
noise_new.apply_gain(noise_gain_db)
|
|
|
|
noise_new.gain_db(noise_gain_db)
|
|
|
|
self.superimpose(noise_new)
|
|
|
|
self.superimpose(noise_new)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
@property
|
|
|
|