gen~ performance is supposed to be similar to C performance (I don’t know if it is identical, but it is supposedly comparable).

My motivations for exploring externals aren’t really about performance. Has more to do with things such as code sharing between Max and non-Max projects, unit testing, structured programming, and to be honest, a preference for text over patching. So, for me, it’s more about workflow and collaboration and maintainability than it is about performance. YMMV.

3 Likes

In order to get the first example from the Designing Audio Objects book to build, I needed to use the current version of the Max SDK, which @tehn links to above, and then replace the c74support directory located in the Code/MaxMSP Code/ folder from the book’s DVD.

I also needed to edit the Build Settings in XCode 7 so that it used a 32-bit Intel architecture, and then I needed to change the values under Base SDK to Latest OS X.

Then I needed to ensure that Max 7 started up in 32-bit mode (Get Info on the Max 7.app file to do this)

At that point the build succeeded and the resulting mirror~ external worked as expected in Max 7.

At some point I’ll read the DVD’s Updates/Max 6/Writing Externals for Max 6.pdf and understand how to target 64-bit architectures, which I believe will work in Max 7 regardless of whether Max was started in 32-bit or 64-bit mode.

1 Like

I totally agree with everything you said. My interest in text based programming came about because there are ideas and programming techniques that are more easily achieved with a text based approach.

I got the examples working with max 7, with some help from HERE

1 Like

A one line change? So easy! Thanks @Evan

OK, nice, if I use the files available at the link @Evan provided, there’s no need to change the architecture Build Settings in XCode. It comes pre-configured for a Universal build with the Latest SDK.

But I did still need to open Max in 32-bit mode, so the Updates/Max 6/Writing Externals for Max 6.pdf is still relevant if you want your externals to work in 64-bit mode.

1 Like

I’m curious now that I read this…

Would some of the older apps like mlrv possibly work correctly if I used Max 7 in 32 bit mode? Would that make any difference?

It’s worth a try. There’s a checkbox for it in Max’s Get Info dialog (Cmd-I with Max.app selected)

1 Like

EYYYYYYY! EVVVVVAAAAAAANNNNNNN! Welcome! So glad to see you here :smile:

(sorry, i’ve nothing to add to this thread, just excited to see Evan from the C74 forums here :sunny: )

If those apps are dependent on any externals, there’s a chance running in 32 bit mode could be helpful.

YOOOOO. Nice to see a familiar face (or avatar, whatever)!

…no wait… i guess i do have some things to add to this thread:

Yes, although gen~ code is based on the language of ‘LUA’, the basic conditionals and other such syntax of C which are used in many languages like Java, C++, etc. are also in gen~ so it can be pretty easily adaptable(LUA is based on C same as many of these other languages).

The fundamental differences in performance are minimal(the DSP routines themselves work pretty much the same calculating things at sample rate and allowing you to hide functions from that calculation within conditional statements wherever necessary). There are some advantages to writing an external in C which have to do with having direct access to the internal ‘struct’(or memory of an external) and performing efficient checks as to whether a signal or a message is attached to the inlets of an external, then catering the DSP method to those(whereas in gen~ it’s all assumed as signals by the time it hits the DSP method within, but certain types of messages are only calculated once per vector…), however, most beginners won’t find these differences to be huge gains or losses.
The main advantages to writing externals in C are found in creating access to some of Max’s more esoteric and special features and functionalities(as mentioned before, you can separate a DSP routine that works specifically for messages as opposed to signals(detecting what’s connected to each inlet then moving from there), but other examples of extending behavior using a C external are: gaining access to specialized functionality within poly~, doing specialized checks and reformatting for changes in external buffer~s, creating behaviors specific to Windows as opposed to MacOSX within the same code, doing special things like handling files, creating a UI within the same audio-handling object, creating different typed outlets(for example in karma~ there’s a regular set of signal outlets, but then an internal scheduler-clock polls certain information to be sent out the last outlet in messaged format), etc.).

Having said all that, if you’re just starting with coding, gen~ is best since it offers a bridge between visual patching and coding(with codebox), and is still highly efficient, but eventually i’ve actually found writing externals in C to be a bit easier simply because i know the SDK and it allows me more modular access to different kinds of functionality and efficiency within one external/code(easier in the sense that i can separate things out to be more readable and develop them in a more organized easy-to-keep-track-of sort of way).

Anyways, best of luck and have fun!

4 Likes

This time-worn tutorial, MaxMSPExternalsTutorial2.41, while a bit dated (errr, 2002 and Code Warrior?!?!) saved my bacon last night when I was puzzling through proxies.

Good cycling74 forum post here with some tips on reading inlets and working with atoms:

1 Like

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