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?