and if the graphical/higher level approach of max is not vibed with there is always the option of Javascript object (logic) + external or gen~ (for dsp). this is what I do !

1 Like

nice! I’ve been playing around with a few of your patches, been having a ton of fun w/ alliterate

1 Like

nice - that would be a very easy one to piece apart too

pyo is very easy to work with and you can do quite a lot with it!

1 Like

If you’re interested in working with C++, I’m hoping you will find madronalib useful at some point. https://github.com/madronalabs/madronalib

In some ways it’s fairly mature and in others it’s very much a work in progress. I’ve been building and supporting software using this code for years, it’s mostly quite stable. On the other hand there’s very little documentation besides what’s in the code itself.

I’m chiming into the conversation here at this point because last week I just put some time into the build system and made a couple of new examples. One example plays a couple of sine waves and the other is a reverb. Both are command-line apps. The readme on the github repo should have enough documentation to enable you to build these for Mac or Windows. Cmake and either XCode or Visual Studio will be needed. So for the folks who have done at least a little C++ before, now is a reasonable time to get started.

Like a lot of people I started out with real-time audio programming making audio processors and performance stuff in Max/MSP. Then I started writing audio externals for MSP and leaning a lot on Javascript in Max to hook them to UIs. At some point I was using so little patching in Max that I switched to just doing music projects in C++, and in a lot of ways I wish I’d started out doing that from the beginning.

The readme.md gives a roadmap of what’s there and the incantations for getting started with cmake. These should build the examples for Mac or Windows. On Windows I use git bash as an interface, though I guess there are other ways to go.

Inspired to write a bit more by Ezra’s comment above: C++, especially since C++11, has a lot of powerful syntax that’s opened up new possibilities for compiling readable ā€œfunctional DSP graphā€ style code into very fast primitives. There are some languages out there that are more beautifully descriptive—Clojure comes to mind—but one criterion I have for making stuff is ā€œuse as few tools as possible.ā€ With modern C++ we can have a single layer that’s reasonably beautiful and very performant.

14 Likes

I’m curious, what C++11 features are you using to build a ā€˜functional DSP graph’?

Lambdas and std::function are probably the best example. A lot of DSP operations are nicely expressed as higher-order functions.

Say you want to upsample a function f: run it at 2x your sample rate and get the results back at your original rate. Without higher-order functions we are kind of stuck thinking in terms of ā€œboxes and linesā€ to to these things: Make an upsampling object, connect the inputs to it, connect the outputs to the function you are upsampling, make a downsampling object, and so on.

With higher-order functions you can send the input signals and your function f itself as inputs to the downsampler. Practically, this makes for smaller and more readable code. Conceptually, it’s a deeper kind of abstraction for building systems.

Another C++11 feature I use is constexpr. This lets you specify that a value must be computed when the program is compiled. So instead of putting computed tables in your code, maybe some comments about how the table was made, you can put the actual code that’s doing the calculation with full confidence that everything will be computed ahead of time.

5 Likes

I see, thanks for the response! I wondered if maybe you were referring to something I wasn’t aware of. Lambdas are definitely very useful, nothing that couldn’t be achieved with functor objects before C++11, but the syntax is neater. I tend to avoid std::function, preferring lamdas (or even c style function pointers), unless I need to capture data, as some implementations always allocate from the heap, even if nothing is captured (I think visual studio compiler does this, although I might be wrong).

constexpr is great. Have you seen this video https://www.youtube.com/watch?v=zBkNBP00wJE&vl=en, it’s basically using C++17 to compile for a Commodore 64, there’s some great examples of the power of constexpr there.

fwiw, std::function implements small-buffer optimization under the hood and always has enough stack for a plain function pointer or a lambda with no captures.

the low-latency ISO working group has std::inplace_function, which is a template for fixed-capacity function wrapper that will never allocate.
[ https://github.com/WG21-SG14/SG14/blob/master/SG14/inplace_function.h ]

i’m a big fan of constexpr as well. weird random thing i recently learned:

  • a constexpr function can be evaluated at runtime with runtime arguments (somehow i didn’t realize this for a long time)
  • if you want to check that a given invocation is happening at compile time, assign result to a std::integral_constant (won’t compile with runtime assignment)

(sorry this is OT but i’m not sure there’s really demand here for a ā€œmodern c++ optimizationā€ thread. maybe?)

looking at madronalib made me ask one dorky question at least: @randy have you found that explicitly forcing data alignment for SIMD makes a measurable performance difference, with contemporary compilers? i stopped worrying about this a while ago, but maybe that’s wrong.

1 Like

std::function implements small-buffer optimization under the hood and always has enough stack for a plain function pointer or a lambda with no captures.

That sounds implementation specific? I’m sure I had an issue with the Microsoft implementation allocating for a simple function (no data), I might have misremembered though. Plus this would have been with VS2013 which only had a partial C++11 implementation.

I did not know about this. Thanks!

that’s absolutely true. i suppose there’s no guarantee. after posting that i did two things: 1) tried to find anything about this in any standard or any optimization flag docs (failed, but i’m sure it’s out there) 2) wrote small program to check.

conclusion: under gcc 7.4, plain funcs or capturing one int did not blow the std::func stack, capturing a bigger struct or several ints did. -O0 and -O3 the same. YMMV.

gory details

stackfunc.cc

#include <functional>

void boo(int bar) {
    long int baz = bar + bar;
    long int blrf = baz + baz;
}

void test_ptr() { 
    std::function<void(int)> fn(boo);
    fn(555);
}

void test_lambda_no_cap() {
    std::function<void(int)> fn = [](int x) { boo(x); };
    fn(666);
}

void test_lambda_one_cap() {
    int x = 777;
    std::function<void()> fn = [x]() { boo(x); };
    fn();
}

void test_lambda_some_caps() {
    int x =888;
    int y = 999;
    int z = 999;
    int a = 999;
    int b = 999;
    int c = 999;
    int w = 999;
    int q = 999;
    int n = 999;
    
    std::function<void()> fn = [x, y, z, a, b, c, w, q, n]() { boo(x); };
    fn();
}


void test_lambda_big_cap() {
    int x = 666;
    
    struct stuff {
	int things[32];   
    };

    struct stuff what;    
    std::function<void()> fn = [what]() { boo(what.things[0]); };
    fn();
}

int main() {
    // results from g++ 7.4.0
    
    // these don't allocate
    test_ptr();
    test_lambda_no_cap();
    test_lambda_one_cap();

    // these do allocate
    test_lambda_some_caps();
    test_lambda_big_cap();
}

Makefile

all:
	g++ stackfunc.cc -O3 -o stackfunc

massif:
	rm ./massif.out.*
	valgrind --tool=massif --time-unit=B --threshold=0 --detailed-freq=1 ./stackfunc

then

make
make massif
ms_print --threshold=0 massif.out.xxxx
2 Likes

Glad you didn’t mind my explain-y response—Having no idea about your level of knowledge I was trying to write something generally useful.

I get your warning about std::function. I’d rather not be in the land where allocating heap is possibly implementation-dependent, but sometimes I decide it’s worth it for maintainability, expressivity, whatever—and of course verify.

1 Like

Yeah I never understood why constexpr is ever allowed to silently punt to runtime. Thanks for the note on std::integral_constant.

re: alignment, the only reason I have any manual alignment code is for 32-bit Windows. 64-bit Windows and any MacOS can be relied on to hand back 16-byte-aligned buffers. It’s not a performance thing but rather that SSE needs aligned data or will throw an exception.

1 Like

I’m not an expert on constexpr, but my understanding regarding constexpr functions is that marking it as constexpr means it ā€˜can’ be evaluated at compile time if provided constexpr arguments, but it can still take non constexpr arguments at runtime. You will get a compile error if you try and assign a constexpr constant from a non constexpr function, so in that regard you can ensure it could be evaluated at compile time.

e.g.

constexpr int add( int a, int b )
{
    return a + b;
}

int main()
{
    constexpr int v = add( 1, 2 );
}

This won’t compile if add() is not constexpr.

1 Like

You should never worry about over explaining :slight_smile: I’ve been programming C++ for a long time, and I still learn things pretty much every day! With C++17 I feel like I almost certainly won’t learn everything before C++20 is ratified!

Were you easily able to get that development board working with faust? I was just reading through their instructions and – although they mention the Ai-Thinker board at the top of their post – latter on they say that the codec used on the TTGO board is the only one currently supported by faust2esp32

Yes, that’s my understanding also—I meant, why did they design it that way instead of enforcing that it must be evaluated at compile time which, in my use cases, is kind of the point.

The cheap one I got off ebay used the same codec so it worked straight up.

Saw this yesterday…

Ah that’s great. I think their tutorial is out of sync with the source code. The tutorial says that only one codec is supported, but in the faust2esp32 source I can see that they’ve added support for the ac101 codec used by that board you bought on eBay. I bought one too, so that’s good news for me.