Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/jiaaro/pydub
Browse files Browse the repository at this point in the history
  • Loading branch information
jiaaro committed Jun 15, 2018
2 parents 3af2b9a + e7a8780 commit 2c0b062
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 86 deletions.
68 changes: 39 additions & 29 deletions pydub/audio_segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def setter(self, func):
self.fset = func
return self


def classproperty(func):
if not isinstance(func, (classmethod, staticmethod)):
func = classmethod(func)
Expand All @@ -83,7 +84,6 @@ def classproperty(func):
"wave": "wav",
}


WavSubChunk = namedtuple('WavSubChunk', ['id', 'position', 'size'])
WavData = namedtuple('WavData', ['audio_format', 'channels', 'sample_rate',
'bits_per_sample', 'raw_data'])
Expand Down Expand Up @@ -209,10 +209,10 @@ def __init__(self, data=None, *args, **kwargs):
data = data if isinstance(data, (basestring, bytes)) else data.read()
except(OSError):
d = b''
reader = data.read(2**31-1)
reader = data.read(2 ** 31 - 1)
while reader:
d += reader
reader = data.read(2**31-1)
reader = data.read(2 ** 31 - 1)
data = d

wav_data = read_wav_audio(data)
Expand Down Expand Up @@ -257,7 +257,6 @@ def raw_data(self):
"""
return self._data


def get_array_of_samples(self):
"""
returns the raw_data as an array of samples
Expand Down Expand Up @@ -290,7 +289,7 @@ def __getitem__(self, millisecond):
if isinstance(millisecond, slice):
if millisecond.step:
return (
self[i:i+millisecond.step]
self[i:i + millisecond.step]
for i in xrange(*millisecond.indices(len(self)))
)

Expand Down Expand Up @@ -468,7 +467,8 @@ def from_mono_audiosegments(cls, *mono_segments):
segs = cls._sync(*mono_segments)

if segs[0].channels != 1:
raise ValueError("AudioSegment.from_mono_audiosegments requires all arguments are mono AudioSegment instances")
raise ValueError(
"AudioSegment.from_mono_audiosegments requires all arguments are mono AudioSegment instances")

channels = len(segs)
sample_width = segs[0].sample_width
Expand Down Expand Up @@ -536,13 +536,13 @@ def is_format(f):
except(OSError):
input_file.flush()
input_file.close()
input_file = NamedTemporaryFile(mode='wb', delete=False, buffering=2**31-1)
input_file = NamedTemporaryFile(mode='wb', delete=False, buffering=2 ** 31 - 1)
file.close()
file = open(orig_file, buffering=2**13-1, mode='rb')
reader = file.read(2**31-1)
file = open(orig_file, buffering=2 ** 13 - 1, mode='rb')
reader = file.read(2 ** 31 - 1)
while reader:
input_file.write(reader)
reader = file.read(2**31-1)
reader = file.read(2 ** 31 - 1)
input_file.flush()
file.close()

Expand Down Expand Up @@ -583,7 +583,9 @@ def is_format(f):

try:
if p.returncode != 0:
raise CouldntDecodeError("Decoding failed. ffmpeg returned error code: {0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format(p.returncode, p_err))
raise CouldntDecodeError(
"Decoding failed. ffmpeg returned error code: {0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format(
p.returncode, p_err))
obj = cls._from_safe_wav(output)
finally:
input_file.close()
Expand Down Expand Up @@ -661,9 +663,9 @@ def is_format(f):
if x['codec_type'] == 'audio']
# This is a workaround for some ffprobe versions that always say
# that mp3/mp4/aac/webm/ogg files contain fltp samples
if (audio_streams[0]['sample_fmt'] == 'fltp' and
(is_format("mp3") or is_format("mp4") or is_format("aac") or
is_format("webm") or is_format("ogg"))):
if (audio_streams[0].get('sample_fmt') == 'fltp' and
(is_format("mp3") or is_format("mp4") or is_format("aac") or
is_format("webm") or is_format("ogg"))):
bits_per_sample = 16
else:
bits_per_sample = audio_streams[0]['bits_per_sample']
Expand All @@ -688,7 +690,9 @@ def is_format(f):

if p.returncode != 0 or len(p_out) == 0:
file.close()
raise CouldntDecodeError("Decoding failed. ffmpeg returned error code: {0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format(p.returncode, p_err))
raise CouldntDecodeError(
"Decoding failed. ffmpeg returned error code: {0}\n\nOutput from ffmpeg/avlib:\n\n{1}".format(
p.returncode, p_err))

p_out = bytearray(p_out)
fix_wav_headers(p_out)
Expand Down Expand Up @@ -716,17 +720,19 @@ def from_wav(cls, file, parameters=None):

@classmethod
def from_raw(cls, file, **kwargs):
return cls.from_file(file, 'raw', sample_width=kwargs['sample_width'], frame_rate=kwargs['frame_rate'], channels=kwargs['channels'])
return cls.from_file(file, 'raw', sample_width=kwargs['sample_width'], frame_rate=kwargs['frame_rate'],
channels=kwargs['channels'])

@classmethod
def _from_safe_wav(cls, file):
file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False)
file.seek(0)
obj = cls(data=file);
obj = cls(data=file)
file.close()
return obj

def export(self, out_f=None, format='mp3', codec=None, bitrate=None, parameters=None, tags=None, id3v2_version='4', cover=None):
def export(self, out_f=None, format='mp3', codec=None, bitrate=None, parameters=None, tags=None, id3v2_version='4',
cover=None):
"""
Export an AudioSegment to a file with given options
Expand Down Expand Up @@ -804,9 +810,10 @@ def export(self, out_f=None, format='mp3', codec=None, bitrate=None, parameters=

if cover is not None:
if cover.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff')) and format == "mp3":
conversion_command.extend(["-i" , cover, "-map", "0", "-map", "1", "-c:v", "mjpeg"])
conversion_command.extend(["-i", cover, "-map", "0", "-map", "1", "-c:v", "mjpeg"])
else:
raise AttributeError("Currently cover images are only supported by MP3 files. The allowed image formats are: .tif, .jpg, .bmp, .jpeg and .png.")
raise AttributeError(
"Currently cover images are only supported by MP3 files. The allowed image formats are: .tif, .jpg, .bmp, .jpeg and .png.")

if codec is not None:
# force audio encoder
Expand Down Expand Up @@ -835,7 +842,7 @@ def export(self, out_f=None, format='mp3', codec=None, bitrate=None, parameters=
raise InvalidID3TagVersion(
"id3v2_version not allowed, allowed versions: %s" % id3v2_allowed_versions)
conversion_command.extend([
"-id3v2_version", id3v2_version
"-id3v2_version", id3v2_version
])

if sys.platform == 'darwin':
Expand All @@ -856,7 +863,9 @@ def export(self, out_f=None, format='mp3', codec=None, bitrate=None, parameters=
log_subprocess_output(p_err)

if p.returncode != 0:
raise CouldntEncodeError("Encoding failed. ffmpeg/avlib returned error code: {0}\n\nCommand:{1}\n\nOutput from ffmpeg/avlib:\n\n{2}".format(p.returncode, conversion_command, p_err))
raise CouldntEncodeError(
"Encoding failed. ffmpeg/avlib returned error code: {0}\n\nCommand:{1}\n\nOutput from ffmpeg/avlib:\n\n{2}".format(
p.returncode, conversion_command, p_err))

output.seek(0)
out_f.write(output.read())
Expand Down Expand Up @@ -1114,11 +1123,11 @@ def overlay(self, seg, position=0, loop=False, times=None, gain_during_overlay=N
if gain_during_overlay:
seg1_overlaid = seg1[pos:pos + seg2_len]
seg1_adjusted_gain = audioop.mul(seg1_overlaid, self.sample_width,
db_to_float(float(gain_during_overlay)))
db_to_float(float(gain_during_overlay)))
output.write(audioop.add(seg1_adjusted_gain, seg2, sample_width))
else:
output.write(audioop.add(seg1[pos:pos + seg2_len], seg2,
sample_width))
sample_width))
pos += seg2_len

# dec times to break our while loop (eventually)
Expand Down Expand Up @@ -1219,7 +1228,7 @@ def fade(self, to_gain=0, from_gain=0, start=None, end=None,

# fades longer than 100ms can use coarse fading (one gain step per ms),
# shorter fades will have audible clicks so they use precise fading
#(one gain step per sample)
# (one gain step per sample)
if duration > 100:
scale_step = gain_delta / duration

Expand Down Expand Up @@ -1266,14 +1275,15 @@ def reverse(self):
)

def _repr_html_(self):
src = """
src = """
<audio controls>
<source src="data:audio/mpeg;base64,{base64}" type="audio/mpeg"/>
Your browser does not support the audio element.
</audio>
"""
fh = self.export()
data = base64.b64encode(fh.read()).decode('ascii')
return src.format(base64=data)
fh = self.export()
data = base64.b64encode(fh.read()).decode('ascii')
return src.format(base64=data)


from . import effects
69 changes: 38 additions & 31 deletions pydub/utils.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
from __future__ import division

from math import log, ceil, floor
import json
import os
import re
from subprocess import Popen, PIPE
import sys
from subprocess import Popen, PIPE
from math import log, ceil
from tempfile import TemporaryFile
from warnings import warn
import json

try:
import audioop
except ImportError:
import pyaudioop as audioop


if sys.version_info >= (3, 0):
basestring = str



FRAME_WIDTHS = {
8: 1,
16: 2,
32: 4,
}
ARRAY_TYPES = {
8: "b",
8: "b",
16: "h",
32: "i",
}
Expand Down Expand Up @@ -78,7 +75,7 @@ def db_to_float(db, using_amplitude=True):
db = float(db)
if using_amplitude:
return 10 ** (db / 20)
else: # using power
else: # using power
return 10 ** (db / 10)


Expand All @@ -92,33 +89,28 @@ def ratio_to_db(ratio, val2=None, using_amplitude=True):
# accept 2 values and use the ratio of val1 to val2
if val2 is not None:
ratio = ratio / val2

# special case for multiply-by-zero (convert to silence)
if ratio == 0:
return -float('inf')

if using_amplitude:
return 20 * log(ratio, 10)
else: # using power
else: # using power
return 10 * log(ratio, 10)


def register_pydub_effect(fn, name=None):
"""
decorator for adding pydub effects to the AudioSegment objects.
example use:
@register_pydub_effect
def normalize(audio_segment):
...
or you can specify a name:
@register_pydub_effect("normalize")
def normalize_audio_segment(audio_segment):
...
"""
if isinstance(fn, basestring):
name = fn
Expand All @@ -136,7 +128,6 @@ def make_chunks(audio_segment, chunk_length):
"""
Breaks an AudioSegment into chunks that are <chunk_length> milliseconds
long.
if chunk_length is 50 then you'll get a list of 50 millisecond long audio
segments back (except the last one, which can be shorter)
"""
Expand All @@ -149,7 +140,7 @@ def which(program):
"""
Mimics behavior of UNIX which command.
"""
#Add .exe program extension for windows support
# Add .exe program extension for windows support
if os.name == "nt" and not program.endswith(".exe"):
program += ".exe"

Expand All @@ -174,6 +165,7 @@ def get_encoder_name():
warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning)
return "ffmpeg"


def get_player_name():
"""
Return enconder default application for system, either avconv or ffmpeg
Expand Down Expand Up @@ -220,6 +212,33 @@ def fsdecode(filename):
raise TypeError("type {0} not accepted by fsdecode".format(type(filename)))


def get_extra_info(stderr):
"""
avprobe sometimes gives more information on stderr than
on the json output. The information has to be extracted
from stderr of the format of:
' Stream #0:0: Audio: flac, 88200 Hz, stereo, s32 (24 bit)'
or (macOS version):
' Stream #0:0: Audio: vorbis'
' 44100 Hz, stereo, fltp, 320 kb/s'
:type stderr: str
:rtype: list of dict
"""
extra_info = {}

re_stream = r'(?P<space_start> +)Stream #0[:\.](?P<stream_id>([0-9]+))(?P<content_0>.+)\n?((?P<space_end> +)(?P<content_1>.+))?'
for i in re.finditer(re_stream, stderr):
if i.group('space_end') is not None and len(i.group('space_start')) <= len(
i.group('space_end')):
content_line = ','.join([i.group('content_0'), i.group('content_1')])
else:
content_line = i.group('content_0')
tokens = [x.strip() for x in re.split('[:,]', content_line) if x]
extra_info[int(i.group('stream_id'))] = tokens
return extra_info


def mediainfo_json(filepath):
"""Return json dictionary with media info(codec, duration, size, bitrate...) from filepath
"""
Expand Down Expand Up @@ -253,18 +272,7 @@ def mediainfo_json(filepath):
# (for example, because the file doesn't exist)
return info

# avprobe sometimes gives more information on stderr than
# on the json output. The information has to be extracted
# from lines of the format of:
# ' Stream #0:0: Audio: flac, 88200 Hz, stereo, s32 (24 bit)'
extra_info = {}
for line in stderr.split("\n"):
match = re.match(' *Stream #0[:\.]([0-9]+)(\(\w+\))?', line)
if match:
stream_id = int(match.group(1))
tokens = [x.strip()
for x in re.split('[:,]', line[match.end():]) if x]
extra_info[stream_id] = tokens
extra_info = get_extra_info(stderr)

audio_streams = [x for x in info['streams'] if x['codec_type'] == 'audio']
if len(audio_streams) == 0:
Expand Down Expand Up @@ -296,7 +304,6 @@ def set_property(stream, prop, value):
set_property(stream, 'sample_fmt', token)
set_property(stream, 'bits_per_sample', 64)
set_property(stream, 'bits_per_raw_sample', 64)

return info


Expand Down
Loading

0 comments on commit 2c0b062

Please sign in to comment.