Workshop hosted at the Audio Developer Conference 2024 in Bristol.
Follow these instructions before attending the workshop. There is high risk that because of low WiFi bandwidth at the conference venue, it will take too long or be impossible to set it up on the spot.
You need to have git
installed and in your PATH. Here are the commands that we will use in this workshop:
git checkout <branch-name> # check out the branch with the given name
git checkout -b <new-branch-name> # create a new branch and check it out
git status # show the status of the working tree
git diff # show the difference between the working tree and the last commit
git add <path-to-file> # stage a file (especially important for untracked files)
git commit -a -m "<commit-message>" # commit all changes with a specific message
WAV files and PNG files require git-lfs
. It should be bundled with your git
installation but you may need to manually initialize it inside the repository. After cloning this repository, run
git lfs install
git lfs fetch
git lfs checkout
You can try playing back files in the data folder to check if it worked correctly.
The complete solution to all the tasks are present on the main
branch. If at any time during the workshop you need to check the reference solution, compare the difference between your working tree and the main
branch.
git diff main
In Visual Studio Code, there's a wonderful GitLens plugin that allows you to visually compare against the main
branch via "GitLens: Compare Working Tree with..." command.
For this workshop, you need to have Python 3 installed and in your PATH. Code was tested with Python 3.11 and 3.12.
It's most comfortable to work in Python using venv
(virtual environment).
# create virtual environment
python3 -m venv venv
# activate it
source venv/bin/activate # macOS, Linux
.\venv\bin\Scripts\Acitvate.ps1 # Windows
# install the dependencies within the environment
pip install .
Now, you should be able to run Python tests:
python -m unittest py/test/test_flanger.py
You also should be able to run the audio processing script:
python py/main.py data/saw200.0Hz5.0s.wav
This workshop requires the following tools installed on your machine and available in your PATH:
- a working C++ compiler that supports C++ 20,
- CMake 3.22 or newer,
- a build system like ninja (recommended), MSBuild (bundled with Visual Studio), xcode-build (bundled with Xcode), or make (UNIX Makefiles)
Once this is in place, you can run classic CMake commands:
# Downloads C++ dependencies and generates the project for ninja (check CMakePresets.json for other generators)
cmake --preset default
# Builds the project
cmake --build --preset default
C++ code in this repository is based on my audio-plugin-template
repository.
The repostitory works with Xcode 16. Older versions of Xcode may be problematic.
On Windows, to have the compiler on your PATH, you need to run a batch script. Assuming, you have Visual Studio 2022 installed, you can run the following command to initialize C++ environment in your shell.
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
To create a Python virtual environment, I needed to install python3.12-venv
package with
sudo apt install python3.12-venv
Before generating the project above, you need to install JUCE dependencies with the following command:
sudo apt update
sudo apt install libasound2-dev libjack-jackd2-dev \
ladspa-sdk \
libcurl4-openssl-dev \
libfreetype-dev libfontconfig1-dev \
libx11-dev libxcomposite-dev libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev libxrender-dev \
libwebkit2gtk-4.1-dev \
libglu1-mesa-dev mesa-common-dev
This workshop consists of several tasks in a logical progression. The goal is to build a working flanger plugin in C++ using the JUCE C++ framework.
You should start each task by checking out the appropriate branch (task1
, task2
, etc.).
git checkout task1
Given this DSP block diagram of a flanger plugin,
Figure 1. Flanger DSP diagram.
write down flanger update equations.
Legend:
-
$x[n]$ is the input signal -
$y[n]$ is the output signal -
$x_h[n]$ is a helper signal (used for convenience) -
$D$ is the length of the delay line -
$\text{feedforward}$ ,$\text{feedback}$ , and$\text{blend}$ are coefficients (all equal to 0.7 for a flanger) -
$s_\text{LFO,unipolar}[n]$ is the unipolar LFO signal (a sine in the [0, 1] range) -
$m$ is the modulated delay value -
$x_h[n-D/2]$ denotes the helper signal delayed by$D/2$ samples
Note
- Write down the equation for
$y[n]$ (the output). - Write down the equation for
$x_h[n]$ (the helper signal). - Write down the equation for the value of the variable delay
$m$ at the$n$ -th processed sample.
These equations are the basis of the prototype.
git checkout task2
- Run
python py/main.py data/saw200.0Hz5.0s.wav
and check that audibly the output signal is the same as the input signal- That's because the flanger returns the input sample in the
process_sample()
function.
- That's because the flanger returns the input sample in the
- Inspect the spectrograms generated in the output folder. Are they identical visually?
- Properly initialize the
Flanger
class instance- Set
max_delay
property of theFlanger
to 2 milliseconds in samples - Instantiate the
delay_line
property as aFractionalDelayLine
instance withmax_delay
- Compute
$D/2$ : half ofmax_delay
and store inmiddle_delay
property - Initialize
feedback
,feedforward
, andblend
properties to 0.7
- Set
- Implement flanger update equations without the LFO in
process_sample()
in flanger.py- Write down your update equations in the
process_sample()
function as comments - Calculate the value of the helper signal sample
$x_h[n]$ - Calculate the output sample
$y[n]$ (use a fixed delay instead of a modulated delay for now) - "Update the buffers": write
$x_h[n]$ into the delay line - Return the output sample from the function
- Run
python py/main.py data/saw200.0Hz5.0s.wav
and check if you hear a spectral coloration of the input. - Inspect the spectrograms generated in the output folder. How is the spectrogram of the output different from the spectrogram of the input? Was it expected?
- Write down your update equations in the
- Add an LFO
- Instantiate an
LFO
instance in theFlanger
constructor - Immediately set its freuqency to 0.1 Hz
- In
process_sample()
,get_next_value()
of the LFO and transform it to unipolar [0, 1] range - Calculate the delay value of the modulated tap for the current sample
- Replace the previously fixed delay value in output sample computation with the LFO-modulated one
- Run
python py/main.py data/saw200.0Hz5.0s.wav
and check if you hear the influence of the LFO (the signal slowly changes its timbre) - Inspect the spectrograms generated in the output folder. How is the spectrogram of the output different from the "fixed modulated delay" version? Was it expected?
- Instantiate an
git checkout task3
In this part, your goal is to create a basic working implementation of the flanger plugin. For this purpose, we'll reuse my audio-plugin-template
repository.
This is the class diagram depicting the connections between the classes in a completed project (except handling multiple channels).
Figure 2. Plugin class diagram.
I intentionally set a high warning level and enabled "set warnings as errors" compilation option. As C++ developers we should utilize to the fullest what the compiler provides because compilers are highly specialized expert pieces of software. In plugin development we have the luxury of starting often with green-field projects (i.e., create a codebase from scratch). Starting out is the best time to enable said options. Every time you run into a compilation error, try to understand what compiler tells you and fix the warning in code.
In this part, you can put all your code into Flanger.h.
- Generate and compile the C++ project as indicated in the getting started section. Does the project build successfully? Can you see the "installation" message?
- Run the end-2-end test with
ctest --preset <used-preset>
. Is the output file the same as the input file? - Run the digital audio workstation (DAW) of your choice. Can you put the "Workshop Flanger" plugin on a track? Does it leave the input unaltered?
- Add feedforward, feedback, and blend coefficients as class members of
Flanger
- Add
FractionalDelayLine
member to theFlanger
- Implement
Flanger::reset()
where you should reset relevant member(s). - Add
maxDelay_
andmiddleDelay_
members to theFlanger
class (analogous to the Python class) - Initialize them in
prepare()
with the same lengths in samples as in Python - Implement update equations in
processSample()
as you did in Python. Use a fixed delay tap instead of a modulated delay tap (exactly as the initial version in Python); you'll add LFO later - Build your project
- Run the end-2-end unit test with
ctest --preset <used-preset>
again. Can you hear a spectral coloration in the output file? Is it the same as in the initial (fixed-delay) Python implementation?
git checkout task4
In the final part of this workshop, you will add the LFO and expose its frequency as a plugin parameter using JUCE APIs. You can implement your own LFO class, however, for simplicity, we'll use JUCE's juce::dsp::Oscillator
. We'll expose the parameter in the GUI using juce::GenericAudioProcessorEditor
. Finally, we'll add stereo processing using juce::dsp::ProcessorDuplicator
; that's why we used prepare()
/process()
/reset()
member function trio.
Usually, we would implement parameter smoothing in this part but juce::dsp::Oscillator
does that for us already.
Note
juce::dsp::Oscillator
adds the generated sample to its input. Thus, to obtain the generated sample, we must call Oscillator
's processSample()
member function with 0
, i.e., const auto generatedSample = myOscillatorInstance_.processSample(0)
.
- Add an LFO
- Add a
juce::dsp::Oscillator
member and initialize it with a sine oscillator -
prepare()
it inFlanger::prepare()
-
reset()
it inFlanger::reset()
- Set its frequency to 0.1 Hz in
Flanger::prepare()
- Compute the unipolar LFO value in
Flanger::processSample()
- Calculate the modulated delay value and use it in the output sample calculation in
Flanger::processSample()
- Compile the project
- Run the end-2-end unit test with
ctest --preset <used-preset>
again. Can you hear the influence of the LFO in the output file? Is it the same as in the final (modulated-delay) Python implementation?
- Add a
- Make parameters (LFO frequency) adjustable
- Create a
juce::AudioParameterValueTreeState apvts_
member inPluginProcessor
- Create a
juce::AudioParameterFloat
instance uponPluginProcessor
construction and add it to theapvts_
; use the following parameters:- ID "flangerFrequencyHz"
- version hint 1
- name "LFO frequency"
- range [0.1, 10] Hz with steps of 0.01
- default value 0.1 Hz
- label "Hz"
- Retrieve the pointer to the parameter (or to its raw value) and store it in a
PluginProcessor
's member - Create a
Flanger::Parameters
struct with a singlewolfsound::Frequency
member; this is to pass the LFO frequency to the flanger - Initialize the member with 0.1
- Create a setter for the parameters, i.e.,
Flanger::setParameters(const Parameters&)
where you update the frequency of thejuce::dsp::Oscillator
instance - Set flanger's parameters with this setter in
processBlock()
after retrieving the plugin parameter value
- Create a
- Return a
juce::GenericAudioProcessorEditor
inPluginProcessor::createEditor()
(we're just working on the DSP part after all) - Compile the plugin
- Run the plugin in the DAW. Can you adjust the LFO frequency and the change is audible?
- Make the plugin stereo using JUCE’s
juce::dsp::ProcessorDuplicator
- Change
PluginProcessor::flanger_
member type tojuce::dsp::ProcessorDuplicator<Flanger<SampleType>, Flanger<SampleType>::Parameters>
- Add an alias in
Flanger::Parameters
:using Ptr = std::shared_ptr<Parameters>
- Add a
Parameters::Ptr
member inFlanger
- Add an
explicit
constructor ofFlanger
that takesParameters::Ptr
as its only argument and stores it in the just added member (you may need to default the default constructor implementation) - Call
setParameters(*parameters_)
inprocess()
before the actual processing begins - Instead of calling
flanger_.setParameters(newParameters)
inprocessBlock()
, set the shared state with*flanger_.state = newParameters
- Compile the project
- Run
AudioProcessor.StereoTest
withctest --preset <used-preset> -R "StereoTest"
. Does it pass? - In the DAW of your choice, load data/saw200.0Hz5.0s_stereo.wav on a track and put the plugin on the same track. Are both channels processed in the same way? Are changes to LFO frequency reflected accordingly?
- Change