C++ audio mixing library design
lisyarus.github.io
lisyarus.github.io
https://www.soundonsound.com/techniques/springs-plates-bucke...
https://www.dsprelated.com/freebooks/pasp/Schroeder_Reverber...
http://www.music.mcgill.ca/~gary/courses/papers/Moorer-Rever...
https://www.dsprelated.com/freebooks/pasp/Freeverb_Main_Loop...
Most reverbs these days are convolutional. I looked at those too! But they require an expensive FFT (usually run in a thread), and the quality sounded "too good" for the audio target I wanted to hit.
After trying a lot of stuff, my main primitive ended up being a delay line hooked up to a low-pass. The output sample is a delay line going through a low-pass. That output is mixed with the incoming sample, and shoved back into the delay. Effectively, this makes a comb filter. I run eight of these in parallel at slightly different delay lengths to comb different frequencies. I was trying to shoot for "digital blur" that had a bit of crunch and aliasing that I wanted, while still sounding like a reverb.
And the track was done by Jake Kaufmann, our composer at Yacht Club Games! I can't take any credit for it.
I started as a C++ programmer (back when C++0x and boost were all the rage), but the more time passes, the more of a C admirer I seem to become.
C is becoming into everything C++ has, minus OOP and the additional type safety.
https://www.open-std.org/jtc1/sc22/wg14/www/wg14_document_lo...
C is not yet at the point where added features, idioms, or concepts have totally removed the ability to return to or regularly restrict yourself to some ‘simple’ subset of the language. However, the push to add more and more to the standard does not inspire hope that this situation will remain.
So while I don’t think it’s there yet, it certainly looks like complexity via continual expansion is the path forward. I may not like it, but it does seem to be a reasonable supposition by GP.
In 2022 I can jump into basically any C codebase and be immediately productive. Which can't be said of some other languages.
Also, I suspect I phrased the ‘C in head CS C in 2022’ line backwards from what I thought I was saying. In my mind I was saying the arguments for the simplicity of C had less to do with the language as it exists now and more to do with the closeness of the language to some ISA/actual machine’s operating characteristic in the past. It was that closeness that enabled (or necessitated) C’s simplicity.
It wasn’t about the changes the committee has made to the language since the 1990’s. The tie in point was (in my mind) implied to be that now that C has become even less of a mapping to actual hardware the simplicity sought in a C replacement should look different, but commenters seem to be looking at a very surface level.
I never particularly cared about ISAs - when I think about the "simplicity" and "closeness to hardware" I mean the control over data layout and control flow. Data layout and architecture of the global data flow is often the key to 90-100% of the performance gains you can expect; and more or less stable binary interfaces mean interopability and modularity, minimizing ecosystems lock-in. Where you need to use a certain instruction is the rare case, and you are recommended to code it in assembly or using compiler extensions.
What did not land in C23, will land in C26, C29, ....
The nullptr concept was introduced into C to fix a type ambiguity when NULL is used with generic selection or varargs functions. The ambiguity could have been solved by mandating that NULL be defined as (void*)0. My issue with nullptr is its an overkill solution that unnecessarily duplicates the concept of NULL in the language.
if (ptr != 0) { foo(*ptr); }
The type mismatch is ugly, but that saves me an include (this particular code minimises its dependencies to maximise portability). - A stream is something you can play as a sound. It may be loaded from a file or generated on the fly. It may be finite or infinite. One important thing is that a stream is single-shot: it doesn’t have a restart method or anything like that.
- A channel is something where a stream is actually played. You can request the channel to stop, or you can replace the stream this channel is playing.
Am I mistaken, or isn't this very close to what GStreamer already does with sinks and sources?- metadata in the streams. GStreamer streams have rich data attached to them that allows both sides of the chain to automatically negotiate the specific format and properties of the data in the stream.
- introspection. Given an opaque/genetic GObject pointer to an element in the processing graph, you can query its configuration and sources/sinks without knowing what the object is up front. This ultimately can allow you to walk the entire graph if you want and inspect all of the elements.
Yeah, Glib and GObject are a massive PITA, thankfully both have C++ bindings and can be used from Vala, so it's not that bad in practice. The C API is a nightmare though.
How about Rust audio (https://github.com/RustAudio) and then FFI?
I am developing the audio lib for the music live coding language I design in Rust and I think the experience of Rust is really good, because of `cargo` (the package management), the community, the syntax and compiler, the ergonomics, and so on. For one of my usages, the audio lib in Rust compiles to wasm and runs in browsers smoothly. I can also write VST plugins with the same audio library in Rust:
https://github.com/chaosprint/glicol
There are also many other Rust audio libs such as dasp, fundsp and hexosynth, all worth checking out. Before I became addicted to Rust audio, I had checked some C++ audio project, such as the Maximillian lib especially the JS bindings, the source code of SuperCollider and the JUCE for VST. But apparently I have made my decision for the reasons abovementioned.
Which the author did quite alright, minus the issue I discussed on separate comment.
You will have better luck moving the audio industry from bad C practices into modern C++, than making most learn a complete new language.
JUCE rules in the audio industry for example.
Also, I don't think the JUCE "rules in the audio industry". First, the most typical use case for JUCE is to write VST plugins. Second, the company ROLI was trying to develop another language called SOUL. Although its progress seems to stop now, it somehow shows that they are trying to provide something different from C++.
So while it is nice for the security of the IT stack, we also need to keep things in perspective.
#[no_mangle]
pub extern "C" fn process(
in_ptr: *mut f32,
out_ptr: *mut f32,
size: usize,
result_ptr: \*mut u8
) {
let _in_buf: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(in_ptr, size) };
}It's interesting (odd?) that the interface doesn't just expose a function to get the next value, similar to a lazy list in Haskell, which also goes well with the original statement in the post that a stream is basically a function. The 'sine_wave' stream for example is 90% filling the array and 10% actual stream logic, which I imagine you're duplicating in other stream implementations.
Also, thanks for the great article and a possible alternative to my own SDL_mixer woes!
I only raised the issue, because this is how new programmers keep doing mistakes in C++ when learning the code from others, specially projects that they find are cool to learn from.
As you seem to use best practices in the rest of the code that seemed strange to me.
void engine::callback(std::span<float> output)
{
std::size_t samples = 0;
if (stream_)
{
samples = stream_->read(output.data(), output.size());
if (samples == 0) stream_ = nullptr;
}
auto remaining = output.subspan(samples);
std::fill(remaining.begin(), remaining.end(), 0.f);
}Even the standard library commits a similar sin with `from_chars` and not taking a `string_view`.
std::span did not exist in the Standard at the time std::from_chars was adopted.
An overload that takes std::span would be easy, and harmless. But as std::from_chars is only ever used from within some other abstraction, any benefit would be minimal.