Softcut~

softcut~

max/msp port of @zebra’s softcut looping engine (part of norns)

! a formal thanks to ezra for taking the time to develop and assist with the port !

the current release is beta, feel free to use this thread to post issues if you’d like to contribute to development.

TODOs:

  • help patcher
  • phase output
  • write-amplitude dead zone
  • soft clipper fixes (disabled in this version)

Requirements

Max/Msp or Max for Live

Documentation

installing

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.

Download

v1.0.1-beta

build form source:

30 Likes

Hi @andrew.

I’ve built the Windows externals based on that cpp file in case anyone is interested in working on Windows.

softcut~.mxe (81.5 KB)
softcut~.mxe64 (107 KB)

1 Like

oh awesome !! do they work? I’ll add them to the official release. need to put a hard drive in my windows machine one of these days.

I didn’t get a chance test them, but the last version worked fine through the Visual Studio Project. I’ll run the test patch tonight to confirm.

1 Like

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.

38%20PM

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

1 Like

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.

also, thanks for taking that on!

2 Likes

ahhh yes compiling the code would be useful !

here’s a quick repo

just a straight modification of the msp example right now ( maxcpp-master goes in the sdk folder which you probably already know that )

I added boost ? couldn’t seem to get it going without ?

anyway that’s probably that tidbit of info I needed to move forward tomorrow but feel free to poke with the repo ofc

2 Likes

oh, good good - sorry yes, just using BOOST_ASSERT i think? should be fine to lose those lines at this point

TBH i will probably not take time to work with this myself (unless needed, no prob) but happy to answer such q’s

1 Like

hey @andrew

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.)

12 Likes

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

3 Likes

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

scope patch

----------begin_max5_patcher----------
1809.3ocyb0zbahCF9bxuBFl8naBu5KfdamoG1K6w8z1NYjwxIzECd.gax1o
429BRPhSqvVNVHuGhisL1O77986qTx2u9pvkUOJZBC9XveGb0Ue+5qtRsT+B
WM75qB2veLqf2ntrvMhlF98hvE52SJdTpVmPfnnaFWNekZwpke8Cj3wEKa2j
WVHjpuHzqKV0JGWEFVcKWl8Pd482UKxj5auTL9lnEAL8unQ8OhP2DE7kgOi9
qQ9zVg9CDFF7k924GWec+CKrjfkhu0ca+K7Kubc0yAKMQPrQBBFIX5zDLggT
LinnFDgONCWWTwkgKBBKxaT+9kEr4I8+jWJeehoksRYUoIwAJ7ENVy2Hjh56
Dk7kEpa5H6EUvgDUw8BGRLnjQjwGmVRsjWd+6inSYvuNunHnIurSSYPJfYtw
pONUYT.XssA0yV8aqEaEkqBpE7UFXIkF5D8YLVYyyzd.IylucQ9NwMqpq1Nd
auiWW1YiZ9M2mnGznFNjPXkHqZkXS2OSKZPGUzPSTV7fN3.K5fxlEJ4yva2v
2IVcGWJqy67YEu9rlAA0fjpWXTzJpVOt7356y4hpx6mTf8lqLubWdS9aEPu4
BZdnpVZ220H2H52RozG+0IZAzE7qSzaRCCNJr0gzkTUjJBy9TXKdIz0vkrtp
dCW88wbXvrZtTD7afov4oNJPFQY.SApVDLaAxZ2rTTahHz4WASYo5ZTXuCEr
SIKwCjEFzndhrSWwAL+UbvHpRMHvErhiu1tYafwhMRbiOJaPUhTwmH9tXil7
6eNvTLHriJy.iRzESoCCCGmec2Rk7B2pF2VvexDIwtQIhhQ6QRLLWJw644kO
afGHqhytosPlm8.urTTzUDVNuT5tHSHPomQZQ.P0IaRsPQu38JMZxp1JLJNP
mfZcj9KaW2j+up6MDkczhCQ59BvZkMP1OtTFuHKqpUKdoPj6bW+GdWkHlaIF
EcBj9PJRMwfDc2OoV6vN1RrScb6aBJ3yg+IOqqq4plGB9iO8wa+qFQcys4kM
x51MhR4sepJS8jla40c7Ym31scV2U2FEEAAjO76sqxqt4a7ceNbDfh7RwKpH
rAoIwMQFHw55NYZ2hTcOzotOzvjNCr2gufo33ovMzY2rexVE.OTbEnCgguXc
JLgSeQEekBFChkH2jkFnZpGA5FeON2ceoVMBYvRi0h3n1gPoZcZJadK0Zprz
IW7jzftNk3TukidBS5rmxJDcEdZbNdNZ30nn3gRN6hawnWnBOqE2UH1IJBht
gZpBTGYaCXMY0V0wwyks8z4oyF3IXZCIvNRmNDhdXHzwQ9llqyK5caWU+zD7
zQkMfPZsodzFI9llaqZxk4Ukl67E4nNein6SRuqKKpp1Zt02H2PPrdhaCFqD
v2s12uIBMYlZUH4DpZ3P0DpUZwQ+REStiE0SPAyAThN85y00wpUNIWn3IqyB
.VWyJlHJyoQTzwOSHWBms6Zj7ZYfwcrF4nIwLLrvw7fTumGren9lBpf.GETQ
ajNPPb5EQQ1GYAYRMBNZpnDMwHwpHnwvEnbFyI3AGk7C0k0i9BAmS0XW7ESM
i3nw6BuI.pdWjA7wO+DtkLDGMqZsZ.PdgKSOo5IL8bUavD5dQPHDe2FLDew6
CN1ycAOonf8+CQABmbomHvJdlQID0MiuLduwagomUgpeiuSzOmOi2tuTFyx1
0qEudPWVZjFTizfc7ovh2aHrHKOuZG3IiGkMGuIwfG1jXfo0oD77uIwSX5xW
Yzz8TNhhGyAUOcGL8T1LlyYbVSvTsQ8yAK6xNFEYtNWGxZkxLkcBmISWoa2+
Lp0TsVl0JuK5W6vV+Nl2ONvQGSu81LN16b2yUWsZKu9oicrBu90eqfoopsNa
7KbX.7Auh4JQiLuj2Ofn8tl9PeASEQ0VfRr.H5atlp5UhZU3jYGXvLvvYAbr
E.m3.IKkZAPHW.DxFcH0EHAVfDI0AHQrwA.4B0DwF0DaNLDsB49SVQ.3Zmu9
SN2EhzDaIsyi3Pv1X8Fqk2mGR13QRbQ3ahMdj3jYQbZEzn4vHB6snCXaRS0e
PfNejh8FmXdJwDlZqv6bc3vDuI7vdyf.4MNE4MjronuXG.DJ19xKOOfX9R1g
nWrLmVAc+fiLUox4EpGQ7TvJD1aJRvaHYkisK5L.7VFYHwaHEeo5MFhuTMGC
r2eiAQyOx.cVHsMwXbQVIvaozAahwjZVXRmejwS.chGfN1Lzod.ZxDVuQd.6
Yov.6fNxLz34GZzTyv.7.1XyXi7.zSjafL+PCS3YG6AnSlkjCVA8DdWryCZa
JeCbQWLVMW2YI.hMLzhAQp2vE91s6D0MCebEnga3esRmaYg5k4k5Wp1LzvZQ
+e.+5qWsmPgp+dljhLYasd2ndjoOrKg8+2MntrMe.6N51AoZ+r524pls7rgc
dt5wvq+w0+G.WyHxq
-----------end_max5_patcher-----------

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.

1 Like

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:

[ https://github.com/catfact/softcut-msp/blob/test-internal-buffer/softcut~.cpp#L24 ]
[ https://github.com/catfact/softcut-msp/blob/test-internal-buffer/softcut~.cpp#L39 ]
[ https://github.com/catfact/softcut-msp/blob/test-internal-buffer/softcut~.cpp#L201 ]
[ https://github.com/catfact/softcut-msp/blob/test-internal-buffer/softcut~.cpp#L276 ]

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.)

2 Likes

Oh oh probably shouldn’t be re-setting the buffer every processing block like index~ does?

Gotta take better look at buffer functions in sdk

like i assumed buffer_locksamples always returns same address but that assumption is worth checking

1 Like

rad rad, this is all great info for me to dive back in with. the processing block thing makes sense - that’s why we got terraces instead of slopes !

1 Like

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 !

1 Like

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; 
} 
1 Like

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.

6 Likes

andrew! this rocks! can’t wait to see where this ends up.

2 Likes

kyle! no way I’m eating kimchi rn !!!

3 Likes

yeah, damn, this is gorgeous. able to pass a non-xcode-havin’ bud the max-sdk-8.0.3 folder you built all this in?