8 releases

0.2.12-alpha.0 Apr 7, 2023
0.2.11-alpha.0 Dec 19, 2022
0.2.5-alpha.0 Jun 21, 2022
0.2.4-alpha.0 Mar 14, 2022
0.1.42-alpha.0 Oct 27, 2021

#4 in #distortion


Used in surge-synthesizer

GPL-3.0 license

155KB
2.5K SLoC

DigiShaper

This code implements a waveshaper algorithm called DigiShaper. A waveshaper takes an input signal and distorts it in a nonlinear way, producing a new output signal with a different timbre or character. In this case, the DigiShaper algorithm maps the input signal onto a piecewise-linear curve with 16 segments, each of which has a different slope. The amount of distortion applied is controlled by a "drive" parameter.

The DigiShaper algorithm uses a number of mathematical concepts to achieve this result. First, it leverages the concept of piecewise-linear functions, which are functions that are defined by different linear equations on different intervals. In this case, the DigiShaper uses 16 linear equations to define its piecewise-linear curve.

Second, the algorithm uses concepts from digital signal processing, such as sample rate conversion and quantization. The input signal is first scaled by a factor of 16, then quantized to an integer value, and finally scaled back down by a factor of 16. This allows the algorithm to implement the piecewise-linear curve with only a small number of discrete levels, which reduces the computational cost of the algorithm.

Third, the algorithm uses SIMD instructions (SSE2) to perform the calculations in parallel on multiple data points at once. This improves the efficiency of the algorithm and makes it suitable for real-time audio processing applications.

Overall, the DigiShaper algorithm is a relatively simple but effective way to apply distortion to an audio signal, and it leverages a number of mathematical concepts from digital signal processing to achieve its result.

2> basically, it boils down to this equation doesn't it: (double)((int)((double)(xp0inv16.f+1.0)))p00.0625f;

Yes, that's correct. The main logic of the DigiShaper algorithm is implemented in this equation, which is used to calculate the output value for a single sample of the input signal:

(double)((int)((double)(x*p0inv*16.f+1.0)))*p0*0.0625f;

Here's a breakdown of what this equation does:

  • x is the input sample value.

  • p0 is the drive parameter, which controls the amount of distortion applied to the signal.

  • p0inv is the reciprocal of p0, which is precomputed to improve efficiency.

  • x * p0inv * 16.f scales the input sample by a factor of 16 and then multiplies it by p0inv.

  • + 1.0 shifts the scaled input signal up by 1.0, so that the output values are in the range [1, 17] rather than [0, 16].

  • (double)((int)(...)) rounds the shifted value down to the nearest integer using a cast to an integer data type, which effectively quantizes the input signal.

  • * p0 * 0.0625f scales the quantized value back down by a factor of 16 and then multiplies it by p0, which applies the distortion to the signal.

So in summary, this equation maps the input signal onto a piecewise-linear curve with 16 segments, each of which has a different slope, and the amount of distortion applied is controlled by the p0 parameter.

3> could you show me a block diagram?

Certainly! Here's a block diagram that shows the main processing steps of the DigiShaper algorithm:

                  +-------------+         +-------------+
Input signal ---> | Scale by 16 | ------> | Quantization| ------> +----------------+
                  +-------------+         +-------------+         | Piecewise-linear|
                                                                    | Curve Mapping  |
                  +-------------+         +-------------+         |                |
Drive parameter --> | Reciprocal  | ------> | Scale by 16 | ------> |                |
                  +-------------+         +-------------+         |                |
                                                                    |                |
                  +-------------+         +-------------+         |                |
Quantized value --> | Cast to int | ------> | Scale by p0 | ------> | Output Signal  |
                  +-------------+         +-------------+         +----------------+

As you can see, the input signal is first scaled by a factor of 16 and then quantized to an integer value, which effectively maps it onto a piecewise-linear curve with 16 segments. The quantized value is then scaled back down by a factor of 16 and multiplied by the drive parameter p0, which applies the distortion to the signal. The output signal is the final result of this processing. The reciprocal of the p0 parameter is precomputed to improve efficiency, and SIMD instructions are used to perform the calculations in parallel on multiple data points at once.

This code implements a waveshaper algorithm called DigiShaper. A waveshaper takes an input signal and distorts it in a nonlinear way, producing a new output signal with a different timbre or character. In this case, the DigiShaper algorithm maps the input signal onto a piecewise-linear curve with 16 segments, each of which has a different slope. The amount of distortion applied is controlled by a "drive" parameter.

The DigiShaper algorithm uses a number of mathematical concepts to achieve this result. First, it leverages the concept of piecewise-linear functions, which are functions that are defined by different linear equations on different intervals. In this case, the DigiShaper uses 16 linear equations to define its piecewise-linear curve.

Second, the algorithm uses concepts from digital signal processing, such as sample rate conversion and quantization. The input signal is first scaled by a factor of 16, then quantized to an integer value, and finally scaled back down by a factor of 16. This allows the algorithm to implement the piecewise-linear curve with only a small number of discrete levels, which reduces the computational cost of the algorithm.

Third, the algorithm uses SIMD instructions (SSE2) to perform the calculations in parallel on multiple data points at once. This improves the efficiency of the algorithm and makes it suitable for real-time audio processing applications.

Overall, the DigiShaper algorithm is a relatively simple but effective way to apply distortion to an audio signal, and it leverages a number of mathematical concepts from digital signal processing to achieve its result.

2> basically, it boils down to this equation doesn't it: (double)((int)((double)(xp0inv16.f+1.0)))p00.0625f;

Yes, that's correct. The main logic of the DigiShaper algorithm is implemented in this equation, which is used to calculate the output value for a single sample of the input signal:

(double)((int)((double)(x*p0inv*16.f+1.0)))*p0*0.0625f;

Here's a breakdown of what this equation does:

  • x is the input sample value.

  • p0 is the drive parameter, which controls the amount of distortion applied to the signal.

  • p0inv is the reciprocal of p0, which is precomputed to improve efficiency.

  • x * p0inv * 16.f scales the input sample by a factor of 16 and then multiplies it by p0inv.

  • + 1.0 shifts the scaled input signal up by 1.0, so that the output values are in the range [1, 17] rather than [0, 16].

  • (double)((int)(...)) rounds the shifted value down to the nearest integer using a cast to an integer data type, which effectively quantizes the input signal.

  • * p0 * 0.0625f scales the quantized value back down by a factor of 16 and then multiplies it by p0, which applies the distortion to the signal.

So in summary, this equation maps the input signal onto a piecewise-linear curve with 16 segments, each of which has a different slope, and the amount of distortion applied is controlled by the p0 parameter.

3> could you show me a block diagram?

Certainly! Here's a block diagram that shows the main processing steps of the DigiShaper algorithm:

                  +-------------+         +-------------+
Input signal ---> | Scale by 16 | ------> | Quantization| ------> +----------------+
                  +-------------+         +-------------+         | Piecewise-linear|
                                                                    | Curve Mapping  |
                  +-------------+         +-------------+         |                |
Drive parameter --> | Reciprocal  | ------> | Scale by 16 | ------> |                |
                  +-------------+         +-------------+         |                |
                                                                    |                |
                  +-------------+         +-------------+         |                |
Quantized value --> | Cast to int | ------> | Scale by p0 | ------> | Output Signal  |
                  +-------------+         +-------------+         +----------------+

As you can see, the input signal is first scaled by a factor of 16 and then quantized to an integer value, which effectively maps it onto a piecewise-linear curve with 16 segments. The quantized value is then scaled back down by a factor of 16 and multiplied by the drive parameter p0, which applies the distortion to the signal. The output signal is the final result of this processing. The reciprocal of the p0 parameter is precomputed to improve efficiency, and SIMD instructions are used to perform the calculations in parallel on multiple data points at once.

4> how can we improve this algorithm?

There are several ways to improve the DigiShaper algorithm:

  1. Use a higher-order curve: The DigiShaper algorithm uses a piecewise-linear curve with 16 segments to map the input signal. Using a higher-order curve (such as a quadratic or cubic curve) could provide a more flexible and expressive distortion effect. However, this would require more computation and memory, so it may not be feasible for some applications.

  2. Use a more sophisticated quantization scheme: The DigiShaper algorithm uses a simple quantization scheme that rounds the scaled input signal down to the nearest integer. Using a more sophisticated quantization scheme (such as rounding to the nearest integer or using a dithering algorithm) could improve the accuracy of the curve mapping and reduce quantization noise.

  3. Add oversampling: Oversampling is a technique that involves processing the signal at a higher sample rate than the output sample rate and then downsampling the result. This can help to reduce aliasing artifacts and improve the overall sound quality of the algorithm.

  4. Use a non-linear mapping: The DigiShaper algorithm uses a linear mapping to map the input signal onto the piecewise-linear curve. Using a non-linear mapping (such as a sigmoid function or a hyperbolic tangent function) could provide a more complex and interesting distortion effect.

  5. Implement a soft clipping function: The DigiShaper algorithm uses hard clipping, which can produce harsh and unpleasant distortion artifacts. Implementing a soft clipping function (such as a tanh function) could provide a smoother and more musical distortion effect.

  6. Add a post-filter: The DigiShaper algorithm does not include any post-processing filtering, which can help to shape the spectral content of the output signal and reduce noise. Adding a post-filter (such as a low-pass or high-pass filter) could improve the overall sound quality of the algorithm.

These are just a few ideas for improving the DigiShaper algorithm. Ultimately, the choice of improvements will depend on the specific application and the desired sound quality.

5> coul dyou explain what you mean about the piecewise-linear curve mapping? i'm not so sure i see how this equation actually does that

Sure! The DigiShaper algorithm maps the input signal onto a piecewise-linear curve with 16 segments, each of which has a different slope. The purpose of this mapping is to introduce distortion into the signal by selectively amplifying or attenuating different parts of the signal.

The equation used in the DigiShaper algorithm achieves this mapping by quantizing the input signal to one of 16 discrete levels, which correspond to the 16 segments of the piecewise-linear curve. Each segment of the curve has a different slope, which determines how much the input signal is amplified or attenuated in that segment.

Here's a simplified example to illustrate how this works. Suppose we have a piecewise-linear curve with three segments, each of which has a different slope:

     /\
    /  \
   /    \
  /      \
 /________\

If we want to map an input signal onto this curve, we can first scale the input signal by a factor of 3 to make it cover the same range as the curve:

   Input signal
        /\
       /  \
      /    \
     /      \
    /________\
  Curve range

Next, we quantize the scaled input signal to one of three discrete levels, which correspond to the three segments of the curve:

    Level 1
        /\
       /  \
      /    \
     /      \
    /________\
    Level 2
        /\
       /  \
      /    \
     /      \
    /________\
    Level 3
        /\
       /  \
      /    \
     /      \
    /________\

Finally, we apply the slope of the segment corresponding to the quantized level to the input signal:

    Level 1   Level 2   Level 3
        /\        /\        /\
       /  \      /  \      /  \
      /    \    /    \    /    \
     /      \  /      \  /      \
    /________\/________\/________\

In the DigiShaper algorithm, the quantization and slope calculation are performed using the equation we looked at earlier:

(double)((int)((double)(x*p0inv*16.f+1.0)))*p0*0.0625f;

The multiplication by p0 at the end of the equation determines the slope of the segment, and the quantization is achieved by casting the result to an integer and then back to a float.

I hope that clarifies how the DigiShaper algorithm maps the input signal onto a piecewise-linear curve!

6> this doesn't sound right, seems like nonsense

I apologize if my explanation was unclear or confusing. Let me try to explain again in a different way.

The DigiShaper algorithm maps the input signal onto a piecewise-linear curve by quantizing the signal to one of 16 discrete levels and then multiplying it by a slope factor. The equation used to achieve this mapping is:

(double)((int)((double)(x*p0inv*16.f+1.0)))*p0*0.0625f;

Let's break this down step by step:

  1. Scale the input signal by a factor of 16 and p0inv:
x * p0inv * 16.f

This scales the input signal so that it covers the range [0, 16], and then scales it further by p0inv. The p0inv factor determines the amount of distortion applied to the signal.

  1. Add 1.0 to the scaled input signal:
x * p0inv * 16.f + 1.0

This shifts the scaled input signal up by 1.0, so that it covers the range [1, 17] instead of [0, 16].

This is done because the next step involves quantizing the signal to integer values, and adding 1.0 ensures that the quantized values are in the range [1, 16] instead of [0, 15].

  1. Cast the result to an integer and then back to a float:
(double)((int)(...))

This quantizes the shifted input signal to one of 16 discrete levels by rounding it down to the nearest integer. The resulting integer is then cast back to a float.

  1. Multiply the quantized value by p0 and a scaling factor:
((double)((int)(...)))*p0*0.0625f

This applies the distortion to the signal by multiplying the quantized value by p0 and a scaling factor of 0.0625f. The scaling factor is used to scale the quantized value back down to the range [0, 1].

The resulting value is the output signal for that input sample, after being mapped onto a piecewise-linear curve with 16 segments, each of which has a different slope determined by p0.

I hope this explanation is clearer and more accurate. Please let me know if you have any further questions!

7> oh, ok so this is like a 16 level quantization function

Yes, that's correct! The DigiShaper algorithm effectively quantizes the input signal to one of 16 discrete levels using a piecewise-linear curve with 16 segments, each of which has a different slope determined by the p0 parameter. The quantization is performed by rounding the shifted input signal down to the nearest integer using a cast to an integer data type, and then scaling the quantized value back down by a factor of 16 and multiplying it by p0 to apply the distortion.

8> and the drive function is a sort of amplifier for the input signal into this quantization circuit... is that correct?

Yes, that's a good way to think about it! The p0 parameter (which is referred to as the "drive" parameter in the DigiShaper algorithm) controls the amount of distortion applied to the input signal. When p0 is set to a high value, the distortion effect will be more pronounced, and when p0 is set to a low value, the effect will be more subtle.

The p0 parameter acts as an amplifier in the sense that it amplifies the input signal into the quantization circuit by scaling the quantized value back up by a factor of p0. This has the effect of boosting the signal amplitude and adding harmonic content to the signal, which results in the distortion effect.

9> what were those triangles you were drawing up there?

I apologize if the triangle diagrams were confusing! They were meant to illustrate the piecewise-linear curve that the DigiShaper algorithm maps the input signal onto.

In a piecewise-linear curve, the curve is defined by a series of line segments with different slopes, as shown in the following diagram:

     /\
    /  \
   /    \
  /      \
 /________\

Each segment of the curve has a different slope, which determines how much the input signal is amplified or attenuated in that segment.

The triangle diagrams were meant to represent this kind of curve, with each triangle representing a line segment of the curve. The size and slope of the triangles would correspond to the size and slope of the line segments in the piecewise-linear curve.

I hope that clarifies what the triangle diagrams were meant to represent!

10> thanks! could you show me how to implement a quantizer circuit in system verilog?

Certainly! Here's an example implementation of a simple quantizer circuit in SystemVerilog:

module quantizer(input logic signed [15:0] in, input logic [3:0] levels,
                 output logic signed [15:0] out);

  // Quantize the input signal to one of the discrete levels
  always_comb begin
    case (in)
      0:     out = levels[0];
      1:     out = levels[1];
      2:     out = levels[2];
      3:     out = levels[3];
      4:     out = levels[4];
      5:     out = levels[5];
      6:     out = levels[6];
      7:     out = levels[7];
      8:     out = levels[8];
      9:     out = levels[9];
      10:    out = levels[10];
      11:    out = levels[11];
      12:    out = levels[12];
      13:    out = levels[13];
      14:    out = levels[14];
      default: out = levels[15];
    endcase
  end

endmodule

In this implementation, the in input is a signed 16-bit signal that represents the input signal to be quantized. The levels input is a 4-bit signal that represents the 16 discrete levels that the input signal can be quantized to. The out output is a signed 16-bit signal that represents the quantized output signal.

The quantization process is implemented using a case statement that checks the value of the input signal and assigns the corresponding level to the output signal. The default case assigns the highest level to the output signal for any input values that are outside the range of the defined levels.

Note that this is a very basic implementation of a quantizer circuit, and it only supports quantization to a fixed set of levels. In practice, more sophisticated quantization schemes (such as rounding to the nearest integer or using a dithering algorithm) may be needed to achieve the desired level of accuracy and reduce quantization noise.

Dependencies

~12–20MB
~302K SLoC