# 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 struct import wave from webkitcorepy import BytesIO, StringIO 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, str): waveFile1 = wave.open(StringIO(in1), 'r') else: waveFile1 = wave.open(BytesIO(in1), 'rb') if isinstance(in2, str): waveFile2 = wave.open(StringIO(in2), 'r') else: waveFile2 = wave.open(BytesIO(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 = list(map(self._diffSample, allData1, allData2, range(max(frameCount1 * channelCount1, frameCount2 * channelCount2)))) cumulativeSampleDiff = sum(results) differingSampleCount = len(list(filter(bool, results))) self._filesAreIdentical = not differingSampleCount self._filesAreIdenticalWithinTolerance = not len(list(filter(lambda x: x > self._tolerance, results))) if differingSampleCount: self._diff.append('') self._diff.append('Total differing samples: %d' % differingSampleCount) self._diff.append('Percentage of differing samples: %0.3f%%' % (100 * float(differingSampleCount) / max(frameCount1, frameCount2))) self._diff.append('Cumulative sample difference: %d' % cumulativeSampleDiff) self._diff.append('Average sample difference: %f' % (float(cumulativeSampleDiff) / differingSampleCount)) def _diffParam(self, param1, param2, paramName): if param1 == param2: return False self._diff.append(paramName) self._diff.append('< %s' % str(param1)) self._diff.append('---') self._diff.append('> %s' % 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.append('Sample #%d' % i) self._diff.append('< %d' % data1) self._diff.append('---') self._diff.append('> %d' % data2) return abs(data1 - data2) def filesAreIdentical(self): return self._filesAreIdentical def filesAreIdenticalWithinTolerance(self): return self._filesAreIdenticalWithinTolerance def diffText(self): return '\n'.join(self._diff)