diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 7d99bc078bf4..e0437c5c06be 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -367,18 +367,12 @@ void SoundSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { SINT SoundSourceM4A::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT_AND_HANDLE(isValidFrameIndex(frameIndex)) { - // EOF reached + if (frameIndex >= getMaxFrameIndex()) { + // EOF m_curFrameIndex = getMaxFrameIndex(); return m_curFrameIndex; } - // Handle trivial case - if (frameIndex == m_curFrameIndex) { - // Nothing to do - return m_curFrameIndex; - } - // NOTE(uklotzde): Resetting the decoder near to the beginning // of the stream when seeking backwards produces invalid sample // values! As a consequence the seeking test fails. @@ -388,6 +382,8 @@ SINT SoundSourceM4A::seekSampleFrame(SINT frameIndex) { // of the stream while decoding. reopenDecoder(); skipSampleFrames(frameIndex); + } + if (frameIndex == m_curFrameIndex) { return m_curFrameIndex; } diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 7a9336b15bbd..e35654de2b18 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -132,9 +132,15 @@ void SoundSourceMediaFoundation::close() { SINT SoundSourceMediaFoundation::seekSampleFrame( SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + DEBUG_ASSERT(isValidFrameIndex(m_currentFrameIndex)); - if (m_currentFrameIndex < frameIndex) { + if (frameIndex >= getMaxFrameIndex()) { + // EOF + m_currentFrameIndex = getMaxFrameIndex(); + return m_currentFrameIndex; + } + + if (frameIndex > m_currentFrameIndex) { // seeking forward SINT skipFramesCount = frameIndex - m_currentFrameIndex; // When to prefer skipping over seeking: @@ -151,23 +157,21 @@ SINT SoundSourceMediaFoundation::seekSampleFrame( skipSampleFrames(skipFramesCount); } } - - if (m_currentFrameIndex == frameIndex) { - // already there + if (frameIndex == m_currentFrameIndex) { return m_currentFrameIndex; } - if (m_pSourceReader == nullptr) { - // reader is dead -> jump to end of stream - return getMaxFrameIndex(); - } - // Discard decoded samples m_sampleBuffer.reset(); // Invalidate current position (end of stream) m_currentFrameIndex = getMaxFrameIndex(); + if (m_pSourceReader == nullptr) { + // reader is dead + return m_currentFrameIndex; + } + // Jump to a position before the actual seeking position. // Prefetching a certain number of frames is necessary for // sample accurate decoding. The decoder needs to decode diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index fa8ba5953e91..d2a0393fdb8a 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -21,7 +21,8 @@ SoundSourceWV::SoundSourceWV(const QUrl& url) m_wpc(nullptr), m_sampleScaleFactor(CSAMPLE_ZERO), m_pWVFile(nullptr), - m_pWVCFile(nullptr) { + m_pWVCFile(nullptr), + m_curFrameIndex(getMinFrameIndex()) { } SoundSourceWV::~SoundSourceWV() { @@ -86,11 +87,24 @@ void SoundSourceWV::close() { delete m_pWVCFile; m_pWVCFile = nullptr; } + m_curFrameIndex = getMinFrameIndex(); } SINT SoundSourceWV::seekSampleFrame(SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + + if (frameIndex >= getMaxFrameIndex()) { + // EOF reached + m_curFrameIndex = getMaxFrameIndex(); + return m_curFrameIndex; + } + + if (frameIndex == m_curFrameIndex) { + return m_curFrameIndex; + } + if (WavpackSeekSample(m_wpc, frameIndex) == true) { + m_curFrameIndex = frameIndex; return frameIndex; } else { qDebug() << "SSWV::seek : could not seek to frame #" << frameIndex; @@ -100,9 +114,23 @@ SINT SoundSourceWV::seekSampleFrame(SINT frameIndex) { SINT SoundSourceWV::readSampleFrames( SINT numberOfFrames, CSAMPLE* sampleBuffer) { + if (sampleBuffer == nullptr) { + // NOTE(uklotzde): The WavPack API does not provide any + // functions for skipping samples in the audio stream. Calling + // API functions with a nullptr buffer does not return. Since + // we don't want to read samples into a temporary buffer that + // has to be allocated we are seeking to the position after + // the skipped samples. + SINT curFrameIndexBefore = m_curFrameIndex; + SINT curFrameIndexAfter = seekSampleFrame(m_curFrameIndex + numberOfFrames); + DEBUG_ASSERT(curFrameIndexBefore <= curFrameIndexAfter); + DEBUG_ASSERT(m_curFrameIndex == curFrameIndexAfter); + return curFrameIndexAfter - curFrameIndexBefore; + } // static assert: sizeof(CSAMPLE) == sizeof(int32_t) SINT unpackCount = WavpackUnpackSamples(m_wpc, reinterpret_cast(sampleBuffer), numberOfFrames); + DEBUG_ASSERT(unpackCount >= 0); if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { // signed integer -> float const SINT sampleCount = frames2samples(unpackCount); @@ -112,6 +140,7 @@ SINT SoundSourceWV::readSampleFrames( sampleBuffer[i] = CSAMPLE(sampleValue) * m_sampleScaleFactor; } } + m_curFrameIndex += unpackCount; return unpackCount; } diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 37e8eeba567e..e79eb446c40a 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -38,6 +38,8 @@ class SoundSourceWV: public SoundSourcePlugin { CSAMPLE m_sampleScaleFactor; QFile* m_pWVFile; QFile* m_pWVCFile; + + SINT m_curFrameIndex; }; class SoundSourceProviderWV: public SoundSourceProvider { diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 9b99415352fd..10297a467971 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -167,9 +167,27 @@ SINT SoundSourceCoreAudio::seekSampleFrame(SINT frameIndex) { SINT SoundSourceCoreAudio::readSampleFrames( SINT numberOfFrames, CSAMPLE* sampleBuffer) { - //if (!m_decoder) return 0; - SINT numFramesRead = 0; + DEBUG_ASSERT(numberOfFrames >= 0); + if (numberOfFrames <= 0) { + return 0; + } + + // Handle special case: Skipping instead of reading + if (sampleBuffer == nullptr) { + SInt64 frameOffset = 0; + const OSStatus osErr = ExtAudioFileTell(m_audioFile, &frameOffset); + if (osErr == noErr) { + const SINT frameIndexBefore = getMinFrameIndex() + frameOffset; + const SINT frameIndexAfter = seekSampleFrame(frameIndexBefore + numberOfFrames); + DEBUG_ASSERT(frameIndexBefore <= frameIndexAfter); + return frameIndexAfter - frameIndexBefore; + } else { + qWarning() << "SSCA: Error to determine the current position for skipping sample frames" << osErr; + return 0; // abort + } + } + SINT numFramesRead = 0; while (numFramesRead < numberOfFrames) { SINT numFramesToRead = numberOfFrames - numFramesRead; diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index aae7a3a92349..1634c21adb14 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -305,6 +305,7 @@ SoundSource::OpenResult SoundSourceMp3::tryOpen(const AudioSourceConfig& /*audio // Abort return OpenResult::FAILED; } + DEBUG_ASSERT(m_seekFrameList.front().frameIndex == getMinFrameIndex()); int mostCommonSamplingRateIndex = kSamplingRateCount; // invalid int mostCommonSamplingRateCount = 0; @@ -351,13 +352,12 @@ SoundSource::OpenResult SoundSourceMp3::tryOpen(const AudioSourceConfig& /*audio // Terminate m_seekFrameList addSeekFrame(m_curFrameIndex, 0); - - // Reset positions - m_curFrameIndex = getMinFrameIndex(); + DEBUG_ASSERT(m_seekFrameList.back().frameIndex == getMaxFrameIndex()); // Restart decoding at the beginning of the audio stream - m_curFrameIndex = restartDecoding(m_seekFrameList.front()); - if (m_curFrameIndex != m_seekFrameList.front().frameIndex) { + restartDecoding(m_seekFrameList.front()); + + if (m_curFrameIndex != getMinFrameIndex()) { qWarning() << "Failed to start decoding:" << m_file.fileName(); // Abort return OpenResult::FAILED; @@ -383,7 +383,7 @@ void SoundSourceMp3::close() { initDecoding(); } -SINT SoundSourceMp3::restartDecoding( +void SoundSourceMp3::restartDecoding( const SeekFrameType& seekFrame) { qDebug() << "restartDecoding @" << seekFrame.frameIndex; @@ -415,14 +415,13 @@ SINT SoundSourceMp3::restartDecoding( mad_synth_mute(&m_madSynth); } - if (!decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { - if (!isStreamValid(m_madStream)) { - // Failure -> Seek to EOF - return getFrameCount(); - } + if (decodeFrameHeader(&m_madFrame.header, &m_madStream, false) + && isStreamValid(m_madStream)) { + m_curFrameIndex = seekFrame.frameIndex; + } else { + // Failure -> Seek to EOF + m_curFrameIndex = getMaxFrameIndex(); } - - return seekFrame.frameIndex; } void SoundSourceMp3::addSeekFrame( @@ -485,20 +484,17 @@ SINT SoundSourceMp3::findSeekFrameIndex( SINT SoundSourceMp3::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - // Handle trivial case - if (m_curFrameIndex == frameIndex) { - // Nothing to do - return m_curFrameIndex; - } - // Handle edge case - if (getMaxFrameIndex() <= frameIndex) { + if (frameIndex >= getMaxFrameIndex()) { // EOF reached m_curFrameIndex = getMaxFrameIndex(); return m_curFrameIndex; } + if (frameIndex == m_curFrameIndex) { + return m_curFrameIndex; + } + SINT seekFrameIndex = findSeekFrameIndex( frameIndex); DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); @@ -513,8 +509,6 @@ SINT SoundSourceMp3::seekSampleFrame(SINT frameIndex) { (seekFrameIndex > (curSeekFrameIndex + kMp3SeekFramePrefetchCount))) { // jump forward // Adjust the seek frame index for prefetching - // Implementation note: The type SINT is unsigned so - // need to be careful when subtracting! if (kMp3SeekFramePrefetchCount < seekFrameIndex) { // Restart decoding kMp3SeekFramePrefetchCount seek frames // before the expected sync position @@ -524,24 +518,22 @@ SINT SoundSourceMp3::seekSampleFrame(SINT frameIndex) { seekFrameIndex = 0; } - m_curFrameIndex = restartDecoding(m_seekFrameList[seekFrameIndex]); - if (getMaxFrameIndex() <= m_curFrameIndex) { - // out of range -> abort - return m_curFrameIndex; - } + restartDecoding(m_seekFrameList[seekFrameIndex]); + DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); } - // Decoding starts before the actual target position + // Decoding starts at or before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); // Skip (= decode and discard) all samples up to the target position - const SINT prefetchFrameCount = frameIndex - m_curFrameIndex; - const SINT skipFrameCount = skipSampleFrames(prefetchFrameCount); - DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); - if (skipFrameCount < prefetchFrameCount) { - qWarning() << "Failed to prefetch sample data while seeking" - << skipFrameCount << "<" << prefetchFrameCount; + if (m_curFrameIndex < frameIndex) { + skipSampleFrames(frameIndex - m_curFrameIndex); + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); + if (m_curFrameIndex < frameIndex) { + qWarning() << "Failed to prefetch sample data while seeking:" + << m_curFrameIndex << "<" << frameIndex; + } } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); @@ -590,7 +582,12 @@ SINT SoundSourceMp3::readSampleFrames( if (mad_frame_decode(&m_madFrame, &m_madStream)) { // Something went wrong when decoding the frame... if (MAD_ERROR_BUFLEN == m_madStream.error) { - // Abort + // Abort when reaching the end of the stream + DEBUG_ASSERT(isUnrecoverableError(m_madStream)); + if (m_curFrameIndex < getMaxFrameIndex()) { + qWarning() << "End of MP3 stream is unreachable:" + << m_curFrameIndex << "<" << getMaxFrameIndex(); + } break; } if (isUnrecoverableError(m_madStream)) { diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 56e2b2c5c8be..c7872ad457a1 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -65,12 +65,9 @@ class SoundSourceMp3: public SoundSource { SINT m_curFrameIndex; // NOTE(uklotzde): Each invocation of initDecoding() must be - // followed by an invocation of finishDecoding(). In between - // 2 matching invocations restartDecoding() might invoked any - // number of times, but only if the files has been opened - // successfully. + // followed by an invocation of finishDecoding(). void initDecoding(); - SINT restartDecoding(const SeekFrameType& seekFrame); + void restartDecoding(const SeekFrameType& seekFrame); void finishDecoding(); // MAD decoder diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 67375f6d60ec..3c9ee20088eb 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -111,7 +111,16 @@ void SoundSourceOggVorbis::close() { SINT SoundSourceOggVorbis::seekSampleFrame( SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + if (frameIndex >= getMaxFrameIndex()) { + // EOF + m_curFrameIndex = getMaxFrameIndex(); + return m_curFrameIndex; + } + + if (frameIndex == m_curFrameIndex) { + return m_curFrameIndex; + } const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 == seekResult) { @@ -166,26 +175,28 @@ SINT SoundSourceOggVorbis::readSampleFrames( numberOfFramesRemaining, ¤tSection); if (0 < readResult) { m_curFrameIndex += readResult; - if (kChannelCountMono == getChannelCount()) { - if (readStereoSamples) { + if (pSampleBuffer != nullptr) { + if (kChannelCountMono == getChannelCount()) { + if (readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *pSampleBuffer++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[0][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + *pSampleBuffer++ = pcmChannels[0][i]; + } + } + } else if (readStereoSamples || (kChannelCountStereo == getChannelCount())) { for (long i = 0; i < readResult; ++i) { *pSampleBuffer++ = pcmChannels[0][i]; - *pSampleBuffer++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[1][i]; } } else { for (long i = 0; i < readResult; ++i) { - *pSampleBuffer++ = pcmChannels[0][i]; - } - } - } else if (readStereoSamples || (kChannelCountStereo == getChannelCount())) { - for (long i = 0; i < readResult; ++i) { - *pSampleBuffer++ = pcmChannels[0][i]; - *pSampleBuffer++ = pcmChannels[1][i]; - } - } else { - for (long i = 0; i < readResult; ++i) { - for (SINT j = 0; j < getChannelCount(); ++j) { - *pSampleBuffer++ = pcmChannels[j][i]; + for (SINT j = 0; j < getChannelCount(); ++j) { + *pSampleBuffer++ = pcmChannels[j][i]; + } } } } diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 681cf0452b8e..7fe7e242d518 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -2,11 +2,23 @@ namespace mixxx { +// Depends on kNumberOfPrefetchFrames (see below) +//static +const CSAMPLE SoundSourceOpus::kMaxDecodingError = 0.01f; + namespace { -// Decoded output of opusfile has a fixed sample rate of 48 kHz +// Decoded output of opusfile has a fixed sample rate of 48 kHz (fullband) const SINT kSamplingRate = 48000; +// http://opus-codec.org +// - Sampling rate 48 kHz (fullband) +// - Frame sizes from 2.5 ms to 60 ms +// => Up to 48000 kHz * 0.06 s = 2880 sample frames per data frame +// Prefetching 2 * 2880 sample frames while seeking limits the decoding +// errors to kMaxDecodingError (see definition below) during our tests. +const SINT kNumberOfPrefetchFrames = 2 * 2880; + // Parameter for op_channel_count() // See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html const int kCurrentStreamLink = -1; // get ... of the current (stream) link @@ -139,7 +151,7 @@ Result SoundSourceOpus::parseTrackMetadataAndCoverArt( return OK; } -SoundSource::OpenResult SoundSourceOpus::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { +SoundSource::OpenResult SoundSourceOpus::tryOpen(const AudioSourceConfig& audioSrcCfg) { // From opus/opusfile.h // On Windows, this string must be UTF-8 (to allow access to // files whose names cannot be represented in the current @@ -171,12 +183,28 @@ SoundSource::OpenResult SoundSourceOpus::tryOpen(const AudioSourceConfig& /*audi const int channelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink); if (0 < channelCount) { - setChannelCount(channelCount); + const SINT streamChannelCount = channelCount; + // opusfile supports to enforce stereo decoding + bool enforceStereoDecoding = + audioSrcCfg.hasValidChannelCount() && + (audioSrcCfg.getChannelCount() <= kChannelCountStereo) && + ((streamChannelCount > kChannelCountStereo) || + // preserve mono signals if stereo signal is not requested explicitly + (audioSrcCfg.getChannelCount() == kChannelCountStereo)); + if (enforceStereoDecoding) { + setChannelCount(kChannelCountStereo); + } else { + setChannelCount(streamChannelCount); + } } else { qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrlString(); return OpenResult::FAILED; } + // Reserve enough capacity for buffering a stereo signal! + const SINT prefetchChannelCount = std::min(getChannelCount(), kChannelCountStereo); + SampleBuffer(prefetchChannelCount * kNumberOfPrefetchFrames).swap(m_prefetchSampleBuffer); + const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink); if (0 <= pcmTotal) { setFrameCount(pcmTotal); @@ -209,11 +237,30 @@ void SoundSourceOpus::close() { SINT SoundSourceOpus::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); + if (frameIndex >= getMaxFrameIndex()) { + // EOF + m_curFrameIndex = getMaxFrameIndex(); + return m_curFrameIndex; + } + + if ((frameIndex > m_curFrameIndex) && // seeking forward + ((frameIndex - m_curFrameIndex) <= 2 * kNumberOfPrefetchFrames)) { + // Prefer skipping over seeking if the seek position is up to + // 2 * kNumberOfPrefetchFrames in front of the current position + skipSampleFrames(frameIndex - m_curFrameIndex); + return m_curFrameIndex; + } + if (frameIndex == m_curFrameIndex) { + return m_curFrameIndex; + } + + SINT seekIndex = std::max(frameIndex - kNumberOfPrefetchFrames, getMinFrameIndex()); + int seekResult = op_pcm_seek(m_pOggOpusFile, seekIndex); if (0 == seekResult) { - m_curFrameIndex = frameIndex; + m_curFrameIndex = seekIndex; + // Skip prefetched frames + skipSampleFrames(frameIndex - seekIndex); } else { qWarning() << "Failed to seek OggOpus file:" << seekResult; const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile); @@ -231,6 +278,13 @@ SINT SoundSourceOpus::seekSampleFrame(SINT frameIndex) { SINT SoundSourceOpus::readSampleFrames( SINT numberOfFrames, CSAMPLE* sampleBuffer) { + if (getChannelCount() == kChannelCountStereo) { + return readSampleFramesStereo( + numberOfFrames, + sampleBuffer, + frames2samples(numberOfFrames)); + } + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); const SINT numberOfFramesTotal = math_min( @@ -239,9 +293,25 @@ SINT SoundSourceOpus::readSampleFrames( CSAMPLE* pSampleBuffer = sampleBuffer; SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { - int readResult = op_read_float(m_pOggOpusFile, - pSampleBuffer, - frames2samples(numberOfFramesRemaining), nullptr); + SINT numberOfSamplesToRead = + frames2samples(numberOfFramesRemaining); + if (sampleBuffer == nullptr) { + // NOTE(uklotzde): The opusfile API does not provide any + // functions for skipping samples in the audio stream. Calling + // API functions with a nullptr buffer does not return. Since + // seeking in Opus files requires prefetching + skipping we + // need to skip sample frame by reading into a temporary + // buffer + pSampleBuffer = m_prefetchSampleBuffer.data(); + if (numberOfSamplesToRead > m_prefetchSampleBuffer.size()) { + numberOfSamplesToRead = m_prefetchSampleBuffer.size(); + } + } + const int readResult = op_read_float( + m_pOggOpusFile, + pSampleBuffer, + numberOfSamplesToRead, + nullptr); if (0 < readResult) { m_curFrameIndex += readResult; pSampleBuffer += frames2samples(readResult); @@ -270,12 +340,27 @@ SINT SoundSourceOpus::readSampleFramesStereo( CSAMPLE* pSampleBuffer = sampleBuffer; SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { - int readResult = op_read_float_stereo(m_pOggOpusFile, + SINT numberOfSamplesToRead = + numberOfFramesRemaining * kChannelCountStereo; + if (sampleBuffer == nullptr) { + // NOTE(uklotzde): The opusfile API does not provide any + // functions for skipping samples in the audio stream. Calling + // API functions with a nullptr buffer does not return. Since + // seeking in Opus files requires prefetching + skipping we + // need to skip sample frame by reading into a temporary + // buffer + pSampleBuffer = m_prefetchSampleBuffer.data(); + if (numberOfSamplesToRead > m_prefetchSampleBuffer.size()) { + numberOfSamplesToRead = m_prefetchSampleBuffer.size(); + } + } + const int readResult = op_read_float_stereo( + m_pOggOpusFile, pSampleBuffer, - numberOfFramesRemaining * 2); // stereo + numberOfSamplesToRead); if (0 < readResult) { m_curFrameIndex += readResult; - pSampleBuffer += readResult * 2; // stereo + pSampleBuffer += readResult * kChannelCountStereo; numberOfFramesRemaining -= readResult; } else { qWarning() << "Failed to read sample data from OggOpus file:" diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index 814faf49bc06..bdf58a00821f 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -1,15 +1,27 @@ #ifndef MIXXX_SOUNDSOURCEOPUS_H #define MIXXX_SOUNDSOURCEOPUS_H -#include "sources/soundsourceprovider.h" - #define OV_EXCLUDE_STATIC_CALLBACKS #include +#include "sources/soundsourceprovider.h" +#include "util/samplebuffer.h" + namespace mixxx { class SoundSourceOpus: public mixxx::SoundSource { public: + // According to the API documentation of op_pcm_seek(): + // "...decoding after seeking may not return exactly the same + // values as would be obtained by decoding the stream straight + // through. However, such differences are expected to be smaller + // than the loss introduced by Opus's lossy compression." + // This implementation internally uses prefetching to compensate + // those differences, although not completely. The following + // constant indicates the maximum expected difference for + // testing purposes. + static const CSAMPLE kMaxDecodingError; + explicit SoundSourceOpus(const QUrl& url); ~SoundSourceOpus() override; @@ -31,6 +43,8 @@ class SoundSourceOpus: public mixxx::SoundSource { OggOpusFile *m_pOggOpusFile; + SampleBuffer m_prefetchSampleBuffer; + SINT m_curFrameIndex; }; diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 7f7af0724e26..ef9f5ac794bf 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -6,7 +6,8 @@ namespace mixxx { SoundSourceSndFile::SoundSourceSndFile(const QUrl& url) : SoundSource(url), - m_pSndFile(nullptr) { + m_pSndFile(nullptr), + m_curFrameIndex(getMinFrameIndex()) { } SoundSourceSndFile::~SoundSourceSndFile() { @@ -56,14 +57,17 @@ SoundSource::OpenResult SoundSourceSndFile::tryOpen(const AudioSourceConfig& /*a setSamplingRate(sfInfo.samplerate); setFrameCount(sfInfo.frames); + m_curFrameIndex = getMinFrameIndex(); + return OpenResult::SUCCEEDED; } void SoundSourceSndFile::close() { - if (m_pSndFile) { + if (m_pSndFile != nullptr) { const int closeResult = sf_close(m_pSndFile); if (0 == closeResult) { m_pSndFile = nullptr; + m_curFrameIndex = getMinFrameIndex(); } else { qWarning() << "Failed to close file:" << closeResult << sf_strerror(m_pSndFile) @@ -74,10 +78,21 @@ void SoundSourceSndFile::close() { SINT SoundSourceSndFile::seekSampleFrame( SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + + if (frameIndex >= getMaxFrameIndex()) { + // EOF + m_curFrameIndex = getMaxFrameIndex(); + return m_curFrameIndex; + } + + if (frameIndex == m_curFrameIndex) { + return m_curFrameIndex; + } const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); if (0 <= seekResult) { + m_curFrameIndex = seekResult; return seekResult; } else { qWarning() << "Failed to seek libsnd file:" << seekResult @@ -88,9 +103,29 @@ SINT SoundSourceSndFile::seekSampleFrame( SINT SoundSourceSndFile::readSampleFrames( SINT numberOfFrames, CSAMPLE* sampleBuffer) { + DEBUG_ASSERT_AND_HANDLE(numberOfFrames >= 0) { + return 0; + } + if (numberOfFrames == 0) { + return 0; + } + if (sampleBuffer == nullptr) { + // NOTE(uklotzde): The libsndfile API does not provide any + // functions for skipping samples in the audio stream. Calling + // API functions with a nullptr buffer does not return. Since + // we don't want to read samples into a temporary buffer that + // has to be allocated we are seeking to the position after + // the skipped samples. + SINT curFrameIndexBefore = m_curFrameIndex; + SINT curFrameIndexAfter = seekSampleFrame(m_curFrameIndex + numberOfFrames); + DEBUG_ASSERT(curFrameIndexBefore <= curFrameIndexAfter); + DEBUG_ASSERT(m_curFrameIndex == curFrameIndexAfter); + return curFrameIndexAfter - curFrameIndexBefore; + } const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); if (0 <= readCount) { + m_curFrameIndex += readCount; return readCount; } else { qWarning() << "Failed to read from libsnd file:" << readCount diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index f02226570892..8ba1c3cd1340 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -29,6 +29,8 @@ class SoundSourceSndFile: public mixxx::SoundSource { OpenResult tryOpen(const AudioSourceConfig& audioSrcCfg) override; SNDFILE* m_pSndFile; + + SINT m_curFrameIndex; }; class SoundSourceProviderSndFile: public SoundSourceProvider { diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index c8be8bcffd02..b4b816db91a2 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -4,6 +4,7 @@ #include "track/trackmetadata.h" #include "sources/soundsourceproxy.h" +#include "sources/soundsourceopus.h" #include "test/mixxxtest.h" #include "util/samplebuffer.h" @@ -76,15 +77,9 @@ class SoundSourceProxyTest: public MixxxTest { const CSAMPLE* expected, const CSAMPLE* actual, const char* errorMessage) { - // According to API documentation of op_pcm_seek(): - // "...decoding after seeking may not return exactly the same - // values as would be obtained by decoding the stream straight - // through. However, such differences are expected to be smaller - // than the loss introduced by Opus's lossy compression." - const CSAMPLE kAcceptableOpusSeekDecodingError = 0.2f; - for (SINT i = 0; i < size; ++i) { - EXPECT_NEAR(expected[i], actual[i], kAcceptableOpusSeekDecodingError) << errorMessage; + EXPECT_NEAR(expected[i], actual[i], + mixxx::SoundSourceOpus::kMaxDecodingError) << errorMessage; } } }; @@ -134,7 +129,7 @@ TEST_F(SoundSourceProxyTest, seekForwardBackward) { for (const auto& filePath: getFilePaths()) { ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath)); - qDebug() << "Seek forward test:" << filePath; + qDebug() << "Seek forward/backward test:" << filePath; mixxx::AudioSourcePointer pContReadSource(openAudioSource(filePath)); // Obtaining an AudioSource may fail for unsupported file formats, @@ -144,9 +139,10 @@ TEST_F(SoundSourceProxyTest, seekForwardBackward) { // skip test file continue; } - const SINT readSampleCount = pContReadSource->frames2samples(kReadFrameCount); - SampleBuffer contReadData(readSampleCount); - SampleBuffer seekReadData(readSampleCount); + SampleBuffer contReadData( + pContReadSource->frames2samples(kReadFrameCount)); + SampleBuffer seekReadData( + pContReadSource->frames2samples(kReadFrameCount)); for (SINT contFrameIndex = 0; pContReadSource->isValidFrameIndex(contFrameIndex); @@ -211,3 +207,155 @@ TEST_F(SoundSourceProxyTest, seekForwardBackward) { } } } + +TEST_F(SoundSourceProxyTest, skipAndRead) { + const SINT kReadFrameCount = 1000; + + for (const auto& filePath: getFilePaths()) { + ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath)); + + qDebug() << "Skip and read test:" << filePath; + + mixxx::AudioSourcePointer pContReadSource(openAudioSource(filePath)); + // Obtaining an AudioSource may fail for unsupported file formats, + // even if the corresponding file extension is supported, e.g. + // AAC vs. ALAC in .m4a files + if (!pContReadSource) { + // skip test file + continue; + } + + mixxx::AudioSourcePointer pSkipReadSource(openAudioSource(filePath)); + ASSERT_FALSE(!pSkipReadSource); + ASSERT_EQ(pContReadSource->getChannelCount(), pSkipReadSource->getChannelCount()); + ASSERT_EQ(pContReadSource->getFrameCount(), pSkipReadSource->getFrameCount()); + + const SINT readSampleCount = pContReadSource->frames2samples(kReadFrameCount); + SampleBuffer contReadData(readSampleCount); + SampleBuffer skipReadData(readSampleCount); + + SINT frameIndex = mixxx::AudioSource::getMinFrameIndex(); + SINT contFrameIndex = mixxx::AudioSource::getMinFrameIndex(); + SINT skipFrameIndex = mixxx::AudioSource::getMinFrameIndex(); + SINT skipCount = 1; + while (pContReadSource->isValidFrameIndex(frameIndex += skipCount)) { + skipCount = frameIndex / 4 + 1; + + qDebug() << "Skipping to:" << frameIndex; + + // Read (and discard samples) until reaching the frame index + // and read next chunk + ASSERT_LE(contFrameIndex, frameIndex); + while (contFrameIndex < frameIndex) { + SINT readCount = std::min(frameIndex - contFrameIndex, kReadFrameCount); + contFrameIndex += pContReadSource->readSampleFrames(readCount, &contReadData[0]); + } + ASSERT_EQ(contFrameIndex, frameIndex); + const SINT contReadFrameCount = + pContReadSource->readSampleFrames(kReadFrameCount, &contReadData[0]); + contFrameIndex += contReadFrameCount; + + // Skip until reaching the frame index and read next chunk + ASSERT_LE(skipFrameIndex, frameIndex); + skipFrameIndex += + pSkipReadSource->skipSampleFrames(frameIndex - skipFrameIndex); + ASSERT_EQ(skipFrameIndex, frameIndex); + SINT skipReadFrameCount = + pSkipReadSource->readSampleFrames(kReadFrameCount, &skipReadData[0]); + skipFrameIndex += skipReadFrameCount; + + // Both buffers should be equal + ASSERT_EQ(contReadFrameCount, skipReadFrameCount); + if (filePath.endsWith(".opus")) { + expectDecodedSamplesEqualOpus( + pContReadSource->frames2samples(contReadFrameCount), + &contReadData[0], + &skipReadData[0], + "Decoding mismatch after skipping"); + } else { + expectDecodedSamplesEqual( + pContReadSource->frames2samples(contReadFrameCount), + &contReadData[0], + &skipReadData[0], + "Decoding mismatch after skipping"); + } + + frameIndex = contFrameIndex; + } + } +} + +TEST_F(SoundSourceProxyTest, seekBoundaries) { + const SINT kReadFrameCount = 1000; + + for (const auto& filePath: getFilePaths()) { + ASSERT_TRUE(SoundSourceProxy::isFileNameSupported(filePath)); + + qDebug() << "Seek boundaries test:" << filePath; + + mixxx::AudioSourcePointer pSeekReadSource(openAudioSource(filePath)); + // Obtaining an AudioSource may fail for unsupported file formats, + // even if the corresponding file extension is supported, e.g. + // AAC vs. ALAC in .m4a files + if (!pSeekReadSource) { + // skip test file + continue; + } + + // Seek to boundaries (alternating) + EXPECT_EQ(pSeekReadSource->getMinFrameIndex(), + pSeekReadSource->seekSampleFrame(pSeekReadSource->getMinFrameIndex())); +#ifdef __APPLE__ + EXPECT_EQ(pSeekReadSource->getMaxFrameIndex() - 1, + pSeekReadSource->seekSampleFrame(pSeekReadSource->getMaxFrameIndex() - 1)); +#else + // TODO(XXX): Seeking near the end of an MP3 stream + // is currently broken for SoundSourceMP3 (libmad) + if (filePath.endsWith(".mp3")) { + qWarning() + << "TODO(XXX): Fix seeking near end of stream for MP3 files" + << "and re-enable this test!"; + } else { + EXPECT_EQ(pSeekReadSource->getMaxFrameIndex() - 1, + pSeekReadSource->seekSampleFrame(pSeekReadSource->getMaxFrameIndex() - 1)); + } +#endif + EXPECT_EQ(pSeekReadSource->getMinFrameIndex() + 1, + pSeekReadSource->seekSampleFrame(pSeekReadSource->getMinFrameIndex() + 1)); + EXPECT_EQ(pSeekReadSource->getMaxFrameIndex(), + pSeekReadSource->seekSampleFrame(pSeekReadSource->getMaxFrameIndex())); + + // Seek to middle of the stream... + const SINT frameOffset = + (pSeekReadSource->getMaxFrameIndex() - pSeekReadSource->getMinFrameIndex()) / 2; + const SINT frameIndex = + mixxx::AudioSource::getMinFrameIndex() + frameOffset; + EXPECT_EQ(frameIndex, pSeekReadSource->seekSampleFrame(frameIndex)); + + // ...and verify read results + mixxx::AudioSourcePointer pContReadSource(openAudioSource(filePath)); + ASSERT_TRUE(pContReadSource); + ASSERT_EQ(frameOffset, pContReadSource->skipSampleFrames(frameOffset)); + SampleBuffer contReadData( + pContReadSource->frames2samples(kReadFrameCount)); + ASSERT_EQ(kReadFrameCount, + pContReadSource->readSampleFrames(kReadFrameCount, &contReadData[0])); + SampleBuffer seekReadData( + pSeekReadSource->frames2samples(kReadFrameCount)); + ASSERT_EQ(kReadFrameCount, + pSeekReadSource->readSampleFrames(kReadFrameCount, &seekReadData[0])); + if (filePath.endsWith(".opus")) { + expectDecodedSamplesEqualOpus( + pContReadSource->frames2samples(kReadFrameCount), + &contReadData[0], + &seekReadData[0], + "Decoding mismatch after seeking"); + } else { + expectDecodedSamplesEqual( + pContReadSource->frames2samples(kReadFrameCount), + &contReadData[0], + &seekReadData[0], + "Decoding mismatch after seeking"); + } + } +} diff --git a/src/util/audiosignal.cpp b/src/util/audiosignal.cpp index 2ae83d6470b5..751332f7d707 100644 --- a/src/util/audiosignal.cpp +++ b/src/util/audiosignal.cpp @@ -4,6 +4,27 @@ namespace mixxx { +// Separate definitions of static class constants are only required +// for LLVM CLang. Otherwise the linker complains about undefined +// symbols!? +#if defined(__clang__) +const SINT AudioSignal::kChannelCountZero; +const SINT AudioSignal::kChannelCountDefault; +const SINT AudioSignal::kChannelCountMono; +const SINT AudioSignal::kChannelCountMin; +const SINT AudioSignal::kChannelCountStereo; +const SINT AudioSignal::kChannelCountMax; +const SINT AudioSignal::kSamplingRateZero; +const SINT AudioSignal::kSamplingRateDefault; +const SINT AudioSignal::kSamplingRateMin; +const SINT AudioSignal::kSamplingRate32kHz; +const SINT AudioSignal::kSamplingRateCD; +const SINT AudioSignal::kSamplingRate48kHz; +const SINT AudioSignal::kSamplingRate96kHz; +const SINT AudioSignal::kSamplingRate192kHz; +const SINT AudioSignal::kSamplingRateMax; +#endif + bool AudioSignal::verifyReadable() const { bool result = true; if (!hasValidChannelCount()) {