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

KeyControl: fix keylock/unlock bugs, reset pitch_adjust #4710

Merged
merged 10 commits into from
Dec 7, 2023

Conversation

ronso0
Copy link
Member

@ronso0 ronso0 commented Apr 3, 2022

Fixes unintented pitch shift when un/locking repeatedly with "lock original key" and "unlock, keep current key", see #4710 (comment) by resetting pitch_adjust when unlocking to linear pitch and locking to original key.

  • adjust pitchRoundtrip test
  • add test for "Unlock, keep current key" (or include it in pitchRoundtrip test)

@ronso0 ronso0 marked this pull request as draft April 3, 2022 19:30
@ronso0
Copy link
Member Author

ronso0 commented Apr 3, 2022

back to draft because I am also looking into another issue with key un/lock and out-of-bounds rate changes (pitch/key can not be reset easily)

@ronso0
Copy link
Member Author

ronso0 commented Apr 5, 2022

The other issue I'm attempting to fix is this:
When I lock and unlock the key often, with rate changes in between, during a set my pitch != 0.0 indicator flashes even though I just restored the original key. Now I finally managed to reproduce this with a script, and also manually with the GUI +/- buttons.
Appearantly the crucial part is to move the rate slider outside the GUI/controller range
Edit and use reversed pitch slider.
MIDI-Through test script: https://gist.github.com/ronso0/7cc9e421ded49e19e1c47192cfe7d523

  • Preferences:
    • Keylock: lock current key
    • Keyunlock: keep current key
    • rate range: 8%, slider down increases rate
  • Procedure:
    • deck1 keylock OFF, rate centered, no prior rate or key adjustments on that deck!
    • load track
    • move rate slider out of bounds (via Sync or scripting engine.setValue(group, "rate", 1.1))
    • lock key
    • reset key to original with reset_key (pitch = 0.0)
    • move rate slider a bit
    • unlock key (pitch should still be 0.0)
    • pitch is something like 1,735221e-17

See 0a6d00d
I don't know how to avoid the calculation round-trip of m_pitchRateInfo members in the first place without making updateRate() even more complex. Hope this solution is okay.

@ronso0 ronso0 added this to the 2.3.3 milestone Apr 8, 2022
@ronso0
Copy link
Member Author

ronso0 commented Apr 23, 2022

ping. Any more comments?

@ronso0
Copy link
Member Author

ronso0 commented Apr 23, 2022

I've just split the last commit into two to make it easier to review.

I'll also manual testing again with all 4 keylock/unlock combinations.

@ronso0 ronso0 marked this pull request as ready for review April 23, 2022 21:47
Comment on lines 14 to 15
#define ENABLE_DEBUG_OUTPUT false

Copy link
Member

Choose a reason for hiding this comment

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

since C++17, conditional compilation is usually better done using if constexpr instead

Suggested change
#define ENABLE_DEBUG_OUTPUT false
constexpr bool kEnableDebugOutput = true;

Copy link
Member

Choose a reason for hiding this comment

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

The difference is that the code in #if is ignored by the compiler, the code in "if constexpr" is checked and optimized away later.

Copy link
Member

Choose a reason for hiding this comment

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

True, but it doesn't make much of a difference in terms of what ends up in the binary... Metaprogramming using the C++ preprocessor is discouraged from (in C++) AFAIK.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alright, thanks!

Copy link
Member

Choose a reason for hiding this comment

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

The preprocessor variant is not type safe. But the preprocessor has the benefit, that you can define these macros from outside (compiler commandline call). But here the define is in the source file.

Comment on lines 259 to 285
// If we just unlocked the original key with speed != +-1.0 we may encounter wrong
// decimals (e.g. 1 - 1.73123e-09) after pitchRatio calculation round-trip.
// Round to 1.0 to avoid false positive pitch offset (pitchOctaves != 0).
double pitchRatioDiffTo1 = fabs(1.0 - m_pitchRateInfo.pitchRatio);
if (0.0 < pitchRatioDiffTo1 && pitchRatioDiffTo1 < pow(10, -9)) { // 0.000000001
m_pitchRateInfo.pitchRatio = 1.0;
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm struggling to understand the reasoning behind this because I struggle to understand the surrounding code. m_pitchRateInfo.pitchRatio is basically imprecise because of rounding errors? But only around 1.0?

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically roundtrip rounding erros, yes.
Tbh I didn't manage to understand how all the ratios relate in all situations either because I'm still confused by the naming, but with debug output enabled it's at least easy to trace how the m_pitchRateInfo values change. In #4710 (comment) I outlined the circumstances and the procedure to reproduce the imprecision.

Copy link
Member

Choose a reason for hiding this comment

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

The issue is that the new statement in line 216 is designed to make m_pitchRateInfo.pitchRatio = 1, but it is done indirect via other variables. It would be straight forward and more easy to understand to set it 1 explicit. This would require more changes in the code though.

Comment on lines 259 to 285
// If we just unlocked the original key with speed != +-1.0 we may encounter wrong
// decimals (e.g. 1 - 1.73123e-09) after pitchRatio calculation round-trip.
// Round to 1.0 to avoid false positive pitch offset (pitchOctaves != 0).
double pitchRatioDiffTo1 = fabs(1.0 - m_pitchRateInfo.pitchRatio);
if (0.0 < pitchRatioDiffTo1 && pitchRatioDiffTo1 < pow(10, -9)) { // 0.000000001
m_pitchRateInfo.pitchRatio = 1.0;
}
Copy link
Member

Choose a reason for hiding this comment

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

The issue is that the new statement in line 216 is designed to make m_pitchRateInfo.pitchRatio = 1, but it is done indirect via other variables. It would be straight forward and more easy to understand to set it 1 explicit. This would require more changes in the code though.

@ronso0
Copy link
Member Author

ronso0 commented Apr 26, 2022

Uff, there is another bug:
(best experienced with src/test/sine-30.wav : )

  • keylock: lock original key
  • keyunlock: keep current key
  • keylock disabled, enable repeat for continous stream
  1. play, change rate to like +4%, no manual key change
  2. lock key => key reset to default, pitch = 0.0
  3. unlock key => no key change, as expected with keep current key
  4. lock key => key lowered, should reset to original key + pitch slider offset
  5. repeating 3. and 4. will lower the key step by step

I'll go with

It would be straight forward and more easy to understand to set it 1 explicit. This would require more changes in the code though.

in order to rebase this whole thing.

@ronso0
Copy link
Member Author

ronso0 commented Apr 28, 2022

Uff, there is another bug: (best experienced with src/test/sine-30.wav : )

  • keylock: lock original key
  • keyunlock: keep current key
  • keylock disabled, enable repeat for continous stream
  1. play, change rate to like +4%, no manual key change
  2. lock key => key reset to default, pitch = 0.0
  3. unlock key => no key change, as expected with keep current key
  4. lock key => key lowered, should reset to original key + pitch slider offset
  5. repeating 3. and 4. will lower the key step by step

When I fix that bug by setting both pitchRatio and pitchTweakRatio manually for each keylock/unlock and locked/unlocked case...
all manual tests succeed, Mixxx behaves as expected.
BUT the Pitchroundtrip test fails here:

Note: Google Test filter = EngineBufferTest.PitchRoundtrip
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from EngineBufferTest
[ RUN      ] EngineBufferTest.PitchRoundtrip
/home/ronso/Downloads/mixxx_source/src/master/src/test/enginebuffertest.cpp:75: Failure
Expected equality of these values:
  0.5
  ControlObject::get(ConfigKey(m_sGroup1, "pitch"))
    Which is: 0

The test assumes that pitch_adjust remains at x.y even if the original key is locked.

@daschuer Unfortunately some rules aren't clear to me from the code comments / manual (pitch_adjust) and I'd appreciate if you could clarify:
How is the pitch_adjust knob supposed to behave?
If we assume that user have that mapped to a rotary knob, should the hardware knob always stay in sync with the internal state even when magic buttons are pressed?
Unfotunately the current behaviour is inconsistent:

  • the knob is static when I "lock at original pitch" after pitch_adjustments (though it doesn't reset to original key, for that I'd need to reset the adjust knob, too)
  • the knob moves when I "unlock, keep current key"
  • the knob moves when I press "match key" or "reset key"

This is very confusing.
Should keylock/unlock be treated differently than match/reset?

To be clear, in this stable banch I don't necessarily want to refactor updateRate to fix that bug and make the test still pass / meet established user expectations.

I don't want to open a can of worms and refactor the stable branch just to fix that bug (and make tests still pass).

hardware knob always stay in sync with the internal state

Edit
We could say the knob should move if there is an audible change (pitch changes):

  • lock original pitch
  • unlock, reset to original pitch
  • match key
  • reset key

Edit 2
If we assume someone has pitch_adjust mapped to a knob softTakeover should be activated because pitch_adjust can jump when using official controls.
The only official mapping in 2.3 that uses pitch_adjust (Traktor Kontrol S3) has softTakeover activated. There is also the Roland DJ-505 mapping which has a Pitch Play mode, but that doesn't take the actual pitch_adjust value into account anyway.
Therefore I consider it low-risk to change the behaviour, even if that could affects users with custom mappings.

@daschuer
Copy link
Member

daschuer commented May 4, 2022

I have just tested with the 2.3 and the control is implemented on top of keylock. It is not changed if the pitch is adjusted by keylock. For my understanding this behavior is desired, because you will touch the control likely to adjust the pitch from what you have currently.
Scenario:

  • Adjust the tempo from matching speed
  • Hit Keylock to avoid chipmunk voices
  • Adjust the pitch for a matching key.

First snap the control from soft-takeover and then notice that the range of +-3 Semitones has already been reached would feel cumbersome. Soft-Takover is required in mapping though, because the control is reset after loading new tracks.

The behavior is described here:

// The Pitch_Adjust knob does not reflect the speedSliderPitchRatio.

and here
// pitch_adjust is the distance to the linear pitch in semitones

The manual is not clear and can make use of an update:

Adjust the pitch in addition to the :term:tempo slider pitch.

@daschuer
Copy link
Member

daschuer commented May 4, 2022

Here is the manual update: mixxxdj/manual#480

@daschuer
Copy link
Member

daschuer commented May 4, 2022

Unfotunately the current behaviour is inconsistent:

  • the knob is static when I "lock at original pitch" after pitch_adjustments (though it doesn't reset to original key, for that I'd need to reset the adjust knob, too)

That can be expected, since the pitch_adjust is applied to all other. Did you expect that the knob is reset and goes out of sync, in such situation? I can imagine to change that behavior, since the adjusted pitch becomes pointless in that case.

  • the knob moves when I "unlock, keep current key"

We need an instance that holds the remaining offset. I can confirm that this is confusing, because the know goes out of sync and there is no indicator at all (except in Shade) that we are in such a blended situation

  • the knob moves when I press "match key" or "reset key"

That the knob is moving after match key can be expected in the same way the rate slider moves when using sync.
I think the same is true for "reset key", but I agree that this is inconsistent with the unlock key-lock behavior.

The "unlock, keep current key" mode is part of an "allow toggling keylock during playback" use case. It allows to adjust the tempo without the pitch, and without a pitch jump during enabling. Not sure if this is used at all in the wild.

During keylock enabled, the the rate slider becomes temporary a tempo only slider. I think the user user expects that the pitch_adjust control is an independent pitch control for that state. If we use pitch_adjust purely for the distance to the linear pitch, it will move out of sync whenever you move the rate slider. I think this was the main reason to apply it on top of keylock to avoid this.

@ronso0
Copy link
Member Author

ronso0 commented May 6, 2022

  • the knob is static when I "lock at original pitch" after pitch_adjustments (though it doesn't reset to original key, for that I'd need to reset the adjust knob, too)

That can be expected, since the pitch_adjust is applied to all other. Did you expect that the knob is reset and goes out of sync, in such situation?

Yes, I'd expect that that the pitch is reset to default, exactly as labeled in the preferences: Keylock mode "Original key". That would be an easy way to reset the key if you don't have reset_key mapped, for example after having matched the key.
If you have pitch_adjust mapped you'd then have access to the full +-3 semitones range.

Also, this fixes the key shift bug with "lock original" + "unlock keep current".

The "unlock, keep current key" mode is part of an "allow toggling keylock during playback" use case. It allows to adjust the tempo without the pitch, and without a pitch jump during enabling. Not sure if this is used at all in the wild.

I use it : ) The motivation to implement it was that I have keylock enabled pretty much all the time, but sometimes I want to produce smooth pitchbend effects just with the jog wheel (or with the tempo slider). Then lock again, if necessary re-sync the beats with the jogwheel.
In conjunction with "Lock current key" this prevents key jumps when toggling the lock. I have reset_key mapped, as well as pitch_adjustup/down and never ran into issues (except the rounding issue I'm trying to fix here).

@daschuer
Copy link
Member

daschuer commented May 6, 2022

Yes, I'd expect that that the pitch is reset to default,

OK, I have no objection to this change. It may actually feel more natural, even though it is a point release of a mature branch. Can we still consider it as part of a bugfix?

I use it : )

Great to hear. It this an unique selling point of Mixxx? Maybe a topic of a future news post ;-)

@ronso0
Copy link
Member Author

ronso0 commented May 6, 2022

Great to hear. It this an unique selling point of Mixxx? Maybe a topic of a future news post ;-)

I started DJing on Linux so don't and didn't use any other soft than Mixxx : )
I just found Traktor users asking to revert the "Engaging Key Lock, resets a potential key offset" "fix"
Disable "Engaging Key Lock, resets a potential key offset"
Serato, Rekordbox, idk
Traktor Pro 3 appears to have separate MIDI controls: Key Lock and Key Lock (Preserve Pitch), though no global preferences option for that 🤷 So at least Mixxx rules out Trakor in that regard since we have "Lock original" and "Keep current key" since #1222 : )

Can we still consider it as part of a bugfix?

It fixes the continuous key shift when toggling keylock repeatedly with "Lock original" and "Unlock, keep current".
So I'd say yes.

even though it is a point release of a mature branch.

I think only users who have pitch_adjust knob mapped to a regular rotary knob¹ get a minor benefit from the current behaviour as that knob is still in sync even after a sudden key change. Those who don't have it will have to resort to key_reset.
¹there is only one official mapping with that

So I think it's the same situation as with the crossfader reset after AutoDJ runs out:
users may take it as it is, but for others the current behaviour is an issue.

@ronso0
Copy link
Member Author

ronso0 commented May 6, 2022

I noticed another bug (and I have no idea why we/I implemented it that way in #1222):
key unlocked, rate off center, apply some pitch_adjust (match_key), disable keylock "Reset to linear pitch"
= pitchTweakRatio is reset but not applied to pitch_adjust
= next time touching that knob pitch would jump

@ronso0
Copy link
Member Author

ronso0 commented May 6, 2022

Furthermore I noticed the pitch/keylock tests don't consider the unlock mode, thus they are partially pointless (assuming the default unlock mode).
I will adjust the PitchRoundtrip test and write a new one to test the "Keep current" unlock mode.

@ronso0
Copy link
Member Author

ronso0 commented May 6, 2022

Considering I started this to fix a small initialization bug and minor anoyance with my use case this is growing rather big...

@ronso0
Copy link
Member Author

ronso0 commented May 11, 2022

I think I will split this up:

  • the first few commits (init keylock from config, restore pitchRatio precision)
  • the other bugs incl. adjusted tests

So the simple fixes can go to the next bugfix release asap.

@daschuer
Copy link
Member

Yes, I did also consider this. This way we can get out 2.3.2 in de upcoming week or such.

@ronso0
Copy link
Member Author

ronso0 commented May 12, 2022

2.3.3, yes.

#4756

@github-actions
Copy link

This PR is marked as stale because it has been open 90 days with no activity.

@github-actions github-actions bot added the stale Stale issues that haven't been updated for a long time. label Sep 19, 2023
@daschuer
Copy link
Member

Oh, sorry this has stalled. Can you resolve it the conflicts?
I will test and review it again.

@daschuer daschuer modified the milestones: 2.3.3, 2.4.0 Sep 19, 2023
@daschuer
Copy link
Member

The conflicts are only in the testing skin.
I have just given this a short spin and I did not discover any issues. So you may want to put this is not a merge able state.

@daschuer
Copy link
Member

Is the track load behavior still in the desired state?
I am unsue what is desired in the middle ground of not putting sliders out if sync but also start with a new track without supprises. Please verify everything matches your use case.

@ronso0
Copy link
Member Author

ronso0 commented Sep 19, 2023

Everything still looks okay when testing with my script.
I'll give this a more thorough spin next weekend with my controller.

I added a simple test for KeepLockedKey mode and removed the skin commit.

@ronso0
Copy link
Member Author

ronso0 commented Sep 19, 2023

I am unsue what is desired in the middle ground of not putting sliders out if sync but also start with a new track without supprises.

I'm not that much into what I did here after my last edit months ago X), please give my some steps to reproduce / understand what you mean.

@ronso0
Copy link
Member Author

ronso0 commented Sep 19, 2023

/home/runner/work/mixxx/mixxx/src/test/enginebuffertest.cpp:62: Failure
Expected equality of these values:
  0.5
  ControlObject::get(ConfigKey(m_sGroup1, "pitch"))
    Which is: 0.5

??
Test passes only on macOS ARM

@ronso0
Copy link
Member Author

ronso0 commented Sep 19, 2023

Everything still looks okay when testing with my script.

In a manual test though I ended up with pitch_adjust != 0 again (couldn't reproduce until now, but I didn't do many runs):

  • Lock: keep current
  • Unlock: keep locked key
  • raise pitch to 10% (outside pitch slider range)
  • lock, unlock, do manual key ajustments etc.
  • reset rate
  • reset key
  • = pitch_adjust != 0, just slightly off (like the rounding trick didn't work)
  • reset pitch_adjust slider = pitch slightly off
  • reset pitch slider = pitch_adjust slightly off
  • etc.
  • = no way to completely reset the pitch controls (except LOCK, reset key, UN-lock)

🤷

Will see if I can reproduce with my controller.

@github-actions github-actions bot removed the stale Stale issues that haven't been updated for a long time. label Sep 20, 2023
@ronso0
Copy link
Member Author

ronso0 commented Sep 27, 2023

/home/runner/work/mixxx/mixxx/src/test/enginebuffertest.cpp:62: Failure
Expected equality of these values:
  0.5
  ControlObject::get(ConfigKey(m_sGroup1, "pitch"))
    Which is: 0.5

?? Test passes only on macOS ARM

Anyone has a clue what's going on?

@ronso0
Copy link
Member Author

ronso0 commented Sep 28, 2023

Alright, ASSERT_NEAR makes the tests succeed.
Ready to roll!

@daschuer wanna take a final look?

@Swiftb0y Swiftb0y dismissed their stale review September 28, 2023 12:14

don't have time for a detailed review, but after taking a quick glance, I have not found any obvious issues.

// If 'current' aka 'not original' key was locked
// TODO(ronso0) Why does it depend on keylock mode?
if (m_keylockMode->get() == kLockCurrentKey) {
// reset to linear pitch
Copy link
Member

@ywwg ywwg Dec 7, 2023

Choose a reason for hiding this comment

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

it's not that we reset to linear pitch, it's that we allow rate slider changes to affect the musical pitch. But 0.0 rate adjustment may no longer the original key.

EDIT: nevermind I misread the switch-case

@ywwg
Copy link
Member

ywwg commented Dec 7, 2023

I wrote a lot of text that was all wrong -- I think this fix is correct! thank you <3

@ywwg ywwg merged commit 446cee4 into mixxxdj:2.4 Dec 7, 2023
@ronso0
Copy link
Member Author

ronso0 commented Dec 7, 2023

🎉

@ronso0 ronso0 deleted the keylock-fix branch December 7, 2023 16:28
@ronso0 ronso0 mentioned this pull request Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants