2
\$\begingroup\$

I'm working with audio which has a lot of two dimensional sound buffers passed around like this:

(float**, int numChannels, int numSamples)

I'm looking to implement a quick array_ref style view (quick, non-owning, traversable view, knows its dimensions), but which allows me to do range-based for loops like this:

BufferView& buff {someRawBuffer, 2, 256};

for (auto chan : buff)
    for (auto samp : chan)
        samp *= 0.25f;

So far I have this, which is far from STL-complete (not what I'm aiming for, readability and brevity are priorities over generality, and I'm looking to learn). It seems to work, but a bit of a mess!

//==============================================================================

class SampleIterator
{
public:
    SampleIterator (float* sample, int numSamples)
        : fp {sample}, curr {sample}, samps {numSamples}
    {}

    float* operator++() { curr++; return curr; }
    float operator*()  { return *curr; }
    bool operator!= (const SampleIterator& other) { return curr != other.curr; }

    float* begin() { return &fp[0]; }
    float* end()   { return &fp[samps]; }

private:
    float* fp;
    float* curr;
    int samps;
};

//==============================================================================

class ChannelIterator
{
public:
    ChannelIterator (float** channel, int numSamples)
        : fpp {channel}, curr {channel}, samps {numSamples}
    {}

    float** operator++() { curr++; return curr; }
    SampleIterator operator*()  { return SampleIterator {*curr, samps}; }
    bool operator!= (const ChannelIterator& other) { return curr != other.curr; }

private:
    float** fpp;
    float** curr;
    int samps;
};

//==============================================================================

class BufferView
{
public:
    BufferView (float** buffer, int numChannels, int numSamples)
        : fpp {buffer}, chans {numChannels}, samps {numSamples}
    {
    }

          float& operator() (int chan, int samp)       { return fpp[chan][samp]; } // debugmode range check?
    const float& operator() (int chan, int samp) const { return fpp[chan][samp]; }

    ChannelIterator begin() { return ChannelIterator {&fpp[0],     samps}; } // const versions?
    ChannelIterator end()   { return ChannelIterator {&fpp[chans], samps}; }

private:
    float** fpp;
    int chans;
    int samps;
};

And the (non-real world) usage sketch is here:

#include <iostream>

int main(int argc, const char * argv[])
{
    float chan1[4] = {1, 2, 3, 4};
    float chan2[4] = {5, 6, 7, 8};
    float* rawbuf[2] = {chan1, chan2};

    for (int chan = 0; chan < 2; ++chan)
        for (int samp = 0; samp < 4; ++samp)
            std::cout << rawbuf[chan][samp] << "\n";

    BufferView bv {buf, 2, 4};

    for (int chan = 0; chan < 2; ++chan)
        for (int samp = 0; samp < 4; ++samp)
            std::cout << bv(chan,samp) << "\n";

    // this is the usage I'm looking for...

    for (auto chan : bv)
        for (auto samp : chan)
            std::cout << "range for loop " << samp << "\n";

    return 0;
}

My main concerns are:

  • Where should I be putting the iterators?
  • There seems to be redundant code? (e.g. data members are same in some cases)
  • Do I need to pass samps (size of 2nd dimension) all the way down through the iterators?
  • Any other way I can be reducing the design to get simple range-based for loop traversal?

I'll gladly discuss the BufferView in general after I've tidied the iterators!

EDIT: Some improvements...

Okay so I've improved the code a little which seems much more structured to me...

//==============================================================================

class ChannelView
{
public:
    ChannelView (float* buffer, int numSamples)
        : fp {buffer}, samps {numSamples}
    {}

          float& operator() (int samp)       { return fp[samp]; }                   // debugmode range check?
    const float& operator() (int samp) const { return fp[samp]; }

    //==============================================================================

    class SampleIterator
    {
    public:
        SampleIterator (float* sample)
            : curr {sample}
        {}

        float* operator++() { curr++; return curr; }
        float operator*() { return *curr; }
        bool operator!= (const SampleIterator& other) { return curr != other.curr; }

    private:
        float* curr;
    };

    SampleIterator begin() { return SampleIterator {&fp[0]    }; }                  // const versions?
    SampleIterator end()   { return SampleIterator {&fp[samps]}; }

private:
    float* fp;
    int samps;
};

//==============================================================================

class BufferView
{
public:
    BufferView (float** buffer, int numChannels, int numSamples)
        : fpp {buffer}, chans {numChannels}, samps {numSamples}
    {}                                                                              //gsl::Expects

          float& operator() (int chan, int samp)       { return fpp[chan][samp]; }  // debugmode range check?
    const float& operator() (int chan, int samp) const { return fpp[chan][samp]; }

    //==============================================================================

    class ChannelIterator
    {
    public:
        ChannelIterator (float** channel, int numSamples)
            : curr {channel}, samps {numSamples}
        {}

        float** operator++() { curr++; return curr; }
        ChannelView operator*() { return ChannelView {*curr, samps}; }
        bool operator!= (const ChannelIterator& other) { return curr != other.curr; }

    private:
        float** curr;
        int samps;
    };

    ChannelIterator begin() { return ChannelIterator {&fpp[0],     samps}; }    // const versions?
    ChannelIterator end()   { return ChannelIterator {&fpp[chans], samps}; }

private:
    float** fpp;
    int chans;
    int samps;
};

//==============================================================================

#include <iostream>

int main(int argc, const char * argv[])
{
    float chan1[4] = {1, 2, 3, 4};
    float chan2[4] = {5, 6, 7, 8};
    float* buf[2] = {chan1, chan2};

    BufferView bv {buf, 2, 4};

    for (auto chan : bv)
        for (auto samp : chan)
            std::cout << "range for loop " << samp << "\n";

    return 0;
}

Main changes:

  • Implemented a ChannelView which is the next level down hierarchically to BufferView.
  • Nested the Iterator classes appropriately
  • Dereference operator in ChannelIterator returns a ChannelView like this ChannelView operator*() { return ChannelView {*curr, samps}; }

What still looks unorthodox to me is that samps (number of samples per channel) is also stored in the ChannelIterator (in order to instantiate the ChannelView on dereference. ChannelView needs to know its size).

It's better than before where samps was passed all the way down 2 classes.

The only other way I could think is if on construction of BufferView we construct and hold a ChannelView member per channel. But that seems like a case of paying-for-what-you-don't-use, if you never need to iterate. So creating a ChannelView only on dereference seems preferable?..

ChannelView operator*() { return ChannelView {*curr, samps}; }

Any ideas greatly appreciated?!

\$\endgroup\$
  • \$\begingroup\$ In what form comes input? Vector, array, C array? If it is the same as in the function declaration you could replace it with std::vector<const std::vector<float>> and it will have roughly the same memory layout and will have iterators out of the box. \$\endgroup\$ – Incomputable Feb 6 '17 at 8:16
  • \$\begingroup\$ @Incomputable The input comes from various libraries with mostly C-style (float** data, int n_chans, int n_samps) (I don't have control over this). So I'm trying to implement a simple non-owning view (similar to GSL::span) which I can quickly/cheaply wrap legacy code. I did think of vector<vector>but that would own data/a copy of the data. \$\endgroup\$ – ternonlerwen Feb 6 '17 at 9:00
  • \$\begingroup\$ Should iterator types of outer loop and inner match? It is possible to do with some template metaprogramming, but it will complicate things a bit. \$\endgroup\$ – Incomputable Feb 6 '17 at 9:22
  • \$\begingroup\$ @Incomputable To give more info, I have to interface with a lot of different library code, all using various representations of a buffer of sound (but nearly all can be manipulated into (float** data, int n_chans, int n_samps) form). Also in a real time DSP loop, you don't want to copy data, you just want to manipulate the data where it is (hence a non-owning view seems to be a solution?) \$\endgroup\$ – ternonlerwen Feb 6 '17 at 9:28
  • \$\begingroup\$ @Incomputable Replied at the same time! I don't think they should match because, when dereferenced, an iterator over an audio buffer should return a channel of float samples. Whereas an iterator over a buffer channel should return a single float sample when dereferenced. (I stand to be corrected!) \$\endgroup\$ – ternonlerwen Feb 6 '17 at 9:33
2
\$\begingroup\$

Good already:

The code should be good enough for the most usage cases. There are some small nitpicks that could be found, but I believe they are constraints of the API used.

  • const versions of the member functions can be added

Recursive iterators:

So I had an idea to create recursive iterators but run into a problem after some time. The problem is that complexity escalates way too fast. Implementation also requires keeping the dimensions of all lower indirections, which probably violates don't pay for what you don't use idiom.

Generic solution:

Despite it is hard to write recursive iterator, it is quite possible to write two level iterator. It can be achieved by simply creating a template and specializing it for the first and second level of indirection. The latter will return recursive_iterator<T*>, whereas itself will be recursive_iterator<T**>. It should be easily achievable since the code already uses the same idea.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.