Developing Max Externals (Tips and Tricks)

didn’t find the setup section from the SDK super helpful but this hella dummy-prove tutorial for mac had my back

also not mentioned, took me way too long to figure out: you gotta restart max for changes to propogate :grimacing:

1 Like

ok weird thing folks: I managed to get an external doing a thing I want it to (sample rate reduction) but only when the input signal is patched to any other object (which doesn’t need to be patched to anything!). then the output signal comes out of the object and works just fine.

I’m guessing I’m just doing something wrong in the setup process but that looks fine? maybe I’m not cleaning up my memory correctly? idk

le repo :pray:

it does’t look to me like this code will produce any output without input. (but maybe i don’t understand the question.)

also, well, it’s not a realtime resampler ehh, nevermind, i see how it works. “rate” is a divisor and it only acts as an integer. (i maintain that this is weird and unpredictable, i see several ways you could go out of bounds on the internal buffer, &c, but that’s as it may be.)

if you want to do SR reduction in realtime, you need interpolated writes. with anything but zero-order interpolation this is quite a bit trickier than interpolated reads, which is why softcut::Resampler exists. (it only deals with writes) - same for some of the hairier parts of karma~ or anything with similar duties.

basically something like this, which of course i have not tested at all:

// these headers are from softcut
// you could of course rip out the relevant bits if you don't want cpp
#include "Resampler.h"
#include "Interpolate.h"

/// Resampler handles interpolated writing.
softcut::Resampler resamp;

// assumption: Resampler::sample_t == float

// we need to do interpolated reads ourselves.
// this requires a second buffer.
// this only needs to be big enough for our interpolation window.
// (if we allowed upsampling things would get more complicated, but why bother?)
#define BUFSIZE 4; 
#define BUFMASK 3;
float buf[BUFSIZE];
unsigned int idx = 0;
double phase = 0.0; // current "playback" phase in [0,1]
double inc; // phase increment per sample;

// write a new value to the buffer, update the index
void writeToBuf(const float x) { 
    buf[idx] = x;
    idx = (idx + 1) & BUFMASK;
}

// this could use other interpolation modes if you want more dirt.
float readFromBuf() {
    // assumptions: 
    // - we always read after write, so `idx` will be the _oldest_ location
    // - idx is already wrapped
    float y0 = buf[(idx + 3) & BUFMASK];
    float y_1 = buf[(idx + 2) & BUFMASK];
    float y_2 = buf[(idx + 1) & BUFMASK];
    float y_3 = buf[idx];
    float y = softcut::Interpolate::hermite(phase, y_3, y_2, y_1, y0);
    phase += inc;
    while (phase > 1.0) { phase -= 1.0; }
    return y;
}

void setRate(double r) { 
    if (r > 1.f) { 
        rate = 1.f;
    } else {
        rate = val;
    }
    calcRate();
}

void setSampleRate(double val) { 
    sr = val;
    calcRate();
}

void calcRate() { 
    inc = rate / sr;
    resamp.setRate(rate);
}

void processSample(const float* in, float* out) { 
    // currently, this returns how many samples were written.
    // if rate < 1, nframes will either be zero or one.
    // if rate > 1, nframes will be >= 1. 
    int nframes = resamp_.processFrame(*in);    

    // this returns a pointer to Resampler's output buffer.
    // after `processFrame()`, the output buf contains N samples.
    
    // in softcut, we'd capture all these in a buffer, and interpolate playback separately.
    // here, we just want to immediately "play back" at the same rate we "recorded" with.

    // write... 
    const float* resampBuf = resamp_.output();
    for(int i=0; i<nframes; ++i) { 
        writeBuf(resampBuf[i]);
    }
    // read out the last value with interpolation
    *out = readBuf();
}
1 Like

to illustrate: it works as pictured, when I delete the cable fromcycle~ to cycle~ (second one being arbitrary), the output from colloquial~ disappears. I would guess it’s a pretty max-specific thing I messed up on. I also wouldn’t be super upset if it only worked this way tho.

ya the vague goal here was to make something like an interpolated version of downsamp~ but there’s a definite vibe of “I messed with the numbers and it sounded cool” If you can’t already tell. maybe needs a new description if I roll with it. and yea technically 1/rate i guess lol. not highly creative with variable names.

I’ll try it out using writes tho ! would this end up writing to the buffer in a pitch-independent way ? (& without a delay I assume, although if it’s really small I don’t really care that much)

oh hell yea this works & sounds great (!!)

(also I’m very here for this writing code straight into discourse business)

edit:

whoaaa, ok, that recording was actually still my wonky version - I compiled wrong (but it still sounds absolutely dank)

here’s ezra’s version:

I think if I mess with the size (spread?) of the interpolation window I can get the best of both worlds tho. brb

didn’t work! anyway I like these both, I think they compliment each other nicely


2 Likes

I’m having the exact same issue. I have the vague intuition it has to do with the way the DSP graph is built, but I’m frankly very lost and confused. How did you fix it?

I’m building a granular wavelet oscillator, and for some reason the phase modulation input only works if whatever feeds it is also plugged into another MSP object sitting on the left side of the oscillator.

never fixed it ! just left the way it wanted to be. my guess is I messed up somewhere with setting up outputs - it feels really tricky to evaluate how it should be done between the level of documentation and amount of examples included in the SDK ¯\ (ツ)

I somehow managed to figure it out! See this C74 thread for more info.

Apparently this is caused by an optimisation trick done by Max to save some memory by using the same buffer for both the input and output of MSP objects — Max fills this buffer with the input initially, and expects it to contain the output by the end of the perform64 call. If your algorithm isn’t designed to work in-place, things will break. The way to solve this is to add the flag Z_NO_INPLACE to disable this behaviour when you create your object (here my object is named test):

void *test_new(t_symbol *s, long argc, t_atom *argv)
{
	t_test *x = (t_test *)object_alloc(test_class);

	if (x) {
		dsp_setup((t_pxobject *)x, 1);
		outlet_new(x, "signal");
        x->ob.z_misc |= Z_NO_INPLACE;
	}
	return (x);
}

My perform routine needed to initialize the output buffer to all zeros, which discarded the input buffer’s content whenever the in-place optimisation was active. My guess is that whenever two or more objects use the same audio source object, only the last connected object to have its output buffer computed can use in-place optimisation since the other objects need to have an unaltered copy of the source buffer to work on. It looks like Max computes the output buffers left to right, so adding an extra MSP node on the right means that in-place optimisation is deactivated for all MSP nodes on its left.

It’s a bit crazy this flag is first mentioned on page 436 of the documentation. I see why this would be fine for filters and simple out[n] = f(in[n]) mappings, but it’s wrong to assume algorithms are generally in-place. Navigating this really is chaotic.

2 Likes

Newer C++ based SDK for anyone interested:
http://cycling74.github.io/min-devkit/

3 Likes

figured out a way to use github actions to automate building max externals for both mac and windows. i basically copied the actions template the min-devkit uses with some minor modifications to make it work. i’ve created a sample project and posted instructions in case anybody wants to try it: https://github.com/scanner-darkly/max-auto-build

could somebody try downloading the release here and confirm it works on mac? https://github.com/scanner-darkly/max-auto-build/releases/tag/v0.0.2 (copy the external to your max library and create a patch with max-auto-build object, it’ll output a message when banged - i should add you’ll need max 8).

i’m planning to refine it a bit - the original template creates artifacts on any commit or PR, and they include files other than just the externals, which is probably not needed while you develop. and for proper releases it should include additional folders/files so that a release is a complete max package.

6 Likes

this is super exciting

  • first one auto-printed
  • next two from bangs
1 Like

awesome, thanks for confirming! yeah this should be super handy for folks that don’t have access to both platforms, and as far as i can tell it’s free. also, the template is not min-devkit specific, so should work for any max external project.

3 Likes

I found it too difficult setting up github actions for my regular Max-api externals. Instead I used this script to set up a Mac VM with Xcode in it. I suspect it to be much easier to deal with compilation errors as well. I had used some C code that’s only available on Windows, apparently, and Xcode just told me the solution. Ableton doesn’t run in it but I had someone else to verify. If it compiles, it runs the same as on Windows.

Hi all,

for those who are looking for CI configuration to build max externals, at OSSIA we are building our library with travis and appveyor first (when Github Action, Gitlab-CI and Azure Devops was not available) and then with Azure Devops, I also made some test with github-action.
All configuration files are available on our repo :

(there is also an appveyor config file but I not allowed to paste it here since I’m a new user…)

One big pros for newer CI plateform is the ease to gather artifact from other pipeline and assemble together to make multiplatform package (we put bot Windows and Mac external in the same package).
Also newer CI are easier to setup and faster than appveyor and travis.

2 Likes