# Copyright (C) 2015 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import StringIO import struct import sys import tempfile import wave class WaveDiff(object): _paramNames = ('Number of channels', 'Sample width', 'Sample rate', 'Number of frames', 'Compression type', 'Compression name') # Audio effect processing is intrinsically imprecise, so we need to always allow tolerance. _tolerance = 1 def __init__(self, in1, in2): if isinstance(in1, file): waveFile1 = wave.open(in1, 'rb') else: waveFile1 = wave.open(StringIO.StringIO(in1), 'rb') if isinstance(in2, file): waveFile1 = wave.open(in2, 'rb') else: waveFile2 = wave.open(StringIO.StringIO(in2), 'rb') params1 = waveFile1.getparams() params2 = waveFile2.getparams() self._diff = '' self._filesAreIdentical = not sum(map(self._diffParam, params1, params2, self._paramNames)) self._filesAreIdenticalWithinTolerance = self._filesAreIdentical if not self._filesAreIdentical: return # Metadata is identical, compare the content now. channelCount1 = waveFile1.getnchannels() frameCount1 = waveFile1.getnframes() sampleWidth1 = waveFile1.getsampwidth() channelCount2 = waveFile2.getnchannels() frameCount2 = waveFile2.getnframes() sampleWidth2 = waveFile2.getsampwidth() allData1 = self._readSamples(waveFile1, sampleWidth1, frameCount1 * channelCount1) allData2 = self._readSamples(waveFile2, sampleWidth2, frameCount2 * channelCount2) results = map(self._diffSample, allData1, allData2, xrange(max(frameCount1 * channelCount1, frameCount2 * channelCount2))) cumulativeSampleDiff = sum(results) differingSampleCount = len(filter(bool, results)) self._filesAreIdentical = not differingSampleCount self._filesAreIdenticalWithinTolerance = not len(filter(lambda x: x > self._tolerance, results)) if differingSampleCount: self._diff += '\n' self._diff += 'Total differing samples: %d\n' % differingSampleCount self._diff += 'Percentage of differing samples: %0.3f%%\n' % (100 * float(differingSampleCount) / max(frameCount1, frameCount2)) self._diff += 'Cumulative sample difference: %d\n' % cumulativeSampleDiff self._diff += 'Average sample difference: %f\n' % (float(cumulativeSampleDiff) / differingSampleCount) def _diffParam(self, param1, param2, paramName): if param1 == param2: return False self._diff += paramName + '\n' self._diff += '< %s\n' % str(param1) self._diff += '---\n' self._diff += '> %s\n' % str(param2) return True @staticmethod def _readSamples(file, sampleWidth, nSamples): allFrames = file.readframes(nSamples) unpackFormat = 'b' if sampleWidth == 1 else 'h' return struct.unpack('<%d%s' % (nSamples, unpackFormat), allFrames) def _diffSample(self, data1, data2, i): if (data1 != data2): self._diff += 'Sample #%d\n' % i self._diff += '< %d\n' % data1 self._diff += '---\n' self._diff += '> %d\n' % data2 return abs(data1 - data2) def filesAreIdentical(self): return self._filesAreIdentical def filesAreIdenticalWithinTolerance(self): return self._filesAreIdenticalWithinTolerance def diffText(self): return self._diff