Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify the "first sound" #4773

Merged
merged 30 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
81f817a
Verify the first sound and issue a warning in mixxx.log
daschuer May 28, 2022
416746f
Use kSilenceThreshold directly and add a coment that it must not be c…
daschuer May 29, 2022
b299836
Use SINT as Length parameter of Analyzer::processSamples()
daschuer May 29, 2022
3708324
Extract static functions to find the first and the last sound
daschuer May 29, 2022
2ba9fe3
Move verifyFirstSound to the AnalyzerSilence class
daschuer May 30, 2022
dd4342a
Initalize nTrackSampleDataLength
daschuer May 31, 2022
9111c7b
Move writing the mixxx.log entries out of AnalyzerSilence::verifyFirs…
daschuer Jun 5, 2022
837e879
Rename function to findFirstSoundInChunk()/findLasttSoundInChunk()
daschuer Jun 5, 2022
d410c06
Refactor silence detections that the last sound is processed backwards
daschuer Jun 5, 2022
d342d6c
Check return value of findFirstSoundInChunk() explicit
daschuer Jun 5, 2022
b94f788
Fix comment for findLastSoundInChunk()
daschuer Jun 5, 2022
2a9fbca
Explain why the uncoditinal verifyFirstSound() should be replaced
daschuer Sep 5, 2022
407dc21
Restore TODO
daschuer Sep 5, 2022
828b6db
reduce nesting
daschuer Sep 5, 2022
43faef4
Use kNumSoundFrameToVerify and explain why two frames are sufficient
daschuer Sep 6, 2022
4cfb776
Merge remote-tracking branch 'upstream/main' into offset_detect
daschuer Sep 6, 2022
d993c03
Apply std::span refactoring
daschuer Sep 6, 2022
eb0339f
AnalyzerGain: Removed unused member variable
daschuer Sep 7, 2022
438f8ea
More SINT in analyzers
daschuer Sep 7, 2022
969cddb
Merge remote-tracking branch 'upstream/main' into offset_detect
daschuer Sep 8, 2022
af1cb4b
Merge remote-tracking branch 'upstream/main' into offset_detect
daschuer Oct 28, 2022
c5317b0
fixup math
daschuer Dec 10, 2022
4a3b059
Rename AudibleSound to 60dBSound to emphasize that this cue point has…
daschuer Dec 10, 2022
8012a2b
Avoid long lines in comments
daschuer Dec 11, 2022
6fcd9e3
Merge remote-tracking branch 'upstream/main' into offset_detect
daschuer Dec 11, 2022
6844798
Add line break after "requires"
daschuer Dec 12, 2022
a8e80b0
Merge remote-tracking branch 'upstream/main' into offset_detect
daschuer Dec 12, 2022
d937a08
Added test AnalyzerSilenceTest,verifyFirstSound
daschuer Dec 13, 2022
96ed941
Added a comment about the temprary nature of thes first sound verific…
daschuer Dec 13, 2022
9f99531
findLastSoundInChunk() returns samples.size() if no sample is found
daschuer Dec 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/analyzer/analyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Analyzer {
// 3. If the initialization failed log the internal error and return false.
virtual bool initialize(const AnalyzerTrack& tio,
mixxx::audio::SampleRate sampleRate,
int totalSamples) = 0;
SINT totalSamples) = 0;

/////////////////////////////////////////////////////////////////////////
// All following methods will only be invoked after initialize()
Expand All @@ -36,7 +36,7 @@ class Analyzer {
// If processing fails the analysis can be aborted early by returning
// false. After aborting the analysis only cleanup() will be invoked,
// but not finalize()!
virtual bool processSamples(const CSAMPLE* pIn, const int iLen) = 0;
virtual bool processSamples(const CSAMPLE* pIn, SINT iLen) = 0;

// Update the track object with the analysis results after
// processing finished successfully, i.e. all available audio
Expand Down Expand Up @@ -77,7 +77,7 @@ class AnalyzerWithState final {
return m_active = m_analyzer->initialize(tio, sampleRate, totalSamples);
}

void processSamples(const CSAMPLE* pIn, const int iLen) {
void processSamples(const CSAMPLE* pIn, SINT iLen) {
if (m_active) {
m_active = m_analyzer->processSamples(pIn, iLen);
if (!m_active) {
Expand Down
4 changes: 2 additions & 2 deletions src/analyzer/analyzerbeats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ AnalyzerBeats::AnalyzerBeats(UserSettingsPointer pConfig, bool enforceBpmDetecti

bool AnalyzerBeats::initialize(const AnalyzerTrack& track,
mixxx::audio::SampleRate sampleRate,
int totalSamples) {
SINT totalSamples) {
if (totalSamples == 0) {
return false;
}
Expand Down Expand Up @@ -197,7 +197,7 @@ bool AnalyzerBeats::shouldAnalyze(TrackPointer pTrack) const {
return true;
}

bool AnalyzerBeats::processSamples(const CSAMPLE *pIn, const int iLen) {
bool AnalyzerBeats::processSamples(const CSAMPLE* pIn, SINT iLen) {
VERIFY_OR_DEBUG_ASSERT(m_pPlugin) {
return false;
}
Expand Down
8 changes: 4 additions & 4 deletions src/analyzer/analyzerbeats.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class AnalyzerBeats : public Analyzer {

bool initialize(const AnalyzerTrack& track,
mixxx::audio::SampleRate sampleRate,
int totalSamples) override;
bool processSamples(const CSAMPLE *pIn, const int iLen) override;
SINT totalSamples) override;
bool processSamples(const CSAMPLE* pIn, SINT iLen) override;
void storeResults(TrackPointer tio) override;
void cleanup() override;

Expand All @@ -50,6 +50,6 @@ class AnalyzerBeats : public Analyzer {

mixxx::audio::SampleRate m_sampleRate;
SINT m_totalSamples;
int m_iMaxSamplesToProcess;
int m_iCurrentSample;
SINT m_iMaxSamplesToProcess;
SINT m_iCurrentSample;
};
4 changes: 2 additions & 2 deletions src/analyzer/analyzerebur128.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ AnalyzerEbur128::~AnalyzerEbur128() {

bool AnalyzerEbur128::initialize(const AnalyzerTrack& tio,
mixxx::audio::SampleRate sampleRate,
int totalSamples) {
SINT totalSamples) {
if (m_rgSettings.isAnalyzerDisabled(2, tio.getTrack()) || totalSamples == 0) {
qDebug() << "Skipping AnalyzerEbur128";
return false;
Expand All @@ -43,7 +43,7 @@ void AnalyzerEbur128::cleanup() {
}
}

bool AnalyzerEbur128::processSamples(const CSAMPLE *pIn, const int iLen) {
bool AnalyzerEbur128::processSamples(const CSAMPLE* pIn, SINT iLen) {
VERIFY_OR_DEBUG_ASSERT(m_pState) {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/analyzer/analyzerebur128.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class AnalyzerEbur128 : public Analyzer {

bool initialize(const AnalyzerTrack& track,
mixxx::audio::SampleRate sampleRate,
int totalSamples) override;
bool processSamples(const CSAMPLE* pIn, const int iLen) override;
SINT totalSamples) override;
bool processSamples(const CSAMPLE* pIn, SINT iLen) override;
void storeResults(TrackPointer tio) override;
void cleanup() override;

Expand Down
12 changes: 5 additions & 7 deletions src/analyzer/analyzergain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
#include "util/timer.h"

AnalyzerGain::AnalyzerGain(UserSettingsPointer pConfig)
: m_rgSettings(pConfig),
m_iBufferSize(0) {
: m_rgSettings(pConfig) {
m_pReplayGain = new ReplayGain();
}

Expand All @@ -22,7 +21,7 @@ AnalyzerGain::~AnalyzerGain() {

bool AnalyzerGain::initialize(const AnalyzerTrack& tio,
mixxx::audio::SampleRate sampleRate,
int totalSamples) {
SINT totalSamples) {
if (m_rgSettings.isAnalyzerDisabled(1, tio.getTrack()) || totalSamples == 0) {
qDebug() << "Skipping AnalyzerGain";
return false;
Expand All @@ -34,14 +33,13 @@ bool AnalyzerGain::initialize(const AnalyzerTrack& tio,
void AnalyzerGain::cleanup() {
}

bool AnalyzerGain::processSamples(const CSAMPLE *pIn, const int iLen) {
bool AnalyzerGain::processSamples(const CSAMPLE* pIn, SINT iLen) {
ScopedTimer t("AnalyzerGain::process()");

int halfLength = static_cast<int>(iLen / 2);
if (halfLength > m_iBufferSize) {
SINT halfLength = static_cast<int>(iLen / 2);
if (halfLength > static_cast<SINT>(m_pLeftTempBuffer.size())) {
m_pLeftTempBuffer.resize(halfLength);
m_pRightTempBuffer.resize(halfLength);
m_iBufferSize = halfLength;
}
SampleUtil::deinterleaveBuffer(m_pLeftTempBuffer.data(),
m_pRightTempBuffer.data(),
Expand Down
5 changes: 2 additions & 3 deletions src/analyzer/analyzergain.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ class AnalyzerGain : public Analyzer {

bool initialize(const AnalyzerTrack& track,
mixxx::audio::SampleRate sampleRate,
int totalSamples) override;
bool processSamples(const CSAMPLE* pIn, const int iLen) override;
SINT totalSamples) override;
bool processSamples(const CSAMPLE* pIn, SINT iLen) override;
void storeResults(TrackPointer tio) override;
void cleanup() override;

Expand All @@ -35,5 +35,4 @@ class AnalyzerGain : public Analyzer {
std::vector<CSAMPLE> m_pLeftTempBuffer;
std::vector<CSAMPLE> m_pRightTempBuffer;
ReplayGain* m_pReplayGain;
int m_iBufferSize;
};
4 changes: 2 additions & 2 deletions src/analyzer/analyzerkey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ AnalyzerKey::AnalyzerKey(const KeyDetectionSettings& keySettings)

bool AnalyzerKey::initialize(const AnalyzerTrack& tio,
mixxx::audio::SampleRate sampleRate,
int totalSamples) {
SINT totalSamples) {
if (totalSamples == 0) {
return false;
}
Expand Down Expand Up @@ -151,7 +151,7 @@ bool AnalyzerKey::shouldAnalyze(TrackPointer tio) const {
return true;
}

bool AnalyzerKey::processSamples(const CSAMPLE *pIn, const int iLen) {
bool AnalyzerKey::processSamples(const CSAMPLE* pIn, SINT iLen) {
VERIFY_OR_DEBUG_ASSERT(m_pPlugin) {
return false;
}
Expand Down
6 changes: 3 additions & 3 deletions src/analyzer/analyzerkey.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class AnalyzerKey : public Analyzer {

bool initialize(const AnalyzerTrack& tio,
mixxx::audio::SampleRate sampleRate,
int totalSamples) override;
bool processSamples(const CSAMPLE *pIn, const int iLen) override;
SINT totalSamples) override;
bool processSamples(const CSAMPLE* pIn, SINT iLen) override;
void storeResults(TrackPointer tio) override;
void cleanup() override;

Expand All @@ -39,7 +39,7 @@ class AnalyzerKey : public Analyzer {
int m_iSampleRate;
int m_iTotalSamples;
int m_iMaxSamplesToProcess;
int m_iCurrentSample;
SINT m_iCurrentSample;

bool m_bPreferencesKeyDetectionEnabled;
bool m_bPreferencesFastAnalysisEnabled;
Expand Down
97 changes: 58 additions & 39 deletions src/analyzer/analyzersilence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,42 @@

namespace {

constexpr CSAMPLE kSilenceThreshold = 0.001f;
// This threshold must not be changed, because this value is also used to
// verify that the track samples have not changed since the last analysis
constexpr CSAMPLE kSilenceThreshold = 0.001f; // -60 dB
// TODO: Change the above line to:
//constexpr CSAMPLE kSilenceThreshold = db2ratio(-60.0f);

bool shouldAnalyze(TrackPointer pTrack) {
CuePointer pIntroCue = pTrack->findCueByType(mixxx::CueType::Intro);
CuePointer pOutroCue = pTrack->findCueByType(mixxx::CueType::Outro);
CuePointer pAudibleSound = pTrack->findCueByType(mixxx::CueType::AudibleSound);
CuePointer pN60dBSound = pTrack->findCueByType(mixxx::CueType::N60dBSound);

if (!pIntroCue || !pOutroCue || !pAudibleSound || pAudibleSound->getLengthFrames() <= 0) {
if (!pIntroCue || !pOutroCue || !pN60dBSound || pN60dBSound->getLengthFrames() <= 0) {
return true;
}
return false;
}

template<typename Iterator>
Iterator first_sound(Iterator begin, Iterator end) {
return std::find_if(begin, end, [](const auto elem) {
return fabs(elem) >= kSilenceThreshold;
});
}

} // anonymous namespace

AnalyzerSilence::AnalyzerSilence(UserSettingsPointer pConfig)
: m_pConfig(pConfig),
m_fThreshold(kSilenceThreshold),
m_iFramesProcessed(0),
m_bPrevSilence(true),
m_iSignalStart(-1),
m_iSignalEnd(-1) {
}

bool AnalyzerSilence::initialize(const AnalyzerTrack& track,
mixxx::audio::SampleRate sampleRate,
int totalSamples) {
SINT totalSamples) {
Q_UNUSED(sampleRate);
Q_UNUSED(totalSamples);

Expand All @@ -44,34 +51,53 @@ bool AnalyzerSilence::initialize(const AnalyzerTrack& track,
}

m_iFramesProcessed = 0;
m_bPrevSilence = true;
m_iSignalStart = -1;
m_iSignalEnd = -1;

return true;
}

bool AnalyzerSilence::processSamples(const CSAMPLE* pIn, const int iLen) {
for (int i = 0; i < iLen; i += mixxx::kAnalysisChannels) {
// Compute max of channels in this sample frame
CSAMPLE fMax = CSAMPLE_ZERO;
for (SINT ch = 0; ch < mixxx::kAnalysisChannels; ++ch) {
CSAMPLE fAbs = fabs(pIn[i + ch]);
fMax = math_max(fMax, fAbs);
}
// static
SINT AnalyzerSilence::findFirstSoundInChunk(std::span<const CSAMPLE> samples) {
return std::distance(samples.begin(), first_sound(samples.begin(), samples.end()));
}

bool bSilence = fMax < m_fThreshold;
// static
SINT AnalyzerSilence::findLastSoundInChunk(std::span<const CSAMPLE> samples) {
// -1 is required, because the distance from the fist sample index (0) to crend() is 1,
SINT ret = std::distance(first_sound(samples.rbegin(), samples.rend()), samples.rend()) - 1;
if (ret == -1) {
ret = samples.size();
}
return ret;
}

if (m_bPrevSilence && !bSilence) {
if (m_iSignalStart < 0) {
m_iSignalStart = m_iFramesProcessed + i / mixxx::kAnalysisChannels;
}
} else if (!m_bPrevSilence && bSilence) {
m_iSignalEnd = m_iFramesProcessed + i / mixxx::kAnalysisChannels;
}
// static
bool AnalyzerSilence::verifyFirstSound(
std::span<const CSAMPLE> samples,
mixxx::audio::FramePos firstSoundFrame) {
const SINT firstSoundSample = findFirstSoundInChunk(samples);
if (firstSoundSample < static_cast<SINT>(samples.size())) {
return mixxx::audio::FramePos::fromEngineSamplePos(firstSoundSample) == firstSoundFrame;
}
return false;
}

m_bPrevSilence = bSilence;
bool AnalyzerSilence::processSamples(const CSAMPLE* pIn, SINT iLen) {
std::span<const CSAMPLE> samples = mixxx::spanutil::spanFromPtrLen(pIn, iLen);
if (m_iSignalStart < 0) {
const SINT firstSoundSample = findFirstSoundInChunk(samples);
if (firstSoundSample < iLen) {
m_iSignalStart = m_iFramesProcessed + firstSoundSample / mixxx::kAnalysisChannels;
}
}
if (m_iSignalStart >= 0) {
const SINT lastSoundSample = findLastSoundInChunk(samples);
if (lastSoundSample < iLen - 1) { // not only sound or silence
m_iSignalEnd = m_iFramesProcessed + lastSoundSample / mixxx::kAnalysisChannels + 1;
}
}

m_iFramesProcessed += iLen / mixxx::kAnalysisChannels;
return true;
}
Expand All @@ -87,30 +113,23 @@ void AnalyzerSilence::storeResults(TrackPointer pTrack) {
m_iSignalEnd = m_iFramesProcessed;
}

// If track didn't end with silence, place signal end marker
// on the end of the track.
if (!m_bPrevSilence) {
m_iSignalEnd = m_iFramesProcessed;
}

const auto firstSoundPosition = mixxx::audio::FramePos(m_iSignalStart);
const auto lastSoundPosition = mixxx::audio::FramePos(m_iSignalEnd);

CuePointer pAudibleSound = pTrack->findCueByType(mixxx::CueType::AudibleSound);
if (pAudibleSound == nullptr) {
pAudibleSound = pTrack->createAndAddCue(
mixxx::CueType::AudibleSound,
CuePointer pN60dBSound = pTrack->findCueByType(mixxx::CueType::N60dBSound);
if (pN60dBSound == nullptr) {
pN60dBSound = pTrack->createAndAddCue(
mixxx::CueType::N60dBSound,
Cue::kNoHotCue,
firstSoundPosition,
lastSoundPosition);
} else {
// The user has no way to directly edit the AudibleSound cue. If the user
// The user has no way to directly edit the N60dBSound cue. If the user
// has deleted the Intro or Outro Cue, this analysis will be rerun when
// the track is loaded again. In this case, adjust the AudibleSound Cue's
// the track is loaded again. In this case, adjust the N60dBSound Cue's
// positions. This could be helpful, for example, when the track length
// is changed in a different program, or the silence detection threshold
// is changed.
pAudibleSound->setStartAndEndPosition(firstSoundPosition, lastSoundPosition);
// is changed in a different program.
pN60dBSound->setStartAndEndPosition(firstSoundPosition, lastSoundPosition);
}

CuePointer pIntroCue = pTrack->findCueByType(mixxx::CueType::Intro);
Expand Down
29 changes: 22 additions & 7 deletions src/analyzer/analyzersilence.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

#include "analyzer/analyzer.h"
#include "analyzer/analyzertrack.h"
#include "audio/frame.h"
#include "preferences/usersettings.h"
#include "util/span.h"

class CuePointer;

Expand All @@ -13,16 +15,29 @@ class AnalyzerSilence : public Analyzer {

bool initialize(const AnalyzerTrack& track,
mixxx::audio::SampleRate sampleRate,
int totalSamples) override;
bool processSamples(const CSAMPLE* pIn, const int iLen) override;
SINT totalSamples) override;
bool processSamples(const CSAMPLE* pIn, SINT iLen) override;
void storeResults(TrackPointer pTrack) override;
void cleanup() override;

/// returns the index of the first sample in the buffer that is above -60 dB
/// or samples.size() if no sample is found
static SINT findFirstSoundInChunk(std::span<const CSAMPLE> samples);

/// returns the index of the last sample in the buffer that is above -60 dB
/// or samples.size() if no sample is found
static SINT findLastSoundInChunk(std::span<const CSAMPLE> samples);

/// Returns true if the first sound if found at the given frame and logs a
/// warning message if not. This can be uses to detect changes since the
/// last analysis run and is an indicator for file edits or decoder
/// changes/issues
static bool verifyFirstSound(std::span<const CSAMPLE> samples,
mixxx::audio::FramePos firstSoundFrame);

private:
UserSettingsPointer m_pConfig;
CSAMPLE m_fThreshold;
int m_iFramesProcessed;
bool m_bPrevSilence;
int m_iSignalStart;
int m_iSignalEnd;
SINT m_iFramesProcessed;
SINT m_iSignalStart;
SINT m_iSignalEnd;
};
Loading