vult-dsp / vult

Vult is a transcompiler well suited to write high-performance DSP code
https://vult-dsp.github.io/vult
Other
490 stars 25 forks source link

Range of output samples, [-1, 1] or [0, 1] #20

Closed cristiano-belloni closed 7 years ago

cristiano-belloni commented 7 years ago

Sorry, this is silly, but I didn't find the answer anywhere in the docs. I'm trying to write a simple waveshaper, like in here - I adapted the code to not use a waveshaping table (creating a 44k samples table kills the vult compiler, LMK if you want me to open an issue about that):

fun distortMdn(x: real) {
    val k = 20.;
    val n_samples = 44100.;
    val deg = 3.14159265358979323846 / 180.0;
    val out = ( 3. + k ) * x * 20. * deg / (3.14159265358979323846 + k * abs(x));
    return out;
}

It does almost nothing to my output, no matter how high the k is. The transfer function is like this).

I'm wondering if this could be due to the range of samples in vult being from 0 to 1 instead of -1 to 1? I couldn't find any reference to what the output range is.

modlfo commented 7 years ago

The range is from -1.0 to 1.0. I created this code that you can paste into the web demo. You can use the CC30 to change the k.

fun shaper(x,k) {
    val pi = 3.1415;
    return (3.0 + k) * x * 20.0 * (pi/180.0)/(pi + k * abs(x));
}
fun process(input:real){
   mem k;
   return shaper(input,k);
}
and noteOn(note:int,velocity:int,channel:int){}
and noteOff(note:int,channel:int) {}
and controlChange(control:int,value:int,channel:int){
  if(control==30)
    k = real(value)/ 1.0;
}
and default(){}

As you increase the k is possible to hear the harmonics. If you have a fixed k it's possible to use the @[table()] attribute. I haven't tried to create very large tables, but the intention behind the implementation is to be able to use small tables that produce a nice sound. The interpolation used with @[table()] is second order. One would require large tables when using first order interpolation.

In this code I created the wave shaper with the @[table()] attribute, and if you move the CC30 if will change between the two implementations. I cannot hear any difference between them.

fun shaper_original(x,k) {
    val pi = 3.1415;
    return (3.0 + k) * x * 20.0 * (pi/180.0)/(pi + k * abs(x));
}
fun shaper_table(x) @[table(size=127, min=-2.0, max=2.0)] {
    val pi = 3.1415;
    val k = 10.0;
    return (3.0 + k) * x * 20.0 * (pi/180.0)/(pi + k * abs(x));
}
fun process(input:real){
   mem mix;
   val o = if mix > 0.5 then shaper_original(input,10.0) else shaper_table(input);
   return o;
}
and noteOn(note:int,velocity:int,channel:int){}
and noteOff(note:int,channel:int) {}
and controlChange(control:int,value:int,channel:int){
   if(control==30)
      mix = real(value)/127.0;
}
and default(){}
cristiano-belloni commented 7 years ago

Thanks. Yes, I feel the harmonics if I turn the knob with my karplus-strong process function. Apart from being simplified and taking k as an input, your implementation is substantially equivalent to mine, but the output is still underwhelming and the effect very subtle, so I guess there's something else in play here. For comparison, pizzicatojs uses the same waveshaper with 0 < k < 100, but their "Distortion" demo sound much more "distorted" than my waveshaper. Maybe the Web Audio API is doing something else in the background? I'll investigate.

Thanks for the table example! That's really cool, and I still had a couple of doubts about tables that your example solved. I still don't understand why you generate the table in a -2.0, 2.0 range, since the input ranges from -1 to -1. Is it to prevent clipping input to return the wrong value? What would happen if I called it with 5, for example?

modlfo commented 7 years ago

When called outside the range, you get the extrapolated value of the limits. That's why I always leave a little bit of space in the range. If possible, I clip the input value to fit the range (check the implementation of saturate here https://github.com/modlfo/vult-examples/blob/master/src/util.vult

In your example, if you want more aggressive distortion, you can do what's done in the guitar pedals, multiply the input. For example:

fun shaper(x,k) {
    val pi = 3.1415;
    return (3.0 + k) * x * 20.0 * (pi/180.0)/(pi + k * abs(x));
}
fun process(input:real){
   mem gain;
   return shaper(gain*input,10.0);
}
and noteOn(note:int,velocity:int,channel:int){}
and noteOff(note:int,channel:int) {}
and controlChange(control:int,value:int,channel:int){
  if(control==30)
    gain = 1.0 + real(value);
}
and default(){
   gain = 1.0;
}

As the gain increases, the sine wave becomes a square signal.

cristiano-belloni commented 7 years ago

Yep, saturate was what triggered https://github.com/modlfo/vult/issues/18 :) Clipping with gain helps a bit, but the sound is still different and underwhelming. I will investigate. Thanks!

modlfo commented 7 years ago

I updated the code. You should be able to use tanh in the web.

modlfo commented 7 years ago

By the way. I tested a tanh distortion with a sample of a real guitar and the distortion sounds really cool. While the same shaper with very ideal waveforms (like sine, or saw) sounds very week.

cristiano-belloni commented 7 years ago

Actually I'm generating the guitar string sounds from a simple Karplus-Strong model (one of the reasons I'm using Vult is to experiment with physical models and feedback). Turns out that the same model, written in javascript, sounds very weak even with the WAA wavershaper node.

Normally what people do is adding a bit of presence with a cabinet emulation based on an impulse response, which I guess is ok - I just need to write the convolution bit. The only hard thing is that I will have to manually load the IR somehow - any plans to add file i/o to Vult in the future, @modlfo? :D

modlfo commented 7 years ago

Yes. I have been experimenting with Wav file reading at compile time. I want to be able of embedding wav files as arrays. But I'm not sure how this can work on the web demo. It will require to generate the code from the terminal and somehow load it on the browser.

As a workaround you can declare an external function that takes an index and gives you back the sample of an IR and provide that function as external Js.

I'll let you know when I have this available.

cristiano-belloni commented 7 years ago

What comes to mind is just let the user specify a baseurl, then wrap the generated code in a fetch callback and put the retrieved file in a typed array. The user just has to host the files somewhere. But of course the CORS headers should be enabled on the hosting side and that would be complicated.

What would be really cool would be an online IDE that hosts the files (code and data) for the user. Then the user would prototype online and loading externals + loading data files would automagically work. Then the user could just export for a given target with a button and download the result. It could even be a service - write once, save as vst / code for dsp board / web audio component.

Is it easy to operate the compiler from javascript? I understand you generate a javascript lib with an utility that traslates ocaml to js. If all the APIs are there, I would like to try and do a PoC. Not being able to load externals in the web IDE is kind of frustrating, so the first target could be that :)

modlfo commented 7 years ago

The compiler API exposes the all the command line options, but the input files need to be passed as strings. This is the npm library https://www.npmjs.com/package/vultlib In the web demo I add a few wrappers for that code.

Regarding the online IDE, I have one idea and I made some progress. Here's the basic idea https://github.com/modlfo/VultPlatform/wiki

I worked a bit in the (local) (AudioEngine)[https://github.com/modlfo/AudioEngine] but I have been slow with the development of the clients. I have been trying to write a simple client (in a private repo) in Elm.

Other idea that I have in mind is making a Visual Studio Code plugin that connects to the AudioEngine to run the code.

If you have a recommendation on how I should generate/export the Js code so is easy to use let me know.

cristiano-belloni commented 7 years ago

The Vult Platform idea is really cool. IMHO it should support multiple files. Also, if many people use it, a pseudo-registry (maybe a thin layer over GitHub) would be a quick win.

Exporting code to Javascript could be complex if loading files comes into play, depending on what is the target. You could probably solve it like this:

Previewing in demo: Have the user upload its audio assets. Generate the javascript code like you do now, but wrap it in parallel fetch calls (maybe using async) to get the assets from whenever they're stored. Firebase Storage, for example, is dirty cheap and easy to use.

Exporting to a file: The user can select whether to export as a ES6 module or an inline module. Generate the code like you do now, then: