-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
refactor: use higher-level std::span
based logic
#13654
refactor: use higher-level std::span
based logic
#13654
Conversation
8db09c9
to
b5c78c6
Compare
noticed recently that the there was no use of the metaparameter within the metronome (and the gain always having the same volume being annoying), so I added a little simple gain knob in the newest commit. |
Unfortunately the non synced mode does not produce a stady click. It sounds more like as rhythm. |
|
||
template<class T> | ||
std::span<T> subspan_clamped(std::span<T> in, typename std::span<T>::size_type offset) { | ||
// TODO (Swiftb0y): should we instead create a wrapper type that implements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should remove this function altogether.
It does not make sense to replace here an illformated span with an empty one and than check for it in the outer scope. Avoid the empty span as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't follow you. There is no ill-formed span (any operation that would produce one is already UB).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see UB as anything can happen, but most likely there are no boundary checks to keep subspan() blazing fast, leading to a size_t overflow or such.
In our case not using the subspan_clamped()
will speed up the code, because we can skip the empty span creation and the later isEmpty()
check, if we will check for offset < size()
in our business logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, but offset < size()
should not be part of the businesslogic. Explicit bounds checks are about as low level you can get so it doesn't make sense to have in high-level business logic. Thats explicitly why I put it into a utility function. From a high level perspective there is now only span
. Either it has some sample content to play, or its empty and has none. This is a clear and intuitive API separation thats well worth paying two instructions for.
01b2a5f
to
b10d4b1
Compare
Fixed. mistakenly used |
double framesPerBeat(mixxx::audio::SampleRate sampleRate, double bpm) { | ||
double framesPerMinute = sampleRate * 60; | ||
return framesPerMinute / bpm; | ||
std::size_t samplesPerBeat(mixxx::audio::SampleRate sampleRate, double bpm) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old name was better, because mixxx::audio::SampleRate is actually frame rate. We used the name sample rate because this is more common for audio and also used in external libraries. In a second step it can be than converted to monoSmaples whith the same value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, are you sure? I wasn't aware of that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, confirmed. fixed in fixup
std::size_t samplesSinceLastClick = | ||
gs->framesSinceLastClick * mixxx::kEngineChannelOutputCount; | ||
std::size_t offset = samplesSinceLastClick % | ||
samplesPerBeat(engineParameters.sampleRate(), | ||
m_pBpmParameter->value()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is somewhere a 2x issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in fixup.
playMonoSamplesWithGain(subspan_clamped(click, gs->framesSinceLastClick), output, gain); | ||
gs->framesSinceLastClick += engineParameters.framesPerBuffer(); | ||
|
||
std::span<CSAMPLE> outputBufferOffset = [&] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we keep the original logic? It is hard to check if all conditions are still considered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. That is the point of this PR. Independent of the std::span
usage, this was the part I wanted to improve because I very much struggled with understanding the logic you came up with here. So I rewrote it in a way I found more intuitive.
I'm confident that all the logic is still there (except #13733 because I couldn't reproduce that here). It just has been disentangled into smaller functions.
We're at an impasse now because we simply disagree about what is more readable. I don't understand your code, you don't understand mine. So I suggest we ask other team members to evaluate what solution they prefer and decide based on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new code looks much cleaner, but I can also not see, if it's functional identical.
May I suggest to implement a unit test for this code first, and than apply the refactured code in a second commit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, do we already test other effect code or do I need to come up with infrastructure for that first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regardless, It'll probably take some time until I come up with reasonable test code...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I experimented a bit with this, but coming up with tests is incredibly tedious and brittle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, so spinning this a bit further. I think verifying the correctness by inspecting the buffer output is not sensible. At least not more than doing some QA testing by ear or by code review.
fe134f4
to
4850f4e
Compare
Heres my attempt at walking through the diff and explaining it along the way (text is always about the diff above)
Here the preconditions are not satisfied so we can simply return with an empty span, which means nothing needs to be done. The else case handles #13727 which is not necessary because we already advance These two chunks are mathematically equivalent just rewritten in a way that I found a little easier to understand/meaningful. These are also equivalent, only that this time I didn't make the assumption that At this point we know that if we need to play new click we know where to play it in the buffer so just play it and record how far into the buffer that was. No need to shift In summary: I highly prefer this solution because it clearly breaks the problem down into subproblems (do we need to play the (remaining) click? where?) that are easily composed back together while hiding many of the uglier details about buffer management in the lower-level functions. I'm fairly confident that this refactor does not introduce regressions while raising maintainability significantly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree fully with @Swiftb0y. This is a solid improvement, nicely done
I see UB as anything can happen, but most likely there are no boundary checks to keep subspan() blazing fast, leading to a size_t overflow or such.
This is what subspan_clamped does. It effectively mitigates the risk of undefined behavior by ensuring that the offset passed to subspan() never exceeds the size of the span.
As for the performance impact:
Cost of subspan_clamped
The subspan_clamped function adds a single std::min operation, which is a straightforward comparison between two size_t values. This operation:
Takes constant time: It’s an O(1) operation, meaning it doesn’t depend on the size of the data. Is negligible in CPU cycles: On modern processors, a comparison and assignment generally take only a few nanoseconds, which is practically negligible, even for audio processing.
Some performance improvements that would be nice to add:
- Make subspan_clamped inline
- Store
engineParameters.samplesPerBuffer()
,engineParameters.framesPerBuffer()
, andengineParameters.sampleRate()
as local variables -- this would negate the additional cycles introduced from the extra function calls added with this PR. Not sure how/when metronomeeffect is instantiated or how we handle the case when the audio device changes elsewhere in the codebase
However, I wouldn't consider those a blocker. I imagine there's a lot better stuff to spend our time benchmarking if performance is an issue. Overall, these changes make the code more robust and maintainable without significant performance sacrifices.
Marked 'request changes' for inlining subspan_clamped. On the other, maybe @Swiftb0y has time to dig in or someone knows the answer already, if not, nbd.
Thank you @ChasonDeshotel for chiming in here. As for your suggestions.
It already is if the compiler decide its worth it (likely is).
Not needed as those accessors implementations are already visible to the compiler, thus also likely inlined. Feel free to verify these claims in compiler explorer. |
After a bit of testing I can confirm that this change works without regressions. I also confirm that the full use of span and the better separation of tasks improve the readability. However there is still an issue with unnecessary playMonoSamplesWithGain() which impairs legibility. I have just proposed Swiftb0y#19 This avoids to call playMonoSamplesWithGain() when not necessary and solves some other issue in one go. It makes the code better traceable, because the cases where actually a new click is started can be better identified, it removes useless calls from the flow saving some ns and avoids the creation of empty spans entirely. The patch also made the handling of the static bpm and quatized mode along with calling span::subspan() and span::last() symmetric and better comparable. What do you think? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
Thats somewhat on purpose. The center of the knob is on 0db but since the sample is already quite close to 0db absolute I didn't want to allow for much range above 0db either, thats why its so asymmetrical. -24db is basically quiet even though its technically 0, its quiet enough. I figured a db readout was more informative than a unitless ratio. Do you disagree?
Thank you. Lets continue the code quality discussion on Swiftb0y#19 |
I agree to that. That is a general issue with many effects. |
great. so then keep the gain knob as is? |
If you insist. I still consider that ugly and prefer the solution we did for the idolators, to adopt the same pattern everywhere and than we can fix the missing unit issue in one go in a later PR. |
I don't insist, though its not gonna be less work to do the ratio now and later in one go. Can you explain the ratio approach a little more to me? I assume that the 12 o'clock position is |
The maximum number is what you like. 3 dB is ~1.4.
|
Right, but that means that the entire |
f9f9da6
to
a092e8f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is already an improvement let's merge it and continue iterating from here.
This is daschuer#97 rebased on main with the primary goal of making the logic easier to understand.