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

Enable beatloop activation directly after activating a hotcue #2190

Closed
wants to merge 8 commits into from
Closed

Enable beatloop activation directly after activating a hotcue #2190

wants to merge 8 commits into from

Conversation

goddisignz
Copy link
Contributor

@goddisignz goddisignz commented Jun 24, 2019

Fxes https://bugs.launchpad.net/mixxx/+bug/1464003 .
Basically the loopcontroller has to set the loop start position to the queued seek position before activating the loop.
Furthermore, when a hotcue that is at the end of an active loop is activated, also disable the loop instead of jumping back to the loop entry.

@@ -958,6 +960,10 @@ void LoopingControl::updateBeatLoopingControls() {
}

void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable) {

// if a seek was queued in the engine buffer wait until it is executed
while(getEngineBuffer()->isSeekQueued()){}
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't look sane to me. A busy wait on a condition that is controlled by another thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I expected something like this but was not sure which other mechanism to use. Just tell me what would me the right one and I will adapt my solution.

Copy link
Member

Choose a reason for hiding this comment

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

A straight forward solution is to predict the effect of the scheduled seek here.

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 you need to make the
LoopSamples loopSamples = m_loopSamples.getValue(); conditional.
If there is a cued seek, you need to use m_queuedSeekPosition.getValue().

By luck you can ignore the quantize flag. If not you need to adjust the loop along with the m_queuedSeekPosition.

* fixes https://bugs.launchpad.net/mixxx/+bug/1464003
* furthermore, when a hotcue that is at the end of an active loop is activated, also disable the loop instead of jumping back to the loop entry
Copy link
Member

@daschuer daschuer left a comment

Choose a reason for hiding this comment

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

Thank you for your update.
I have added some comments.

Does it work as expected now?

It would be great to have a unit test for this case.
Can you had one? Do you know how?


// if a seek was queued in the engine buffer move the current sample to its position
ControlValueAtomic<double> seekPosition;
if(getEngineBuffer()->isSeekQueued(seekPosition)){
Copy link
Member

Choose a reason for hiding this comment

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

Please use git-clang-format HEAD~1 to fix some code style issues.
https://www.mixxx.org/wiki/doku.php/coding_guidelines#command_line

setNewPlaypos(position, false);
adjustingPhase = true;
}
setNewPlaypos(syncPosition, adjustingPhase);
Copy link
Member

Choose a reason for hiding this comment

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

The double setNewPlaypos() call reads a bit like a hack. Can we add for example a new parameter to identify the situation explicit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, this is more like a workaround but as I am still new to the project I didn't want to change parameters of already existing methods. We could have a seekedPosition and syncedPosition in setNewPlaypos and notifySeek and let the controller decide what to do with this information. How about that?

@@ -1284,6 +1296,14 @@ bool EngineBuffer::isTrackLoaded() {
return false;
}

bool EngineBuffer::isSeekQueued(ControlValueAtomic<double> &seekPos) {
Copy link
Member

Choose a reason for hiding this comment

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

we use only const references in Mixxx. I think we can use here a "double* pSeekPosition" as second return value.

@@ -1284,6 +1296,14 @@ bool EngineBuffer::isTrackLoaded() {
return false;
}

bool EngineBuffer::isSeekQueued(ControlValueAtomic<double> &seekPos) {
if(!m_iSeekQueued){
Copy link
Member

Choose a reason for hiding this comment

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

Is this check redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, was.

@@ -958,6 +959,14 @@ void LoopingControl::updateBeatLoopingControls() {
}

void LoopingControl::slotBeatLoop(double beats, bool keepStartPoint, bool enable) {

// if a seek was queued in the engine buffer move the current sample to its position
ControlValueAtomic<double> seekPosition;
Copy link
Member

Choose a reason for hiding this comment

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

we can use here a plain double, because it lives on the stack and is not used concurrently.

@goddisignz
Copy link
Contributor Author

goddisignz commented Jun 30, 2019

I will make changes and recommit.
It works as expected except in one case. If the synced position is in front of an active loop the playposition does not end up in front of the end of the loop. I would expect it to be like that.
Not sure if the loopcontroller can take care of that currently.

}

// seek finished after setNewPlaypos
m_iSeekQueued.fetchAndStoreRelaxed(SEEK_NONE);
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you able to justify that the reordering of atomic memory operations still works as expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I couldn't. But it this line won't be necessary anymore.

Copy link
Member

@daschuer daschuer left a comment

Choose a reason for hiding this comment

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

Some more comments. We are getting close. Thank you.

@@ -1125,7 +1125,7 @@ void EngineBuffer::processSeek(bool paused) {
// call anyway again.

SeekRequests seekType = static_cast<SeekRequest>(
m_iSeekQueued.fetchAndStoreRelease(SEEK_NONE));
m_iSeekQueued.fetchAndStoreRelaxed(SEEK_NONE));
Copy link
Member

Choose a reason for hiding this comment

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

here wee need to be sure that m_queuedSeekPosition.getValue() is not accessed before reading and writing m_iSeekQueued so I think fetchAndSubAcquire() would be the right call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That must be an autocomplete-mistake. I wanted to change it back to the original version.

Copy link
Member

Choose a reason for hiding this comment

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

But it was originally wrong. We need the Aquire memory fence here, to stop the pipeline executing the reading of the seek position too early.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I will change that. But which value should I subtract?
Are you sure fetchAndSubAcquire shouldn't be fetchAndStoreAcquire?

Copy link
Member

Choose a reason for hiding this comment

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

Ups ... of cause.

@@ -768,7 +769,7 @@ void LoopingControl::slotLoopEndPos(double pos) {
}

// This is called from the engine thread
void LoopingControl::notifySeek(double dNewPlaypos, bool adjustingPhase) {
void LoopingControl::notifySeek(double dNewPlaypos, double dSyncedNewPlayPos, bool adjustingPhase) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you clarify and finally ad a code comment why, we need to pass two positions here?

I am unsure, if we still need the adjustingPhase value. Is it only read here? Original it was to prevent jump out of a loop when adjusting phase.
Now we have the original and the adjusted position to detect this.

What was the case, you need this code for?
What are the cases, we need to considder?

Can you rename dNewPlayposition to
something more descriptive?
Like dRequestedPlaypos or such?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Assume an active loop starting exactly at hotcue A. If I activate the hotcue the synced position is very likely in front of the loop. The adjustingPhase is still false, as it is was a SEEK_STANDARD_PHASE. If we set the adjustingPhase to true in that case, the loop would not be disabled for any other seek position outside the loop.
Thus, the loopcontroller should know about the position requested by the user but the remaining controllers should still be notified with the synced position.

I assume that the adjustPhase is still necessary. When there was no sync, both positions are equal. But (I guess) the same holds true if only a sync without any seeking is requested.

Copy link
Contributor Author

@goddisignz goddisignz Jul 2, 2019

Choose a reason for hiding this comment

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

I just had a thought on solving this case differently in the processSeek method. Let the loopingcontroller calculate a position inside a loop (if enabled) based on the requested and the synced position. If the requested position is outside the loop return the synced position and behave as currently. If the requested position is inside but the synced position is outside the loop, correct the synced position into the loop.

double syncPosition = position;
if (!paused && (seekType & SEEK_PHASE)) {
    syncPosition = m_pBpmControl->getNearestPositionInPhase(position, true, true);
    position = m_pLoopingControl->getSyncPositionInsideLoop(position, syncPosition);
}

The setNewPos and notifySeek could be left unchanged.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, even better readability. Thank you.

But why do we need syncPosition outside the if block?
I think it would be even more clean to have a requested position as well and not reuse position for this.

Copy link
Member

@daschuer daschuer left a comment

Choose a reason for hiding this comment

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

some more comments.

LoopSamples loopSamples = m_loopSamples.getValue();

// if the request itself is outside loop do nothing
// loop will be disabled later by notifySeek(...)
Copy link
Member

Choose a reason for hiding this comment

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

or does it make sense to have the loop disable logic here?


// the synced position is in front of the loop
// adjust the synced position to same amount in front of the loop end
if (dSyncedPlayPos <= loopSamples.start) {
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 it should be just <

// the synced position is in front of the loop
// adjust the synced position to same amount in front of the loop end
if (dSyncedPlayPos <= loopSamples.start) {
return loopSamples.end - (loopSamples.start - dSyncedPlayPos);
Copy link
Member

Choose a reason for hiding this comment

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

this can still be outside the loop in case the loop is only a fraction of a beat.

Copy link
Contributor Author

@goddisignz goddisignz Jul 5, 2019

Choose a reason for hiding this comment

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

You're right, but easy to solve.
BTW: This whole method only works on beatloops (N-bar or fraction of N). Should we also care about loops which are non-beatloops or just ignore that case?

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 it is independent from where loop in and out are set.

We may have this condition:
B____I__O
B=beat
I=loop in
O= loop out

We need only make sure that the time between. B and I is rolled of in the loop in a way that the requested point in the loop is reached as if the seek had been done to B.

So yes, we can ignore the case. The user is responsible to use a beat loop if he wishes the loop is in beat.

// the synced position is behind the loop
// adjust the synced position to same amount behind the loop start
if (dSyncedPlayPos >= loopSamples.end) {
return loopSamples.start + (dSyncedPlayPos - loopSamples.end);
Copy link
Member

Choose a reason for hiding this comment

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

the same here, this can still be behind the loop in case of small loops.

}

if (position != m_filepos_play) {
setNewPlaypos(position, adjustingPhase);
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 we can now get rid of the adjustingPhase flag, because the position is always inside the loop now.

@Holzhaus Holzhaus mentioned this pull request Jul 9, 2019
5 tasks
@goddisignz
Copy link
Contributor Author

I will use create a new PR to fix https://bugs.launchpad.net/mixxx/+bug/1778246 in the 2.1 release.
The remaining changes will be put to a new PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants