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