Cycling74 / min-devkit

Tools, documentation, and reference implementation of a Max Package built using the Min-API.
MIT License
158 stars 31 forks source link

Non-audio buffer example #124

Closed jamesb93 closed 5 years ago

jamesb93 commented 5 years ago

Hi Tim,

It would be good to provide an example similar to poke~ for dealing with buffers outside of an audio-read context. One aspect in particular which is not so clear is how to deal with locking/unlocking buffers in this context.

tap commented 5 years ago

Hi James,

Here is some code from an object that snaps an incoming time to the nearest zero-crossing time in a buffer. It is able to do this both for signals or for floats. What you'll see is that in the number method the type of the buffer_lock is made non-audio by providing the template parameter "false" with buffer_lock<false>.

/// @file
/// @copyright  Copyright (c) 2019, Cycling '74
/// @author     Timothy Place
/// @license    Usage of this file and its contents is governed by the MIT License

#include "c74_min.h"

using namespace c74::min;

class tap_buffer_snap_tilde : public object<tap_buffer_snap_tilde>, public vector_operator<> {  
public:

    MIN_DESCRIPTION { "Snap a time to the nearest zero-crossing location in a buffer." };
    MIN_TAGS        { "buffer" };
    MIN_AUTHOR      { "Timothy Place" };
    MIN_RELATED     { "buffer~, record~, tap.buffer.record~" };

    inlet<>     m_input         { this, "(signal/number) buffer location in milliseconds" };
    outlet<>    m_output_signal { this, "(signal) nearest zero-crossing in milliseconds", "signal" };
    outlet<>    m_output_number { this, "(number) nearest zero-crossing in milliseconds" };

    argument<symbol> name_arg   { this, "name", "Name of the buffer to reference.",
        MIN_ARGUMENT_FUNCTION {
            m_buffer.set(arg);
        }
    };

    buffer_reference m_buffer { this };

    tap_buffer_snap_tilde() {
        update_samplerate();
    }

    message<> m_number { this, "number", "A location in the buffer to be quantized to the nearest zero-crossing.",
        MIN_FUNCTION {
            if (m_buffer) {
                buffer_lock<false> b(m_buffer);
                m_output_number.send(calc(b, args[0]));
            }
            else
                cerr << "invalid buffer" << endl;
            return {};
        }
    };

    message<> dspsetup { this, "dspsetup",
        MIN_FUNCTION {
            update_samplerate();
            return {};
        }
    };

    void operator()(audio_bundle input, audio_bundle output) {
        auto            in = input.samples(0);
        auto            out = output.samples(0);
        buffer_lock<>   b(m_buffer);

        if (b.valid()) {
            for (auto n=0; n<input.frame_count(); ++n)
                out[n] = calc(b, in[n]);
        }
        else {
            output.clear();
        }
    }

private:
    number m_ms_samplerate;
    number m_ms_inv_samplerate;

    void update_samplerate() {
        m_ms_samplerate = samplerate() * 0.001;
        m_ms_inv_samplerate = (1.0 / samplerate()) * 1000.0;
    }

    template<bool audio_thread>
    number calc(buffer_lock<audio_thread>&b, number x) {
        auto    frames = b.frame_count();
        int     snap = std::round(x * m_ms_samplerate); // convert to samples
        int     index = MIN_CLAMP(snap, 0, frames);
        sample  current_samp = b[index];

        if (current_samp != 0.0) {
            enum {
                unknown,
                nearest_zerocross_is_higher,
                nearest_zerocross_is_lower
            } flag = unknown;
            bool    above_zero = (current_samp > 0);
            auto    i = 0;
            int     index1;
            int     index2;

            while (flag == unknown) {
                ++i;
                index1 = wrap<int>(index+i, 0, frames);
                index2 = wrap<int>(index-i, 0, frames);

                if (above_zero != (b[index1] > 0.0))
                    flag = nearest_zerocross_is_higher;
                else if (above_zero != (b[index2] > 0.0))
                    flag = nearest_zerocross_is_lower;

                if (i >= frames) // no zero-crossing found!
                    break;
            }

            if (flag == nearest_zerocross_is_higher)
                snap = index1;
            else if (flag == nearest_zerocross_is_lower)
                snap = index2;
        }

        x = snap * m_ms_inv_samplerate;
        return x;
    }
};

MIN_EXTERNAL(tap_buffer_snap_tilde);
jamesb93 commented 5 years ago

This is excellent. Thanks!

Is there a way to buffer.set() using an attribute rather than an argument?

tap commented 5 years ago

You can wrap the call to buffer.set() an attribute that you create yourself. The buffer reference doesn't provide the name as an attribute because there are many situations where the name of the attribute needs to be something different.

Here is some dirty (untested, browser-window coding) that might get you close:

attribute<symbol> m_name_attr { this, "name", "foo",
        description { "The name of the buffer to reference" },
        setter {
            MIN_FUNCTION {
                symbol name = args[0];
                m_buffer.set(name);
                return { args };
            }
        }
};
jamesb93 commented 5 years ago

This excellent help. Thanks!

jamesb93 commented 5 years ago

Can I reopen this and ask if its possible in addition to resizing in milliseconds to be able to resize buffers in samples, as well as specify channel amounts?

tap commented 5 years ago

Ask and ye shall receive :-) This is now done on master and demoed in the min.buffer.loop~ example object.

jamesb93 commented 5 years ago

Excellent, thank you! In the meantime I found the undocumented second argument to sizeinsamps and size for buffer~ but this is definitely a feature that should exist in the API. Now I can continue making my bespoke buffer modifier :)