Implementing buffer~ editing operations in Max 6 (lengthy post)

Aug 24, 2012 at 3:47pm

Implementing buffer~ editing operations in Max 6 (lengthy post)

Hi folks,

I would like to definetely understand how to implement methods that correctly read and write from/to buffer~ objects.
The challenge is to take care of all the locking and deferral problems that make interacting with buffer~ not a trivial task.
I am not interested about Max 4 or 5. I’d rather talk specifically about Max 6, since this last update of Max changed some fundamental aspects of dealing with buffer~, making certain things easier than they were in the past.

I would like to start with a conceptually simple example that will go through most of the problems one might encounter.

Let’s say I want to write a method that copies a portion of a source buffer~ to another destination buffer~.
The destination buffer~ will be resized to match the portion copied from the source buffer~.

So far the simplified version of my code looks something like this:

typedef struct _myobj
{
    t_object   obj;
    void       *qelem;
    t_buffer   *srcbuf;
    t_buffer   *dstbuf;
    t_symbol   *srcname;
    t_symbol   *dstname;
    float      beg_ms;
    float      end_ms;
} t_myobj;

void *myobj_new(t_symbol *sym, long argc, t_atom *argv)
{
    t_myobj *x;

    if (!(x = object_alloc(myobj_class))) {
        return NULL;
    }
    x->qelem = qelem_new(x, (method)myobj_qfn);
    x->srcbuf = NULL;
    x->dstbuf = NULL;
    x->srcname = NULL;
    x->dstname = NULL;
    x->beg_ms = 0.f;
    x->end_ms = 0.f;
}

void myobj_free(t_myobj *x)
{
    qelem_free(x->qelem);
}

void myobj_set(t_myobj *x, t_symbol *sym, long argc, t_atom *argv)
{
    if (sym != x->srcname) {
        x->srcbuf = (t_buffer *)globalsymbol_reference((t_object *)x, sym->s_name, "buffer~");
        if (x->srcname) {
            globalsymbol_dereference((t_object *)x, x->srcname->s_name, "buffer~");
        }
        x->srcname = sym;
    }
}

// that's where the action starts to happen...
void myobj_copysrctodst(t_myobj *x, t_symbol *sym, long argc, t_atom *argv)
{
    t_buffer *srcbuf = x->srcbuf;	// assuming we have already bound to the source buffer~

    if (srcbuf) {
        if (argc >= 3 && argv) {

            // First step is to resize the destination buffer~.
	    // I believe we don't need to implement any deferral or locking for that.
            t_symbol *dstname = atom_getsym(argv);
            float beg_ms = atom_getfloat(argv+1);
            float end_ms = atom_getfloat(argv+2);
            float size_ms = end_ms - beg_ms;

            x->dstname = dstname;
            x->beg_ms = beg_ms;
            x->end_ms = end_ms;

            // this will make us able to receive notifications from the destination buffer~
            // upon completion of the resizing operation.
            x->dstbuf = (t_buffer *)globalsymbol_reference((t_object *)x, dstname->s_name, "buffer~");
            if (!x->dstbuf) {
                object_error((t_object *)x, "buffer~ %s not found", dstname->s_name);
                return;
            }

            // I am assuming that the appropriate locking and deferral mechanism
            // is handled inside this function.
            object_attr_setfloat(x->dstbuf, gensym("size"), size_ms);
        }
    }
}

Now we need to check in our notify method when the buffer~ is done resizing, and then proceed to copy the selected samples at low priority.

Now in this example, for the sake of simplicity, I am not concerned about the copying operation per se, but more on how to structure the locking mechanism and the resizing of the destination buffer~ around it. We are reading from one buffer~ and writing into another. I understand there are more cases to be handled than what I am showing in my code, but I only want to understand the right structure to adopt.

The first thing I am concerned about is whether I want my method to be deferred to low priority or not.
I say I definetely want to defer it to low priority. The reason being that I could potentially copy a big chunk of memory resulting in a relatively slow operation. I don’t want to affect the timing of the scheduler (when in Overdrive) with this potentially slow operation. I might also have to guarantee a certain order of execution from the moment I started resizing the destination buffer. I am not sure if that is an asynchronous operation or not and – above all – I am not sure that when I receive the ‘buffer_modified’ notification I am guaranteed that my destination buffer is done with resizing and ready to be written into.

In any case, here are the options I can think of:

1) defer();
2) defer_low();
3) qelem_set();

I will immediately discard defer() since the deferral is performed by putting the method at the front of the low priority queue, with the potential risk of changing order of execution. defer_low() could be a pretty good option, however still not ideal in my mind because even though it always defers, it doesn’t prevent backlogging of the low priority queue if the method was called at a fast pace. This could happen for example if someone were to make a metro object bang my copysrctodst() method at a fast rate (I am not sure why anybody would want to do that, but it’s a possible scenario nevertheless…)
I see qelem_set() as ideal because it combines the benefits of a defer_low() + the USURP behavior that a queue provides.
I am aware that with qelem_set() I am creating an asynchronous operation, however – as far as I can tell – this operation will be guaranteed to come after the resizing operation performed by myobj_copysrctodst() on the destination buffer.

So, I would do something like this:

t_max_err myobj_notify(t_myobj *x, t_symbol *sym, t_symbol *msg, void *sender, void *data)
{
    if (msg == gensym("buffer_modified")) {

        t_buffer *b = (t_buffer *)data;
        if (b == x->dstbuf) {

            // first, we detach from the destination buffer~...
            globalsymbol_dereference((t_object *)x, x->dstname->s_name, "buffer~");

            // ...and then set the qelem.
            qelem_set(x->qelem);
        }
    }
}

void myobj_qfn(t_myobj *x)
{
    t_buffer *srcbuf = x->srcbuf;
    t_buffer *dstbuf = x->dstbuf;

    if (buffer_edit_begin(srcbuf) != MAX_ERR_NONE) {
        return;
    }
    if (buffer_edit_begin(dstbuf) != MAX_ERR_NONE) {
        buffer_edit_end(srcbuf, 1);
        return;
    }
    long src_beg_frame = (long)(x->beg_ms * srcbuf->b_msr + 0.5f);
    long src_end_frame = (long)(x->end_ms * srcbuf->b_msr + 0.5f);
    long src_size_frame = src_end_frame - src_beg_frame;

    float *src_beg_tab = srcbuf->b_samples + src_beg_frame * srcbuf->b_nchans;
    float *src_end_tab = srcbuf->b_samples + src_end_frame * srcbuf->b_nchans;

    float *dst_beg_tab = dstbuf->b_samples;
    float *dst_end_tab = dstbuf->b_samples + (dstbuf->b_frames - 1) * dstbuf->b_nchans;

    // With a big for-loop here we copy the samples from the source to the destination buffer~.
    // As I said, I am not concerned about this for now...

    buffer_edit_end(dstbuf, 1);
    buffer_edit_end(srcbuf, 1);

    // we invalidate the destination buffer~
    object_method(dstbuf, _sym_dirty);

    // we remove any reference to it
    x->dstbuf = NULL;
}

Of course, this code unforgivingly crashes Max.
So here are my questions (…or things that I am doing that I am not sure if I should be doing…)

1 – I stay bound to my destination buffer for as little as possible, however is it still possible that while I am bound to it
I may receive some ‘buffer_modified’ notifications triggered from somewhere else ? That could interfere with my algorithm…

2 – Theoretically speaking I need to lock both buffers, the buffer~ I am reading from as well as the one I am writing into.
The above code shows how I have implemented it, but I have a gut feeling that’s the source of my problems.
Can I use a double buffer_edit_begin() call for both buffers at the same time, just like in the above code,
or is there a better way of doing it ?

3 – I don’t know if buffer~ resizing in response to the ‘size’ attribute/message is performed at low priority or not.
So, am I guaranteed to receive the ‘buffer_modified’ notification only when the resize operation is complete
and the destination buffer~ is ready to be written into?

4 – Would it be possible to have a look at code of the ‘crop’ method of the buffer~ object? That should do pretty much
what I am looking for with the only difference that everything will be executed on the same buffer~.

5 – What is the exact meaning of the “valid” parameter in the buffer_edit_end() function?

Hopefully this will also be helpful to others who are struggling with similar issues related to buffer~.
Sorry for the lengthy post and thanks for any help.

- Luigi

#64082
Aug 24, 2012 at 7:49pm

Hi Luigi,

I have likely missed some of your concerns, but I’ll try to get most of them!

0. Just as background for those not familiar… Calling things with defer() isn’t so bad. The benefit is that if you are called from the main thread then the method you call is executed immediately with no introduction of asynchrony.

1. If you currently have the lease on the buffer (by calling buffer_perform_begin()) then this notification can not be generated while you are operating on the buffer. Note, however, that it could be possible that a defer’d notification would still be in the queue and might be processed at the same time as you are operating on the buffer (assuming that you are not operating the main thread).

2. You can call buffer_perform_begin() on both buffers. It is possible, depending on what your extern is doing, that you will want the entirety of the operation protected. In this case you may want to create your own mutex and then wrap the entire operation with that. But as a minimum and without any further context, it looks like buffer_perform_begin() and buffer_perform_end() on both buffers will be adequate.

3. Buffer is a resized in main thread. If you call it from another thread then it is internally defer()’d to the main thread. So if your method is itself called via defer() then the buffer resize is, in fact, synchronous. That means you don’t need to worry about notifications which could be ambiguos in nature.

4. The “crop” method for buffer~ is deferred and happens entirely in the main thread. It does a bunch of things that you won’t be able to do in code external to the buffer~ object itself, so it’s not a good example. Here’s how it works though:

a. allocate a temporary vector of floats the size to which the buffer is being cropped with sysmem_newptr()
b. use a for loop to copy the part of the buffer that is requested
c. set the “size” attribute. since this method is already in the main thread this happens synchronously.
d. copy the samples back from the temporary vector to the buffer
e. free the temporary vector
f. end the buffer operation, call the “dirty” message, etc.

5. Good question. I think you don’t actually want to call buffer_edit_end(). Instead, the functions you want to use are buffer_perform_begin() and buffer_perform_end(). The comments in the headers will say that these are for perform methods, but it’s okay to use them in non-perform methods. We only use buffer_edit_begin() and buffer_edit_end() in gen~ code and not for normal MSP objects.

Hope this helps!
Tim

#231107
Aug 25, 2012 at 1:47pm

Hi Tim,

thanks for your comments. They helped a great deal.
Everything is much clearer now and – above all – I got something working.

After your remarks, now I have something like this:

void myobj_copysrctodst(t_myobj *x, t_symbol *sym, long argc, t_atom *argv)
{
    t_buffer *srcbuf = x->srcbuf;	// assuming we have already bound to the source buffer~

    if (srcbuf) {

        if (argc >= 3 && argv) {

            t_symbol *dstname = atom_getsym(argv);
            t_buffer *dstbuf = (t_buffer *)dstname->s_thing;

            if (!dstbuf || NOGOOD(dstbuf) || !object_classname_compare(dstbuf, gensym("buffer~"))) {
                object_error((t_object *)x, "no buffer~ %s", dstname->s_name);
                return;
            }

            float beg_ms = atom_getfloat(argv+1);
            float end_ms = atom_getfloat(argv+2);
            float size_ms = end_ms - beg_ms;

            // handle mismatching sampling rates
            object_method(dstbuf, gensym("sr"), srcbuf->b_sr);

            // object_attr_setfloat() and myobj_doit() should be behaving
            // as one synchronous operation now, so no notifications are necessary.
            object_attr_setfloat(dstbuf, gensym("size"), size_ms);

            x->dstbuf = dstbuf;
            x->beg_ms = beg_ms;
            x->end_ms = end_ms;
            defer(x, (method)myobj_doit, NULL, 0, NULL);
        }
    }
}

void myobj_doit(t_myobj *x)
{
    t_buffer *srcbuf = x->srcbuf;
    t_buffer *dstbuf = x->dstbuf;

    if (buffer_perform_begin(srcbuf) != MAX_ERR_NONE) {
        return;
    }
    long src_beg_frame = (long)(x->beg_ms * srcbuf->b_msr + 0.5f);
    long src_end_frame = (long)(x->end_ms * srcbuf->b_msr + 0.5f);
    long src_size_frame = src_end_frame - src_beg_frame;

    float *src_beg_tab = srcbuf->b_samples + src_beg_frame * srcbuf->b_nchans;
    float *src_end_tab = srcbuf->b_samples + src_end_frame * srcbuf->b_nchans;

    if (buffer_perform_begin(dstbuf) != MAX_ERR_NONE) {
        buffer_perform_end(srcbuf);
        return;
    }
    float *dst_beg_tab = dstbuf->b_samples;
    float *dst_end_tab = dstbuf->b_samples + (dstbuf->b_frames - 1) * dstbuf->b_nchans;

    // With a big for-loop here we copy the samples from the source to the destination buffer~.

    buffer_perform_end(dstbuf);
    buffer_perform_end(srcbuf);

    // there seems to be no need to invalidate the destination buffer~ ???
    //object_method(dstbuf, _sym_dirty);

    // we remove any reference to the destination buffer~
    x->dstbuf = NULL;
}

1)
By using defer() from the main thread it will all execute immediately and the operation will be synchronous.
However, the way I have it right now is that if we are in the scheduler thread only the resizing operation will be defer()’d to the front of the low priority queue… following immediately by myobj_doit() which will be also deferred to the front of the low priority queue, therefore reversing the order of execution.
So, if my thinking is correct, even though the synchronicity of the whole composite operation will still be kept, the order will be reversed.
Now, if I understand you correctly, you are advising to defer the whole myobj_copysrctodst() method, not only myobj_doit(), so that the whole composite operation will happen in the main thread and everything will be totally synchronous with no risk of reversing execution. Also no notifications will be necessary.

So… something like this:

void myobj_copy_to_another_buffer(t_myobj *x, t_symbol *sym, long argc, t_atom *argv)
{
    defer(x, (method)myobj_copysrctodst, sym, argc, argv);
}

void myobj_copysrctodst(t_myobj *x, t_symbol *sym, long argc, t_atom *argv)
{
    t_buffer *srcbuf = x->srcbuf;	// assuming we have already bound to the source buffer~

    if (srcbuf) {

        if (argc >= 3 && argv) {

            t_symbol *dstname = atom_getsym(argv);
            t_buffer *dstbuf = (t_buffer *)dstname->s_thing;

            if (!dstbuf || NOGOOD(dstbuf) || !object_classname_compare(dstbuf, gensym("buffer~"))) {
                object_error((t_object *)x, "no buffer~ %s", dstname->s_name);
                return;
            }

            float beg_ms = atom_getfloat(argv+1);
            float end_ms = atom_getfloat(argv+2);
            float size_ms = end_ms - beg_ms;

            // handle mismatching sampling rates
            object_method(dstbuf, gensym("sr"), srcbuf->b_sr);

            // object_attr_setfloat() and defer() should be behaving
            // as one synchronous operation now, so no notifications are necessary.
            object_attr_setfloat(dstbuf, gensym("size"), size_ms);

            x->dstbuf = dstbuf;
            x->beg_ms = beg_ms;
            x->end_ms = end_ms;
            myobj_doit(x);
        }
    }
}

Did I understand you correctly ?

2)
Why don’t I need to send the ‘dirty’ method to the destination buffer~ after having done writing into it?
It seems that even if I link a waveform~ object to the destination buffer~ everything still updates perfectly without invalidating the buffer~.

3)
Would it be a good design practice to put the whole myobj_copysrctodst() method inside a clock so that if any of the buffer_perform_begin() failed,
the routine could still be tried at a later time when the buffer~ might be valid/available ? Of course, after having successfully written into the destination buffer~ the clock will be unset. I guess what I am asking is if a similar scenario needs to be taken care of (with a clock for example) or if the buffer_perform_begin() function automatically incorporates some kind of mechanism that keeps trying to acquire the buffer~ in case buffer~ was not valid/available right away.

4)
In myobj_copysrctodst() I am not protecting anything. I mean, I am not doing anything crazy there, however I am still changing the sampling rate
and calling the size attribute. Is it ok if nothing is protected there ? Maybe I am getting a little paranoid about this… :)

In any case, thank you so much for your help, Tim.

- Luigi

#231108
Aug 27, 2012 at 5:00pm

Hi,

1. “you are advising to defer the whole myobj_copysrctodst() method, not only myobj_doit(), so that the whole composite operation will happen in the main thread and everything will be totally synchronous with no risk of reversing execution. Also no notifications will be necessary.”

Correct.

2. Changing the size of the buffer will itself trigger a “dirty” message to buffer. Without checking I’m not sure if it will do this when the buffer size stays the same.

3. I would *not* put your potential large copying operation on the scheduler thread, as this will degrade the performance of the scheduler! You could, if you wanted, do something like you are talking about on the main thread by using a qelem. Then from inside of the qelem routine call qelem_set() again so that method will be revisited the next time the low-priority queue is serviced.

4. As long as you are using the buffer’s accessor methods and not directly accessing the struct you should not need to be paranoid ;-)

Cheers,
Tim

#231109
Aug 27, 2012 at 10:18pm

Tim, thanks again for replying.

One last thing:

2. I checked and if the buffer size stays the same there is no ‘dirty’ message sent to buffer~.
Is there a way to check if the resizing operation has actually triggered a ‘dirty’ message…. maybe by checking the b_modtime field???

- Luigi

#231110
Aug 28, 2012 at 12:43am

There is nothing wrong with calling the dirty method an extra time. That’s best way to do this. Any direct member access of the t_buffer is subject to potential breakage in the future.

best,
Tim

#231111
Aug 28, 2012 at 4:39pm

Ok, thanks so much for your help, Tim.
I got everything working well.

Cheers.

- Luigi

#231112
Sep 12, 2012 at 10:11am

It might be I am missing something obvious, but today a new issue came up regarding buffer~ resizing.

I have a ‘copy’ method as follows:

void myobj_copysrctodst(t_myobj *x, t_symbol *sym, long argc, t_atom *argv)
{
    t_buffer *srcbuf = x->srcbuf;	// assuming we have already bound to the source buffer~

    if (srcbuf) {

        if (argc >= 3 && argv) {

            t_symbol *dstname = atom_getsym(argv);
            t_buffer *dstbuf = (t_buffer *)dstname->s_thing;

            if (!dstbuf || NOGOOD(dstbuf) || !object_classname_compare(dstbuf, gensym("buffer~"))) {
                object_error((t_object *)x, "no buffer~ %s", dstname->s_name);
                return;
            }

            float beg_ms = atom_getfloat(argv+1);
            float end_ms = atom_getfloat(argv+2);
            float size_ms = end_ms - beg_ms;

            // handle mismatching sampling rates
            object_method(dstbuf, gensym("sr"), srcbuf->b_sr);

            // object_attr_setfloat() and defer() should be behaving
            // as one synchronous operation now, so no notifications are necessary.
            object_attr_setfloat(dstbuf, gensym("size"), size_ms);

            x->dstbuf = dstbuf;
            x->beg_ms = beg_ms;
            x->end_ms = end_ms;

            myobj_docopysrctodst(x);
        }
    }
}

There are instances where the number of channels between the source and destination buffers is not equal.
In that case my algorithm fails because the resizing adjusts only the length of the buffer~ keeping the number of channels as it was when buffer~ was initially created. So I am left with two buffers with a mismatching number of channels and I am trying to copy one to the other. It doesn’t work…

So, is there a way to programmatically tell buffer~ to resize itself AND change the number of channels as well ?

Thanks.

- Luigi

#231113
Sep 24, 2012 at 12:21pm

There is currently no way to do that, unfortunately. I’ve just created a new feature request ticket for this.

best,
Tim

#231114

You must be logged in to reply to this topic.