Waveshaper Implementation Advice

Hey all. I’m working on my first audio plugin with JUCE, building a pretty simple waveshaper almost exactly like Image Line’s Fruity Waveshaper: https://www.image-line.com/support/FLHelp/html/plugins/Fruity%20WaveShaper.htm

The idea is that for each input sample x, the output sample is f(x) where f is a function from [-1,1] to [-1,1] defined by whatever curve the user draws in the graph.

I’m building this with a lookup table with 1024 elements as a std::vector<float> on my main AudioProcessor so that my output loop can essentially just write output[i] = table[floor(input[i] * table_size)] (maybe with some kind of interpolation). This member is public such that my PluginEditor component can read the values to draw the curve and also write new values back to the table as the user draws in the graph.

My concern is about reads and writes to the same vector from the GUI and the main DSP loop, but I don’t know what the best solution is. I obviously don’t want to lock in the dsp loop, so that kind of approach seems like a solid no. Maybe redraw a new 1024-sample vector on every mouse interaction and then atomically set the AudioProcessor’s pointer to the new vector? That seems like it could be resource heavy if I’m constantly making and throwing away vectors. So I’m really not sure here.

If you have any advice or ideas, I’d love to hear your thoughts! Thank you!

Hi,
No dsp guru here, but as far as two objects sharing an array of data are involved, I would go with double buffering:

  1. the audio processor exposes an array of twice your desired size and an int property which is the reading start point. (Allocation only once @processor constructor)
  2. you init the half of the array [readStart…readstart+size] with the starting wave (setcurrentprogram ||setstateinformation)
  3. when you modify the wave on the gui ( i suggest onmouseup at first, not every single interaction) you write the other half of the wave array, and signal the processor by modifying its readstart property. Same happens when you load a preset.
  4. in the processor render block you read the readstart property and go from there in the array
  5. optional you can interpolate the two halves of the array in the render block, e.g by using a single juce linearsmoothedvalue,
    In other words: in the processor always read both halves of the array (actually it will read two small groups of values, since, you know the input value and you only need to do the interpolation for f (x), with the linearsmoothed value going from 0 to 1 or from 1 to 0, depending on the readstart property.
    If you implement this, you must make sure that the smoothing time is less than the minimum time distance between two updates from the GUI. Don’t know if juce sets a minimum predictable timespan between two mouse clicks or mouse ups, you might have to do some trick on the gui part to make this happen.
  6. careful to properly update the readstart propery only to 0 or size/2. You might want to encapsulate actions on the property in dedicated processor methods.

Sorry for the poor formatting (writing from tablet) later today i might update the post with some proper pseudocode.

All the best,
Michelangelo

1 Like

Hello !

A few remarks :

  • If you use a lookup table for your processing, you need to use an interpolation algorithm. Otherwise, the lack of continuity might cause extra distortion, and the intermediate values might be wrong anyway. Doing linear interpolation can be enough to solve your issue if your buffer size is high enough (1024 is good I think).
  • Don’t forget to set the behaviour of your waveshaper when the input is outside the lookup table range.
  • The best solution for your concurrency issue is the use of a lock-free queue. Have a look for the documentation of the JUCE class AbstractFifo. By using it, you’ll be able to read and write from different threads without needing any locking. It involves the use of atomic read and write pointers. But it might need some tweaking to handle the fact that you don’t have to refresh all the time the content of the array…
  • So another solution easier to use might be to have two different arrays, one that is used for processing, and the other one for refreshing. When a refreshing is done, you can send a command in the processing function to switch the purpose of the two arrays.

If:

  1. The vector does not resize, and
  2. You can live with the vector briefly containing partial information from the previous table and new updated information from a new table.

Then there is no risk of a crash by just modifying the values in the vector from a different thread.

For extra protection against cock-up I think std::array<float, 1024> would prevent an inadvertent resize causing a crash.

If you want to avoid the situation where the vectors is partially updated, double buffering looks like the most efficient option. If you use atomic variables for determining when to use which buffer, look at std::atomic<> with the release and acquire memory ordering constraints, (which on intel are a shit load quicker than the default and should be all you need).

I’m super late getting back to you all on this, but thank you so much! That was all super helpful. I’m using a single std::array<1024, float> for the time being, accepting partial information during updates. I intend to move on to a double-buffered approach for a subsequent version of the plugin. Thank you!