-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Add SlewDistortion effect and oversampling support #7641
base: master
Are you sure you want to change the base?
Conversation
Great to have another plugin added to LMMS.
Have you checked all the division and modulo operators? If not, i would recommend manually adding a ternary to find and fix the nans. I don't have much else to say |
This PR is 100% ready to go, so reviews would be appreciated. The only remaining issue is that oversampling NaN bug, but I can no longer reproduce it (I only let it run for around an hour though), so it may have been fixed in a recent change. Testing would be helpful to see if anybody else runs into that issue or any other issues. Please also let me know if there are any new features or waveshaping modes that would be helpful. |
This distortion plugin is so cool! The idea of limiting the rate of change of the waveform is super creative. Things I noticed which may or may not be bugs
I still need to read through more carefully to understand all the code, but I'm impressed how you used SIMD instructions to speed up everything. Even with multiple instances of Slew Distortion running, the cpu meter is chilling! Also, are SSE2 instructions only available on certain cpus? It seems that Intel was the one who started doing it, but maybe all modern cpus have this sort of thing? |
This is a bug, thank you.
Both of these are ultimately LMMS's fault, for several reasons, but mainly due to its lack of parameter interpolation (the same issue is noticeable with most parameters in the program...). However, I can make it so the plugin itself interpolates the bias values specifically. It's a band-aid solution which would normally be bad since it would make automation inaccurate, but LMMS's automation is already inaccurate, so it should work great.
Yes, different CPUs support different versions of SIMD instructions. Normally I'd use higher SSE versions and AVX instructions and such, but I don't want to exclude people using older hardware. Essentially all desktop CPUs made past 2004 come with SSE2 support, so it's a very safe cutoff point. This is why, for example, you see me doing stuff like this as a
Instead of just this:
The latter requires SSE4.1 which didn't fully take over until around 2011. |
Super cool stuff! First take. The Up/Down knobs are the only knobs that go full 360. It looks a bit odd. Is there any special reason for this? It would look better if it had the same range/layout as the other knobs. |
I agree, unfortunately that's an LMMS bug. It happens whenever a knob marked as logarithmic has values both above and below 0 asymmetrically. Edit: Correction, it's simply whenever a knob has values below 0, doesn't have its top bound touching exactly 0, and is asymmetric around 0. |
@regulus79 I added parameter interpolation to the bias. You should be able to shake it around as violently as you'd like without any crackles whatsoever. |
|
||
std::array<float, 4> in = {0}; | ||
std::array<float, 4> out = {0}; | ||
const std::array<float, 4> drive = {drive1, drive1, drive2, drive2}; |
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'm curious why these variables are stored in arrays of length 4, if the first two and last two are the same. Is it to match the way SSE2 works with vectors of 4 floats at a time?
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.
Yes. This obviously isn't the way you'd want to do things normally, but I was writing the code with the intention of converting it over to SSE2 from day 1. This non-SSE2 code won't ever run for over 99.99% of people, so it's best to essentially treat it as a giant comment, so you can see what the SSE2 code is achieving if you don't understand a certain part of it.
It's a wonderful new machine. Thanks!
Do you use the FPE stuff to fine this out? I haven't been able to replicate this in three hours time. I'm running it in gdb with FPE. |
That's good to hear. I tried testing it with FPE a couple days ago, but couldn't manage to get it to spit out the NaN during that time. I may have accidentally fixed the bug at some point. Automating stuff isn't necessary, I already did an extensive test with every parameter strapped to max rate white noise LFOs and there were no issues. The aforementioned bug would just happen... eventually... whenever oversampling was currently enabled. I'll try once again to reproduce it tonight. If I don't run into it, I think we can safely consider it fixed. |
While waiting for it to compile with the FPE flag again, I had it running and managed to run into the bug. It's definitely still present, unfortunately. Hopefully I can manage to catch it with the FPE flag set. |
I managed to snag it:
And in exactly the same spot in the SSE2 code:
This... doesn't seem like helpful information though. The SSE2 error indicates that the issue happens specifically at the multiplication of the input and drive values, which I assume means the input holds an invalid value, and that input comes directly from the upsampler output. Soooooo... this tells us "the oversampling process can rarely spit out an invalid value seemingly at random", which we already knew. I'm not sure how LMMS's FPE flag works. Is there a reason this didn't place the error location in the oversampling class instead of the effect? I don't see how it could be possible that both the input and drive are valid values, and then multiplying them suddenly isn't, so I assume the NaN appears earlier in the chain and the SIGFPE just isn't thrown until later for some reason. |
@LostRobotMusic I think you may inspect variables when you get a SIGFPE, like |
I notice that the multiband processing is done by simply taking a lowpass of the incoming signal to get the lower band, and then subtracting that from the original signal to get the upper band. Is that right? I would have expected an allpass to be necessary, or is it not needed in this situation? |
Highpass is simply the inverse of lowpass, and the same phase shifts are applied to both bands in this case. Allpasses aren't necessary until you have at least three bands. The reason for this is that in a 3-band setup, two of the bands need to be sent through both crossover filters, and the remaining band only needs to be sent through one crossover filter, meaning that one band will have had a different phase response applied to it than the other two. The allpass is necessary to match its phase with the other two bands. In the case of a 2-band setup, both bands were calculated with exactly one filter, so their phases already align and no extra allpass is needed. |
Wait, it turns out I'm partially incorrect. Everything I said in that message is true except "and the same phase shifts are applied to both bands in this case". Only specific designs for lowpass/highpass filters can correctly derive one by subtracting the other, and Linkwitz-Riley is not one of those cases. I did a white noise test with this plugin and it passed at all split frequencies, but I neglected to try adjusting the band gains or other parameters, which starts to reveal issues. I'll need a separate Linkwitz-Riley highpass filter for the crossover to be implemented properly. Thank you for pointing this out. (For full clarity, no, an allpass filter is not needed, but the crossover does need to be implemented as separate lowpass and highpass filters) |
The issue has been fixed. |
It took over eight hours of running it to get it to happen again, but I finally captured something, this time in a different spot (most likely because I changed the filter logic):
|
I have reproduced the
|
You'll want to compile with |
LMMS didn't have any particularly impressive distortion plugins, so I decided to make one for everybody.
The GUI was designed by thismoon.
Slew Distortion is a 2-band slew rate limiter and distortion plugin. Almost the entirety of its audio processing code is written in pure SSE2 instructions, meaning it can process all four channels (two stereo channels x two bands) simultaneously in a single thread, resulting in extremely low CPU usage for what it provides.
Unlike most distortion plugins, this one has an optional dynamics-restoring feature which can restore all of the dynamic range that distortion usually removes from your signal, meaning you can push it as hard as you want to change your sound's timbre without worrying about things sounding too squashed.
The slew rate limit can be set for both upward and downward movement independently, and can be modulated internally using an envelope follower so it morphs in time with the volume.
Most distortion plugins, especially the ones in LMMS currently, majorly struggle with aliasing, which introduces ugly and harsh high frequencies that are inharmonic and can even completely destroy the cleanliness of your song. This plugin supports up to 32x oversampling, meaning up to an extra ten whole octaves of overhead before aliased frequencies become audible, far more than you'd ever realistically need.
Ten waveshaping types are featured. Let me know if there's another one you'd like for me to add and I'd be happy to add it in!
(And yes, as mentioned, every single one of these distortion types are fully implemented as pure SSE2 instructions. I have not been getting eight hours of sleep.)
My personal favorites are Tanh and Sinusoidal, since they are mathematically smooth and infinitely differentiable. In less stupid terms, they can generally be pushed far more aggressively before they start to create any harsh upper frequencies.
But only having a few waveshaping presets to choose from is no fun, so I've added a couple handy knobs to help you sculpt your own with almost no performance detriment. Warp and Crush are algorithms of my own invention which allow you to pass lower-amplitude sample values through without distortion, and concentrate the waveshaping up at the higher peaks.
(They can also be used to create waveshapes essentially identical to those used in the relatively famous Camelcrusher plugin, which is fun.)
Slew Distortion comes with a handy visualizer on the right side to show you exactly what you're doing to the waveshaping curve, no guesswork needed.
I recommend trying the plugin out for yourself, but here are some old videos I recorded during the development process.
Singleband demo: https://youtu.be/nqabNFl46Vo
Multiband demo: https://youtu.be/jqdMEhJw_8I
These demos kinda suck, but it should hopefully give a general idea of a few things it can do. Obviously its more creative features would be far more useful on individual tracks than full mixes.
KNOWN BUGS (do not merge until these are resolved):
The graph axes are off-center, I somehow didn't notice until just now. It's an easy fix.FIXEDDonations for developing free audio software is literally my only source of income, and I can't quite afford enough food to keep myself fully healthy, so if you're willing and able, any contributions would be deeply appreciated: https://www.patreon.com/c/lostrobot