Writing a script
So, now we know how to load and save scripts, we can write our first script. Traditionally, the first thing you should learn in DSP (kind of "Hello world!") is adjusting the gain (volume) of the signal.
There is a gain script in the "Factory Scripts - Utility" that does what we'll learn.
But let's try to make it all from the ground up.
To start a new script you can load an empty "default" script and save it under the new name to your scripts folder. After that click "Edit Script" to open it in your text editor.
So now we see the script contains only the name and description. Let's change it, save and reload to see if everything works fine.
- string name="My Gain Test";
- string description="This is my first plugin"; // this line is optional
If after reloading you mouse over the plugin title you'll see the plugin description. The description is optional, so you can omit it.
Now let's do some processing.
Usually, plugins receive data in blocks, for example, a block of 512 samples (for each channel). The maximum block size usually is the same as user's ASIO settings. We'll talk about processing blocks later, but for now, let's take a look at an even more convenient way.
To make our life simpler (so that we don't have to care about blocks) Blue Cat Audio implemented yet another function called processSample, that we might learn first.
What it means
The first word void means than this function doesn't return any value (so it's like a "procedure" in other languages).
This function receives an array of "doubles". Double is a type of data (like int or float), that holds a floating-point value with double precision (it takes 8 bytes = 64 bits to represent a number), so in other words, we'll be doing 64-bit audio processing.
The & sign after array<double> means that we receive this array "by reference". So we can change its values and it will affect the original array. If there was no & sign, we'd work with a copy of the array.
Every element in this array is the current sample value for that channel in values from -1 to 1. The picture represents the maximum amplitude values in digital form (values above 1 and below -1 would be considered "clipping").
For example, if we're on the stereo channel, we'll get an array of two elements:
- ioSample[0] = sample value for left channel
- ioSample[1] = sample value for right channel
If we're on the mono channel, there will be only ioSample[0]. For multiple channels tracks or sidechain inputs there will be more elements in this array.
Number of channels
The current number of input channels is kept in "audioInputsCount" variable. There's also "audioOutputsCount" for outputs.
More in "DSP API" chapter.
So, let's modify these sample values:
- ioSample[0] = ioSample[0] * 0.5;
- }
This way we get a sample value of our left channel (or mono) and make it twice lower in amplitude, which is ~6 dB lower.
The better way to write this would be slightly shorter. And let's add the right channel as well.
- ioSample[0] *= 0.5;
- ioSample[1] *= 0.5;
- }
a *= b; is a short way to write a = a*b;
If we want our plugin to work on both mono and stereo channels, we could write something like that:
- if (audioOutputsCount > 1) {
- // for stereo+
- ioSample[0] *= 0.5;
- ioSample[1] *= 0.5;
- } else {
- // for mono
- ioSample[0] *= 0.5;
- }
But another good way would be simply cycling through all available channels:
- for(uint i=0;i<audioInputsCount;i++) {
- ioSample[i]*=0.5;
- }
Adding a knob
Okay! We probably don't want a plugin with a fixed gain, let's add a couple of knobs to control the gain of left and right channels independently.
- array<double> inputParameters(2); // this is how many input controls we want
Now we get two knobs. You can select the style of the knobs from the dropdown menu.
Let's give our input parameters good names:
- array<string> inputParametersNames={"Left", "Right"}; // names of input params
If we don't set min and max values for our params they will return values from 0 to 1 (showing 0 to 100% in the interface).
So we can multiply our sample values by the value of these params. You get the value of the first knob in inputParameters[0] and the second in inputParameters[1].
- {
- if (audioOutputsCount == 2) {
- ioSample[0] *= inputParameters[0]; // left
- ioSample[1] *= inputParameters[1]; // right
- }
- }
Note that this script will work only on stereo channels. You can make an additional check that it's used on a stereo track in the "initialize" function.
- // if not a stereo channel
- if (audioOutputsCount != 2) {
- print("Stereo channels only!"); // print to log file
- return false; // stops processing a script
- }
- // if all ok
- return true;
- }
New functions
Initialize function is called before everything else, so you can prepare and check some parameters in it, and if you return false, the script will not run further.
The print function can be used for debugging, it saves a line to a log file that you can open clicking on the status bar.
Now let's do some optimization. One of the things you should avoid in AngelScript is accessing the arrays too often, because "under the hood" it is converted to function call, which takes time. So we can save some CPU if we don't access inputParameters[] every sample, but save its values in another variables.
- // we'll keep our gain values here (set default to 1)
- double gainLeft = 1, gainRight = 1;
- // this is main audio processing function
- if (audioOutputsCount > 1) {
- ioSample[0] *= gainLeft;
- ioSample[1] *= gainRight;
- }
- }
- // this function is called when inputParameters change
- gainLeft = inputParameters[0];
- gainRight = inputParameters[1];
- }
We've just added a couple of new variables (gainLeft and gainRight) and a new function updateInputParameters. This function is called only when our input parameters change. And it's called before every processSample call while the input parameter is changing. Let's say you're slowly turning your know, or there's an automation of your parameter in the DAW, this updateInputParameters function will be called for every sample while any parameter changes.
When the parameters are static, it will not be executed and we'll save some CPU inside processSample.
It will get even better when you have a lot of input parameters, and when you have to calculate some values based on these parameters. You'll do recalculation only when these parameters change, and not on every sample.
Gain in dB
Now, the final step for this lesson: let's make our knobs control gain in dB and not in absolute values.
For that let's add additional info to our input parameters:
- array<double> inputParameters(2); // this is how many input controls we need
- array<string> inputParametersNames={"Left", "Right"}; // names of input params
- array<string> inputParametersUnits={"dB", "dB"}; // which units to use
- array<double> inputParametersMin={-20, -20}; // min values
- array<double> inputParametersMax={20, 20}; // max values
- array<double> inputParametersDefault={0, 0}; // default values
Now our knobs show and return "db" values from -20 to 20. And we have to convert them to "gain" values from 0 to 1+ to multiply with our sample values (it will be bigger than 1, cause we want to increase gain as well). Let's use a special formula to convert db values to gain values:
- // convert dB values to gain values
- // formula: gain=10^(gaindB/20)
- gainLeft = pow(10,inputParameters[0]/20);
- gainRight = pow(10,inputParameters[1]/20);
- }
And now we're done!
Here's the full script that we've created:
- // Double gain demo
- string name="My Gain Test";
- string description="This is my first script"; // this line is optional
- array<double> inputParameters(2); // this is how many input controls we need
- array<string> inputParametersNames={"Left", "Right"}; // names of input params
- array<string> inputParametersUnits={"dB", "dB"}; // which units to use
- array<double> inputParametersMin={-20, -20}; // min values
- array<double> inputParametersMax={20, 20}; // max values
- array<double> inputParametersDefault={0, 0}; // default values
- // we'll keep our gain values here (set default to 1)
- double gainLeft = 1, gainRight = 1;
- // this function is called before everything else
- // if not a stereo channel
- if (audioOutputsCount != 2) {
- print("Stereo channels only!");
- return false; // cancels script execution
- }
- // if all ok return true
- return true;
- }
- // this is main audio processing function
- if (audioOutputsCount > 1) {
- ioSample[0] *= gainLeft;
- ioSample[1] *= gainRight;
- }
- }
- // this function is called when inputParameters change
- // convert dB values to gain values
- // formula: gain=10^(gaindB/20)
- gainLeft = pow(10,inputParameters[0]/20);
- gainRight = pow(10,inputParameters[1]/20);
- }