drop softcut~.mxo (softcut~.mxe or softcut~.mxe64 for Windows) from the .zip into your project folder, or add it to your search path. now softcut~ will be an available object
using
reference the softcut luadocs for the list of softcut commands. here they take the form of a message contaning: " command value ". for now, softcut~ only supports mono buffers.
some more norns/max dev questions for this thread:
I’ve been spending the last few days studying up and working on an external for softcut (using MaxCpp per @zebra’s suggestion above). It’s been pretty approachable, I think I’m almost ready to get a successful build but I’m getting these kind of confounding Undefined Symbol errors from the linker.
That looks like it’s coming from the sofcut code, but we know that works so I’m wondering if it’s somewhere in my Xcode target config or c++ versions or somewhere on that side of thing. From what I’ve gathered this error is supposed to mean a function is declared and not defined, but seems like it can also point to trickier issues.
This is the only code I’ve actually written, doesn’t really seem like it’s the source of the problem though.
Summary
#include "maxcpp6.h"
#include "softcut/SoftCut.h"
#include "softcut/Types.h"
#include "Utilities.h"
// inherit from the MSP base class, template-specialized for myself:
class Softcut_msp : public MspCpp6<Softcut_msp> {
public:
enum { MaxBlockFrames = 2048, NumVoices = 6 };
enum { BufFrames = 16777216 };
softcut::SoftCut<NumVoices> cut;
float buf[2][BufFrames];
bool enabled[NumVoices];
softcut::phase_t quantPhase[NumVoices];
Softcut_msp(t_symbol * sym, long ac, t_atom * av) {
setupIO(NumVoices, NumVoices);
post("object created");
for(int i=0; i<NumVoices; ++i) {
cut.setVoiceBuffer(i, buf[i&1], BufFrames);
}
}
~Softcut_msp() {
post("object freed");
}
// methods:
void bang(long inlet) {
post("bang in inlet %i!", inlet);
}
void test(long inlet, t_symbol * s, long ac, t_atom * av) {
post("%s in inlet %i (%i args)", s->s_name, inlet, ac);
}
// default signal processing method is called 'perform'
void perform(double **ins, long numins, double **outs, long numouts, long sampleframes) {
// example code to invert inputs
for (long v = 0; v < numouts; v++) {
double * in = ins[v];
double * out = outs[v];
if (!enabled[v]) {
continue;
}
cut.processBlock(v, (const float *) in, (float*) out, (int) sampleframes);
}
}
};
C74_EXPORT int main(void) {
// create a class with the given name:
Softcut_msp::makeMaxClass("softcut~");
REGISTER_METHOD(Softcut_msp, bang);
REGISTER_METHOD_GIMME(Softcut_msp, test);
}
anywho, I’ll keep digging but wanted to post in case anyone had any quick pointers
it looks like you are not compiling SoftCutVoice.cpp
is your xcode project on github? that might be a better place to share stuff (there could be quite a bit more of this) and i can probably help
(that said, i’ve found setting up xcode to build max externals to be a serious PITA in the past. i just end up customizing sample projects and leaving them in the SDK directory tree.)
@andrew oh but real quick, take a look at the CMake targets for crone:
you will probably want to compile src/softcut/*.cpp
IIRC, softcut doesn’t have any library dependencies, but does need a standard like c++11 or c++14 or something.
just a heads-up, and sorry this is kinda off topic, but
i forked your softcut port to check it out, and ended just starting over:
[ https://github.com/catfact/softcut-msp ]
spoiler: doesn’t work yet. something is borked with the buffer format or i dunno. i will continue to poke at this, but wanted to let you know asap, in case it saves duplicate work.
some differences with this project:
uses max8 sdk, not max6
doesn’t use maxcpp, which doesn’t seem necessary anymore
uses new softcut sources from norns repo, not stale ones from the standalone version
i cleaned out the boost dependencies
the external only implements one voice
takes buffer argument from MSP instead of managing a huge buffer itself
(these last two things make it a much better citizen of max-land i think. i would probably make some more changes eventually, like a signal-rate position reset trigger.)
ah nice nice, thnx for the info. the last two were in the plan once I got some output happening but sounds like yr taking it in a better direction that what I could attempt to throw together !
rlly excited to have this in my patches - appreciate the contribution as always @zebra
took a look at this myself, short of really finding anywhere close to the problem I * did * make a nice picture to see what it’s doing to a sine wave loaded into the buffer
so uhhhh, maybe that helps point to something, idk ! seems like a sample rate thing. tried x->scv.setSampleRate(44100.0) instead of grabbing it from max but no dice. maybe something missing in Resampler.h, or sample rate data missing elsewhere.
anywayyy, happy to throw time & limited knowledge at this. the good news is the current version is pretty good at turning low frequency inputs into idm beats.
it’s not a samplerate problem and it’s not Resampler (which is used for varispeed writes, only comes into play when rate != 1.0, and is really a pretty simple thing.)
i just went ahead and did a quick test of my next suspicion, which is that i’m somehow not setting / using the max-supplied audio buffer correctly, and that was fruitful.
i used a ~21-second internal, self-allocated buffer instead, and it seems to work. (so far - this was an extremely quick test without changing any parameters or anything.)
you can see that branch test-internal-buffer
here are the relevant lines that were changed in the test:
somehow, if i use the max-supplied buffer, it’s like the same value is written to each index in each processing block. i’m sure it’s something stupid. im using the index~ example project for reference. th one lazy corner i was cutting is assuming the buffer is mono - softcut expects this, so to do otherwise would mean de-interleaving into a temp ringbuffer, processing, and re-interleaving - more bookkeeeping there. (it would have to be a ringbuffer for the varispeed stuff to work, i think.)
but anyways digging into that is next step. i don’t have time to do more this weekend but might be able to over next two weeks while i’m off the clock and travelling.
one tip i can offer is that you can in fact attach xcode to the Max process and set breakpoints for debugging. (so you can for example esaily check that SR and other params are geting set, that phase is getting incremented correctly, &c.)
ok ! so I have it working, but just at a specific buffer size. the test patch plays through included long sample just fine, and recording looks good so far as well.
looks like the problem was softcut needing the buffer framecount as a power of 2 (?), so here I’m just setting framecount manually and sticking with a buffer size that rounds up a bit higher (rolling with 2^23 atm). since the buffer size is set in ms in max the conversion never comes out to a rational, so I’m guessing I have to set framecount internally in samples, then just make sure the max buffer is higher.
aaaaanyway this definitely feels good enough to use - I think I will go ahead and publish !
ah ok. if that is really the issue, then the fix is pretty easy: wrapping the buffer frame index is broken out to a utility function for exactly this reason:
change it to use a while-loop instead of a bitmask. (and bufMask_ variable should then not be needed.) (and yea, the assert we disabled would have caught this! woops)
using a bitmask is a tiny optimization that is not worth it in MSP world. (but i’m used to SuperCollider world where buffer allocation uses frames and it’s normal to use nextPowerOfTwo and bitmask tricks. sorry about that.)
but, i am not 100% convinced this is the problem really… when i tried with large buffers in MSP, i saw a very consitent artifact that didn’t just “kick in” when the buffer was wrapped.
(i’m sorry i’m armchair right now, i’m away from a Mac.)
if that works, seems like a fine workaround and definitely indicates that the wrapping func is the issue. you could just set internal framecount to the next highest power-of-two smaller than the actual buffer size:
int highestPowerof2(int n)
{
int res = 0;
for (int i=n; i>=1; i--)
{
// If i is a power of 2
if ((i & (i-1)) == 0)
{
res = i;
break;
}
}
return res;
}
I’ll let this lovely response evolve and look into it some more, but for reference, the bug I was fighting actually granulated the buffer in a specific way. output was quite beautiful at times. only artifacts I noticed were clicks between those grains.