blob: fb0b7d108efae57ceb7259ca34a2160e2114dfd1 [file] [log] [blame]
# 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)