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

Add SlewDistortion effect and oversampling support #7641

Open
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

LostRobotMusic
Copy link
Contributor

@LostRobotMusic LostRobotMusic commented Jan 2, 2025

image

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!
image
(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.

image


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 oversampling class spits out a NaN like once per hour or so, regardless of the input or situation. Even if you feed it nothing but pure silence, it'll be totally fine for an hour or more and then spit out a NaN for no reason. I've spent far too many hours trying to get this one figured out, but as you can probably imagine, it's impossibly difficult to debug an issue that almost never happens. I haven't figured out whether the bug is with my class, my usage of the class, or perhaps with HIIR itself (surely not?). If anybody spots the issue, I'd be enormously grateful.
  • The graph axes are off-center, I somehow didn't notice until just now. It's an easy fix. FIXED

Donations 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

@Rossmaxx
Copy link
Contributor

Rossmaxx commented Jan 2, 2025

Great to have another plugin added to LMMS.

The oversampling class spits out a NaN like once per hour or so, regardless of the input or situation. Even if you feed it nothing but pure silence, it'll be totally fine for an hour or more and then spit out a NaN for no reason. I've spent far too many hours trying to get this one figured out, but as you can probably imagine, it's impossibly difficult to debug an issue that almost never happens. I haven't figured out whether the bug is with my class, my usage of the class, or perhaps with HIIR itself (surely not?).

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

@LostRobotMusic LostRobotMusic added needs style review A style review is currently required for this PR needs code review A functional code review is currently required for this PR needs testing This pull request needs more testing labels Jan 5, 2025
@LostRobotMusic
Copy link
Contributor Author

LostRobotMusic commented Jan 5, 2025

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.

@regulus79
Copy link
Contributor

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

  • The plugin help page stays open when you start a new project.
  • Moving the bias slider causes crackles in the audio. It's only most noticeable when sliding it by hand. Automating it also has quiet crackles, but I mean, that's to be expected with lmms's automation, so it's not really a bug.

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?

@LostRobotMusic
Copy link
Contributor Author

LostRobotMusic commented Jan 6, 2025

  • The plugin help page stays open when you start a new project.

This is a bug, thank you.
(Edit: I can't figure out how to fix it though... Qt is kinda miserable to work with...)

  • Moving the bias slider causes crackles in the audio. It's only most noticeable when sliding it by hand. Automating it also has quiet crackles, but I mean, that's to be expected with lmms's automation, so it's not really a bug.

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.

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?

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 floor function:

__m128 trunc = _mm_cvtepi32_ps(_mm_cvttps_epi32(input));
__m128 floor = _mm_sub_ps(trunc, _mm_and_ps(_mm_cmplt_ps(input, trunc), one));

Instead of just this:

__m128 floor = _mm_floor_ps(input);

The latter requires SSE4.1 which didn't fully take over until around 2011.

@zonkmachine
Copy link
Member

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.

@LostRobotMusic
Copy link
Contributor Author

LostRobotMusic commented Jan 6, 2025

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.

@LostRobotMusic
Copy link
Contributor Author

@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};
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@zonkmachine
Copy link
Member

It's a wonderful new machine. Thanks!

  • The oversampling class spits out a NaN like once per hour or so,

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.
I'll try and automate stuff and leave it up for a long run tomorrow but for now it seems to work.

@LostRobotMusic
Copy link
Contributor Author

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.

@LostRobotMusic
Copy link
Contributor Author

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.

@LostRobotMusic
Copy link
Contributor Author

I managed to snag it:

Thread 19 "lmms::AudioEngi" received signal SIGFPE, Arithmetic exception.
[Switching to Thread 0x7fffda0006c0 (LWP 161437)]
0x00007fffdabb2236 in lmms::SlewDistortion::processImpl (this=0x5555576b61b0, 
    buf=0x5555568f61b0, frames=256)
    at /home/lostrobot/lmms-slew-distortion/plugins/SlewDistortion/SlewDistortion.cpp:550
550				m_inPeakDisplay[1] = std::max(m_inPeakDisplay[1], std::abs(in[1] * drive[1]));
(gdb) bt
#0  0x00007fffdabb2236 in lmms::SlewDistortion::processImpl
    (this=0x5555576b61b0, buf=0x5555568f61b0, frames=256)
    at /home/lostrobot/lmms-slew-distortion/plugins/SlewDistortion/SlewDistortion.cpp:550
#1  0x0000555555825929 in lmms::Effect::processAudioBuffer
    (this=0x5555576b61b0, buf=0x5555568f61b0, frames=256)
    at /home/lostrobot/lmms-slew-distortion/src/core/Effect.cpp:133
#2  0x0000555555827e87 in lmms::EffectChain::processAudioBuffer
    (this=0x555556936730, _buf=0x5555568f61b0, _frames=256, hasInputNoise=false) at /home/lostrobot/lmms-slew-distortion/src/core/EffectChain.cpp:201
#3  0x0000555555833d5b in lmms::MixerChannel::doProcessing
    (this=0x555556936720)
    at /home/lostrobot/lmms-slew-distortion/src/core/Mixer.cpp:172
#4  0x00005555557d1b5d in lmms::ThreadableJob::process (this=0x555556936720)
    at /home/lostrobot/lmms-slew-distortion/include/ThreadableJob.h:77
#5  0x00005555557d1422 in lmms::AudioEngineWorkerThread::JobQueue::run
    (this=0x555555c21e80 <lmms::AudioEngineWorkerThread::globalJobQueue>)
    at /home/lostrobot/lmms-slew-distortion/src/core/AudioEngineWorkerThread.cpp:87
#6  0x00005555557d17e0 in lmms::AudioEngineWorkerThread::run
    (this=0x555556912460)
    at /home/lostrobot/lmms-slew-distortion/src/core/AudioEngineWorkerThread.cpp:176

And in exactly the same spot in the SSE2 code:

Thread 18 "lmms::AudioEngi" received signal SIGFPE, Arithmetic exception.
[Switching to Thread 0x7fffd96006c0 (LWP 149339)]
0x00007fffd83b1c90 in _mm_mul_ps(float __vector(4), float __vector(4)) (
    __B=..., __A=...)
    at /usr/lib/gcc/x86_64-linux-gnu/13/include/xmmintrin.h:204
204	  return (__m128) ((__v4sf)__A * (__v4sf)__B);
(gdb) bt
#0  0x00007fffd83b1c90 in _mm_mul_ps(float __vector(4), float __vector(4))
    (__B=..., __A=...)
    at /usr/lib/gcc/x86_64-linux-gnu/13/include/xmmintrin.h:204
#1  lmms::SlewDistortion::processImpl
    (this=0x5555589474a0, buf=0x5555569f7720, frames=256)
    at /home/lostrobot/lmms-slew-distortion/plugins/SlewDistortion/SlewDistortion.cpp:177
#2  0x0000555555825929 in lmms::Effect::processAudioBuffer
    (this=0x5555589474a0, buf=0x5555569f7720, frames=256)
    at /home/lostrobot/lmms-slew-distortion/src/core/Effect.cpp:133
#3  0x0000555555827e87 in lmms::EffectChain::processAudioBuffer
    (this=0x55555690b1b0, _buf=0x5555569f7720, _frames=256, hasInputNoise=false) at /home/lostrobot/lmms-slew-distortion/src/core/EffectChain.cpp:201
#4  0x0000555555833d5b in lmms::MixerChannel::doProcessing
    (this=0x55555690b1a0)
    at /home/lostrobot/lmms-slew-distortion/src/core/Mixer.cpp:172
#5  0x00005555557d1b5d in lmms::ThreadableJob::process (this=0x55555690b1a0)
    at /home/lostrobot/lmms-slew-distortion/include/ThreadableJob.h:77
#6  0x00005555557d1422 in lmms::AudioEngineWorkerThread::JobQueue::run
    (this=0x555555c21e80 <lmms::AudioEngineWorkerThread::globalJobQueue>)
    at /home/lostrobot/lmms-slew-distortion/src/core/AudioEngineWorkerThread.cpp:87

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.

@PhysSong
Copy link
Member

PhysSong commented Jan 7, 2025

@LostRobotMusic I think you may inspect variables when you get a SIGFPE, like print in[1]

@regulus79
Copy link
Contributor

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?

@LostRobotMusic
Copy link
Contributor Author

LostRobotMusic commented Jan 7, 2025

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.

@LostRobotMusic
Copy link
Contributor Author

LostRobotMusic commented Jan 7, 2025

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)

@LostRobotMusic
Copy link
Contributor Author

The issue has been fixed.

@LostRobotMusic
Copy link
Contributor Author

@LostRobotMusic I think you may inspect variables when you get a SIGFPE, like print in[1]

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):

Thread 23 "lmms::AudioEngi" received signal SIGFPE, Arithmetic exception.
[Switching to Thread 0x7fffc2a006c0 (LWP 77952)]
0x00007fffd42ad7b6 in lmms::LinkwitzRiley<(unsigned char)2>::update (
    this=0x555558a050e0, in=-7.87341929e+17, ch=1 '\001')
    at /home/lostrobot/lmms-slew-distortion/include/BasicFilters.h:129
129			return y;
(gdb) bt
#0  0x00007fffd42ad7b6 in lmms::LinkwitzRiley<(unsigned char)2>::update
    (this=0x555558a050e0, in=-7.87341929e+17, ch=1 '\001')
    at /home/lostrobot/lmms-slew-distortion/include/BasicFilters.h:129
#1  0x00007fffd42a6a1a in lmms::SlewDistortion::processImpl
    (this=0x555558a04520, buf=0x555556a5a370, frames=256)
    at /home/lostrobot/lmms-slew-distortion/plugins/SlewDistortion/SlewDistortion.cpp:163
#2  0x0000555555825929 in lmms::Effect::processAudioBuffer
    (this=0x555558a04520, buf=0x555556a5a370, frames=256)
    at /home/lostrobot/lmms-slew-distortion/src/core/Effect.cpp:133
#3  0x0000555555827e87 in lmms::EffectChain::processAudioBuffer
    (this=0x55555694cfe0, _buf=0x555556a5a370, _frames=256, hasInputNoise=false) at /home/lostrobot/lmms-slew-distortion/src/core/EffectChain.cpp:201
#4  0x0000555555833d5b in lmms::MixerChannel::doProcessing
    (this=0x55555694cfd0)
    at /home/lostrobot/lmms-slew-distortion/src/core/Mixer.cpp:172
#5  0x00005555557d1b5d in lmms::ThreadableJob::process (this=0x55555694cfd0)
    at /home/lostrobot/lmms-slew-distortion/include/ThreadableJob.h:77
#6  0x00005555557d1422 in lmms::AudioEngineWorkerThread::JobQueue::run
    (this=0x555555c21e80 <lmms::AudioEngineWorkerThread::globalJobQueue>)
    at /home/lostrobot/lmms-slew-distortion/src/core/AudioEngineWorkerThread.cpp:87
#7  0x00005555557d17e0 in lmms::AudioEngineWorkerThread::run
--Type <RET> for more, q to quit, c to continue without paging--
    (this=0x555556976cb0)
    at /home/lostrobot/lmms-slew-distortion/src/core/AudioEngineWorkerThread.cpp:176
#8  0x00007ffff6adb674 in ??? () at /lib/x86_64-linux-gnu/libQt5Core.so.5
#9  0x00007ffff5c9ca94 in start_thread (arg=<optimized out>)
    at ./nptl/pthread_create.c:447
#10 0x00007ffff5d29c3c in clone3 ()
    at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78

(gdb) print m_z1
$14 = {_M_elems = {-1.7740041950941395e+33, 1.6803881779843317e+43}}
(gdb) print m_z2
$15 = {_M_elems = {-1.3756514217544949e+33, 1.7935301826790472e+43}}
(gdb) print m_z3
$16 = {_M_elems = {-1.1499862344965942e+33, 1.7935301020373433e+43}}
(gdb) print m_z4
$17 = {_M_elems = {-1.0265020723473698e+33, 1.7361273530778537e+43}}
(gdb) print x
$18 = 1.6803881779843317e+43
(gdb) print in
$19 = -7.87341929e+17

@regulus79
Copy link
Contributor

regulus79 commented Jan 8, 2025

I have reproduced the nans, but it's not crashing; the output of the plugin is just stuck at 0. Do I need a special compiler flag to get it to crash?

(I Ctrl-C'd it after seeing the nan warnings, but it stopped in a random place, so I added a breakpoint here to get me to the right spot)
...
(gdb) break LinkwitzRiley::update
Breakpoint 1 at 0x7fff9c7e6682: LinkwitzRiley::update. (5 locations)
(gdb) continue
Continuing.
[Switching to Thread 0x7fffc7fff6c0 (LWP 123074)]

Thread 15 "lmms::AudioEngi" hit Breakpoint 1.1, lmms::LinkwitzRiley<(unsigned char)2>::update (this=0x55555739a380, in=-nan(0x400000), ch=0 '\000')
    at /home/user/lmms/include/BasicFilters.h:120
120			const double x = in - ( m_z1[ch] * m_b1 ) - ( m_z2[ch] * m_b2 ) -
...
(gdb) print in
$1 = -nan(0x400000)
(gdb) print ch
$2 = 0 '\000'
(gdb) print x
$3 = -nan(0x8000000000000)
(gdb) print y
$4 = -nan(0x8000000000000)
(gdb) print m_z1
warning: RTTI symbol not found for class 'lmms::LinkwitzRiley<(unsigned char)2>'
$5 = {_M_elems = {-nan(0x8000000000000), -nan(0x8000000000000)}}
(gdb) print m_z2
warning: RTTI symbol not found for class 'lmms::LinkwitzRiley<(unsigned char)2>'
$6 = {_M_elems = {-nan(0x8000000000000), -nan(0x8000000000000)}}
(gdb) print m_z3
warning: RTTI symbol not found for class 'lmms::LinkwitzRiley<(unsigned char)2>'
$7 = {_M_elems = {-nan(0x8000000000000), -nan(0x8000000000000)}}
(gdb) print m_z4
warning: RTTI symbol not found for class 'lmms::LinkwitzRiley<(unsigned char)2>'
$8 = {_M_elems = {-nan(0x8000000000000), -nan(0x8000000000000)}}

I still have gdb attached, so if anyone wants to run any commands, just let me know.

@LostRobotMusic
Copy link
Contributor Author

LostRobotMusic commented Jan 8, 2025

You'll want to compile with -DCMAKE_BUILD_TYPE=Debug -DWANT_DEBUG_FPE=ON in CMake. Note that this makes a lot of things in LMMS break (e.g. Qt-dragging literally anything).

@LostRobotMusic
Copy link
Contributor Author

Minor UI update (we both forgot to add band labels somehow):
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs code review A functional code review is currently required for this PR needs style review A style review is currently required for this PR needs testing This pull request needs more testing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants