Git Product home page Git Product logo

nathanjhood / biquads Goto Github PK

View Code? Open in Web Editor NEW
13.0 13.0 1.0 12.54 MB

Simple two-pole equalizer with variable oversampling.

Home Page: https://www.stoneydsp.com/projects/biquads

License: GNU General Public License v3.0

C++ 68.22% CMake 20.74% Objective-C++ 4.20% C 6.84%
audio audio-processing audio-unit bilinear-transformation biquad biquads cmake cplusplus cpp direct-form-i direct-form-ii direct-form-transposed dsp equalizer juce juce-framework stoneydsp vst3

biquads's Introduction

Hi! I'm Nathan (aka StoneyDSP).

I am a software developer with a strong interest in system-level programming.

StoneyDSP's GitHub stats

I am also a musician making Audio software plugins using a variety of languages and workflows, and also have experience in web development.

StoneyDSP's GitHub stats

In my personal work, I have developed an interest in automating build systems for system-agnostic interoperabilities and cross-compiling routines, with an eye for driving continuous integration/deployment pipelines in modern software development and production workflows.

Tech Stack

Here you will find public repos of my personal research and development of audio plugins using popular frameworks such as JUCE and my own DSP, and more recently; explorations in cross-compiling toolchain technologies (WSL, MSYS2, MinGW, etc) and popular buildsystems (CMake, Meson, Make, Autotools, etc); code bindings to families such as the Win32 API, Linux Standards Base and Unix Single Specification; and usage of the Component Object Model alongside the C and C++ standard libraries, which you may download, compile, use, and study for free (mostly MIT or GPL2/3).

Biquads UBento
noderc base64
OrfanidisBiquad CxxWin
NonLinearFilters modules
AudioPlugin-SVF AudioPlugin
cmodule http2

Much of my code as presented here is something of a personal archive, a series of breadcrumbs through a journey of personal development. I anticipate that my work might mostly be useful as study pieces for other learners on the same path. My projects are typically either something I needed for a specific reason, or something I needed to learn about. The codebase itself strives for a minimal dependency overhead, in favour of longer shelf-life, cross-environment stability, and lower cognitive load. I tend toward traditionalism over modernism, but consider myself flexible and emperically-minded. If you have any suggestions or questions, get in touch - I love to chat code!

Coffee! That's how I get things done!! If you'd like to see me get more things done, please kindly consider buying me a coffee or two!

StoneyDSP on Patreon

biquads's People

Contributors

nathanjhood avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

davechambers

biquads's Issues

Oversampling-related issues #1 - "numChannels changed"

Current version: 1.0.4b

Currently, our plugin is fixed to only allow for stereo (essentially dual-mono) processing. While it would be nice to have an entirely flexible bus architecture to support as many channels as the client requests, this is not possible with the current code due to the OS scheme in place.

Our oversampling is initialized in our ProcessWrapper constructor to have 2 channels - a fixed (constant) integer value, with no means of changing this during run-time if desired. The supported busses layouts code has been chosen to reflect this hard-coded channel count, for now.

In order to have channel-extensible oversampling, we would need to have the correct build/initialize/reset mechanisms in place, likely driven by events coming from Audio Processor's "numChannelsChanged()".

The alternative is to restrict the plugin to pre-defined stereo (dual-mono) processing and reflect as such in the literature.

Optimization - create a "Equalizer band parameter group" factory method to generate and process an array of "Equalizer band parameter group"

Chore

Describe the chore

I have successfully moved the Biquads DSP into one of my favourite C++ constructs - the auto-ranged 'for' loop - meaning that each audio block is now processed by a Biquad array, before the next block comes in.

This is a huge improvement over the previous method, where the same block was being passed from Biquad to Biquad per sample-tick, which was creating classic audio block artefacts (crackling/noise, focused in the high end of the audio spectrum).

Interesting side-note: in developing the above, JUCE's 'declare non-copyable with leak detector' macro really shone a light on the exact syntax to make sure we're not making any copies of the processing array by mistake... this macro might be very handy for a generic Var class that we might validate non-copying on the audio thread DSP as a unit test in the future? hmmm...

Now, I am keen to do the same array-iteration/range-based-for-loop thing for the parameters, which are now all tidily grouped together, given the current build's project version number for compatibility control (very nice feature!) and all sit under a global parameter group, which is also versioned nicely.

The tricky part is the construction and initialization of an unknown number of parameter groups, containing as-yet unspecified parameters. Consider the labels and unique ID's which all need to be known at construction/initialization, but accept string classes.

After giving this some preliminary consideration, I am thinking that the best thing to do might be:

  • Define an object, which is a juce audio parameter group, containing one frequency float, one gain float, one resonance float, one type choice, and one bypass boolean
  • Define a factory function for constructing one of the above object on-call
  • When calling 'createParameterLayout', pass in the size_t of BiquadArray, which we define globally, and use it to construct an array (or container class of some sort) of size BiquadArray, with each element in the array being a call to the EQ band parameter group factory method
  • If my logic is sound (aside from being abstract), then we should be able use this global size_t BiquadArray to drive the generation of two array types; one containing the DSP processors, and one containing a matching group of parameters.

One current benefit/issue (concurrently) in the current implementation is that the entire list of params is being updated on every new audio block... this means that parameters should be updatable simultaneously, perhaps over Midi or channel data of some sort (also think CLAP, VCV Rack, etc). This is also a drain, as the entire thing is written in a big ugly list, that all gets processed one at a time, per call to update().

It will be much better to use some sort of looping construct to processes each param group 'for each' DSP processor, but ideally, while maintaining this 'non-blocking' principle, where a user should in theory be able to update two un-related parameters from two different equalizer bands at the same time, and not have one block the other. If blocking between params and/or param groups cannot be avoided, then it must be managed with logic and reason (haw haw).

This doesn't really need to be a blazing fast CPU fest at all... but those repetitive lists of params and updates is quite an eyesore for a language as rich as C++, and most of the time, using these more modern constructs leads to much safer and more performant code (plus the things you learn in doing so).

Additional context

I created a size_t of 4 which is the number of Biquads to hold in the array. I feel that this var might actually sit nicely at a more global level, where it can be referenced as a single source of truth for how many "equalizer bands" our plugin should manage; both DSP array, and the number of parameter groups to be generated.

This size_t must be a compile-time constant, probably via a constexpr, since we are not intending to attempt dynamic creation and deletion of N number of bands at runtime. If we were ever to go down the route of only generating the visual elements for the active number of bands, then that should not be dynamically constucting and deconstructing any DSP classes, nor parameter groups. I advise creating groups in pow2 numbers, with an ideal limit being 16 bands (think 16 MIDI channels). The requirement in such a case would be to have 16 bands - and their corresponding parameters - constructed on plugin instantiation, and then the "show/hide" of inactive bands would only pertain to graphical rendering, such as in our Editor unit - which is currently not implemented at all.

GUI decisions...

Chore

Describe the chore

I'm strictly not a visual/graphical designer. Unfortunately I have absolutely no natural talent for visual design, not even basic 101 stuff like using the right text colour against a given background.

One can only set out to achieve so many things in one's lifetime. I have no aspirations to improve my visual design.

The GUI side of an audio plaugin seems to be even more work than the audio side itself. I'm not really up for that kind of time-sink on something as auxiliary as graphics.

For 1.2.0, I've reverted to the juce GenericAudioProcessorEditor.

I may have a go at overriding this class, or ideally just tweaking the look and feel, on a framework-level basis (i.e., applicable to all my audio plugin work, poentially under StoneyDSP::stoneydsp_graphics).

I'm quite happy with the generic editor for solving the issue for me. It does however mean that the undo manager (and potential preset manager) are not connected to the actual audio processor and thus not implemented in the final build, this way. But for a one-band EQ for analysis purposes, that shouldn't hurt anybody.

(Honestly, this DSP is not what I personally would use for any real-life audio mixdown...)
s
One small issue is that the images in the documentation do not match the current release build visually. Those images are also very quick screengrabs that were done before I realized anybody would ever take a passing interest in this project.

If we stick with the generic editor; the images should be re-done, partially to include the revised GUI.

If we re-implement the old GUI: the images should probably be re-done anyway, cropped to fit sizes that are sane for web browser contexts.

This may be a long-standing chore to file future GUI work under.

Refactor - StoneyDSP Library

The existing implementation is still a little messy; previous attempts at improving things within the codebase have lead to some complexity that is probably not necessary, given that the project is mostly just academic in nature.

Moreover, I've gotten to grips with making API's and modules with C++ and CMake in graceful ways that should now allow me to build my own library/extension for JUCE.

My intention therefore is to begin parting out the relevant source code for modularisation. This is already somewhat considered in some of the design aspects of the C++ used code, but there is currently no larger library or framework supplying these files to (potential) consumers.

The C++ module code shall be supported by a possibly optional CMake API, designed to provide consumers with easy CMake targets to link with in their projects, allowing them to easily pull in the C++ modules from various sources such as vcpkg, or even have system installations of the library, with which to build.

Furthermore, the proposed work would also allow some scope for the creation, development, and testing of a custom vcpkg registry from which the modules could be supplied.

Ideally, this external project should be able to do a CMake find_package(StoneyDSP 1.0.0 CONFIG REQUIRED) and then a target_link_libraries(Biquads PRIVATE stoneydsp::stoneydsp_audio stoneydsp::stoneydsp_graphics), and then only require the standard ProcessBlock() and MainComponent from the JUCE audio plugin template.

I have begun work on the library itself, and will be using this project as a test/example of the library.

Oversampling-related issues #2 - Frequency compensation schemes and depth

Current version: 1.0.04b

The typical BiQuad filter implementation is only safe within a range of it's frequency parameter - in our case, we have clamped the "frequency" parameter to min (SR/24576.0) and max (SR/2.125) ranges, and placed assertions if our parameter goes beyond the range of 20hz...20,000hz.

In our latest build, we have variable oversampling (up to x16) which can be used to combat the ill-effects of digital cramping (at the expense of heavy phase distortion, no less!) which affects the audio path only.

During testing and building, any increase in OS rate would be reflected in our filter's centre frequency shifting upwards by the same multiplicative amount as our selected OS setting. A quick and dirty fix is to apply our variable named "overSampling factor" to the frequency parameter by a division, before going into the biquad processor.

The above works just fine operationally - the correct centre frequency is returned by the filter output. However, we are able to crash via assertion if we move up to, say, x16 oversampling and bring our frequency down toward it's minimum position. The position is marked on the GUI as 20hz, but of course our division is causing the actual number to be changed inside our BiQuad processor - our hz value, as the BiQaud processor sees it, goes beyond the lower bounds of 20hz, causing the assertion.

The above quick and dirty fix has been substituted for a BiQuad processor-wide fix in which the "sampleRate" variable which is now made public, accessed via the Wrapper and multiplied by "overSampling factor" - which is all updated on parameter change - and this new SR, being processor-wide, is then propagated to the assertions as well as the parameter control. Everything makes sense again.

Making the sampleRate variable public is not really an ideal fix in the wider world - this variable shouldn't really be accessible outside by the user (or other processors), particularly at run time. It may indeed be possible to call the BiQuad's prepare() method each time the OS is changed, but this also will potentially incur much more run-time hassle than is needed.

A solid fix would leave the sampleRate variable private, similar to the juce DSP modules, but allow our filter's real centre frequency (and related safety assertions) to be oversampling-friendly.

Optimization - CI/CD

Chore

Describe the chore

The CI/CD is all driven from the GitHub workflows files. Currently, I am maintaining one per target platform (current total 4: x64-windows, x64-linux, x64-osx, arm64-osx), due primarily to the subtle differences between platforms that make a generic workflow difficult to define. The primary issues include path handling, especially with regards to caching, restoring, and distributing build artefacts.

I am also driving builds and tests for two build configurations - Debug and Release. The Debug build is really handy to have, and quite conventional to distribute as part of a pre-built package from a package manager (vcpkg...). These are again currently specified uniquely within each workflow file, despite only one single var being different between both builds.

Clearly, we can benefit a lot from using matrixes for build types. I have only hesitated since I have been experimenting with using mutli-config generators (Ninja Multi-Config, etc) to speed things up.

Unfortunately, JUCE doesn't seem to play nice with the typical CMAKE_CONFIGURATION_TYPES var usually used to tell the multi-config generator which build types to produce. When using this setup, find_package(juce CONFIG REQUIRED) fails on the CI/CD runs... perhaps we could use a less restrictive call to find the juce CMake package, but this will probably open up a whole new level of issues which are currently locked down due to the strictness of the design, an integrity which I'd much prefer to maintain.

Matrixes can also work for the multi-platform handling, as per pamplejuce... however, I find that nice clear workflow files which resemble an actual human interaction with a project's required build steps to be very useful and informative, personally, when perusing other users' projects. Given the 'tutorial-ish' nature of the project, I quite prefer to have nice clear workflows I can point visitors to, so that they may understand the entire CMake configure/build/test/install/pack routine.

Spaghetti-like workflows are not only unclear and un-informative for project newcomers; they are also much more prone to failing for subtle reasons, and are much harder to debug.

For now, I have been very deliberate and repetitive in the workflow runs because the clarity is of much greater benefit than boiling it all down into some sort of yaml-flavoured delicacy which even I won't understand, when I revisit this project after the next long break...

I don't see that repetition as a bad thing, in the case of the workflows - DRY is more important within the codebase itself, whereas the workflows should work, and if well-made, will also be very useful to others.

However, I am currently generating multiple build caches per push. This doesn't feel responsible of me. There is also more work to be done to address version management, and logic to control whether or not any artefacts even should be cached (think validation).

Generally the CI/CD is working great now that I have done all of the dirty repetitive work, so I am not in a big hurry to go and break it all. But, every time I watch the thing run, I feel that this could be done much more efficiently and programmatically.

I have been able to regularize/generalize the less meaty parts of the workflows, particularly the CMake config/build/test/install part of the routine. The dependency resolution(s) and artefact caching/uploading are a whole other matter, and would likely still require a distinct job each, meaning that we'd have these various un-related pre- and post-build steps sandwiching a matrix... Very tricky because the workspace gets cleared between jobs... so, caching a buildpath-per-matrix-run scenario starts to arise... this kind of complexity makes me wonder if it would actually be preferable over the current implementation, or not...

This issue will be a dumping ground for further thoughts and ideas on the subject, before deciding on how to improve and optimize the current CI/CD. I probably won't make any further changes for the time being, until my thoughts on what are more fully established here.

Refactor - Biquads Audio Processor

Chore

Describe the chore

  • Some confusing logic re: ownership and lifetime of objects
  • Pointers/references to forward-declared objects has led to some redundancy and potential for mis-direction
  • Lack of iterations over biquad DSP processing blocks causing audio artefacts
  • Project is not fully compatible between CMake and Projucer due to project layout requirements of Projucer

Additional context

May be a long-standing issue, due to the number of changes required.

CI/CD artefacts downloading

Bug report

Thank you for reporting an issue with StoneyDSP.

If you are looking for help, please try these first:

Before opening a bug report, please verify the following:

  • [ x ] I confirm this is a bug with StoneyDSP, not with my own application or with JUCE.
  • [ x ] I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug?

GitHub Actions artefacts not quite set up correctly.

Some package names are not fully formed and the process is not quite correctly configured, being that I just copy/pasted the workflow from my StoneyDSP library repo unedited.

Detailed steps on how to reproduce the bug?

Steps to reproduce the behavior, please provide code snippets or a repository, e.g:

  1. Run a GitHub action on the Biqauds repo
  2. Check the final "upload artefact" steps for status
  3. Observe fail

What is the expected behaviour?

Need to fix the package names and versions on the workflow files so that artefacts get cached for releases.

Operating system(s), version(s), architectures?

  • windows-latets
  • linux-latets
  • macos-latest
  • macos-arm-latest

Plug-in formats, plug-in host applications (DAWs) (if applicable)?

.zip and .tar packages, for now.

Code of Conduct
By submitting this issue, you agree to follow our Code of Conduct.

Implementations of shelving filters (1- and 2- pole)

Currently all Shelving Filter coefficients fall to the default settings and are detached from the GUI, meaning they are effectively blank entries.

The standard 2-pole shelving coefficient code is fairly convoluted and will likely require some new memory allocations purely for these filter selections, as a trade-off against bigger (and repeated) computations on each GUI update.

The 1-pole coeffs are much easier to implement, but there are two schools of thought on how these should work - one of which adheres to the "-3dB cutoff point" of the other 1-pole filters, and another that compensates the corner frequency as the "gain" slider is changed.

Possibly, both version of the 1-pole filter will be included separately. On the other hand, it might be better to map the gain compensation to the "resonance" control, thus offering compensated AND un-compensated shelves under a single entry.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.