Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to create audiosegment from a segment of a file #345

Merged
merged 9 commits into from
Mar 6, 2021
4 changes: 4 additions & 0 deletions API.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ The first argument is the path (as a string) of the file to read, **or** a file
**`raw` only** — `1` for mono, `2` for stereo.
- `frame_rate` | example: `2`
**`raw` only** — Also known as sample rate, common values are `44100` (44.1kHz - CD audio), and `48000` (48kHz - DVD audio)
- `start_second` | example: `2.0` | default: `None`
Offset (in seconds) to start loading the audio file. If `None`, the audio will start loading from the beginning.
- `duration` | example: `2.5` | default: `None`
Number of seconds to be loaded. If `None`, full audio will be loaded.


### AudioSegment(…).export()
Expand Down
5 changes: 4 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ Carlos del Castillo
Yudong Sun
github: sunjerry019

Jorge Perianez
github: JPery

Chendi Luo
github: Creonalia

Daniel Lefevre
gitHub: dplefevre
gitHub: dplefevre
81 changes: 69 additions & 12 deletions pydub/audio_segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def from_mono_audiosegments(cls, *mono_segments):
)

@classmethod
def from_file_using_temporary_files(cls, file, format=None, codec=None, parameters=None, **kwargs):
def from_file_using_temporary_files(cls, file, format=None, codec=None, parameters=None, start_second=None, duration=None, **kwargs):
orig_file = file
file, close_file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False)

Expand All @@ -526,7 +526,14 @@ def is_format(f):
obj = cls._from_safe_wav(file)
if close_file:
file.close()
return obj
if start_second is None and duration is None:
return obj
elif start_second is not None and duration is None:
return obj[start_second*1000:]
elif start_second is None and duration is not None:
return obj[:duration*1000]
else:
return obj[start_second*1000:(start_second+duration)*1000]
except:
file.seek(0)
elif is_format("raw") or is_format("pcm"):
Expand All @@ -542,7 +549,14 @@ def is_format(f):
obj = cls(data=file.read(), metadata=metadata)
if close_file:
file.close()
return obj
if start_second is None and duration is None:
return obj
elif start_second is not None and duration is None:
return obj[start_second * 1000:]
elif start_second is None and duration is not None:
return obj[:duration * 1000]
else:
return obj[start_second * 1000:(start_second + duration) * 1000]

input_file = NamedTemporaryFile(mode='wb', delete=False)
try:
Expand Down Expand Up @@ -581,10 +595,17 @@ def is_format(f):
conversion_command += [
"-i", input_file.name, # input_file options (filename last)
"-vn", # Drop any video streams if there are any
"-f", "wav", # output options (filename last)
output.name
"-f", "wav" # output options (filename last)
]

if start_second is not None:
conversion_command += ["-ss", str(start_second)]

if duration is not None:
conversion_command += ["-t", str(duration)]

conversion_command += [output.name]

if parameters is not None:
# extend arguments with arbitrary set
conversion_command.extend(parameters)
Expand All @@ -610,10 +631,18 @@ def is_format(f):
os.unlink(input_file.name)
os.unlink(output.name)

return obj
if start_second is None and duration is None:
return obj
elif start_second is not None and duration is None:
return obj[0:]
elif start_second is None and duration is not None:
return obj[:duration * 1000]
else:
return obj[0:duration * 1000]


@classmethod
def from_file(cls, file, format=None, codec=None, parameters=None, **kwargs):
def from_file(cls, file, format=None, codec=None, parameters=None, start_second=None, duration=None, **kwargs):
orig_file = file
try:
filename = fsdecode(file)
Expand All @@ -637,7 +666,14 @@ def is_format(f):

if is_format("wav"):
try:
return cls._from_safe_wav(file)
if start_second is None and duration is None:
return cls._from_safe_wav(file)
elif start_second is not None and duration is None:
return cls._from_safe_wav(file)[start_second*1000:]
elif start_second is None and duration is not None:
return cls._from_safe_wav(file)[:duration*1000]
else:
return cls._from_safe_wav(file)[start_second*1000:(start_second+duration)*1000]
except:
file.seek(0)
elif is_format("raw") or is_format("pcm"):
Expand All @@ -650,7 +686,14 @@ def is_format(f):
'channels': channels,
'frame_width': channels * sample_width
}
return cls(data=file.read(), metadata=metadata)
if start_second is None and duration is None:
return cls(data=file.read(), metadata=metadata)
elif start_second is not None and duration is None:
return cls(data=file.read(), metadata=metadata)[start_second*1000:]
elif start_second is None and duration is not None:
return cls(data=file.read(), metadata=metadata)[:duration*1000]
else:
return cls(data=file.read(), metadata=metadata)[start_second*1000:(start_second+duration)*1000]

conversion_command = [cls.converter,
'-y', # always overwrite existing files
Expand Down Expand Up @@ -703,10 +746,17 @@ def is_format(f):

conversion_command += [
"-vn", # Drop any video streams if there are any
"-f", "wav", # output options (filename last)
"-"
"-f", "wav" # output options (filename last)
]

if start_second is not None:
conversion_command += ["-ss", str(start_second)]

if duration is not None:
conversion_command += ["-t", str(duration)]

conversion_command += ["-"]

if parameters is not None:
# extend arguments with arbitrary set
conversion_command.extend(parameters)
Expand All @@ -732,7 +782,14 @@ def is_format(f):
if close_file:
file.close()

return obj
if start_second is None and duration is None:
return obj
elif start_second is not None and duration is None:
return obj[0:]
elif start_second is None and duration is not None:
return obj[:duration * 1000]
else:
return obj[0:duration * 1000]

@classmethod
def from_mp3(cls, file, parameters=None):
Expand Down
65 changes: 65 additions & 0 deletions test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,71 @@ def test_lowpass_filter_cutoff_frequency(self):
self.assertAlmostEqual(less_treble.dBFS, s.dBFS, places=0)


class PartialAudioSegmentLoadTests(unittest.TestCase):

def setUp(self):
self.wave_path_str = os.path.join(data_dir, 'test1.wav')
self.mp3_path_str = os.path.join(data_dir, 'test1.mp3')
self.raw_path_str = os.path.join(data_dir, 'test1.raw')

def tearDown(self):
AudioSegment.converter = get_encoder_name()

def test_partial_load_duration_equals_cropped_mp3_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.mp3_path_str)[:1000]
partial_seg2 = AudioSegment.from_file(self.mp3_path_str, duration=1.)
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_start_second_equals_cropped_mp3_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.mp3_path_str)[1000:]
partial_seg2 = AudioSegment.from_file(self.mp3_path_str, start_second=1.)[0:]
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_start_second_and_duration_equals_cropped_mp3_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.mp3_path_str)[1000:2000]
partial_seg2 = AudioSegment.from_file(self.mp3_path_str, start_second=1., duration=1.)
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_duration_equals_cropped_wav_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.wave_path_str)[:1000]
partial_seg2 = AudioSegment.from_file(self.wave_path_str, duration=1.)
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_start_second_equals_cropped_wav_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.wave_path_str)[1000:]
partial_seg2 = AudioSegment.from_file(self.wave_path_str, start_second=1.)[0:]
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_start_second_and_duration_equals_cropped_wav_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.wave_path_str)[1000:2000]
partial_seg2 = AudioSegment.from_file(self.wave_path_str, start_second=1., duration=1.)
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_duration_equals_cropped_raw_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.raw_path_str, format="raw", sample_width=2, frame_rate=32000, channels=2)[:1000]
partial_seg2 = AudioSegment.from_file(self.raw_path_str, format="raw", sample_width=2, frame_rate=32000, channels=2, duration=1.)
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_start_second_equals_cropped_raw_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.raw_path_str, format="raw", sample_width=2, frame_rate=32000, channels=2)[1000:]
partial_seg2 = AudioSegment.from_file(self.raw_path_str, format="raw", sample_width=2, frame_rate=32000, channels=2, start_second=1.)[0:]
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)

def test_partial_load_start_second_and_duration_equals_cropped_raw_audio_segment(self):
partial_seg1 = AudioSegment.from_file(self.raw_path_str, format="raw", sample_width=2, frame_rate=32000, channels=2)[1000:2000]
partial_seg2 = AudioSegment.from_file(self.raw_path_str, format="raw", sample_width=2, frame_rate=32000, channels=2, start_second=1., duration=1.)
self.assertEqual(len(partial_seg1), len(partial_seg2))
self.assertEqual(partial_seg1._data, partial_seg2._data)


if __name__ == "__main__":
import sys

Expand Down