Skip to content

Commit

Permalink
Merge pull request #2252 from uklotzde/adjust_audio_stream_range
Browse files Browse the repository at this point in the history
Analysis: Adjust audio stream length/range if inaccurate or on errors
  • Loading branch information
daschuer authored Oct 8, 2019
2 parents aab0496 + b3a1af8 commit 2944e2f
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 97 deletions.
107 changes: 68 additions & 39 deletions src/analyzer/analyzerthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ AnalyzerThread::AnalyzerThread(
m_pConfig(std::move(pConfig)),
m_modeFlags(modeFlags),
m_nextTrack(MpscFifoConcurrency::SingleProducer),
m_sampleBuffer(mixxx::kAnalysisSamplesPerBlock),
m_sampleBuffer(mixxx::kAnalysisSamplesPerChunk),
m_emittedState(AnalyzerThreadState::Void) {
std::call_once(registerMetaTypesOnceFlag, registerMetaTypesOnce);
}
Expand Down Expand Up @@ -154,8 +154,7 @@ void AnalyzerThread::doRun() {
if (processTrack) {
const auto analysisResult = analyzeAudioSource(audioSource);
DEBUG_ASSERT(analysisResult != AnalysisResult::Pending);
if ((analysisResult == AnalysisResult::Complete) ||
(analysisResult == AnalysisResult::Partial)) {
if (analysisResult == AnalysisResult::Finished) {
// The analysis has been finished, and is either complete without
// any errors or partial if it has been aborted due to a corrupt
// audio file. In both cases don't reanalyze tracks during this
Expand Down Expand Up @@ -224,75 +223,105 @@ AnalyzerThread::AnalysisResult AnalyzerThread::analyzeAudioSource(

mixxx::AudioSourceStereoProxy audioSourceProxy(
audioSource,
mixxx::kAnalysisFramesPerBlock);
mixxx::kAnalysisFramesPerChunk);
DEBUG_ASSERT(audioSourceProxy.channelCount() == mixxx::kAnalysisChannels);

// Analysis starts now
emitBusyProgress(kAnalyzerProgressNone);

mixxx::IndexRange remainingFrames = audioSource->frameIndexRange();
auto result = remainingFrames.empty() ? AnalysisResult::Complete : AnalysisResult::Pending;
while (result == AnalysisResult::Pending) {
DEBUG_ASSERT(!remainingFrames.empty());
mixxx::IndexRange remainingFrameRange = audioSource->frameIndexRange();
while (!remainingFrameRange.empty()) {
sleepWhileSuspended();
if (isStopping()) {
return AnalysisResult::Cancelled;
}

// 1st step: Decode next chunk of audio data
const auto inputFrameIndexRange =
remainingFrames.splitAndShrinkFront(
math_min(mixxx::kAnalysisFramesPerBlock, remainingFrames.length()));
DEBUG_ASSERT(!inputFrameIndexRange.empty());

// Split the range for the next chunk from the remaining (= to-be-analyzed) frames
auto chunkFrameRange =
remainingFrameRange.splitAndShrinkFront(
math_min(mixxx::kAnalysisFramesPerChunk, remainingFrameRange.length()));
DEBUG_ASSERT(!chunkFrameRange.empty());

// Request the next chunk of audio data
const auto readableSampleFrames =
audioSourceProxy.readSampleFrames(
mixxx::WritableSampleFrames(
inputFrameIndexRange,
chunkFrameRange,
mixxx::SampleBuffer::WritableSlice(m_sampleBuffer)));
// The returned range fits into the requested range
DEBUG_ASSERT(readableSampleFrames.frameIndexRange() <= chunkFrameRange);

// Sometimes the duration of the audio source is inaccurate and adjusted
// while reading. We need to adjust all frame ranges to reflect this new
// situation by restoring all invariants and consistency requirements!

// Shrink the original range of the current chunks to the actual available
// range.
chunkFrameRange = intersect(chunkFrameRange, audioSourceProxy.frameIndexRange());
// The audio data that has just been read should still fit into the adjusted
// chunk range.
DEBUG_ASSERT(readableSampleFrames.frameIndexRange() <= chunkFrameRange);

// We also need to adjust the remaining frame range for the next requests.
remainingFrameRange = intersect(remainingFrameRange, audioSourceProxy.frameIndexRange());
// Currently the range will never grow, but lets also account for this case
// that might become relevant in the future.
VERIFY_OR_DEBUG_ASSERT(remainingFrameRange.empty() ||
remainingFrameRange.end() == audioSourceProxy.frameIndexRange().end()) {
if (chunkFrameRange.length() < mixxx::kAnalysisFramesPerChunk) {
// If we have read an incomplete chunk while the range has grown
// we need to discard the read results and re-read the current
// chunk!
remainingFrameRange = span(remainingFrameRange, chunkFrameRange);
continue;
}
DEBUG_ASSERT(remainingFrameRange.end() < audioSourceProxy.frameIndexRange().end());
kLogger.warning()
<< "Unexpected growth of the audio source while reading"
<< mixxx::IndexRange::forward(
remainingFrameRange.end(), audioSourceProxy.frameIndexRange().end());
remainingFrameRange.growBack(
audioSourceProxy.frameIndexRange().end() - remainingFrameRange.end());
}

sleepWhileSuspended();
if (isStopping()) {
return AnalysisResult::Cancelled;
}

// 2nd: step: Analyze chunk of decoded audio data
if (readableSampleFrames.frameLength() == mixxx::kAnalysisFramesPerBlock ||
remainingFrames.empty()) {
// Complete chunk of audio samples has been read for analysis
if (!readableSampleFrames.frameIndexRange().empty()) {
for (auto&& analyzer : m_analyzers) {
analyzer.processSamples(
readableSampleFrames.readableData(),
readableSampleFrames.readableLength());
}
if (remainingFrames.empty()) {
result = AnalysisResult::Complete;
}
} else {
// Partial chunk of audio samples has been read, but not the final.
// A decoding error must have occurred, maybe a corrupt file?
kLogger.warning()
<< "Aborting analysis after failure to read sample data:"
<< "expected frames =" << inputFrameIndexRange
<< ", actual frames =" << readableSampleFrames.frameIndexRange();
result = AnalysisResult::Partial;
}

// Don't check again for paused/stopped and simply finish the
// current iteration by emitting progress.
// Don't check again for paused/stopped again and simply finish
// the current iteration by emitting progress.

// 3rd step: Update & emit progress
const double frameProgress =
double(audioSource->frameLength() - remainingFrames.length()) /
double(audioSource->frameLength());
const AnalyzerProgress progress =
frameProgress *
(kAnalyzerProgressFinalizing - kAnalyzerProgressNone);
DEBUG_ASSERT(progress > kAnalyzerProgressNone);
DEBUG_ASSERT(progress <= kAnalyzerProgressFinalizing);
emitBusyProgress(progress);
if (audioSource->frameLength() > 0) {
const double frameProgress =
double(audioSource->frameLength() - remainingFrameRange.length()) /
double(audioSource->frameLength());
const AnalyzerProgress progress =
frameProgress *
(kAnalyzerProgressFinalizing - kAnalyzerProgressNone);
DEBUG_ASSERT(progress > kAnalyzerProgressNone);
DEBUG_ASSERT(progress <= kAnalyzerProgressFinalizing);
emitBusyProgress(progress);
} else {
// Unreadable audio source
DEBUG_ASSERT(remainingFrameRange.empty());
emitBusyProgress(kAnalyzerProgressUnknown);
}
}

return result;
return AnalysisResult::Finished;
}

void AnalyzerThread::emitBusyProgress(AnalyzerProgress busyProgress) {
Expand Down
3 changes: 1 addition & 2 deletions src/analyzer/analyzerthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ class AnalyzerThread : public WorkerThread {

enum class AnalysisResult {
Pending,
Partial,
Complete,
Finished,
Cancelled,
};
AnalysisResult analyzeAudioSource(
Expand Down
6 changes: 3 additions & 3 deletions src/analyzer/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace mixxx {
// seems to do fine. Signal processing during analysis uses the same,
// fixed number of channels like the engine does, usually 2 = stereo.
constexpr mixxx::AudioSignal::ChannelCount kAnalysisChannels = mixxx::kEngineChannelCount;
constexpr SINT kAnalysisFramesPerBlock = 4096;
constexpr SINT kAnalysisSamplesPerBlock =
kAnalysisFramesPerBlock * kAnalysisChannels;
constexpr SINT kAnalysisFramesPerChunk = 4096;
constexpr SINT kAnalysisSamplesPerChunk =
kAnalysisFramesPerChunk * kAnalysisChannels;

// Only analyze the first minute in fast-analysis mode.
constexpr int kFastAnalysisSecondsToAnalyze = 60;
Expand Down
2 changes: 1 addition & 1 deletion src/analyzer/plugins/analyzersoundtouchbeats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace mixxx {

AnalyzerSoundTouchBeats::AnalyzerSoundTouchBeats()
: m_downmixBuffer(kAnalysisFramesPerBlock),
: m_downmixBuffer(kAnalysisFramesPerChunk), // mono, i.e. 1 sample per frame
m_fResultBpm(0.0f) {
}

Expand Down
3 changes: 2 additions & 1 deletion src/engine/cachingreader/cachingreaderchunk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames(
mixxx::WritableSampleFrames(
sourceFrameIndexRange,
mixxx::SampleBuffer::WritableSlice(m_sampleBuffer)));
DEBUG_ASSERT(m_bufferedSampleFrames.frameIndexRange() <= sourceFrameIndexRange);
DEBUG_ASSERT(m_bufferedSampleFrames.frameIndexRange().empty() ||
m_bufferedSampleFrames.frameIndexRange() <= sourceFrameIndexRange);
return m_bufferedSampleFrames.frameIndexRange();
}

Expand Down
55 changes: 24 additions & 31 deletions src/engine/cachingreader/cachingreaderworker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,48 +37,42 @@ ReaderStatusUpdate CachingReaderWorker::processReadRequest(
// Before trying to read any data we need to check if the audio source
// is available and if any audio data that is needed by the chunk is
// actually available.
const auto chunkFrameIndexRange = pChunk->frameIndexRange(m_pAudioSource);
if (intersect(chunkFrameIndexRange, m_readableFrameIndexRange).empty()) {
auto chunkFrameIndexRange = pChunk->frameIndexRange(m_pAudioSource);
DEBUG_ASSERT(!m_pAudioSource ||
chunkFrameIndexRange <= m_pAudioSource->frameIndexRange());
if (chunkFrameIndexRange.empty()) {
ReaderStatusUpdate result;
result.init(CHUNK_READ_INVALID, pChunk, m_readableFrameIndexRange);
result.init(CHUNK_READ_INVALID, pChunk,
m_pAudioSource ? m_pAudioSource->frameIndexRange() : mixxx::IndexRange());
return result;
}

// Try to read the data required for the chunk from the audio source
// and adjust the max. readable frame index if decoding errors occur.
const mixxx::IndexRange bufferedFrameIndexRange = pChunk->bufferSampleFrames(
m_pAudioSource,
mixxx::SampleBuffer::WritableSlice(m_tempReadBuffer));
DEBUG_ASSERT(!m_pAudioSource ||
bufferedFrameIndexRange <= m_pAudioSource->frameIndexRange());
// The readable frame range might have changed
chunkFrameIndexRange = intersect(chunkFrameIndexRange, m_pAudioSource->frameIndexRange());
DEBUG_ASSERT(bufferedFrameIndexRange.empty() ||
bufferedFrameIndexRange <= chunkFrameIndexRange);

ReaderStatus status = bufferedFrameIndexRange.empty() ? CHUNK_READ_EOF : CHUNK_READ_SUCCESS;
if (chunkFrameIndexRange != bufferedFrameIndexRange) {
if (bufferedFrameIndexRange != chunkFrameIndexRange) {
kLogger.warning()
<< m_group
<< "Failed to read chunk samples for frame index range:"
<< "actual =" << bufferedFrameIndexRange
<< ", expected =" << chunkFrameIndexRange;
<< "expected =" << chunkFrameIndexRange
<< ", actual =" << bufferedFrameIndexRange;
if (bufferedFrameIndexRange.empty()) {
// Adjust upper bound: Consider all audio data following
// the read position until the end as unreadable
m_readableFrameIndexRange.shrinkBack(m_readableFrameIndexRange.end() - chunkFrameIndexRange.start());
status = CHUNK_READ_INVALID; // not EOF (see above)
} else {
// Adjust lower bound of readable audio data
if (chunkFrameIndexRange.start() < bufferedFrameIndexRange.start()) {
m_readableFrameIndexRange.shrinkFront(bufferedFrameIndexRange.start() - m_readableFrameIndexRange.start());
}
// Adjust upper bound of readable audio data
if (chunkFrameIndexRange.end() > bufferedFrameIndexRange.end()) {
m_readableFrameIndexRange.shrinkBack(m_readableFrameIndexRange.end() - bufferedFrameIndexRange.end());
}
status = CHUNK_READ_INVALID; // overwrite EOF (see above)
}
kLogger.warning()
<< "Readable frames in audio source reduced to"
<< m_readableFrameIndexRange
<< "from originally"
<< m_pAudioSource->frameIndexRange();
}

ReaderStatusUpdate result;
result.init(status, pChunk, m_readableFrameIndexRange);
result.init(status, pChunk,
m_pAudioSource ? m_pAudioSource->frameIndexRange() : mixxx::IndexRange());
return result;
}

Expand Down Expand Up @@ -130,7 +124,6 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) {
}

// Unload the track
m_readableFrameIndexRange = mixxx::IndexRange();
m_pAudioSource.reset(); // Close open file handles

if (!pTrack) {
Expand Down Expand Up @@ -175,8 +168,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) {
// Initially assume that the complete content offered by audio source
// is available for reading. Later if read errors occur this value will
// be decreased to avoid repeated reading of corrupt audio data.
m_readableFrameIndexRange = m_pAudioSource->frameIndexRange();
if (m_readableFrameIndexRange.empty()) {
if (m_pAudioSource->frameIndexRange().empty()) {
m_pAudioSource.reset(); // Close open file handles
kLogger.warning()
<< m_group
Expand All @@ -197,13 +189,14 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) {
}

const auto update =
ReaderStatusUpdate::trackLoaded(m_readableFrameIndexRange);
ReaderStatusUpdate::trackLoaded(
m_pAudioSource->frameIndexRange());
m_pReaderStatusFIFO->writeBlocking(&update, 1);

// Emit that the track is loaded.
const SINT sampleCount =
CachingReaderChunk::frames2samples(
m_readableFrameIndexRange.length());
m_pAudioSource->frameLength());
emit trackLoaded(pTrack, m_pAudioSource->sampleRate(), sampleCount);
}

Expand Down
7 changes: 0 additions & 7 deletions src/engine/cachingreader/cachingreaderworker.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,6 @@ class CachingReaderWorker : public EngineWorker {
// before conversion to a stereo signal.
mixxx::SampleBuffer m_tempReadBuffer;

// The maximum readable frame index of the AudioSource. Might
// be adjusted when decoding errors occur to prevent reading
// the same chunk(s) over and over again.
// This frame index references the frame that follows the
// last frame with readable sample data.
mixxx::IndexRange m_readableFrameIndexRange;

QAtomicInt m_stop;
};

Expand Down
58 changes: 58 additions & 0 deletions src/sources/audiosource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,62 @@ WritableSampleFrames AudioSource::clampWritableSampleFrames(
frames2samples(writableFrameIndexRange.length())));
}

ReadableSampleFrames AudioSource::readSampleFrames(
WritableSampleFrames sampleFrames) {
const auto writable =
clampWritableSampleFrames(sampleFrames);
if (writable.frameIndexRange().empty()) {
// result is empty
return ReadableSampleFrames(writable.frameIndexRange());
} else {
// forward clamped request
ReadableSampleFrames readable = readSampleFramesClamped(writable);
DEBUG_ASSERT(readable.frameIndexRange().empty() ||
readable.frameIndexRange() <= writable.frameIndexRange());
if (readable.frameIndexRange() != writable.frameIndexRange()) {
kLogger.warning()
<< "Failed to read sample frames:"
<< "expected =" << writable.frameIndexRange()
<< ", actual =" << readable.frameIndexRange();
auto shrinkedFrameIndexRange = m_frameIndexRange;
if (readable.frameIndexRange().empty()) {
// Adjust upper bound: Consider all audio data following
// the read position until the end as unreadable
shrinkedFrameIndexRange.shrinkBack(
shrinkedFrameIndexRange.end() - writable.frameIndexRange().start());
} else {
// Adjust lower bound of readable audio data
if (writable.frameIndexRange().start() < readable.frameIndexRange().start()) {
shrinkedFrameIndexRange.shrinkFront(
readable.frameIndexRange().start() - shrinkedFrameIndexRange.start());
}
// Adjust upper bound of readable audio data
if (writable.frameIndexRange().end() > readable.frameIndexRange().end()) {
shrinkedFrameIndexRange.shrinkBack(
shrinkedFrameIndexRange.end() - readable.frameIndexRange().end());
}
}
DEBUG_ASSERT(shrinkedFrameIndexRange < m_frameIndexRange);
kLogger.info()
<< "Shrinking readable frame index range:"
<< "before =" << m_frameIndexRange
<< ", after =" << shrinkedFrameIndexRange;
// Propagate the adjustments to all participants in the
// inheritance hierarchy.
// NOTE(2019-08-31, uklotzde): This is an ugly hack to overcome
// the previous assumption that the frame index range is immutable
// for the whole lifetime of an AudioSource. As we know now it is
// not and for a future re-design we need to account for this fact!!
adjustFrameIndexRange(shrinkedFrameIndexRange);
}
return readable;
}
}

void AudioSource::adjustFrameIndexRange(
IndexRange frameIndexRange) {
DEBUG_ASSERT(frameIndexRange <= m_frameIndexRange);
m_frameIndexRange = frameIndexRange;
}

} // namespace mixxx
Loading

0 comments on commit 2944e2f

Please sign in to comment.