Chris@756: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@756: Chris@756: /* Chris@756: Sonic Visualiser Chris@756: An audio file viewer and annotation editor. Chris@756: Centre for Digital Music, Queen Mary, University of London. Chris@756: This file copyright 2013 Chris Cannam. Chris@756: Chris@756: This program is free software; you can redistribute it and/or Chris@756: modify it under the terms of the GNU General Public License as Chris@756: published by the Free Software Foundation; either version 2 of the Chris@756: License, or (at your option) any later version. See the file Chris@756: COPYING included with this distribution for more information. Chris@756: */ Chris@756: Chris@756: #ifndef TEST_AUDIO_FILE_READER_H Chris@756: #define TEST_AUDIO_FILE_READER_H Chris@756: Chris@756: #include "../AudioFileReaderFactory.h" Chris@756: #include "../AudioFileReader.h" Chris@1313: #include "../WavFileWriter.h" Chris@756: Chris@756: #include "AudioTestData.h" Chris@756: Chris@756: #include Chris@756: Chris@756: #include Chris@756: #include Chris@756: #include Chris@756: Chris@756: #include Chris@756: Chris@756: using namespace std; Chris@756: Chris@1263: static QString audioDir = "svcore/data/fileio/test/testfiles"; Chris@1313: static QString diffDir = "svcore/data/fileio/test/diffs"; Chris@756: Chris@756: class AudioFileReaderTest : public QObject Chris@756: { Chris@756: Q_OBJECT Chris@756: Chris@756: const char *strOf(QString s) { Chris@756: return strdup(s.toLocal8Bit().data()); Chris@756: } Chris@756: Chris@1313: void getFileMetadata(QString filename, Chris@1313: QString &extension, Chris@1313: sv_samplerate_t &rate, Chris@1313: int &channels, Chris@1313: int &bitdepth) { Chris@1313: Chris@1313: QStringList fileAndExt = filename.split("."); Chris@1313: QStringList bits = fileAndExt[0].split("-"); Chris@1313: Chris@1313: extension = fileAndExt[1]; Chris@1313: rate = bits[0].toInt(); Chris@1313: channels = bits[1].toInt(); Chris@1313: bitdepth = 16; Chris@1313: if (bits.length() > 2) { Chris@1313: bitdepth = bits[2].toInt(); Chris@1313: } Chris@1313: } Chris@1313: Chris@1313: void getExpectedThresholds(QString filename, Chris@1313: bool resampled, Chris@1313: bool gapless, Chris@1313: bool normalised, Chris@1313: double &maxLimit, Chris@1313: double &rmsLimit) { Chris@1313: Chris@1313: QString extension; Chris@1313: sv_samplerate_t fileRate; Chris@1313: int channels; Chris@1313: int bitdepth; Chris@1313: getFileMetadata(filename, extension, fileRate, channels, bitdepth); Chris@1313: Chris@1313: if (normalised) { Chris@1313: Chris@1313: if (extension == "ogg") { Chris@1313: Chris@1313: // Our ogg is not especially high quality and is Chris@1313: // actually further from the original if normalised Chris@1313: Chris@1313: maxLimit = 0.1; Chris@1313: rmsLimit = 0.03; Chris@1313: Chris@1313: } else if (extension == "m4a" || extension == "aac") { Chris@1313: cannam@1314: // Like ogg but more so, quite far off in signal terms cannam@1314: // and even worse if normalised cannam@1314: maxLimit = 0.1; cannam@1314: rmsLimit = 0.1; Chris@1313: Chris@1313: } else if (extension == "mp3") { Chris@1313: Chris@1313: if (resampled && !gapless) { Chris@1313: Chris@1313: // We expect worse figures here, because the Chris@1313: // combination of uncompensated encoder delay + Chris@1313: // resampling results in a fractional delay which Chris@1313: // means the decoded signal is slightly out of Chris@1313: // phase compared to the test signal Chris@1313: Chris@1313: maxLimit = 0.1; Chris@1313: rmsLimit = 0.05; Chris@1313: Chris@1313: } else { Chris@1313: Chris@1313: maxLimit = 0.05; Chris@1313: rmsLimit = 0.01; Chris@1313: } Chris@1313: Chris@1313: } else { Chris@1313: Chris@1313: // supposed to be lossless then (wav, aiff, flac) Chris@1313: Chris@1313: if (bitdepth >= 16 && !resampled) { Chris@1313: maxLimit = 1e-3; Chris@1313: rmsLimit = 3e-4; Chris@1313: } else { Chris@1313: maxLimit = 0.01; Chris@1313: rmsLimit = 5e-3; Chris@1313: } Chris@1313: } Chris@1313: Chris@1313: } else { // !normalised Chris@1313: Chris@1313: if (extension == "ogg") { Chris@1313: Chris@1313: maxLimit = 0.06; Chris@1313: rmsLimit = 0.03; Chris@1313: Chris@1313: } else if (extension == "m4a" || extension == "aac") { Chris@1313: cannam@1314: maxLimit = 0.06; cannam@1314: rmsLimit = 0.03; Chris@1313: Chris@1313: } else if (extension == "mp3") { Chris@1313: Chris@1313: // all mp3 figures are worse when not normalising Chris@1313: maxLimit = 0.1; Chris@1313: rmsLimit = 0.05; Chris@1313: Chris@1313: } else { Chris@1313: Chris@1313: // supposed to be lossless then (wav, aiff, flac) Chris@1313: Chris@1313: if (bitdepth >= 16 && !resampled) { Chris@1313: maxLimit = 1e-3; Chris@1313: rmsLimit = 3e-4; Chris@1313: } else { Chris@1313: maxLimit = 0.02; Chris@1313: rmsLimit = 0.01; Chris@1313: } Chris@1313: } Chris@1313: } Chris@1313: } Chris@1313: Chris@1313: QString testName(QString filename, int rate, bool norm, bool gapless) { Chris@1313: return QString("%1 at %2%3%4") Chris@1313: .arg(filename) Chris@1313: .arg(rate) Chris@1313: .arg(norm ? " normalised": "") Chris@1313: .arg(gapless ? "" : " non-gapless"); Chris@1313: } Chris@1313: Chris@756: private slots: Chris@756: void init() Chris@756: { Chris@756: if (!QDir(audioDir).exists()) { Chris@756: cerr << "ERROR: Audio test file directory \"" << audioDir << "\" does not exist" << endl; Chris@756: QVERIFY2(QDir(audioDir).exists(), "Audio test file directory not found"); Chris@756: } Chris@1313: if (!QDir(diffDir).exists() && !QDir().mkpath(diffDir)) { Chris@1313: cerr << "ERROR: Audio diff directory \"" << diffDir << "\" does not exist and could not be created" << endl; Chris@1313: QVERIFY2(QDir(diffDir).exists(), "Audio diff directory not found and could not be created"); Chris@1313: } Chris@756: } Chris@756: Chris@756: void read_data() Chris@756: { Chris@756: QTest::addColumn("audiofile"); Chris@1313: QTest::addColumn("rate"); Chris@1313: QTest::addColumn("normalised"); Chris@1313: QTest::addColumn("gapless"); Chris@756: QStringList files = QDir(audioDir).entryList(QDir::Files); Chris@1313: int readRates[] = { 44100, 48000 }; Chris@1313: bool norms[] = { false, true }; Chris@1313: bool gaplesses[] = { true, false }; Chris@756: foreach (QString filename, files) { Chris@1313: for (int rate: readRates) { Chris@1313: for (bool norm: norms) { Chris@1313: for (bool gapless: gaplesses) { Chris@1313: Chris@1313: if (QFileInfo(filename).suffix() != "mp3" && Chris@1313: !gapless) { Chris@1313: continue; Chris@1313: } Chris@1313: Chris@1313: QString desc = testName(filename, rate, norm, gapless); Chris@1313: Chris@1313: QTest::newRow(strOf(desc)) Chris@1313: << filename << rate << norm << gapless; Chris@1313: } Chris@1313: } Chris@1313: } Chris@756: } Chris@756: } Chris@756: Chris@756: void read() Chris@756: { Chris@756: QFETCH(QString, audiofile); Chris@1313: QFETCH(int, rate); Chris@1313: QFETCH(bool, normalised); Chris@1313: QFETCH(bool, gapless); Chris@756: Chris@1313: sv_samplerate_t readRate(rate); Chris@1313: Chris@1313: cerr << "\naudiofile = " << audiofile << endl; Chris@1313: Chris@1313: AudioFileReaderFactory::Parameters params; Chris@1313: params.targetRate = readRate; Chris@1313: params.normalisation = (normalised ? Chris@1313: AudioFileReaderFactory::Normalisation::Peak : Chris@1313: AudioFileReaderFactory::Normalisation::None); Chris@1313: params.gaplessMode = (gapless ? Chris@1313: AudioFileReaderFactory::GaplessMode::Gapless : Chris@1313: AudioFileReaderFactory::GaplessMode::Gappy); Chris@757: Chris@756: AudioFileReader *reader = Chris@756: AudioFileReaderFactory::createReader Chris@1313: (audioDir + "/" + audiofile, params); Chris@1313: Chris@756: if (!reader) { Chris@820: #if ( QT_VERSION >= 0x050000 ) Chris@763: QSKIP("Unsupported file, skipping"); Chris@820: #else Chris@820: QSKIP("Unsupported file, skipping", SkipSingle); Chris@820: #endif Chris@756: } Chris@756: Chris@1313: QString extension; Chris@1313: sv_samplerate_t fileRate; Chris@1313: int channels; Chris@1313: int fileBitdepth; Chris@1313: getFileMetadata(audiofile, extension, fileRate, channels, fileBitdepth); Chris@1313: Chris@1313: QString diffFile = testName(audiofile, rate, normalised, gapless); Chris@1313: diffFile.replace(".", "_"); Chris@1313: diffFile.replace(" ", "_"); Chris@1313: diffFile += ".wav"; Chris@1313: diffFile = QDir(diffDir).filePath(diffFile); Chris@1313: WavFileWriter diffWriter(diffFile, readRate, channels, Chris@1313: WavFileWriter::WriteToTarget); //!!! NB WriteToTemporary not working, why? Chris@1313: QVERIFY(diffWriter.isOK()); Chris@1313: Chris@1313: QCOMPARE((int)reader->getChannelCount(), channels); Chris@1313: QCOMPARE(reader->getNativeRate(), fileRate); Chris@1040: QCOMPARE(reader->getSampleRate(), readRate); Chris@757: Chris@757: AudioTestData tdata(readRate, channels); Chris@756: Chris@756: float *reference = tdata.getInterleavedData(); Chris@1040: sv_frame_t refFrames = tdata.getFrameCount(); Chris@756: Chris@756: // The reader should give us exactly the expected number of Chris@759: // frames, except for mp3/aac files. We ask for quite a lot Chris@759: // more, though, so we can (a) check that we only get the Chris@759: // expected number back (if this is not mp3/aac) or (b) take Chris@759: // into account silence at beginning and end (if it is). Chris@1041: vector test = reader->getInterleavedFrames(0, refFrames + 5000); Chris@1040: sv_frame_t read = test.size() / channels; Chris@756: Chris@1313: bool perceptual = (extension == "mp3" || Chris@1313: extension == "aac" || Chris@1313: extension == "m4a"); Chris@1313: Chris@1313: if (perceptual && !gapless) { Chris@1313: // allow silence at start and end Chris@759: QVERIFY(read >= refFrames); Chris@757: } else { Chris@759: QCOMPARE(read, refFrames); Chris@757: } Chris@757: Chris@1313: bool resampled = readRate != fileRate; Chris@1313: double maxLimit, rmsLimit; Chris@1313: getExpectedThresholds(audiofile, Chris@1313: resampled, Chris@1313: gapless, Chris@1313: normalised, Chris@1313: maxLimit, rmsLimit); Chris@1313: Chris@1313: double edgeLimit = maxLimit * 3; // in first or final edgeSize frames Chris@1313: if (resampled && edgeLimit < 0.1) edgeLimit = 0.1; Chris@759: int edgeSize = 100; Chris@759: Chris@759: // And we ignore completely the last few frames when upsampling Chris@1313: int discard = 1 + int(round(readRate / fileRate)); Chris@759: Chris@759: int offset = 0; Chris@759: Chris@1313: if (perceptual) { Chris@759: cannam@1314: // Look for an initial offset. cannam@1314: // cannam@1314: // We know the first channel has a sinusoid in it. It cannam@1314: // should have a peak at 0.4ms (see AudioTestData.h) but cannam@1314: // that might have been clipped, which would make it cannam@1314: // imprecise. We can tell if it's clipped, though, as cannam@1314: // there will be samples having exactly identical cannam@1314: // values. So what we look for is the peak if it's not cannam@1314: // clipped and, if it is, the first zero crossing after cannam@1314: // the peak, which should be at 0.8ms. cannam@1314: Chris@1296: int expectedPeak = int(0.0004 * readRate); cannam@1314: int expectedZC = int(0.0008 * readRate); cannam@1314: bool foundPeak = false; cannam@1314: for (int i = 1; i+1 < read; ++i) { cannam@1314: float prevSample = test[(i-1) * channels]; cannam@1314: float thisSample = test[i * channels]; cannam@1314: float nextSample = test[(i+1) * channels]; cannam@1314: if (thisSample > 0.8 && nextSample < thisSample) { cannam@1314: foundPeak = true; cannam@1314: if (thisSample > prevSample) { cannam@1314: // not clipped cannam@1314: offset = i - expectedPeak - 1; cannam@1314: break; cannam@1314: } cannam@1314: } cannam@1314: if (foundPeak && (thisSample >= 0.0 && nextSample < 0.0)) { cannam@1314: cerr << "thisSample = " << thisSample << ", nextSample = " cannam@1314: << nextSample << endl; cannam@1314: offset = i - expectedZC - 1; Chris@759: break; Chris@759: } Chris@759: } Chris@1313: cannam@1314: int fileRateEquivalent = int((offset / readRate) * fileRate); cannam@1314: Chris@1313: std::cerr << "offset = " << offset << std::endl; cannam@1314: std::cerr << "at file rate would be " << fileRateEquivalent << std::endl; Chris@1313: Chris@1313: // Previously our m4a test file had a fixed offset of 1024 Chris@1313: // at the file sample rate -- this may be because it was Chris@1313: // produced by FAAC which did not write in the delay as Chris@1313: // metadata? We now have an m4a produced by Core Audio Chris@1313: // which gives a 0 offset. What to do... Chris@1313: Chris@1313: // Anyway, mp3s should have 0 offset in gapless mode and Chris@1313: // "something else" otherwise. Chris@1313: Chris@1313: if (gapless) { Chris@1313: QCOMPARE(offset, 0); Chris@1313: } Chris@759: } Chris@756: Chris@1313: vector> diffs(channels); Chris@1313: Chris@756: for (int c = 0; c < channels; ++c) { Chris@1313: Chris@1313: double maxDiff = 0.0; Chris@1313: double totalDiff = 0.0; Chris@1313: double totalSqrDiff = 0.0; Chris@1313: int maxIndex = 0; Chris@1313: Chris@1296: for (int i = 0; i < refFrames; ++i) { Chris@1296: int ix = i + offset; Chris@1296: if (ix >= read) { cannam@1308: cerr << "ERROR: audiofile " << audiofile << " reads truncated (read-rate reference frames " << i << " onward, of " << refFrames << ", are lost)" << endl; Chris@1296: QVERIFY(ix < read); Chris@1296: } Chris@1313: Chris@1313: float signeddiff = Chris@1313: test[ix * channels + c] - Chris@1313: reference[i * channels + c]; Chris@1313: Chris@1313: diffs[c].push_back(signeddiff); Chris@1313: Chris@1296: if (ix + discard >= read) { Chris@1296: // we forgive the very edge samples when Chris@1296: // resampling (discard > 0) Chris@1296: continue; Chris@1296: } Chris@1313: Chris@1313: double diff = fabs(signeddiff); Chris@1313: Chris@1313: totalDiff += diff; Chris@1313: totalSqrDiff += diff * diff; Chris@1313: Chris@757: // in edge areas, record this only if it exceeds edgeLimit Chris@1313: if (i < edgeSize || i + edgeSize >= refFrames) { Chris@1313: if (diff > edgeLimit && diff > maxDiff) { Chris@1313: maxDiff = diff; Chris@1313: maxIndex = i; Chris@757: } Chris@757: } else { Chris@1313: if (diff > maxDiff) { Chris@1313: maxDiff = diff; Chris@1313: maxIndex = i; Chris@757: } Chris@756: } Chris@756: } Chris@1313: Chris@1313: double meanDiff = totalDiff / double(refFrames); Chris@1313: double rmsDiff = sqrt(totalSqrDiff / double(refFrames)); cannam@1308: cannam@1314: /* Chris@1313: cerr << "channel " << c << ": mean diff " << meanDiff << endl; Chris@1313: cerr << "channel " << c << ": rms diff " << rmsDiff << endl; Chris@1313: cerr << "channel " << c << ": max diff " << maxDiff << " at " << maxIndex << endl; cannam@1314: */ Chris@1313: if (rmsDiff >= rmsLimit) { Chris@1313: cerr << "ERROR: for audiofile " << audiofile << ": RMS diff = " << rmsDiff << " for channel " << c << " (limit = " << rmsLimit << ")" << endl; Chris@1313: QVERIFY(rmsDiff < rmsLimit); Chris@1313: } Chris@1313: if (maxDiff >= maxLimit) { Chris@1313: cerr << "ERROR: for audiofile " << audiofile << ": max diff = " << maxDiff << " at frame " << maxIndex << " of " << read << " on channel " << c << " (limit = " << maxLimit << ", edge limit = " << edgeLimit << ", mean diff = " << meanDiff << ", rms = " << rmsDiff << ")" << endl; Chris@1313: QVERIFY(maxDiff < maxLimit); Chris@1313: } Chris@1313: Chris@1313: // and check for spurious material at end Chris@1313: Chris@1309: for (sv_frame_t i = refFrames; i + offset < read; ++i) { Chris@1309: sv_frame_t ix = i + offset; Chris@1313: float quiet = 0.1; //!!! allow some ringing - but let's come back to this, it should tail off cannam@1308: float mag = fabsf(test[ix * channels + c]); cannam@1308: if (mag > quiet) { Chris@1313: cerr << "ERROR: audiofile " << audiofile << " contains spurious data after end of reference (found sample " << test[ix * channels + c] << " at index " << ix << " of channel " << c << " after reference+offset ended at " << refFrames+offset << ")" << endl; cannam@1308: QVERIFY(mag < quiet); cannam@1308: } cannam@1308: } Chris@756: } Chris@1313: Chris@1313: float **ptrs = new float*[channels]; Chris@1313: for (int c = 0; c < channels; ++c) { Chris@1313: ptrs[c] = diffs[c].data(); Chris@1313: } Chris@1313: diffWriter.writeSamples(ptrs, refFrames); Chris@1313: delete[] ptrs; Chris@756: } Chris@756: }; Chris@756: Chris@756: #endif