1 #include <QTemporaryFile>
2 #include <QtDebug>
3
4 #include "sources/audiosourcestereoproxy.h"
5 #include "sources/soundsourceproxy.h"
6 #include "test/mixxxtest.h"
7 #include "track/track.h"
8 #include "track/trackmetadata.h"
9 #include "util/samplebuffer.h"
10
11 namespace {
12
13 const QDir kTestDir(QDir::current().absoluteFilePath("src/test/id3-test-data"));
14
15 const SINT kBufferSizes[] = {
16 256,
17 512,
18 768,
19 1024,
20 1536,
21 2048,
22 3072,
23 4096,
24 6144,
25 8192,
26 12288,
27 16384,
28 24576,
29 32768,
30 };
31
32 const SINT kMaxReadFrameCount = kBufferSizes[sizeof(kBufferSizes) / sizeof(kBufferSizes[0]) - 1];
33
34 const CSAMPLE kMaxDecodingError = 0.01f;
35
36 } // anonymous namespace
37
38 class SoundSourceProxyTest : public MixxxTest {
39 protected:
getFileNameSuffixes()40 static QStringList getFileNameSuffixes() {
41 QStringList availableFileNameSuffixes;
42 availableFileNameSuffixes
43 << ".aiff"
44 << "-alac.caf"
45 << ".flac"
46 // Files encoded with iTunes 12.3.0 caused issues when
47 // decoding with FFMpeg 3.x, because their start_time
48 // was not correctly handled. The actual FFmpeg version
49 // that fixed this bug is unknown.
50 << "-itunes-12.3.0-aac.m4a"
51 << "-itunes-12.7.0-aac.m4a"
52 << "-ffmpeg-aac.m4a"
53 #if defined(__FFMPEG__) || defined(__COREAUDIO__)
54 << "-itunes-12.7.0-alac.m4a"
55 #endif
56 << "-png.mp3"
57 << "-vbr.mp3"
58 << ".ogg"
59 << ".opus"
60 << ".wav"
61 << ".wma"
62 << ".wv";
63
64 QStringList supportedFileNameSuffixes;
65 for (const auto& fileNameSuffix : qAsConst(availableFileNameSuffixes)) {
66 // We need to check for the whole file name here!
67 if (SoundSourceProxy::isFileNameSupported(fileNameSuffix)) {
68 supportedFileNameSuffixes << fileNameSuffix;
69 } else {
70 qInfo()
71 << "Ignoring unsupported file type"
72 << fileNameSuffix;
73 }
74 }
75 return supportedFileNameSuffixes;
76 }
77
getFilePaths()78 static QStringList getFilePaths() {
79 QStringList filePaths;
80 const QStringList fileNameSuffixes = getFileNameSuffixes();
81 for (const auto& fileNameSuffix : fileNameSuffixes) {
82 filePaths.append(kTestDir.absoluteFilePath("cover-test" + fileNameSuffix));
83 }
84 return filePaths;
85 }
86
openAudioSource(const QString & filePath,const mixxx::SoundSourceProviderPointer & pProvider=nullptr)87 static mixxx::AudioSourcePointer openAudioSource(
88 const QString& filePath,
89 const mixxx::SoundSourceProviderPointer& pProvider = nullptr) {
90 auto pTrack = Track::newTemporary(filePath);
91 SoundSourceProxy proxy(pTrack, pProvider);
92
93 // All test files are mono, but we are requesting a stereo signal
94 // to test the upscaling of channels
95 mixxx::AudioSource::OpenParams openParams;
96 const auto channelCount = mixxx::audio::ChannelCount(2);
97 openParams.setChannelCount(mixxx::audio::ChannelCount(2));
98 auto pAudioSource = proxy.openAudioSource(openParams);
99 if (pAudioSource) {
100 if (pAudioSource->getSignalInfo().getChannelCount() != channelCount) {
101 // Wrap into proxy object
102 pAudioSource = mixxx::AudioSourceStereoProxy::create(
103 pAudioSource,
104 kMaxReadFrameCount);
105 }
106 EXPECT_EQ(pAudioSource->getSignalInfo().getChannelCount(), channelCount);
107 qInfo()
108 << "Opened file" << filePath
109 << "using provider" << proxy.getProvider()->getDisplayName();
110 }
111 return pAudioSource;
112 }
113
expectDecodedSamplesEqual(SINT size,const CSAMPLE * expected,const CSAMPLE * actual,const char * errorMessage)114 static void expectDecodedSamplesEqual(
115 SINT size,
116 const CSAMPLE* expected,
117 const CSAMPLE* actual,
118 const char* errorMessage) {
119 for (SINT i = 0; i < size; ++i) {
120 EXPECT_NEAR(expected[i], actual[i],
121 kMaxDecodingError) << errorMessage;
122 }
123 }
124
skipSampleFrames(mixxx::AudioSourcePointer pAudioSource,mixxx::IndexRange skipRange)125 mixxx::IndexRange skipSampleFrames(
126 mixxx::AudioSourcePointer pAudioSource,
127 mixxx::IndexRange skipRange) {
128 mixxx::IndexRange skippedRange;
129 while (skippedRange.length() < skipRange.length()) {
130 // Seek in forward direction by decoding and discarding samples
131 const auto nextRange =
132 mixxx::IndexRange::forward(
133 skippedRange.empty() ? skipRange.start() : skippedRange.end(),
134 math_min(
135 skipRange.length() - skippedRange.length(),
136 pAudioSource->getSignalInfo().samples2frames(m_skipSampleBuffer.size())));
137 EXPECT_FALSE(nextRange.empty());
138 EXPECT_TRUE(nextRange.isSubrangeOf(skipRange));
139 const auto readRange = pAudioSource->readSampleFrames(
140 mixxx::WritableSampleFrames(
141 nextRange,
142 mixxx::SampleBuffer::WritableSlice(
143 m_skipSampleBuffer.data(),
144 m_skipSampleBuffer.size()))).frameIndexRange();
145 if (readRange.empty()) {
146 return skippedRange;
147 }
148 EXPECT_TRUE(readRange.start() == nextRange.start());
149 EXPECT_TRUE(readRange.isSubrangeOf(skipRange));
150 if (skippedRange.empty()) {
151 skippedRange = readRange;
152 } else {
153 EXPECT_TRUE(skippedRange.end() == nextRange.start());
154 skippedRange.growBack(nextRange.length());
155 }
156 }
157 return skippedRange;
158 }
159
SoundSourceProxyTest()160 SoundSourceProxyTest()
161 : m_skipSampleBuffer(kMaxReadFrameCount) {
162 }
163
164 private:
165 mixxx::SampleBuffer m_skipSampleBuffer;
166 };
167
TEST_F(SoundSourceProxyTest,open)168 TEST_F(SoundSourceProxyTest, open) {
169 // This test piggy-backs off of the cover-test files.
170 const QStringList filePaths = getFilePaths();
171 for (const auto& filePath : filePaths) {
172 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath));
173 const auto fileUrl = QUrl::fromLocalFile(filePath);
174 const auto providerRegistrations =
175 SoundSourceProxy::allProviderRegistrationsForUrl(fileUrl);
176 for (const auto& providerRegistration : providerRegistrations) {
177 mixxx::AudioSourcePointer pAudioSource = openAudioSource(
178 filePath,
179 providerRegistration.getProvider());
180 // Obtaining an AudioSource may fail for unsupported file formats,
181 // even if the corresponding file extension is supported, e.g.
182 // AAC vs. ALAC in .m4a files
183 if (!pAudioSource) {
184 // skip test file
185 continue;
186 }
187 EXPECT_LT(0, pAudioSource->getSignalInfo().getChannelCount());
188 EXPECT_LT(0, pAudioSource->getSignalInfo().getSampleRate());
189 EXPECT_FALSE(pAudioSource->frameIndexRange().empty());
190 }
191 }
192 }
193
TEST_F(SoundSourceProxyTest,openEmptyFile)194 TEST_F(SoundSourceProxyTest, openEmptyFile) {
195 const QStringList fileNameSuffixes = getFileNameSuffixes();
196 for (const auto& fileNameSuffix : fileNameSuffixes) {
197 const auto tmpFileName =
198 mixxxtest::createEmptyTemporaryFile("emptyXXXXXX" + fileNameSuffix);
199 const mixxxtest::FileRemover tmpFileRemover(tmpFileName);
200
201 ASSERT_TRUE(QFile::exists(tmpFileName));
202 ASSERT_TRUE(!tmpFileName.isEmpty());
203 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(tmpFileName));
204 auto pTrack = Track::newTemporary(tmpFileName);
205 SoundSourceProxy proxy(pTrack);
206
207 auto pAudioSource = proxy.openAudioSource();
208 EXPECT_TRUE(!pAudioSource);
209 }
210 }
211
TEST_F(SoundSourceProxyTest,readArtist)212 TEST_F(SoundSourceProxyTest, readArtist) {
213 auto pTrack = Track::newTemporary(TrackFile(
214 kTestDir, "artist.mp3"));
215 SoundSourceProxy proxy(pTrack);
216 proxy.updateTrackFromSource();
217 EXPECT_EQ("Test Artist", pTrack->getArtist());
218 }
219
TEST_F(SoundSourceProxyTest,readNoTitle)220 TEST_F(SoundSourceProxyTest, readNoTitle) {
221 // We need to verify every track has at least a title to not have empty lines in the library
222
223 // Test a file with no metadata
224 auto pTrack1 = Track::newTemporary(TrackFile(
225 kTestDir, "empty.mp3"));
226 SoundSourceProxy proxy1(pTrack1);
227 proxy1.updateTrackFromSource();
228 EXPECT_EQ("empty", pTrack1->getTitle());
229
230 // Test a reload also works
231 pTrack1->setTitle("");
232 proxy1.updateTrackFromSource(SoundSourceProxy::ImportTrackMetadataMode::Again);
233 EXPECT_EQ("empty", pTrack1->getTitle());
234
235 // Test a file with other metadata but no title
236 auto pTrack2 = Track::newTemporary(TrackFile(
237 kTestDir, "cover-test-png.mp3"));
238 SoundSourceProxy proxy2(pTrack2);
239 proxy2.updateTrackFromSource();
240 EXPECT_EQ("cover-test-png", pTrack2->getTitle());
241
242 // Test a reload also works
243 pTrack2->setTitle("");
244 proxy2.updateTrackFromSource(SoundSourceProxy::ImportTrackMetadataMode::Again);
245 EXPECT_EQ("cover-test-png", pTrack2->getTitle());
246
247 // Test a file with a title
248 auto pTrack3 = Track::newTemporary(TrackFile(
249 kTestDir, "cover-test-jpg.mp3"));
250 SoundSourceProxy proxy3(pTrack3);
251 proxy3.updateTrackFromSource();
252 EXPECT_EQ("test22kMono", pTrack3->getTitle());
253 }
254
TEST_F(SoundSourceProxyTest,TOAL_TPE2)255 TEST_F(SoundSourceProxyTest, TOAL_TPE2) {
256 auto pTrack = Track::newTemporary(TrackFile(
257 kTestDir, "TOAL_TPE2.mp3"));
258 SoundSourceProxy proxy(pTrack);
259 mixxx::TrackMetadata trackMetadata;
260 EXPECT_EQ(mixxx::MetadataSource::ImportResult::Succeeded, proxy.importTrackMetadata(&trackMetadata));
261 EXPECT_EQ("TITLE2", trackMetadata.getTrackInfo().getArtist());
262 EXPECT_EQ("ARTIST", trackMetadata.getAlbumInfo().getTitle());
263 EXPECT_EQ("TITLE", trackMetadata.getAlbumInfo().getArtist());
264 // The COMM:iTunPGAP comment should not be read
265 EXPECT_TRUE(trackMetadata.getTrackInfo().getComment().isNull());
266 }
267
TEST_F(SoundSourceProxyTest,seekForwardBackward)268 TEST_F(SoundSourceProxyTest, seekForwardBackward) {
269 const SINT kReadFrameCount = 10000;
270
271 const QStringList filePaths = getFilePaths();
272 for (const auto& filePath : filePaths) {
273 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath));
274 qDebug() << "Seek forward/backward test:" << filePath;
275
276 const auto fileUrl = QUrl::fromLocalFile(filePath);
277 const auto providerRegistrations =
278 SoundSourceProxy::allProviderRegistrationsForUrl(fileUrl);
279 for (const auto& providerRegistration : providerRegistrations) {
280 mixxx::AudioSourcePointer pContReadSource = openAudioSource(
281 filePath,
282 providerRegistration.getProvider());
283
284 // Obtaining an AudioSource may fail for unsupported file formats,
285 // even if the corresponding file extension is supported, e.g.
286 // AAC vs. ALAC in .m4a files
287 if (!pContReadSource) {
288 // skip test file
289 continue;
290 }
291 mixxx::SampleBuffer contReadData(
292 pContReadSource->getSignalInfo().frames2samples(kReadFrameCount));
293 mixxx::SampleBuffer seekReadData(
294 pContReadSource->getSignalInfo().frames2samples(kReadFrameCount));
295
296 SINT contFrameIndex = pContReadSource->frameIndexMin();
297 while (pContReadSource->frameIndexRange().containsIndex(contFrameIndex)) {
298 const auto readFrameIndexRange =
299 mixxx::IndexRange::forward(contFrameIndex, kReadFrameCount);
300 qDebug() << "Seeking and reading" << readFrameIndexRange;
301
302 // Read next chunk of frames for Cont source without seeking
303 const auto contSampleFrames =
304 pContReadSource->readSampleFrames(
305 mixxx::WritableSampleFrames(
306 readFrameIndexRange,
307 mixxx::SampleBuffer::WritableSlice(contReadData)));
308 ASSERT_FALSE(contSampleFrames.frameIndexRange().empty());
309 ASSERT_TRUE(contSampleFrames.frameIndexRange().isSubrangeOf(readFrameIndexRange));
310 ASSERT_EQ(contSampleFrames.frameIndexRange().start(), readFrameIndexRange.start());
311 contFrameIndex += contSampleFrames.frameLength();
312
313 const SINT sampleCount =
314 pContReadSource->getSignalInfo().frames2samples(contSampleFrames.frameLength());
315
316 mixxx::AudioSourcePointer pSeekReadSource = openAudioSource(
317 filePath,
318 providerRegistration.getProvider());
319
320 ASSERT_FALSE(!pSeekReadSource);
321 ASSERT_EQ(
322 pContReadSource->getSignalInfo().getChannelCount(),
323 pSeekReadSource->getSignalInfo().getChannelCount());
324 ASSERT_EQ(pContReadSource->frameIndexRange(), pSeekReadSource->frameIndexRange());
325
326 // Seek source to next chunk and read it
327 auto seekSampleFrames =
328 pSeekReadSource->readSampleFrames(
329 mixxx::WritableSampleFrames(
330 readFrameIndexRange,
331 mixxx::SampleBuffer::WritableSlice(seekReadData)));
332
333 // Both buffers should be equal
334 ASSERT_EQ(contSampleFrames.frameIndexRange(), seekSampleFrames.frameIndexRange());
335 expectDecodedSamplesEqual(
336 sampleCount,
337 &contReadData[0],
338 &seekReadData[0],
339 "Decoding mismatch after seeking forward");
340
341 // Seek backwards to beginning of chunk and read again
342 seekSampleFrames =
343 pSeekReadSource->readSampleFrames(
344 mixxx::WritableSampleFrames(
345 readFrameIndexRange,
346 mixxx::SampleBuffer::WritableSlice(seekReadData)));
347
348 // Both buffers should again be equal
349 ASSERT_EQ(contSampleFrames.frameIndexRange(), seekSampleFrames.frameIndexRange());
350 expectDecodedSamplesEqual(
351 sampleCount,
352 &contReadData[0],
353 &seekReadData[0],
354 "Decoding mismatch after seeking backward");
355 }
356 }
357 }
358 }
359
TEST_F(SoundSourceProxyTest,skipAndRead)360 TEST_F(SoundSourceProxyTest, skipAndRead) {
361 for (auto kReadFrameCount : kBufferSizes) {
362 const QStringList filePaths = getFilePaths();
363 for (const auto& filePath : filePaths) {
364 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath));
365 qDebug() << "Skip and read test:" << filePath;
366
367 const auto fileUrl = QUrl::fromLocalFile(filePath);
368 const auto providerRegistrations =
369 SoundSourceProxy::allProviderRegistrationsForUrl(fileUrl);
370 for (const auto& providerRegistration : providerRegistrations) {
371 mixxx::AudioSourcePointer pContReadSource = openAudioSource(
372 filePath,
373 providerRegistration.getProvider());
374 // Obtaining an AudioSource may fail for unsupported file formats,
375 // even if the corresponding file extension is supported, e.g.
376 // AAC vs. ALAC in .m4a files
377 if (!pContReadSource) {
378 // skip test file
379 continue;
380 }
381 SINT contFrameIndex = pContReadSource->frameIndexMin();
382
383 mixxx::AudioSourcePointer pSkipReadSource = openAudioSource(
384 filePath,
385 providerRegistration.getProvider());
386 ASSERT_FALSE(!pSkipReadSource);
387 ASSERT_EQ(
388 pContReadSource->getSignalInfo().getChannelCount(),
389 pSkipReadSource->getSignalInfo().getChannelCount());
390 ASSERT_EQ(pContReadSource->frameIndexRange(), pSkipReadSource->frameIndexRange());
391 SINT skipFrameIndex = pSkipReadSource->frameIndexMin();
392
393 mixxx::SampleBuffer contReadData(
394 pContReadSource->getSignalInfo().frames2samples(kReadFrameCount));
395 mixxx::SampleBuffer skipReadData(
396 pSkipReadSource->getSignalInfo().frames2samples(kReadFrameCount));
397
398 SINT minFrameIndex = pContReadSource->frameIndexMin();
399 SINT skipCount = 1;
400 while (pContReadSource->frameIndexRange().containsIndex(
401 minFrameIndex += skipCount)) {
402 skipCount = minFrameIndex / 4 + 1; // for next iteration
403
404 qDebug() << "Skipping to:" << minFrameIndex;
405
406 const auto readFrameIndexRange =
407 mixxx::IndexRange::forward(minFrameIndex, kReadFrameCount);
408
409 // Read (and discard samples) until reaching the desired frame index
410 // and read next chunk
411 ASSERT_LE(contFrameIndex, minFrameIndex);
412 while (contFrameIndex < minFrameIndex) {
413 auto skippingFrameIndexRange =
414 mixxx::IndexRange::forward(
415 contFrameIndex,
416 std::min(minFrameIndex - contFrameIndex, kReadFrameCount));
417 auto const skippedSampleFrames =
418 pContReadSource->readSampleFrames(
419 mixxx::WritableSampleFrames(
420 skippingFrameIndexRange,
421 mixxx::SampleBuffer::WritableSlice(contReadData)));
422 ASSERT_FALSE(skippedSampleFrames.frameIndexRange().empty());
423 ASSERT_EQ(skippedSampleFrames.frameIndexRange().start(), contFrameIndex);
424 contFrameIndex += skippedSampleFrames.frameLength();
425 }
426 ASSERT_EQ(minFrameIndex, contFrameIndex);
427 const auto contSampleFrames =
428 pContReadSource->readSampleFrames(
429 mixxx::WritableSampleFrames(
430 readFrameIndexRange,
431 mixxx::SampleBuffer::WritableSlice(contReadData)));
432 ASSERT_FALSE(contSampleFrames.frameIndexRange().empty());
433 ASSERT_TRUE(contSampleFrames.frameIndexRange()
434 .isSubrangeOf(readFrameIndexRange));
435 ASSERT_EQ(contSampleFrames.frameIndexRange().start(),
436 readFrameIndexRange.start());
437 contFrameIndex += contSampleFrames.frameLength();
438
439 const SINT sampleCount =
440 pContReadSource->getSignalInfo().frames2samples(
441 contSampleFrames.frameLength());
442
443 // Skip until reaching the frame index and read next chunk
444 ASSERT_LE(skipFrameIndex, minFrameIndex);
445 while (skipFrameIndex < minFrameIndex) {
446 auto const skippedFrameIndexRange =
447 skipSampleFrames(pSkipReadSource,
448 mixxx::IndexRange::between(skipFrameIndex, minFrameIndex));
449 ASSERT_FALSE(skippedFrameIndexRange.empty());
450 ASSERT_EQ(skippedFrameIndexRange.start(), skipFrameIndex);
451 skipFrameIndex += skippedFrameIndexRange.length();
452 }
453 ASSERT_EQ(minFrameIndex, skipFrameIndex);
454 const auto skippedSampleFrames =
455 pSkipReadSource->readSampleFrames(
456 mixxx::WritableSampleFrames(
457 readFrameIndexRange,
458 mixxx::SampleBuffer::WritableSlice(skipReadData)));
459
460 skipFrameIndex += skippedSampleFrames.frameLength();
461
462 // Both buffers should be equal
463 ASSERT_EQ(contSampleFrames.frameIndexRange(),
464 skippedSampleFrames.frameIndexRange());
465 expectDecodedSamplesEqual(
466 sampleCount,
467 &contReadData[0],
468 &skipReadData[0],
469 "Decoding mismatch after skipping");
470
471 minFrameIndex = contFrameIndex;
472 }
473 }
474 }
475 }
476 }
477
TEST_F(SoundSourceProxyTest,seekBoundaries)478 TEST_F(SoundSourceProxyTest, seekBoundaries) {
479 const SINT kReadFrameCount = 1000;
480 const QStringList filePaths = getFilePaths();
481 for (const auto& filePath : filePaths) {
482 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath));
483 qDebug() << "Seek boundaries test:" << filePath;
484
485 const auto fileUrl = QUrl::fromLocalFile(filePath);
486 const auto providerRegistrations =
487 SoundSourceProxy::allProviderRegistrationsForUrl(fileUrl);
488 for (const auto& providerRegistration : providerRegistrations) {
489 mixxx::AudioSourcePointer pSeekReadSource = openAudioSource(
490 filePath,
491 providerRegistration.getProvider());
492 // Obtaining an AudioSource may fail for unsupported file formats,
493 // even if the corresponding file extension is supported, e.g.
494 // AAC vs. ALAC in .m4a files
495 if (!pSeekReadSource) {
496 // skip test file
497 continue;
498 }
499 mixxx::SampleBuffer seekReadData(
500 pSeekReadSource->getSignalInfo().frames2samples(kReadFrameCount));
501
502 std::vector<SINT> seekFrameIndices;
503 // Seek to boundaries (alternating)...
504 seekFrameIndices.push_back(pSeekReadSource->frameIndexMin());
505 seekFrameIndices.push_back(pSeekReadSource->frameIndexMax() - 1);
506 seekFrameIndices.push_back(pSeekReadSource->frameIndexMin() + 1);
507 seekFrameIndices.push_back(pSeekReadSource->frameIndexMax());
508 // ...seek to middle of the stream...
509 seekFrameIndices.push_back(
510 pSeekReadSource->frameIndexMin() +
511 pSeekReadSource->frameLength() / 2);
512 // ...and to the boundaries again in opposite order...
513 seekFrameIndices.push_back(pSeekReadSource->frameIndexMax());
514 seekFrameIndices.push_back(pSeekReadSource->frameIndexMin() + 1);
515 seekFrameIndices.push_back(pSeekReadSource->frameIndexMax() - 1);
516 seekFrameIndices.push_back(pSeekReadSource->frameIndexMin());
517 // ...near the end and back to middle of the stream...
518 seekFrameIndices.push_back(
519 pSeekReadSource->frameIndexMax() - 4 * kReadFrameCount);
520 seekFrameIndices.push_back(
521 pSeekReadSource->frameIndexMin() + pSeekReadSource->frameLength() / 2);
522 // ...before the middle and then near the end of the stream...
523 seekFrameIndices.push_back(pSeekReadSource->frameIndexMin() +
524 pSeekReadSource->frameLength() / 2 - 4 * kReadFrameCount);
525 seekFrameIndices.push_back(
526 pSeekReadSource->frameIndexMax() - 4 * kReadFrameCount);
527 // ...to the moddle of the stream and then skipping kReadFrameCount samples.
528 seekFrameIndices.push_back(
529 pSeekReadSource->frameIndexMin() + pSeekReadSource->frameLength() / 2);
530 seekFrameIndices.push_back(pSeekReadSource->frameIndexMin() +
531 pSeekReadSource->frameLength() / 2 + 2 * kReadFrameCount);
532
533 // Read and verify results
534 for (SINT seekFrameIndex : seekFrameIndices) {
535 const auto readFrameIndexRange =
536 mixxx::IndexRange::forward(seekFrameIndex, kReadFrameCount);
537 qDebug() << "Reading and verifying" << readFrameIndexRange;
538
539 const auto expectedFrameIndexRange = intersect(
540 readFrameIndexRange,
541 pSeekReadSource->frameIndexRange());
542
543 mixxx::AudioSourcePointer pContReadSource = openAudioSource(
544 filePath,
545 providerRegistration.getProvider());
546 ASSERT_FALSE(!pContReadSource);
547 ASSERT_EQ(
548 pSeekReadSource->getSignalInfo().getChannelCount(),
549 pContReadSource->getSignalInfo().getChannelCount());
550 ASSERT_EQ(pSeekReadSource->frameIndexRange(), pContReadSource->frameIndexRange());
551 const auto skipFrameIndexRange =
552 skipSampleFrames(pContReadSource,
553 mixxx::IndexRange::between(
554 pContReadSource->frameIndexMin(),
555 seekFrameIndex));
556 ASSERT_TRUE(skipFrameIndexRange.empty() ||
557 (skipFrameIndexRange.end() == seekFrameIndex));
558 mixxx::SampleBuffer contReadData(
559 pContReadSource->getSignalInfo().frames2samples(kReadFrameCount));
560 const auto contSampleFrames =
561 pContReadSource->readSampleFrames(
562 mixxx::WritableSampleFrames(
563 readFrameIndexRange,
564 mixxx::SampleBuffer::WritableSlice(contReadData)));
565 ASSERT_EQ(expectedFrameIndexRange, contSampleFrames.frameIndexRange());
566
567 const auto seekSampleFrames =
568 pSeekReadSource->readSampleFrames(
569 mixxx::WritableSampleFrames(
570 readFrameIndexRange,
571 mixxx::SampleBuffer::WritableSlice(seekReadData)));
572 ASSERT_EQ(expectedFrameIndexRange, seekSampleFrames.frameIndexRange());
573
574 if (seekSampleFrames.frameIndexRange().empty()) {
575 continue; // nothing to do
576 }
577
578 const SINT sampleCount =
579 pSeekReadSource->getSignalInfo().frames2samples(
580 seekSampleFrames.frameLength());
581 expectDecodedSamplesEqual(
582 sampleCount,
583 &contReadData[0],
584 &seekReadData[0],
585 "Decoding mismatch after seeking");
586 }
587 }
588 }
589 }
590
TEST_F(SoundSourceProxyTest,readBeyondEnd)591 TEST_F(SoundSourceProxyTest, readBeyondEnd) {
592 const SINT kReadFrameCount = 1000;
593 const QStringList filePaths = getFilePaths();
594 for (const auto& filePath : filePaths) {
595 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath));
596 qDebug() << "read beyond end test:" << filePath;
597
598 const auto fileUrl = QUrl::fromLocalFile(filePath);
599 const auto providerRegistrations =
600 SoundSourceProxy::allProviderRegistrationsForUrl(fileUrl);
601 for (const auto& providerRegistration : providerRegistrations) {
602 mixxx::AudioSourcePointer pAudioSource = openAudioSource(
603 filePath,
604 providerRegistration.getProvider());
605 // Obtaining an AudioSource may fail for unsupported file formats,
606 // even if the corresponding file extension is supported, e.g.
607 // AAC vs. ALAC in .m4a files
608 if (!pAudioSource) {
609 // skip test file
610 continue;
611 }
612
613 // Seek to position near the end
614 const SINT seekIndex = pAudioSource->frameIndexMax() - (kReadFrameCount / 2);
615 const SINT remainingFrames = pAudioSource->frameIndexMax() - seekIndex;
616 ASSERT_GT(remainingFrames, 0);
617 ASSERT_LT(remainingFrames, kReadFrameCount);
618
619 mixxx::SampleBuffer readBuffer(
620 pAudioSource->getSignalInfo().frames2samples(kReadFrameCount));
621
622 // Read beyond the end, starting within the valid range
623 EXPECT_EQ(mixxx::IndexRange::forward(seekIndex, remainingFrames),
624 pAudioSource
625 ->readSampleFrames(mixxx::WritableSampleFrames(
626 mixxx::IndexRange::forward(
627 seekIndex, kReadFrameCount),
628 mixxx::SampleBuffer::WritableSlice(
629 readBuffer)))
630 .frameIndexRange());
631
632 // Read beyond the end, starting at the upper boundary of the valid range
633 EXPECT_EQ(mixxx::IndexRange::forward(pAudioSource->frameIndexMax(), 0),
634 pAudioSource
635 ->readSampleFrames(mixxx::WritableSampleFrames(
636 mixxx::IndexRange::forward(
637 pAudioSource->frameIndexMax(), kReadFrameCount),
638 mixxx::SampleBuffer::WritableSlice(
639 readBuffer)))
640 .frameIndexRange());
641
642 // Read beyond the end, starting beyond the upper boundary of the valid range
643 EXPECT_EQ(mixxx::IndexRange::forward(pAudioSource->frameIndexMax() + 1, 0),
644 pAudioSource
645 ->readSampleFrames(mixxx::WritableSampleFrames(
646 mixxx::IndexRange::forward(
647 pAudioSource->frameIndexMax() + 1, kReadFrameCount),
648 mixxx::SampleBuffer::WritableSlice(
649 readBuffer)))
650 .frameIndexRange());
651 }
652 }
653 }
654
TEST_F(SoundSourceProxyTest,regressionTestCachingReaderChunkJumpForward)655 TEST_F(SoundSourceProxyTest, regressionTestCachingReaderChunkJumpForward) {
656 // NOTE(uklotzde, 2017-12-10): Potential regression test for an infinite
657 // seek/read loop in SoundSourceMediaFoundation. Unfortunately this
658 // test doesn't fail even prior to fixing the reported bug.
659 // https://github.com/mixxxdj/mixxx/pull/1317#issuecomment-349674161
660 const QStringList filePaths = getFilePaths();
661 for (auto kReadFrameCount : kBufferSizes) {
662 for (const auto& filePath : filePaths) {
663 ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath));
664
665 const auto fileUrl = QUrl::fromLocalFile(filePath);
666 const auto providerRegistrations =
667 SoundSourceProxy::allProviderRegistrationsForUrl(fileUrl);
668 for (const auto& providerRegistration : providerRegistrations) {
669 mixxx::AudioSourcePointer pAudioSource = openAudioSource(
670 filePath,
671 providerRegistration.getProvider());
672 // Obtaining an AudioSource may fail for unsupported file formats,
673 // even if the corresponding file extension is supported, e.g.
674 // AAC vs. ALAC in .m4a files
675 if (!pAudioSource) {
676 // skip test file
677 continue;
678 }
679
680 mixxx::SampleBuffer readBuffer(
681 pAudioSource->getSignalInfo().frames2samples(kReadFrameCount));
682
683 // Read chunk from beginning
684 auto firstChunkRange = mixxx::IndexRange::forward(
685 pAudioSource->frameIndexMin(), kReadFrameCount);
686 EXPECT_EQ(
687 firstChunkRange,
688 pAudioSource->readSampleFrames(
689 mixxx::WritableSampleFrames(
690 firstChunkRange,
691 mixxx::SampleBuffer::WritableSlice(readBuffer)))
692 .frameIndexRange());
693
694 // Read chunk from near the end, rounded to chunk boundary
695 auto secondChunkRange = mixxx::IndexRange::forward(
696 ((pAudioSource->frameIndexMax() - 2 * kReadFrameCount) /
697 kReadFrameCount) *
698 kReadFrameCount,
699 kReadFrameCount);
700 EXPECT_EQ(
701 secondChunkRange,
702 pAudioSource->readSampleFrames(
703 mixxx::WritableSampleFrames(
704 secondChunkRange,
705 mixxx::SampleBuffer::WritableSlice(readBuffer)))
706 .frameIndexRange());
707 }
708 }
709 }
710 }
711