EMCT: COMPUTING MAJOR PROJECT
Finding a way to process audio samples and instantiate DSP behaviour.
In this post I want to delve into one of the largest parts of this project — the RustyProcessor and the advanced Digital Signal Processing (DSP) that exists inside of them.
As development of the RustyAudioLibrary continues, I’m now aiming to standardise how audio is manipulated within the library. This has led to the creation of the RustyProcessor base class, which is serving as the base model for DSP units functioning within the library. I opted for a method commonly used among other libraries — instantiating a Pure Virtual Function called process() that accepts a pointer to either an audio buffer or an audio sample. This decision led me to weigh the trade-offs between sample-by-sample and audio buffer processing. Given my familiarity with one over the other, I opted for sample-by-sample despite the efficiency trade-off from the increased process() function calls.
RustyProcessors can be as basic as signal multipliers or as complex as the RustyCompressor. The beauty of the RustyProcessor lies in its standardised behaviour that allows for adding more and more DSP units to the system. This flexibility was a critical factor in my decision to implement DSP as I did and another example of why building a library, NOT plugins has been a good move.
Managing the signal path has been the next challenge and I believe the method I am instantiating now will be able to handle everything I need from it. The RustyAudioEngine will manage a “signal chain”. This will be achieved by using a vector of pointers to the instantiated RustyProcessor objects. Then RustyAudioEngine iterating through the vector, sequentially calling the process function on each RustyProcessor and then outputting the samples.
This approach simulates a signal chain but has already led to questions. Where do the instantiated RustyProcessors live? If they are not inside the RustyAudioEngine, where are they? How will the RustyAudioEngine call the process() functions from these objects if they are not within the scope of the audio engine?
To answer these questions, I started development on the RustyAudioLibAPI, a top-level API that will manage the creation of units following a factory-style creation function method. This strategy means the top-level API will manage units and maintain ownership of everything instantiated within RustyAudioLibrary. Users, therefore, will not directly interact with the units they instantiate but instead call a creation function like createRustyGainUnit(), which will handle the creation of the unit and return a pointer to the object for further manipulation.
Making progress with these components has also turned my mind to another area too, advanced DSP. I want to build at least one notably advanced RustyProcessor that instantiates very heavy DSP. I’ve ended up landing on Compression. The design of a compressor is a sophisticated form of dynamic processing involving multiple steps. I have began building this unit but am running into serious difficulties regarding debugging and analysing my code at runtime. I have an idea to construct a Python environment that could facilitate audio manipulation, letting me prototype my equations somewhere much easier than in my C++ code. I’ve yet to get round to this though, but I’ve worked a lot in Python for sound and signal processing as well as machine learning so don’t think I’ll have too much difficulty getting it all to work.
In conclusion, the RustyProcessors are at the forefront of the libraries functionality. They are a testament to the journey I have taken so far, and interestingly act as small plugins in and of themselves. With each RustyProcessor being a small audio effect in it’s own right. But within the larger ecosystem of RustyAudioLib which will allow for more experimentation.
Stay tuned for more but it’s likely I won’t write another post until the project is finished. So, see you then!
Written: Jul 5, 2023