annotate data/fileio/test/AudioFileReaderTest.h @ 1313:ff9697592bef 3.0-integration

Add gapless preference to prefs dialog; much work on audio read tests
author Chris Cannam
date Thu, 01 Dec 2016 17:45:40 +0000
parents 2e7fcdd5f627
children 00cae2d5ee7e
rev   line source
Chris@756 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@756 2
Chris@756 3 /*
Chris@756 4 Sonic Visualiser
Chris@756 5 An audio file viewer and annotation editor.
Chris@756 6 Centre for Digital Music, Queen Mary, University of London.
Chris@756 7 This file copyright 2013 Chris Cannam.
Chris@756 8
Chris@756 9 This program is free software; you can redistribute it and/or
Chris@756 10 modify it under the terms of the GNU General Public License as
Chris@756 11 published by the Free Software Foundation; either version 2 of the
Chris@756 12 License, or (at your option) any later version. See the file
Chris@756 13 COPYING included with this distribution for more information.
Chris@756 14 */
Chris@756 15
Chris@756 16 #ifndef TEST_AUDIO_FILE_READER_H
Chris@756 17 #define TEST_AUDIO_FILE_READER_H
Chris@756 18
Chris@756 19 #include "../AudioFileReaderFactory.h"
Chris@756 20 #include "../AudioFileReader.h"
Chris@1313 21 #include "../WavFileWriter.h"
Chris@756 22
Chris@756 23 #include "AudioTestData.h"
Chris@756 24
Chris@756 25 #include <cmath>
Chris@756 26
Chris@756 27 #include <QObject>
Chris@756 28 #include <QtTest>
Chris@756 29 #include <QDir>
Chris@756 30
Chris@756 31 #include <iostream>
Chris@756 32
Chris@756 33 using namespace std;
Chris@756 34
Chris@1263 35 static QString audioDir = "svcore/data/fileio/test/testfiles";
Chris@1313 36 static QString diffDir = "svcore/data/fileio/test/diffs";
Chris@756 37
Chris@756 38 class AudioFileReaderTest : public QObject
Chris@756 39 {
Chris@756 40 Q_OBJECT
Chris@756 41
Chris@756 42 const char *strOf(QString s) {
Chris@756 43 return strdup(s.toLocal8Bit().data());
Chris@756 44 }
Chris@756 45
Chris@1313 46 void getFileMetadata(QString filename,
Chris@1313 47 QString &extension,
Chris@1313 48 sv_samplerate_t &rate,
Chris@1313 49 int &channels,
Chris@1313 50 int &bitdepth) {
Chris@1313 51
Chris@1313 52 QStringList fileAndExt = filename.split(".");
Chris@1313 53 QStringList bits = fileAndExt[0].split("-");
Chris@1313 54
Chris@1313 55 extension = fileAndExt[1];
Chris@1313 56 rate = bits[0].toInt();
Chris@1313 57 channels = bits[1].toInt();
Chris@1313 58 bitdepth = 16;
Chris@1313 59 if (bits.length() > 2) {
Chris@1313 60 bitdepth = bits[2].toInt();
Chris@1313 61 }
Chris@1313 62 }
Chris@1313 63
Chris@1313 64 void getExpectedThresholds(QString filename,
Chris@1313 65 bool resampled,
Chris@1313 66 bool gapless,
Chris@1313 67 bool normalised,
Chris@1313 68 double &maxLimit,
Chris@1313 69 double &rmsLimit) {
Chris@1313 70
Chris@1313 71 QString extension;
Chris@1313 72 sv_samplerate_t fileRate;
Chris@1313 73 int channels;
Chris@1313 74 int bitdepth;
Chris@1313 75 getFileMetadata(filename, extension, fileRate, channels, bitdepth);
Chris@1313 76
Chris@1313 77 if (normalised) {
Chris@1313 78
Chris@1313 79 if (extension == "ogg") {
Chris@1313 80
Chris@1313 81 // Our ogg is not especially high quality and is
Chris@1313 82 // actually further from the original if normalised
Chris@1313 83
Chris@1313 84 maxLimit = 0.1;
Chris@1313 85 rmsLimit = 0.03;
Chris@1313 86
Chris@1313 87 } else if (extension == "m4a" || extension == "aac") {
Chris@1313 88
Chris@1313 89 //!!! to be worked out
Chris@1313 90 maxLimit = 1e-10;
Chris@1313 91 rmsLimit = 1e-10;
Chris@1313 92
Chris@1313 93 } else if (extension == "mp3") {
Chris@1313 94
Chris@1313 95 if (resampled && !gapless) {
Chris@1313 96
Chris@1313 97 // We expect worse figures here, because the
Chris@1313 98 // combination of uncompensated encoder delay +
Chris@1313 99 // resampling results in a fractional delay which
Chris@1313 100 // means the decoded signal is slightly out of
Chris@1313 101 // phase compared to the test signal
Chris@1313 102
Chris@1313 103 maxLimit = 0.1;
Chris@1313 104 rmsLimit = 0.05;
Chris@1313 105
Chris@1313 106 } else {
Chris@1313 107
Chris@1313 108 maxLimit = 0.05;
Chris@1313 109 rmsLimit = 0.01;
Chris@1313 110 }
Chris@1313 111
Chris@1313 112 } else {
Chris@1313 113
Chris@1313 114 // supposed to be lossless then (wav, aiff, flac)
Chris@1313 115
Chris@1313 116 if (bitdepth >= 16 && !resampled) {
Chris@1313 117 maxLimit = 1e-3;
Chris@1313 118 rmsLimit = 3e-4;
Chris@1313 119 } else {
Chris@1313 120 maxLimit = 0.01;
Chris@1313 121 rmsLimit = 5e-3;
Chris@1313 122 }
Chris@1313 123 }
Chris@1313 124
Chris@1313 125 } else { // !normalised
Chris@1313 126
Chris@1313 127 if (extension == "ogg") {
Chris@1313 128
Chris@1313 129 maxLimit = 0.06;
Chris@1313 130 rmsLimit = 0.03;
Chris@1313 131
Chris@1313 132 } else if (extension == "m4a" || extension == "aac") {
Chris@1313 133
Chris@1313 134 //!!! to be worked out
Chris@1313 135 maxLimit = 1e-10;
Chris@1313 136 rmsLimit = 1e-10;
Chris@1313 137
Chris@1313 138 } else if (extension == "mp3") {
Chris@1313 139
Chris@1313 140 // all mp3 figures are worse when not normalising
Chris@1313 141 maxLimit = 0.1;
Chris@1313 142 rmsLimit = 0.05;
Chris@1313 143
Chris@1313 144 } else {
Chris@1313 145
Chris@1313 146 // supposed to be lossless then (wav, aiff, flac)
Chris@1313 147
Chris@1313 148 if (bitdepth >= 16 && !resampled) {
Chris@1313 149 maxLimit = 1e-3;
Chris@1313 150 rmsLimit = 3e-4;
Chris@1313 151 } else {
Chris@1313 152 maxLimit = 0.02;
Chris@1313 153 rmsLimit = 0.01;
Chris@1313 154 }
Chris@1313 155 }
Chris@1313 156 }
Chris@1313 157 }
Chris@1313 158
Chris@1313 159 QString testName(QString filename, int rate, bool norm, bool gapless) {
Chris@1313 160 return QString("%1 at %2%3%4")
Chris@1313 161 .arg(filename)
Chris@1313 162 .arg(rate)
Chris@1313 163 .arg(norm ? " normalised": "")
Chris@1313 164 .arg(gapless ? "" : " non-gapless");
Chris@1313 165 }
Chris@1313 166
Chris@756 167 private slots:
Chris@756 168 void init()
Chris@756 169 {
Chris@756 170 if (!QDir(audioDir).exists()) {
Chris@756 171 cerr << "ERROR: Audio test file directory \"" << audioDir << "\" does not exist" << endl;
Chris@756 172 QVERIFY2(QDir(audioDir).exists(), "Audio test file directory not found");
Chris@756 173 }
Chris@1313 174 if (!QDir(diffDir).exists() && !QDir().mkpath(diffDir)) {
Chris@1313 175 cerr << "ERROR: Audio diff directory \"" << diffDir << "\" does not exist and could not be created" << endl;
Chris@1313 176 QVERIFY2(QDir(diffDir).exists(), "Audio diff directory not found and could not be created");
Chris@1313 177 }
Chris@756 178 }
Chris@756 179
Chris@756 180 void read_data()
Chris@756 181 {
Chris@756 182 QTest::addColumn<QString>("audiofile");
Chris@1313 183 QTest::addColumn<int>("rate");
Chris@1313 184 QTest::addColumn<bool>("normalised");
Chris@1313 185 QTest::addColumn<bool>("gapless");
Chris@756 186 QStringList files = QDir(audioDir).entryList(QDir::Files);
Chris@1313 187 int readRates[] = { 44100, 48000 };
Chris@1313 188 bool norms[] = { false, true };
Chris@1313 189 bool gaplesses[] = { true, false };
Chris@756 190 foreach (QString filename, files) {
Chris@1313 191 for (int rate: readRates) {
Chris@1313 192 for (bool norm: norms) {
Chris@1313 193 for (bool gapless: gaplesses) {
Chris@1313 194
Chris@1313 195 if (QFileInfo(filename).suffix() != "mp3" &&
Chris@1313 196 !gapless) {
Chris@1313 197 continue;
Chris@1313 198 }
Chris@1313 199
Chris@1313 200 QString desc = testName(filename, rate, norm, gapless);
Chris@1313 201
Chris@1313 202 QTest::newRow(strOf(desc))
Chris@1313 203 << filename << rate << norm << gapless;
Chris@1313 204 }
Chris@1313 205 }
Chris@1313 206 }
Chris@756 207 }
Chris@756 208 }
Chris@756 209
Chris@756 210 void read()
Chris@756 211 {
Chris@756 212 QFETCH(QString, audiofile);
Chris@1313 213 QFETCH(int, rate);
Chris@1313 214 QFETCH(bool, normalised);
Chris@1313 215 QFETCH(bool, gapless);
Chris@756 216
Chris@1313 217 sv_samplerate_t readRate(rate);
Chris@1313 218
Chris@1313 219 cerr << "\naudiofile = " << audiofile << endl;
Chris@1313 220
Chris@1313 221 AudioFileReaderFactory::Parameters params;
Chris@1313 222 params.targetRate = readRate;
Chris@1313 223 params.normalisation = (normalised ?
Chris@1313 224 AudioFileReaderFactory::Normalisation::Peak :
Chris@1313 225 AudioFileReaderFactory::Normalisation::None);
Chris@1313 226 params.gaplessMode = (gapless ?
Chris@1313 227 AudioFileReaderFactory::GaplessMode::Gapless :
Chris@1313 228 AudioFileReaderFactory::GaplessMode::Gappy);
Chris@757 229
Chris@756 230 AudioFileReader *reader =
Chris@756 231 AudioFileReaderFactory::createReader
Chris@1313 232 (audioDir + "/" + audiofile, params);
Chris@1313 233
Chris@756 234 if (!reader) {
Chris@820 235 #if ( QT_VERSION >= 0x050000 )
Chris@763 236 QSKIP("Unsupported file, skipping");
Chris@820 237 #else
Chris@820 238 QSKIP("Unsupported file, skipping", SkipSingle);
Chris@820 239 #endif
Chris@756 240 }
Chris@756 241
Chris@1313 242 QString extension;
Chris@1313 243 sv_samplerate_t fileRate;
Chris@1313 244 int channels;
Chris@1313 245 int fileBitdepth;
Chris@1313 246 getFileMetadata(audiofile, extension, fileRate, channels, fileBitdepth);
Chris@1313 247
Chris@1313 248 QString diffFile = testName(audiofile, rate, normalised, gapless);
Chris@1313 249 diffFile.replace(".", "_");
Chris@1313 250 diffFile.replace(" ", "_");
Chris@1313 251 diffFile += ".wav";
Chris@1313 252 diffFile = QDir(diffDir).filePath(diffFile);
Chris@1313 253 WavFileWriter diffWriter(diffFile, readRate, channels,
Chris@1313 254 WavFileWriter::WriteToTarget); //!!! NB WriteToTemporary not working, why?
Chris@1313 255 QVERIFY(diffWriter.isOK());
Chris@1313 256
Chris@1313 257 QCOMPARE((int)reader->getChannelCount(), channels);
Chris@1313 258 QCOMPARE(reader->getNativeRate(), fileRate);
Chris@1040 259 QCOMPARE(reader->getSampleRate(), readRate);
Chris@757 260
Chris@757 261 AudioTestData tdata(readRate, channels);
Chris@756 262
Chris@756 263 float *reference = tdata.getInterleavedData();
Chris@1040 264 sv_frame_t refFrames = tdata.getFrameCount();
Chris@756 265
Chris@756 266 // The reader should give us exactly the expected number of
Chris@759 267 // frames, except for mp3/aac files. We ask for quite a lot
Chris@759 268 // more, though, so we can (a) check that we only get the
Chris@759 269 // expected number back (if this is not mp3/aac) or (b) take
Chris@759 270 // into account silence at beginning and end (if it is).
Chris@1041 271 vector<float> test = reader->getInterleavedFrames(0, refFrames + 5000);
Chris@1040 272 sv_frame_t read = test.size() / channels;
Chris@756 273
Chris@1313 274 bool perceptual = (extension == "mp3" ||
Chris@1313 275 extension == "aac" ||
Chris@1313 276 extension == "m4a");
Chris@1313 277
Chris@1313 278 if (perceptual && !gapless) {
Chris@1313 279 // allow silence at start and end
Chris@759 280 QVERIFY(read >= refFrames);
Chris@757 281 } else {
Chris@759 282 QCOMPARE(read, refFrames);
Chris@757 283 }
Chris@757 284
Chris@1313 285 bool resampled = readRate != fileRate;
Chris@1313 286 double maxLimit, rmsLimit;
Chris@1313 287 getExpectedThresholds(audiofile,
Chris@1313 288 resampled,
Chris@1313 289 gapless,
Chris@1313 290 normalised,
Chris@1313 291 maxLimit, rmsLimit);
Chris@1313 292
Chris@1313 293 double edgeLimit = maxLimit * 3; // in first or final edgeSize frames
Chris@1313 294 if (resampled && edgeLimit < 0.1) edgeLimit = 0.1;
Chris@759 295 int edgeSize = 100;
Chris@759 296
Chris@759 297 // And we ignore completely the last few frames when upsampling
Chris@1313 298 int discard = 1 + int(round(readRate / fileRate));
Chris@759 299
Chris@759 300 int offset = 0;
Chris@759 301
Chris@1313 302 if (perceptual) {
Chris@759 303
Chris@1313 304 // Look for an initial offset. What we're looking for is
Chris@1296 305 // the first peak of the sinusoid in the first channel
Chris@1296 306 // (since we may have only the one channel). This should
Chris@1313 307 // appear at 0.4ms (see AudioTestData.h).
Chris@1313 308
Chris@1296 309 int expectedPeak = int(0.0004 * readRate);
Chris@1296 310 for (int i = 1; i < read; ++i) {
Chris@1296 311 if (test[i * channels] > 0.8 &&
Chris@1296 312 test[(i+1) * channels] < test[i * channels]) {
Chris@1296 313 offset = i - expectedPeak - 1;
Chris@759 314 break;
Chris@759 315 }
Chris@759 316 }
Chris@1313 317
Chris@1313 318 std::cerr << "offset = " << offset << std::endl;
Chris@1313 319 std::cerr << "at file rate would be " << (offset / readRate) * fileRate << std::endl;
Chris@1313 320
Chris@1313 321 // Previously our m4a test file had a fixed offset of 1024
Chris@1313 322 // at the file sample rate -- this may be because it was
Chris@1313 323 // produced by FAAC which did not write in the delay as
Chris@1313 324 // metadata? We now have an m4a produced by Core Audio
Chris@1313 325 // which gives a 0 offset. What to do...
Chris@1313 326
Chris@1313 327 // Anyway, mp3s should have 0 offset in gapless mode and
Chris@1313 328 // "something else" otherwise.
Chris@1313 329
Chris@1313 330 if (gapless) {
Chris@1313 331 QCOMPARE(offset, 0);
Chris@1313 332 }
Chris@759 333 }
Chris@756 334
Chris@1313 335 vector<vector<float>> diffs(channels);
Chris@1313 336
Chris@756 337 for (int c = 0; c < channels; ++c) {
Chris@1313 338
Chris@1313 339 double maxDiff = 0.0;
Chris@1313 340 double totalDiff = 0.0;
Chris@1313 341 double totalSqrDiff = 0.0;
Chris@1313 342 int maxIndex = 0;
Chris@1313 343
Chris@1313 344 // cerr << "\nchannel " << c << ": ";
cannam@1308 345
Chris@1296 346 for (int i = 0; i < refFrames; ++i) {
Chris@1296 347 int ix = i + offset;
Chris@1296 348 if (ix >= read) {
cannam@1308 349 cerr << "ERROR: audiofile " << audiofile << " reads truncated (read-rate reference frames " << i << " onward, of " << refFrames << ", are lost)" << endl;
Chris@1296 350 QVERIFY(ix < read);
Chris@1296 351 }
Chris@1313 352
Chris@1313 353 float signeddiff =
Chris@1313 354 test[ix * channels + c] -
Chris@1313 355 reference[i * channels + c];
Chris@1313 356
Chris@1313 357 diffs[c].push_back(signeddiff);
Chris@1313 358
Chris@1296 359 if (ix + discard >= read) {
Chris@1296 360 // we forgive the very edge samples when
Chris@1296 361 // resampling (discard > 0)
Chris@1296 362 continue;
Chris@1296 363 }
Chris@1313 364
Chris@1313 365 double diff = fabs(signeddiff);
Chris@1313 366
Chris@1313 367 totalDiff += diff;
Chris@1313 368 totalSqrDiff += diff * diff;
Chris@1313 369
Chris@757 370 // in edge areas, record this only if it exceeds edgeLimit
Chris@1313 371 if (i < edgeSize || i + edgeSize >= refFrames) {
Chris@1313 372 if (diff > edgeLimit && diff > maxDiff) {
Chris@1313 373 maxDiff = diff;
Chris@1313 374 maxIndex = i;
Chris@757 375 }
Chris@757 376 } else {
Chris@1313 377 if (diff > maxDiff) {
Chris@1313 378 maxDiff = diff;
Chris@1313 379 maxIndex = i;
Chris@757 380 }
Chris@756 381 }
Chris@756 382 }
Chris@1313 383
Chris@1313 384 double meanDiff = totalDiff / double(refFrames);
Chris@1313 385 double rmsDiff = sqrt(totalSqrDiff / double(refFrames));
cannam@1308 386
Chris@1313 387 cerr << "channel " << c << ": mean diff " << meanDiff << endl;
Chris@1313 388 cerr << "channel " << c << ": rms diff " << rmsDiff << endl;
Chris@1313 389 cerr << "channel " << c << ": max diff " << maxDiff << " at " << maxIndex << endl;
Chris@1313 390
Chris@1313 391 if (rmsDiff >= rmsLimit) {
Chris@1313 392 cerr << "ERROR: for audiofile " << audiofile << ": RMS diff = " << rmsDiff << " for channel " << c << " (limit = " << rmsLimit << ")" << endl;
Chris@1313 393 QVERIFY(rmsDiff < rmsLimit);
Chris@1313 394 }
Chris@1313 395 if (maxDiff >= maxLimit) {
Chris@1313 396 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 397 QVERIFY(maxDiff < maxLimit);
Chris@1313 398 }
Chris@1313 399
Chris@1313 400 // and check for spurious material at end
Chris@1313 401
Chris@1309 402 for (sv_frame_t i = refFrames; i + offset < read; ++i) {
Chris@1309 403 sv_frame_t ix = i + offset;
Chris@1313 404 float quiet = 0.1; //!!! allow some ringing - but let's come back to this, it should tail off
cannam@1308 405 float mag = fabsf(test[ix * channels + c]);
cannam@1308 406 if (mag > quiet) {
Chris@1313 407 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 408 QVERIFY(mag < quiet);
cannam@1308 409 }
cannam@1308 410 }
Chris@756 411 }
Chris@1313 412
Chris@1313 413 float **ptrs = new float*[channels];
Chris@1313 414 for (int c = 0; c < channels; ++c) {
Chris@1313 415 ptrs[c] = diffs[c].data();
Chris@1313 416 }
Chris@1313 417 diffWriter.writeSamples(ptrs, refFrames);
Chris@1313 418 delete[] ptrs;
Chris@756 419 }
Chris@756 420 };
Chris@756 421
Chris@756 422 #endif