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

Reworked beat_active control: #3608

Merged
merged 18 commits into from
Mar 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4719d5e
Reworked beat_active control:
JoergAtGithub Jan 30, 2021
143f255
Added switch, to select if findPrevNextBeats should compare with or w…
JoergAtGithub Feb 21, 2021
1e6d066
Added testcases for NoTolerance mode of findPrevNextBeats
JoergAtGithub Feb 21, 2021
f8c60f3
Merge remote-tracking branch 'upstream/main' into improve_beat_indicator
JoergAtGithub Feb 21, 2021
1a8f2c2
Improved if cases for NoTolerance code in findPrevNextBeats
JoergAtGithub Feb 21, 2021
d407101
Merge remote-tracking branch 'upstream/main' into improve_beat_indicator
JoergAtGithub Feb 22, 2021
92c48ce
Merge remote-tracking branch 'upstream/main' into improve_beat_indicator
JoergAtGithub Feb 23, 2021
5cb6696
Moved the 5 states into an internal state machine
JoergAtGithub Feb 27, 2021
dd1b2b1
Added the loop cases:
JoergAtGithub Feb 27, 2021
6adb30c
Removed getBeatContextNoLookup() by using findPrevNextBeats without t…
JoergAtGithub Mar 1, 2021
8b88974
Renamed NoTolerance to snapToNearBeats and inverted the bool, to matc…
JoergAtGithub Mar 1, 2021
93feabe
const -> constexpr
JoergAtGithub Mar 2, 2021
fd64411
Added ToDo comment about moving the updateIndicators to waveform upda…
JoergAtGithub Mar 2, 2021
c3493ea
Handled the cue point case. Don't show beat indication after (hot) cu…
JoergAtGithub Mar 5, 2021
d7ab0d0
Changed reverse beat indication state from -1 to +2
JoergAtGithub Mar 5, 2021
9a00a1e
Merge branch 'main' into improve_beat_indicator
JoergAtGithub Mar 5, 2021
f04c35a
pre-commit
JoergAtGithub Mar 5, 2021
277ed26
Improved responsiveness when pressing play/cue button at while track …
JoergAtGithub Mar 6, 2021
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
10 changes: 1 addition & 9 deletions src/engine/controls/bpmcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ bool BpmControl::getBeatContext(const mixxx::BeatsPointer& pBeats,

double dPrevBeat;
double dNextBeat;
if (!pBeats->findPrevNextBeats(dPosition, &dPrevBeat, &dNextBeat)) {
if (!pBeats->findPrevNextBeats(dPosition, &dPrevBeat, &dNextBeat, false)) {
daschuer marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

Expand Down Expand Up @@ -605,14 +605,6 @@ bool BpmControl::getBeatContextNoLookup(
if (dpBeatPercentage != nullptr) {
*dpBeatPercentage = dBeatLength == 0.0 ? 0.0 :
(dPosition - dPrevBeat) / dBeatLength;
// Because findNext and findPrev have an epsilon built in, sometimes
// the beat percentage is out of range. Fix it.
if (*dpBeatPercentage < 0) {
++*dpBeatPercentage;
}
if (*dpBeatPercentage > 1) {
--*dpBeatPercentage;
}
}

return true;
Expand Down
172 changes: 157 additions & 15 deletions src/engine/controls/clockcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,31 @@
#include "preferences/usersettings.h"
#include "track/track.h"

namespace {
constexpr double kBlinkInterval = 0.20; // LED is on 20% of the beat period
constexpr double kStandStillTolerance =
0.005; // (seconds) Minimum change, to he last evaluated position
constexpr double kSignificiantRateThreshold =
0.1; // If rate is significiant, update indicator also inside standstill tolerance
} // namespace

ClockControl::ClockControl(const QString& group, UserSettingsPointer pConfig)
: EngineControl(group, pConfig) {
m_pCOBeatActive = new ControlObject(ConfigKey(group, "beat_active"));
m_pCOBeatActive->set(0.0);
m_pCOSampleRate = new ControlProxy("[Master]","samplerate");
m_pCOBeatActive->setReadOnly();
m_pCOBeatActive->forceSet(0.0);
m_lastEvaluatedSample = 0;
m_PrevBeatSamples = 0;
m_InternalState = StateMachine::outsideIndicationArea;
m_NextBeatSamples = 0;
m_blinkIntervalSamples = 0;
m_pLoopEnabled = new ControlProxy(group, "loop_enabled", this);
m_pLoopStartPosition = new ControlProxy(group, "loop_start_position", this);
m_pLoopEndPosition = new ControlProxy(group, "loop_end_position", this);
}

ClockControl::~ClockControl() {
delete m_pCOBeatActive;
delete m_pCOSampleRate;
}

// called from an engine worker thread
Expand All @@ -30,27 +45,154 @@ void ClockControl::trackLoaded(TrackPointer pNewTrack) {

void ClockControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) {
// Clear on-beat control
m_pCOBeatActive->set(0.0);
m_pCOBeatActive->forceSet(0.0);
m_pBeats = pBeats;
}

void ClockControl::process(const double dRate,
const double currentSample,
const int iBuffersize) {
const double currentSample,
const int iBuffersize) {
Q_UNUSED(dRate);
Q_UNUSED(currentSample);
Q_UNUSED(iBuffersize);
double samplerate = m_pCOSampleRate->get();
}

// TODO(XXX) should this be customizable, or latency dependent?
const double blinkSeconds = 0.100;
void ClockControl::updateIndicators(const double dRate,
const double currentSample,
const double sampleRate) {
/* This method sets the control beat_active is set to the following values:
* 0.0 --> No beat indication (ouside 20% area or play direction changed while indication was on)
* 1.0 --> Forward playing, set at the beat and set back to 0.0 at 20% of beat distance
* 2.0 --> Reverse playing, set at the beat and set back to 0.0 at -20% of beat distance
*/

// Multiply by two to get samples from frames. Interval is scaled linearly
// by the rate.
const double blinkIntervalSamples = 2.0 * samplerate * (1.0 * dRate) * blinkSeconds;
// No position change since last indicator update (e.g. deck stopped) -> No indicator update needed
// The kSignificiantRateThreshold condition ensures an immidiate indicator update, when the play/cue button is pressed
if ((currentSample <= (m_lastEvaluatedSample + kStandStillTolerance * sampleRate)) &&
(currentSample >= (m_lastEvaluatedSample - kStandStillTolerance * sampleRate)) &&
(fabs(dRate) <= kSignificiantRateThreshold)) {
return;
}

// Position change more significiantly, but rate is zero. Occurs when pressing a cue point
// The m_InternalState needs to be taken into account here to prevent uneccessary events (state 0 -> state 0)
if ((dRate == 0.0) && (m_InternalState != StateMachine::outsideIndicationArea)) {
m_InternalState = StateMachine::outsideIndicationArea;
m_pCOBeatActive->forceSet(0.0);
}

double prevIndicatorSamples;
double nextIndicatorSamples;

const mixxx::BeatsPointer pBeats = m_pBeats;
if (pBeats) {
double closestBeat = pBeats->findClosestBeat(currentSample);
double distanceToClosestBeat = fabs(currentSample - closestBeat);
m_pCOBeatActive->set(distanceToClosestBeat < blinkIntervalSamples / 2.0);
if ((currentSample >= m_NextBeatSamples) ||
(currentSample <= m_PrevBeatSamples)) {
pBeats->findPrevNextBeats(currentSample,
&m_PrevBeatSamples,
&m_NextBeatSamples,
false); // Precise compare without tolerance needed
}
} else {
m_PrevBeatSamples = -1;
m_NextBeatSamples = -1;
}

// Loops need special handling
if (m_pLoopEnabled->toBool()) {
const double loop_start_position = m_pLoopStartPosition->get();
const double loop_end_position = m_pLoopEndPosition->get();

if ((m_PrevBeatSamples < loop_start_position) && (m_NextBeatSamples >= loop_end_position)) {
// No beat inside loop -> show beat indication at loop_start_position
daschuer marked this conversation as resolved.
Show resolved Hide resolved
prevIndicatorSamples = loop_start_position;
nextIndicatorSamples = loop_end_position;
} else {
prevIndicatorSamples = m_PrevBeatSamples;
nextIndicatorSamples = m_NextBeatSamples;
}

if ((m_PrevBeatSamples != -1) && (m_NextBeatSamples != -1)) {
// Don't overwrite interval at begin/end of track
if ((loop_end_position - loop_start_position) <
(m_NextBeatSamples - m_PrevBeatSamples)) {
// Loops smaller than beat distance -> Set m_blinkIntervalSamples based on loop period
m_blinkIntervalSamples =
(loop_end_position - loop_start_position) *
kBlinkInterval;
} else {
m_blinkIntervalSamples =
(nextIndicatorSamples - prevIndicatorSamples) *
kBlinkInterval;
}
}
} else {
prevIndicatorSamples = m_PrevBeatSamples;
nextIndicatorSamples = m_NextBeatSamples;

if ((prevIndicatorSamples != -1) && (nextIndicatorSamples != -1)) {
// Don't overwrite interval at begin/end of track
m_blinkIntervalSamples =
(nextIndicatorSamples - prevIndicatorSamples) *
kBlinkInterval;
}
}

// The m_InternalState needs to be taken into account, to show a reliable beat indication for loops
if (dRate > 0.0) {
if (m_lastPlayDirection == true) {
if ((currentSample > prevIndicatorSamples) &&
(currentSample <
prevIndicatorSamples + m_blinkIntervalSamples) &&
(m_InternalState != StateMachine::afterBeatActive) &&
(m_InternalState != StateMachine::afterBeatDirectionChanged)) {
m_InternalState = StateMachine::afterBeatActive;
m_pCOBeatActive->forceSet(1.0);
} else if ((currentSample > prevIndicatorSamples +
m_blinkIntervalSamples) &&
((m_InternalState == StateMachine::afterBeatActive) ||
(m_InternalState == StateMachine::afterBeatDirectionChanged))) {
m_InternalState = StateMachine::outsideIndicationArea;
m_pCOBeatActive->forceSet(0.0);
}
} else {
// Play direction changed while beat indicator was on and forward playing
if ((currentSample < nextIndicatorSamples) &&
(currentSample >=
nextIndicatorSamples - m_blinkIntervalSamples) &&
(m_InternalState != StateMachine::beforeBeatDirectionChanged)) {
m_InternalState = StateMachine::beforeBeatDirectionChanged;
m_pCOBeatActive->forceSet(0.0);
}
}
m_lastPlayDirection = true; // Forward
} else if (dRate < 0.0) {
if (m_lastPlayDirection == false) {
if ((currentSample < nextIndicatorSamples) &&
(currentSample >
nextIndicatorSamples - m_blinkIntervalSamples) &&
(m_InternalState != StateMachine::beforeBeatActive) &&
(m_InternalState != StateMachine::beforeBeatDirectionChanged)) {
m_InternalState = StateMachine::beforeBeatActive;
m_pCOBeatActive->forceSet(2.0);
} else if ((currentSample < nextIndicatorSamples -
m_blinkIntervalSamples) &&
((m_InternalState == StateMachine::beforeBeatActive) ||
(m_InternalState == StateMachine::beforeBeatDirectionChanged))) {
m_InternalState = StateMachine::outsideIndicationArea;
m_pCOBeatActive->forceSet(0.0);
}
} else {
// Play direction changed while beat indicator was on and reverse playing
if ((currentSample > prevIndicatorSamples) &&
(currentSample <=
prevIndicatorSamples + m_blinkIntervalSamples) &&
(m_InternalState != StateMachine::afterBeatDirectionChanged)) {
m_InternalState = StateMachine::afterBeatDirectionChanged;
m_pCOBeatActive->forceSet(0.0);
}
}
m_lastPlayDirection = false; // Reverse
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer

Suggested change
}
} else {
m_pCOBeatActive->forceSet(0.0);
}

Because I experience that the LED is sometimes on and sometimes off when paused at a cue point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look how to solve the cue point issue. The proposed else branch will not do this. It will never be reached, because this is captured before, to not switch the indicator off due the jitter of the DVS pitch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied a fix for this, which should not break the use case of searching the beat position using DVS or jog. Please test, if this solves the cue point issue for you too.

m_lastEvaluatedSample = currentSample;
}
34 changes: 33 additions & 1 deletion src/engine/controls/clockcontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,44 @@ class ClockControl: public EngineControl {
void process(const double dRate, const double currentSample,
const int iBufferSize) override;

void updateIndicators(const double dRate,
const double currentSample,
const double sampleRate);

void trackLoaded(TrackPointer pNewTrack) override;
void trackBeatsUpdated(mixxx::BeatsPointer pBeats) override;

private:
ControlObject* m_pCOBeatActive;
ControlProxy* m_pCOSampleRate;

// ControlObjects that come from LoopingControl
ControlProxy* m_pLoopEnabled;
ControlProxy* m_pLoopStartPosition;
ControlProxy* m_pLoopEndPosition;

double m_lastEvaluatedSample;

enum class StateMachine : int {
afterBeatDirectionChanged =
2, /// Direction changed to reverse playing while forward playing indication was on
afterBeatActive =
1, /// Forward playing, set at the beat and set back to 0.0 at 20% of beat distance
outsideIndicationArea =
0, /// Outside -20% ... +20% of the beat distance
beforeBeatActive =
-1, /// Reverse playing, set at the beat and set back to 0.0 at -20% of beat distance
beforeBeatDirectionChanged =
-2 /// Direction changed to forward playing while reverse playing indication was on
};

StateMachine m_InternalState;

double m_PrevBeatSamples;
double m_NextBeatSamples;
double m_blinkIntervalSamples;

// True is forward direction, False is reverse
bool m_lastPlayDirection;

// m_pBeats is written from an engine worker thread
mixxx::BeatsPointer m_pBeats;
Expand Down
2 changes: 1 addition & 1 deletion src/engine/controls/loopingcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable
// The closest beat might be ahead of play position and will cause a catching loop.
double prevBeat;
double nextBeat;
pBeats->findPrevNextBeats(currentSample, &prevBeat, &nextBeat);
pBeats->findPrevNextBeats(currentSample, &prevBeat, &nextBeat, true);

if (m_pQuantizeEnabled->toBool() && prevBeat != -1) {
double beatLength = nextBeat - prevBeat;
Expand Down
2 changes: 1 addition & 1 deletion src/engine/controls/quantizecontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ void QuantizeControl::lookupBeatPositions(double dCurrentSample) {
mixxx::BeatsPointer pBeats = m_pBeats;
if (pBeats) {
double prevBeat, nextBeat;
pBeats->findPrevNextBeats(dCurrentSample, &prevBeat, &nextBeat);
pBeats->findPrevNextBeats(dCurrentSample, &prevBeat, &nextBeat, true);
m_pCOPrevBeat->set(prevBeat);
m_pCONextBeat->set(nextBeat);
}
Expand Down
6 changes: 6 additions & 0 deletions src/engine/enginebuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,12 @@ void EngineBuffer::updateIndicators(double speed, int iBufferSize) {
(double)iBufferSize / m_trackSamplesOld,
fractionalPlayposFromAbsolute(m_dSlipPosition),
tempoTrackSeconds);

// TODO: Especially with long audio buffers, jitter is visible. This can be fixed by moving the
// ClockControl::updateIndicators into the waveform update loop which is synced with the display refresh rate.
// Via the visual play position it's possible to access to the sample that is currently played,
// and not the one that have been processed as in the current solution.
m_pClockControl->updateIndicators(speed * m_baserate_old, m_filepos_play, m_pSampleRate->get());
}

void EngineBuffer::hintReader(const double dRate) {
Expand Down
28 changes: 24 additions & 4 deletions src/test/beatgridtest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ TEST(BeatGridTest, TestNthBeatWhenOnBeat) {

// Also test prev/next beat calculation.
double prevBeat, nextBeat;
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat);
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat, true);
EXPECT_NEAR(position, prevBeat, kMaxBeatError);
EXPECT_NEAR(position + beatLength, nextBeat, kMaxBeatError);

// Also test prev/next beat calculation without snaping tolerance
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat, false);
EXPECT_NEAR(position, prevBeat, kMaxBeatError);
EXPECT_NEAR(position + beatLength, nextBeat, kMaxBeatError);

Expand Down Expand Up @@ -112,10 +117,15 @@ TEST(BeatGridTest, TestNthBeatWhenOnBeat_BeforeEpsilon) {

// Also test prev/next beat calculation.
double prevBeat, nextBeat;
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat);
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat, true);
EXPECT_NEAR(kClosestBeat, prevBeat, kMaxBeatError);
EXPECT_NEAR(kClosestBeat + beatLength, nextBeat, kMaxBeatError);

// Also test prev/next beat calculation without snaping tolerance
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat, false);
EXPECT_NEAR(kClosestBeat - beatLength, prevBeat, kMaxBeatError);
EXPECT_NEAR(kClosestBeat, nextBeat, kMaxBeatError);

// Both previous and next beat should return the closest beat.
EXPECT_NEAR(kClosestBeat, pGrid->findNextBeat(position), kMaxBeatError);
EXPECT_NEAR(kClosestBeat, pGrid->findPrevBeat(position), kMaxBeatError);
Expand Down Expand Up @@ -148,7 +158,12 @@ TEST(BeatGridTest, TestNthBeatWhenOnBeat_AfterEpsilon) {

// Also test prev/next beat calculation.
double prevBeat, nextBeat;
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat);
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat, true);
EXPECT_NEAR(kClosestBeat, prevBeat, kMaxBeatError);
EXPECT_NEAR(kClosestBeat + beatLength, nextBeat, kMaxBeatError);

// Also test prev/next beat calculation without snaping tolerance
pGrid->findPrevNextBeats(position, &prevBeat, &nextBeat, false);
EXPECT_NEAR(kClosestBeat, prevBeat, kMaxBeatError);
EXPECT_NEAR(kClosestBeat + beatLength, nextBeat, kMaxBeatError);

Expand Down Expand Up @@ -185,7 +200,12 @@ TEST(BeatGridTest, TestNthBeatWhenNotOnBeat) {

// Also test prev/next beat calculation
double foundPrevBeat, foundNextBeat;
pGrid->findPrevNextBeats(position, &foundPrevBeat, &foundNextBeat);
pGrid->findPrevNextBeats(position, &foundPrevBeat, &foundNextBeat, true);
EXPECT_NEAR(previousBeat, foundPrevBeat, kMaxBeatError);
EXPECT_NEAR(nextBeat, foundNextBeat, kMaxBeatError);

// Also test prev/next beat calculation without snaping tolerance
pGrid->findPrevNextBeats(position, &foundPrevBeat, &foundNextBeat, false);
EXPECT_NEAR(previousBeat, foundPrevBeat, kMaxBeatError);
EXPECT_NEAR(nextBeat, foundNextBeat, kMaxBeatError);
}
Expand Down
Loading