Hello community, here is the log from the commit of package python-pydub for openSUSE:Factory checked in at 2018-07-17 09:42:11 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pydub (Old) and /work/SRC/openSUSE:Factory/.python-pydub.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-pydub" Tue Jul 17 09:42:11 2018 rev:2 rq:623065 version:0.22.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pydub/python-pydub.changes 2018-05-15 10:33:20.164333417 +0200 +++ /work/SRC/openSUSE:Factory/.python-pydub.new/python-pydub.changes 2018-07-17 09:43:33.637034268 +0200 @@ -1,0 +2,16 @@ +Wed Jul 11 17:18:18 UTC 2018 - alarrosa@suse.com + +- Update to 0.22.1 + * Fix pydub.utils.mediainfo_json() to work with newer, + backwards-incompatible versions of ffprobe/avprobe + +- Update to 0.22.0 + * Adds support for audio with frame rates (sample rates) of 48k and higher + (requires scipy) (PR #262, fixes #134, #237, #209) + * Adds support for PEP 519 File Path protocol (PR #252) + * Fixes a few places where handles to temporary files are kept open (PR #280) + * Add the license file to the python package to aid other packaging projects + (PR #279, fixes #274) + * Big fix for pydub.silence.detect_silence() (PR #263) + +------------------------------------------------------------------- Old: ---- pydub-0.21.0.tar.gz New: ---- pydub-0.22.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pydub.spec ++++++ --- /var/tmp/diff_new_pack.kHUTY0/_old 2018-07-17 09:43:34.029032831 +0200 +++ /var/tmp/diff_new_pack.kHUTY0/_new 2018-07-17 09:43:34.029032831 +0200 @@ -20,7 +20,7 @@ # Test data missing %bcond_with test Name: python-pydub -Version: 0.21.0 +Version: 0.22.1 Release: 0 Summary: Manipulate audio with Python License: MIT @@ -31,12 +31,13 @@ BuildRequires: %{python_module devel} BuildRequires: %{python_module setuptools} BuildRequires: fdupes -BuildRequires: ffmpeg BuildRequires: python-rpm-macros %if %{with test} BuildRequires: %{python_module scipy} +BuildRequires: ffmpeg %endif -Recommends: python-scipt +Recommends: python-scipy +Requires: ffmpeg BuildArch: noarch %python_subpackages ++++++ pydub-0.21.0.tar.gz -> pydub-0.22.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/PKG-INFO new/pydub-0.22.1/PKG-INFO --- old/pydub-0.21.0/PKG-INFO 2018-02-22 15:07:37.000000000 +0100 +++ new/pydub-0.22.1/PKG-INFO 2018-06-16 00:47:27.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pydub -Version: 0.21.0 +Version: 0.22.1 Summary: Manipulate audio with an simple and easy high level interface Home-page: http://pydub.com Author: James Robert diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/pydub/audio_segment.py new/pydub-0.22.1/pydub/audio_segment.py --- old/pydub-0.21.0/pydub/audio_segment.py 2018-02-22 15:06:07.000000000 +0100 +++ new/pydub-0.22.1/pydub/audio_segment.py 2018-06-16 00:46:47.000000000 +0200 @@ -8,7 +8,9 @@ import sys import struct from .logging_utils import log_conversion, log_subprocess_output +from .utils import mediainfo_json, fsdecode import base64 +from collections import namedtuple try: from StringIO import StringIO @@ -69,6 +71,7 @@ self.fset = func return self + def classproperty(func): if not isinstance(func, (classmethod, staticmethod)): func = classmethod(func) @@ -81,6 +84,66 @@ "wave": "wav", } +WavSubChunk = namedtuple('WavSubChunk', ['id', 'position', 'size']) +WavData = namedtuple('WavData', ['audio_format', 'channels', 'sample_rate', + 'bits_per_sample', 'raw_data']) + + +def extract_wav_headers(data): + # def search_subchunk(data, subchunk_id): + pos = 12 # The size of the RIFF chunk descriptor + subchunks = [] + while pos + 8 < len(data) and len(subchunks) < 10: + subchunk_id = data[pos:pos + 4] + subchunk_size = struct.unpack_from('<I', data[pos + 4:pos + 8])[0] + subchunks.append(WavSubChunk(subchunk_id, pos, subchunk_size)) + if subchunk_id == b'data': + # 'data' is the last subchunk + break + pos += subchunk_size + 8 + + return subchunks + + +def read_wav_audio(data, headers=None): + if not headers: + headers = extract_wav_headers(data) + + fmt = [x for x in headers if x.id == b'fmt '] + if not fmt or fmt[0].size < 16: + raise CouldntDecodeError("Couldn't find fmt header in wav data") + fmt = fmt[0] + pos = fmt.position + 8 + audio_format = struct.unpack_from('<H', data[pos:pos + 2])[0] + if audio_format != 1 and audio_format != 0xFFFE: + raise CouldntDecodeError("Unknown audio format 0x%X in wav data" % + audio_format) + + channels = struct.unpack_from('<H', data[pos + 2:pos + 4])[0] + sample_rate = struct.unpack_from('<I', data[pos + 4:pos + 8])[0] + bits_per_sample = struct.unpack_from('<H', data[pos + 14:pos + 16])[0] + + data_hdr = headers[-1] + if data_hdr.id != b'data': + raise CouldntDecodeError("Couldn't find data header in wav data") + + pos = data_hdr.position + 8 + return WavData(audio_format, channels, sample_rate, bits_per_sample, + data[pos:pos + data_hdr.size]) + + +def fix_wav_headers(data): + headers = extract_wav_headers(data) + if not headers or headers[-1].id != b'data': + return + + # Set the file size in the RIFF chunk descriptor + data[4:8] = struct.pack('<I', len(data) - 8) + + # Set the data size in the data subchunk + pos = headers[-1].position + data[pos + 4:pos + 8] = struct.pack('<I', len(data) - pos - 8) + class AudioSegment(object): """ @@ -146,25 +209,21 @@ 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 - raw = wave.open(StringIO(data), 'rb') - - raw.rewind() - self.channels = raw.getnchannels() - self.sample_width = raw.getsampwidth() - self.frame_rate = raw.getframerate() + wav_data = read_wav_audio(data) + if not wav_data: + raise CouldntDecodeError("Couldn't read wav audio from data") + + self.channels = wav_data.channels + self.sample_width = wav_data.bits_per_sample // 8 + self.frame_rate = wav_data.sample_rate self.frame_width = self.channels * self.sample_width - - raw.rewind() - - # the "or b''" base case is a work-around for a python 3.4 - # see https://github.com/jiaaro/pydub/pull/107 - self._data = raw.readframes(float('inf')) or b'' + self._data = wav_data.raw_data # Convert 24-bit audio to 32-bit audio. # (stdlib audioop and array modules do not support 24-bit data) @@ -185,7 +244,6 @@ old_bytes = struct.pack(pack_fmt, b0, b1, b2) byte_buffer.write(old_bytes) - self._data = byte_buffer.getvalue() self.sample_width = 4 self.frame_width = self.channels * self.sample_width @@ -199,7 +257,6 @@ """ return self._data - def get_array_of_samples(self): """ returns the raw_data as an array of samples @@ -232,7 +289,7 @@ 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))) ) @@ -410,7 +467,8 @@ 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 @@ -433,7 +491,7 @@ ) @classmethod - def from_file(cls, file, format=None, codec=None, parameters=None, **kwargs): + def from_file_using_temporary_files(cls, file, format=None, codec=None, parameters=None, **kwargs): orig_file = file file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False) @@ -447,11 +505,15 @@ return True if isinstance(orig_file, basestring): return orig_file.lower().endswith(".{0}".format(f)) + if isinstance(orig_file, bytes): + return orig_file.lower().endswith((".{0}".format(f)).encode('utf8')) return False if is_format("wav"): try: - return cls._from_safe_wav(file) + obj = cls._from_safe_wav(file) + file.close() + return obj except: file.seek(0) elif is_format("raw") or is_format("pcm"): @@ -464,7 +526,9 @@ 'channels': channels, 'frame_width': channels * sample_width } - return cls(data=file.read(), metadata=metadata) + obj = cls(data=file.read(), metadata=metadata) + file.close() + return obj input_file = NamedTemporaryFile(mode='wb', delete=False) try: @@ -472,13 +536,15 @@ except(OSError): input_file.flush() input_file.close() - input_file = NamedTemporaryFile(mode='wb', delete=False, buffering=2**31-1) - file = open(orig_file, buffering=2**13-1, mode='rb') - reader = file.read(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) while reader: input_file.write(reader) - reader = file.read(2**31-1) + reader = file.read(2 ** 31 - 1) input_file.flush() + file.close() output = NamedTemporaryFile(mode="rb", delete=False) @@ -517,7 +583,9 @@ 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() @@ -528,6 +596,113 @@ return obj @classmethod + def from_file(cls, file, format=None, codec=None, parameters=None, **kwargs): + orig_file = file + try: + filename = fsdecode(file) + except TypeError: + filename = None + file = _fd_or_path_or_tempfile(file, 'rb', tempfile=False) + + if format: + format = format.lower() + format = AUDIO_FILE_EXT_ALIASES.get(format, format) + + def is_format(f): + f = f.lower() + if format == f: + return True + + if filename: + return filename.lower().endswith(".{0}".format(f)) + + return False + + if is_format("wav"): + try: + return cls._from_safe_wav(file) + except: + file.seek(0) + elif is_format("raw") or is_format("pcm"): + sample_width = kwargs['sample_width'] + frame_rate = kwargs['frame_rate'] + channels = kwargs['channels'] + metadata = { + 'sample_width': sample_width, + 'frame_rate': frame_rate, + 'channels': channels, + 'frame_width': channels * sample_width + } + return cls(data=file.read(), metadata=metadata) + + conversion_command = [cls.converter, + '-y', # always overwrite existing files + ] + + # If format is not defined + # ffmpeg/avconv will detect it automatically + if format: + conversion_command += ["-f", format] + + if codec: + # force audio decoder + conversion_command += ["-acodec", codec] + + if filename: + conversion_command += ["-i", filename] + stdin_parameter = None + stdin_data = None + else: + conversion_command += ["-i", "-"] + stdin_parameter = subprocess.PIPE + stdin_data = file.read() + + info = mediainfo_json(orig_file) + if info: + audio_streams = [x for x in info['streams'] + 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].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'] + acodec = 'pcm_s%dle' % bits_per_sample + conversion_command += ["-acodec", acodec] + + conversion_command += [ + "-vn", # Drop any video streams if there are any + "-f", "wav", # output options (filename last) + "-" + ] + + if parameters is not None: + # extend arguments with arbitrary set + conversion_command.extend(parameters) + + log_conversion(conversion_command) + + p = subprocess.Popen(conversion_command, stdin=stdin_parameter, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p_out, p_err = p.communicate(input=stdin_data) + + 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)) + + p_out = bytearray(p_out) + fix_wav_headers(p_out) + obj = cls._from_safe_wav(BytesIO(p_out)) + + file.close() + + return obj + + @classmethod def from_mp3(cls, file, parameters=None): return cls.from_file(file, 'mp3', parameters) @@ -545,20 +720,25 @@ @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) - return 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 out_f (string): - Path to destination audio file + Path to destination audio file. Also accepts os.PathLike objects on + python >= 3.6 format (string) Format for destination audio file. @@ -630,9 +810,10 @@ 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 @@ -661,7 +842,7 @@ 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': @@ -682,7 +863,9 @@ 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()) @@ -940,11 +1123,11 @@ 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) @@ -978,7 +1161,9 @@ output.write(seg2[crossfade:]._data) output.seek(0) - return seg1._spawn(data=output) + obj = seg1._spawn(data=output) + output.close() + return obj def fade(self, to_gain=0, from_gain=0, start=None, end=None, duration=None): @@ -1043,7 +1228,7 @@ # 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 @@ -1090,14 +1275,15 @@ ) 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/pydub/pyaudioop.py new/pydub-0.22.1/pydub/pyaudioop.py --- old/pydub-0.21.0/pydub/pyaudioop.py 2017-01-06 14:35:20.000000000 +0100 +++ new/pydub-0.22.1/pydub/pyaudioop.py 2018-05-24 18:05:38.000000000 +0200 @@ -1,4 +1,9 @@ -import __builtin__ +try: + from __builtin__ import max as builtin_max + from __builtin__ import min as builtin_min +except ImportError: + from builtins import max as builtin_max + from builtins import min as builtin_min import math import struct from fractions import gcd @@ -79,7 +84,7 @@ def _get_clipfn(size, signed=True): maxval = _get_maxval(size, signed) minval = _get_minval(size, signed) - return lambda val: __builtin__.max(min(val, maxval), minval) + return lambda val: builtin_max(min(val, maxval), minval) def _overflow(val, size, signed=True): @@ -109,7 +114,7 @@ if len(cp) == 0: return 0 - return __builtin__.max(abs(sample) for sample in _get_samples(cp, size)) + return builtin_max(abs(sample) for sample in _get_samples(cp, size)) def minmax(cp, size): @@ -117,8 +122,8 @@ max_sample, min_sample = 0, 0 for sample in _get_samples(cp, size): - max_sample = __builtin__.max(sample, max_sample) - min_sample = __builtin__.min(sample, min_sample) + max_sample = builtin_max(sample, max_sample) + min_sample = builtin_min(sample, min_sample) return min_sample, max_sample @@ -542,4 +547,4 @@ def adpcm2lin(cp, size, state): - raise NotImplementedError() \ No newline at end of file + raise NotImplementedError() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/pydub/silence.py new/pydub-0.22.1/pydub/silence.py --- old/pydub-0.21.0/pydub/silence.py 2018-02-22 15:06:07.000000000 +0100 +++ new/pydub-0.22.1/pydub/silence.py 2018-05-24 18:04:53.000000000 +0200 @@ -42,7 +42,7 @@ current_range_start = prev_i for silence_start_i in silence_starts: - continuous = (silence_start_i == prev_i + 1) + continuous = (silence_start_i == prev_i + seek_step) # sometimes two small blips are enough for one particular slice to be # non-silent, despite the silence all running together. Just combine diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/pydub/utils.py new/pydub-0.22.1/pydub/utils.py --- old/pydub-0.21.0/pydub/utils.py 2017-05-10 01:01:52.000000000 +0200 +++ new/pydub-0.22.1/pydub/utils.py 2018-06-16 00:46:47.000000000 +0200 @@ -1,10 +1,11 @@ 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 @@ -13,19 +14,16 @@ 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", } @@ -58,6 +56,14 @@ if isinstance(fd, basestring): fd = open(fd, mode=mode) + try: + if isinstance(fd, os.PathLike): + fd = open(fd, mode=mode) + except AttributeError: + # module os has no attribute PathLike, so we're on python < 3.6. + # The protocol we're trying to support doesn't exist, so just pass. + pass + return fd @@ -69,7 +75,7 @@ db = float(db) if using_amplitude: return 10 ** (db / 20) - else: # using power + else: # using power return 10 ** (db / 10) @@ -83,33 +89,28 @@ # 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 @@ -127,7 +128,6 @@ """ 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) """ @@ -140,7 +140,7 @@ """ 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" @@ -165,6 +165,7 @@ 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 @@ -193,12 +194,123 @@ return "ffprobe" +def fsdecode(filename): + """Wrapper for os.fsdecode which was introduced in python 3.2 .""" + + if sys.version_info >= (3, 2): + PathLikeTypes = (basestring, bytes) + if sys.version_info >= (3, 6): + PathLikeTypes += (os.PathLike,) + if isinstance(filename, PathLikeTypes): + return os.fsdecode(filename) + else: + if isinstance(filename, bytes): + return filename.decode(sys.getfilesystemencoding()) + if isinstance(filename, basestring): + return 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 + """ + prober = get_prober_name() + command_args = [ + "-v", "info", + "-show_format", + "-show_streams", + ] + try: + command_args += [fsdecode(filepath)] + stdin_parameter = None + stdin_data = None + except TypeError: + command_args += ["-"] + stdin_parameter = PIPE + file = _fd_or_path_or_tempfile(filepath, 'rb', tempfile=False) + file.seek(0) + stdin_data = file.read() + + command = [prober, '-of', 'json'] + command_args + res = Popen(command, stdin=stdin_parameter, stdout=PIPE, stderr=PIPE) + output, stderr = res.communicate(input=stdin_data) + output = output.decode("utf-8", 'ignore') + stderr = stderr.decode("utf-8", 'ignore') + + info = json.loads(output) + + if not info: + # If ffprobe didn't give any information, just return it + # (for example, because the file doesn't exist) + return info + + extra_info = get_extra_info(stderr) + + audio_streams = [x for x in info['streams'] if x['codec_type'] == 'audio'] + if len(audio_streams) == 0: + return info + + # We just operate on the first audio stream in case there are more + stream = audio_streams[0] + + def set_property(stream, prop, value): + if prop not in stream or stream[prop] == 0: + stream[prop] = value + + for token in extra_info[stream['index']]: + m = re.match('([su]([0-9]{1,2})p?) \(([0-9]{1,2}) bit\)$', token) + m2 = re.match('([su]([0-9]{1,2})p?)( \(default\))?$', token) + if m: + set_property(stream, 'sample_fmt', m.group(1)) + set_property(stream, 'bits_per_sample', int(m.group(2))) + set_property(stream, 'bits_per_raw_sample', int(m.group(3))) + elif m2: + set_property(stream, 'sample_fmt', m2.group(1)) + set_property(stream, 'bits_per_sample', int(m2.group(2))) + set_property(stream, 'bits_per_raw_sample', int(m2.group(2))) + elif re.match('(flt)p?( \(default\))?$', token): + set_property(stream, 'sample_fmt', token) + set_property(stream, 'bits_per_sample', 32) + set_property(stream, 'bits_per_raw_sample', 32) + elif re.match('(dbl)p?( \(default\))?$', token): + set_property(stream, 'sample_fmt', token) + set_property(stream, 'bits_per_sample', 64) + set_property(stream, 'bits_per_raw_sample', 64) + return info + + def mediainfo(filepath): """Return dictionary with media info(codec, duration, size, bitrate...) from filepath """ - from .audio_segment import AudioSegment - prober = get_prober_name() command_args = [ "-v", "quiet", @@ -238,4 +350,4 @@ else: info[key] = value - return info \ No newline at end of file + return info diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/pydub.egg-info/PKG-INFO new/pydub-0.22.1/pydub.egg-info/PKG-INFO --- old/pydub-0.21.0/pydub.egg-info/PKG-INFO 2018-02-22 15:07:37.000000000 +0100 +++ new/pydub-0.22.1/pydub.egg-info/PKG-INFO 2018-06-16 00:47:27.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pydub -Version: 0.21.0 +Version: 0.22.1 Summary: Manipulate audio with an simple and easy high level interface Home-page: http://pydub.com Author: James Robert diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/setup.py new/pydub-0.22.1/setup.py --- old/pydub-0.21.0/setup.py 2018-02-22 15:06:52.000000000 +0100 +++ new/pydub-0.22.1/setup.py 2018-06-16 00:44:56.000000000 +0200 @@ -8,7 +8,7 @@ setup( name='pydub', - version='0.21.0', + version='0.22.1', author='James Robert', author_email='jiaaro@gmail.com', description='Manipulate audio with an simple and easy high level interface', @@ -17,6 +17,9 @@ url='http://pydub.com', packages=['pydub'], long_description=__doc__, + package_data={ + '': ['LICENSE'], + }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pydub-0.21.0/test/test.py new/pydub-0.22.1/test/test.py --- old/pydub-0.21.0/test/test.py 2018-02-22 15:06:07.000000000 +0100 +++ new/pydub-0.22.1/test/test.py 2018-06-16 00:46:47.000000000 +0200 @@ -3,14 +3,15 @@ import sys import unittest from tempfile import ( - NamedTemporaryFile, - mkdtemp, - gettempdir + NamedTemporaryFile, + mkdtemp, + gettempdir ) import tempfile import struct from pydub import AudioSegment +from pydub.audio_segment import extract_wav_headers from pydub.utils import ( db_to_float, ratio_to_db, @@ -55,6 +56,110 @@ self.assertEqual(12, ratio_to_db(db_to_float(12, using_amplitude=False), using_amplitude=False)) +if sys.version_info >= (3, 6): + class PathLikeObjectTests(unittest.TestCase): + + class MyPathLike: + def __init__(self, path): + self.path = path + + def __fspath__(self): + return self.path + + def setUp(self): + self.mp3_path_str = os.path.join(data_dir, 'test1.mp3') + + from pathlib import Path + self.mp3_pathlib_path = Path(self.mp3_path_str) + + self.mp3_path_like_str = self.MyPathLike(self.mp3_path_str) + self.mp3_path_like_bytes = self.MyPathLike(bytes(self.mp3_path_str, sys.getdefaultencoding())) + + def test_audio_segment_from_pathlib_path(self): + seg1 = AudioSegment.from_file(self.mp3_path_str) + seg2 = AudioSegment.from_file(self.mp3_pathlib_path) + + self.assertEqual(len(seg1), len(seg2)) + self.assertEqual(seg1._data, seg2._data) + self.assertTrue(len(seg1) > 0) + + def test_audio_segment_from_path_like_str(self): + seg1 = AudioSegment.from_file(self.mp3_path_str) + seg2 = AudioSegment.from_file(self.mp3_path_like_str) + + self.assertEqual(len(seg1), len(seg2)) + self.assertEqual(seg1._data, seg2._data) + self.assertTrue(len(seg1) > 0) + + def test_audio_segment_from_path_like_bytes(self): + seg1 = AudioSegment.from_file(self.mp3_path_str) + seg2 = AudioSegment.from_file(self.mp3_path_like_bytes) + + self.assertEqual(len(seg1), len(seg2)) + self.assertEqual(seg1._data, seg2._data) + self.assertTrue(len(seg1) > 0) + + def test_non_existant_pathlib_path(self): + from pathlib import Path + path = Path('this/path/should/not/exist/do/not/make/this/exist') + with self.assertRaises(FileNotFoundError): + _ = AudioSegment.from_file(path) + + path = Path('') + # On Unicies this will raise a IsADirectoryError, on Windows this + # will result in a PermissionError. Both of these are subclasses of + # OSError. We aren't so much worried about the specific exception + # here, just that reading a file from an empty path is an error. + with self.assertRaises(OSError): + _ = AudioSegment.from_file(path) + + def test_non_existant_path_like_str(self): + path = self.MyPathLike('this/path/should/not/exist/do/not/make/this/exist') + with self.assertRaises(FileNotFoundError): + _ = AudioSegment.from_file(path) + + path = self.MyPathLike('') + with self.assertRaises(FileNotFoundError): + _ = AudioSegment.from_file(path) + + def test_non_existant_path_like_bytes(self): + path = self.MyPathLike(bytes('this/path/should/not/exist/do/not/make/this/exist', sys.getdefaultencoding())) + with self.assertRaises(FileNotFoundError): + _ = AudioSegment.from_file(path) + + path = self.MyPathLike(bytes('', sys.getdefaultencoding())) + with self.assertRaises(FileNotFoundError): + _ = AudioSegment.from_file(path) + + def assertWithinRange(self, val, lower_bound, upper_bound): + self.assertTrue(lower_bound < val < upper_bound, + "%s is not in the acceptable range: %s - %s" % + (val, lower_bound, upper_bound)) + + def assertWithinTolerance(self, val, expected, tolerance=None, + percentage=None): + if percentage is not None: + tolerance = val * percentage + lower_bound = val - tolerance + upper_bound = val + tolerance + self.assertWithinRange(val, lower_bound, upper_bound) + + def test_export_pathlib_path(self): + seg1 = AudioSegment.from_file(self.mp3_path_str) + from pathlib import Path + path = Path(tempfile.gettempdir()) / 'pydub-test-export-8ajds.mp3' + try: + seg1.export(path, format='mp3') + seg2 = AudioSegment.from_file(path, format='mp3') + + self.assertTrue(len(seg1) > 0) + self.assertWithinTolerance(len(seg1), + len(seg2), + percentage=0.01) + finally: + os.unlink(path) + + class FileAccessTests(unittest.TestCase): def setUp(self): @@ -79,6 +184,7 @@ def setUp(self): global test1, test2, test3, testparty, testdcoffset if not test1: + a = os.path.join(data_dir, 'test1.mp3') test1 = AudioSegment.from_mp3(os.path.join(data_dir, 'test1.mp3')) test2 = AudioSegment.from_mp3(os.path.join(data_dir, 'test2.mp3')) test3 = AudioSegment.from_mp3(os.path.join(data_dir, 'test3.mp3')) @@ -115,7 +221,8 @@ self.assertWithinRange(val, lower_bound, upper_bound) def test_direct_instantiation_with_bytes(self): - seg = AudioSegment(b'RIFF\x28\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00\x00}\x00\x00\x00\xf4\x01\x00\x04\x00\x10\x00data\x04\x00\x00\x00\x00\x00\x00\x00') + seg = AudioSegment( + b'RIFF\x28\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x02\x00\x00}\x00\x00\x00\xf4\x01\x00\x04\x00\x10\x00data\x04\x00\x00\x00\x00\x00\x00\x00') self.assertEqual(seg.frame_count(), 1) self.assertEqual(seg.channels, 2) self.assertEqual(seg.sample_width, 2) @@ -134,6 +241,31 @@ # the data length should have grown by exactly 4:3 (24 bits turn into 32 bits) self.assertEqual(len(seg24.raw_data) * 3, len24 * 4) + def test_192khz_audio(self): + test_files = [('test-192khz-16bit.wav', 16), + ('test-192khz-24bit.wav', 32), + ('test-192khz-32bit.flac', 32), + ('test-192khz-32bit.wav', 32), + ('test-192khz-64bit.wav', 64)] + base_file, bit_depth = test_files[0] + path = os.path.join(data_dir, base_file) + base = AudioSegment.from_file(path) + + headers = extract_wav_headers(open(path, 'rb').read()) + data16_size = headers[-1].size + self.assertEqual(len(base.raw_data), data16_size) + self.assertEqual(base.frame_rate, 192000) + self.assertEqual(base.sample_width, bit_depth / 8) + + for test_file, bit_depth in test_files[1:]: + path = os.path.join(data_dir, test_file) + seg = AudioSegment.from_file(path) + self.assertEqual(seg.sample_width, bit_depth / 8) + self.assertEqual(seg.frame_rate, 192000) + self.assertEqual(len(seg.raw_data), len(base.raw_data) * + seg.sample_width / base.sample_width) + self.assertEqual(seg.frame_rate, 192000) + def test_concat(self): catted_audio = self.seg1 + self.seg2 @@ -348,7 +480,6 @@ self.assertEqual(seg_lchannel.frame_count(), seg.frame_count()) self.assertEqual(seg_rchannel.frame_count(), seg.frame_count()) - def test_apply_gain_stereo(self): seg = self.seg1 @@ -430,7 +561,8 @@ def test_export_as_raw(self): seg = self.seg1 exported_raw = seg.export(format='raw') - seg_exported_raw = AudioSegment.from_raw(exported_raw, sample_width=seg.sample_width, frame_rate=seg.frame_rate, channels = seg.channels) + seg_exported_raw = AudioSegment.from_raw(exported_raw, sample_width=seg.sample_width, frame_rate=seg.frame_rate, + channels=seg.channels) self.assertWithinTolerance(len(seg_exported_raw), len(seg), @@ -525,10 +657,12 @@ if sys.platform == 'win32': tmp_mp3_file.close() - seg.export(tmp_mp3_file.name) + fd = seg.export(tmp_mp3_file.name) + fd.close() for i in range(3): - AudioSegment.from_mp3(tmp_mp3_file.name).export(tmp_mp3_file.name, "mp3") + fd = AudioSegment.from_mp3(tmp_mp3_file.name).export(tmp_mp3_file.name, "mp3") + fd.close() tmp_seg = AudioSegment.from_mp3(tmp_mp3_file.name) self.assertFalse(len(tmp_seg) < len(seg)) @@ -780,14 +914,12 @@ else: raise Exception("AudioSegment.invert_phase() didn't catch a bad input (mono)") - s_inv = s.invert_phase() self.assertFalse(s == s_inv) self.assertTrue(s.rms == s_inv.rms) self.assertTrue(s == s_inv.invert_phase()) - - s_inv_right = s.invert_phase(channels=(0,1)) + s_inv_right = s.invert_phase(channels=(0, 1)) left, right = s_inv_right.split_to_mono() self.assertFalse(s_mono == s_inv_right) @@ -795,7 +927,7 @@ self.assertTrue(left == s_mono) self.assertFalse(right == s_mono) - s_inv_left = s.invert_phase(channels=(1,0)) + s_inv_left = s.invert_phase(channels=(1, 0)) left, right = s_inv_left.split_to_mono() self.assertFalse(s_mono == s_inv_left) @@ -803,7 +935,6 @@ self.assertFalse(left == s_mono) self.assertTrue(right == s_mono) - def test_max_dBFS(self): sine_0_dbfs = Sine(1000).to_audio_segment() sine_minus_3_dbfs = Sine(1000).to_audio_segment(volume=-3.0) @@ -871,6 +1002,7 @@ tempfile.tempdir = orig_tmpdir os.rmdir(new_tmpdir) + class SilenceTests(unittest.TestCase): def setUp(self): @@ -1013,7 +1145,7 @@ self.assertRaises(OSError, func) def test_init_AudioSegment_data_buffer(self): - seg = AudioSegment(data = "\0" * 34, sample_width=2, frame_rate=4, channels=1) + seg = AudioSegment(data="\0" * 34, sample_width=2, frame_rate=4, channels=1) self.assertEqual(seg.duration_seconds, 4.25) @@ -1021,25 +1153,20 @@ self.assertEqual(seg.frame_rate, 4) - def test_init_AudioSegment_data_buffer_with_missing_args_fails(self): - - func = partial(AudioSegment, data = "\0" * 16, sample_width=2, frame_rate=2) + func = partial(AudioSegment, data="\0" * 16, sample_width=2, frame_rate=2) self.assertRaises(MissingAudioParameter, func) - func = partial(AudioSegment, data = "\0" * 16, sample_width=2, channels=1) + func = partial(AudioSegment, data="\0" * 16, sample_width=2, channels=1) self.assertRaises(MissingAudioParameter, func) - func = partial(AudioSegment, data = "\0" * 16, frame_rate=2, channels=1) + func = partial(AudioSegment, data="\0" * 16, frame_rate=2, channels=1) self.assertRaises(MissingAudioParameter, func) - def test_init_AudioSegment_data_buffer_with_bad_values_fails(self): - - func = partial(AudioSegment, data = "\0" * 14, sample_width=4, frame_rate=2, channels=1) + func = partial(AudioSegment, data="\0" * 14, sample_width=4, frame_rate=2, channels=1) self.assertRaises(ValueError, func) - def test_exporting(self): seg = AudioSegment.from_wav(self.wave_file) exported = AudioSegment.from_wav(seg.export(format="wav")) @@ -1084,8 +1211,6 @@ self.assertAlmostEqual(less_treble.dBFS, s.dBFS, places=0) - - if __name__ == "__main__": import sys