Multi dimentional slice conversion problems

Hello!

I do a lot of work with audio and I’ve been really excited to try using zig for some dsp.

I’ve implemented a little clap plugin that makes a simple delay effect.

In doing that I’ve found that there are time where I want to move between different representations of the multi-channel audio data and I’ve struggled when I’ve needed to do that.

When representing buffers of multi-channel audio the relevant built-in types are: [][]f32, [*][*]f32, and an interleaved buffer f32 where the size is numChannels*bufferSize

I wanted to make my function signatures for dsp algorithms be like the following:

const DelayFX = struct {
    // ... algorithm state
    pub fn process(self:*DelayFX, input:[][]f32, output:[][]f32) void {
        // ... implementation
    }
};

in order to interface with the extern c arrays provided the host I ultimately wrote:

pub const AudioSpan = struct {
    input: ?[*][*]f32 = null,
    output: ?[*][*]f32 = null,
    shape: [2]usize = .{ 0, 0 },
    range: [2]usize = .{ 0, 0 },
};

const DelayFX = struct {
    // ... algorithm state
    pub fn process(self:*DelayFX, block: AudioSpan) void {
        // ... implementation
    }
};

I then wrapped up the [*][*]f32 provided by the host and used the range parameter to control which part of the memory got processed. Maybe this is a fine solution? I was excited by the idea of just using the built in slice functionality as I feel like I’m partially duplicating it…

I would have loved to convert from [*][*]f32 to [][]f32 but found this very error prone and hard to abstract.

I’ve made a little test to try to learn how to do these conversions more fluidly. I’ve so far failed to abstract any of the conversions! I feel like there is something the language is trying to tell me about the underlying data representations but the results have been confusing… When I try to abstract the conversion from one type to another I will get results that to me feel like I am accessing uninitialized memory and getting undefined values.

fn MutliSliceToMultiPtr(comptime T: type, allocator: std.mem.Allocator, slice: [][]T) ![*][*]T {
    var ptrArray: [][*]f32 = try allocator.alloc([*]f32, slice.len);
    for (0..(slice.len - 1)) |r| {
        ptrArray[r] = @ptrCast(&slice[r]);
    }

    return @ptrCast(&ptrArray);
}

test "multi pointer to multi slice conversion" {
    var array = [2][4]f32{
        .{ 1, 2, 3, 4 },
        .{ 4, 3, 2, 1 },
    };

    // Begin conversion test 1
    // TODO: how could this conversion be extracted to a comptime function?
    var arrayOfSlice: [2][]f32 = .{
        &array[0],
        &array[1],
    };

    const slice: [][]f32 = @ptrCast(&arrayOfSlice);

    for (0..array.len - 1) |c| {
        for (0..array[0].len - 1) |i| {
            try std.testing.expectApproxEqAbs(array[c][i], slice[c][i], 0.0001);
        }
    }
   // end

    // begin conversion test 2
    var buffer: [@sizeOf([*]f32) * 16]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const multiPtr: [*][*]f32 = try MutliSliceToMultiPtr(f32, fba.allocator(), slice);

    // TODO: this works but the above fn call does not... even trying to abstract the code below by moving it directly into a function call fails
    //  var ptrArray: [2][*]f32 = .{
    //      @ptrCast(&array[0]),
    //      @ptrCast(&array[1]),
    //  };
    //const multiPtr: [*][*]f32 = @ptrCast(&ptrArray);

    for (0..array.len - 1) |c| {
        for (0..array[0].len - 1) |i| {
            try std.testing.expectApproxEqAbs(array[c][i], multiPtr[c][i], 0.0001);
        }
    }
// end 

    // begin conversion test 3
    // TODO: Make a function that abstracts the following conversion
    var arrayOfSlice2 = [2][]f32{
        multiPtr[0][0 .. array[0].len - 1],
        multiPtr[1][0 .. array[0].len - 1],
    };

    const multiSlice: [][]f32 = &arrayOfSlice2;

    for (0..array.len - 1) |c| {
        for (0..array[0].len - 1) |i| {
            try std.testing.expectApproxEqAbs(array[c][i], multiSlice[c][i], 0.0001);
        }
    }
}

Does anyone have any pointers for how to do this kind of thing?

store the data flat instead of as nested ptrs/slices, it adds a bit of math to index it, but it simplifies everything else about managing it.

1 Like

thanks for for this! I like aspects of this solution and agree it simplifies things. I could abstract the math away in a struct like so:

pub const AudioSpan = struct {
    data: [*]f32,
    shape: [2]usize = .{ 0, 0 },

    pub fn get(self: *AudioSpan, channel: usize, index: usize) f32 {
        return self.data[index * (channel+1)];
    }

    pub fn set(self: *AudioSpan, channel: usize, index: usize, value: f32) void {
        self.data[index * (channel+1)] = value;
    }

};

const DelayFX = struct {
    // ... algorithm state
    pub fn process(self:*DelayFX, input: AudioSpan, output: AudioSpan) void {
        // ... implementation
    }
};

One thing that I think is a lot nicer is that I would be able to process segments of the flattened block by taking [startSample*numChannels..endSample*numChannels] from the flattened [*]f32

It would mean at the top layer I would need to copy the buffer provided by the host ([*][*]f32) into some buffer I allocate like the structure above but that might be OK to do once for each block processed by the plugin using a bump allocator.

I would still like to know how to convert between the different multi-dimensional types. I felt quite lost trying to write the test in the first post. I’d love to know why some of the solutions work inline but fail when pulled out to function calls for instance…

The issue was they did different things.

fn MutliSliceToMultiPtr(comptime T: type, allocator: std.mem.Allocator, slice: [][]T) ![*][*]T {
    var ptrArray: [][*]f32 = try allocator.alloc([*]f32, slice.len);
    for (0..(slice.len - 1)) |r| {
        ptrArray[r] = @ptrCast(&slice[r]);
    }

    return @ptrCast(&ptrArray);
}

Slices are already pointers (+ length), and &slice[r] gets a pointer to the inner slice.
What you want to do is get the pointer of the slice like so slice[r].ptr.

also avoid pointer casts, none of them are necessary in any of the code you shared

1 Like

Thank you!

This was really helpful. I sat and thought through the structure of a slice and which the & got me versus the .ptr. I was able to pull out two functions and make my test pass.

Here is my solution for converting between these types for anyone else to build on/critique. I will probably take @vulpesx 's initial advice and try to work with a flat slice for handling multi-channel audio.

fn MutliSliceToMultiPtr(comptime T: type, allocator: std.mem.Allocator, slice: [][]T) ![*][*]T {
    var ptrArray: [][*]f32 = try allocator.alloc([*]f32, slice.len);
    for (0..(slice.len - 1)) |r| {
        ptrArray[r] = slice[r].ptr;
    }

    return ptrArray.ptr;
}

fn MultiPtrToMultiSlice(comptime T: type, allocator: std.mem.Allocator, multiptr: [*][*]T, shape: [2]usize) ![][]T {
    var sliceOfSlice = try allocator.alloc([]f32, shape[0]);
    for (0..shape[0]) |r| {
        sliceOfSlice[r] = multiptr[r][0..shape[1]];
    }
    return sliceOfSlice;
}

test "multi pointer to multi slice conversion" {
    var array = [2][4]f32{
        .{ 1, 2, 3, 4 },
        .{ 4, 3, 2, 1 },
    };

    var arrayOfSlice: [2][]f32 = .{
        &array[0],
        &array[1],
    };

    const slice: [][]f32 = &arrayOfSlice;

    for (0..array.len - 1) |c| {
        for (0..array[0].len - 1) |i| {
            try std.testing.expectApproxEqAbs(array[c][i], slice[c][i], 0.0001);
        }
    }

    var buffer: [@sizeOf([*]f32) * 16]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const multiPtr: [*][*]f32 = try MutliSliceToMultiPtr(f32, fba.allocator(), slice);

    for (0..array.len - 1) |c| {
        for (0..array[0].len - 1) |i| {
            try std.testing.expectApproxEqAbs(array[c][i], multiPtr[c][i], 0.0001);
        }
    }

    const multiSlice: [][]f32 = try MultiPtrToMultiSlice(f32, fba.allocator(), multiPtr, .{ array.len, array[0].len });

    for (0..array.len - 1) |c| {
        for (0..array[0].len - 1) |i| {
            try std.testing.expectApproxEqAbs(array[c][i], multiSlice[c][i], 0.0001);
        }
    }
}