#include "buffer.h" #include #include #include using namespace emscripten; template void register_buffer_bindings(const char *name) { class_(name) .template smart_ptr_constructor, int &&, size_t &&>( name, &std::make_shared) .class_function("fromArray(inputTypedArrays)", &T::fromArray, nonnull()) .function("fill(value)", select_overload(&T::fill)) .function( "fill(offset, count, value)", select_overload(&T::fill)) .function("noise", &T::noise) .function( "set(channel, inputTypedArray)", select_overload( &T::js_set)) .function( "set(channel, offset, inputTypedArray)", select_overload(&T::js_set)) .function("set(channels)", select_overload( &T::js_set)) .function( "view", select_overload(&T::js_view)) .function("view(channel)", select_overload( &T::js_view)) .function("setInto(outputTypedArrays)", select_overload( &T::js_setInto)) .function( "setInto(channel, outputTypedArray)", select_overload( &T::js_setInto)) .property("channelCount", &T::getChannelCount) .property("frameCount", &T::getFrameCount) .function("peak", &T::peak) .function("interleaveTo(destination)", select_overload &) const>( &T::interleaveTo)) .function("deinterleaveFrom(source)", select_overload &)>( &T::deinterleaveFrom)); } EMSCRIPTEN_BINDINGS(buffer) { register_buffer_bindings("BufferF32"); register_buffer_bindings("BufferF64"); register_type("Float32Array"); register_type("Float64Array"); register_type("Float32Array[]"); register_type("Float64Array[]"); }; TEST_CASE("buffer c++ tests", "[buffer]") { { float xs[4] = {1, 2, 3, 4}; BufferF32 buf = BufferF32::fromVLA(2, 2, xs); REQUIRE(buf.getFrameCount() == 2); REQUIRE(buf.getChannelCount() == 2); REQUIRE(buf[0][0] == 1.f); REQUIRE(buf[0][1] == 2.f); REQUIRE(buf[1][0] == 3.f); REQUIRE(buf[1][1] == 4.f); } { BufferF32 buf(1, 10); for (int i = 0; i < 5; i++) { buf[0][i] = i; } for (int i = 0; i < 5; i++) { buf[0][i + 5] = 6 - i; } REQUIRE(buf.peak() == 6.f); } { BufferF32 buf(1, 100); buf.fill(0.f); REQUIRE(buf.getFrameCount() == 100); REQUIRE(buf.getChannelCount() == 1); auto sub = buf.slice(1, 10); REQUIRE(sub.getFrameCount() == 9); REQUIRE(sub.getChannelCount() == 1); sub.fill(1.f); REQUIRE(std::all_of(sub.getChannelData(0), sub.getChannelData(0) + 9, [](float v) { return v == 1.f; })); REQUIRE(std::all_of(buf.getChannelData(0) + 10, buf.getChannelData(0) + 100, [](float v) { return v == 0.f; })); REQUIRE(buf[0][0] == 0.f); REQUIRE(buf[0][1] == 1.f); } { BufferF32 buf(2, 123); buf.fill(0.f); REQUIRE(buf.getFrameCount() == 123); REQUIRE(buf.getChannelCount() == 2); auto right = buf.sliceChannel(1); right[0][0] = 123.f; REQUIRE(right[0][0] == 123.f); REQUIRE(buf[1][0] == 123.f); REQUIRE(buf[1][1] == 0.f); buf.cloneChannel(0, 1); REQUIRE(buf[1][0] == 0.f); right.fill(2.f); buf.mixDownInPlace(); REQUIRE(std::all_of(buf.getChannelData(0), buf.getChannelData(0) + buf.getFrameCount(), [](float v) { return v == 1.f; })); BufferF32 buf2(2, 10); buf2.noise(); buf.set(10, buf2); for (int channel = 0; channel < buf.getChannelCount(); channel++) { REQUIRE(std::equal(buf[channel] + 10, buf[channel] + 10 + buf2.getFrameCount(), buf2[channel])); } buf.set(1, 20, 10, buf2.getChannelData(0)); REQUIRE(std::all_of(buf[0] + 20, buf[0] + 30, [](float v) { return v == 1.f; })); REQUIRE(std::equal(buf[1] + 20, buf[1] + 30, buf2[0])); buf.fill(10, 1, 123.f); for (int channel = 0; channel < buf.getChannelCount(); channel++) { REQUIRE(buf[channel][10] == 123.f); REQUIRE(buf[channel][11] == buf2[channel][1]); } REQUIRE(!buf.hasNaN()); buf[0][0] = 0.f / 0.f; REQUIRE(buf.hasNaN()); } { BufferF32 buf(2, 123); buf.fill(0.f); buf = buf; // should have no effect REQUIRE(buf[0][0] == 0.f); auto buf2 = buf; buf2.fill(1.f); REQUIRE(buf[0][0] == 1.f); auto buf3(buf); buf3.fill(2.f); REQUIRE(buf[0][0] == 2.f); BufferF32 buf4(2, 10); buf4 = buf; buf4.fill(3.f); REQUIRE(buf[0][0] == 3.f); BufferF32 buf5(std::move(buf)); buf5.fill(4.f); REQUIRE(buf2[0][0] == 4.f); } // Test interleaveTo { BufferF32 stereo(2, 4); stereo[0][0] = 1.f; stereo[0][1] = 2.f; stereo[0][2] = 3.f; stereo[0][3] = 4.f; stereo[1][0] = 5.f; stereo[1][1] = 6.f; stereo[1][2] = 7.f; stereo[1][3] = 8.f; BufferF32 interleaved(1, 8); stereo.interleaveTo(interleaved); REQUIRE(interleaved[0][0] == 1.f); REQUIRE(interleaved[0][1] == 5.f); REQUIRE(interleaved[0][2] == 2.f); REQUIRE(interleaved[0][3] == 6.f); REQUIRE(interleaved[0][4] == 3.f); REQUIRE(interleaved[0][5] == 7.f); REQUIRE(interleaved[0][6] == 4.f); REQUIRE(interleaved[0][7] == 8.f); } // Test deinterleaveFrom { BufferF32 interleaved(1, 8); interleaved[0][0] = 1.f; interleaved[0][1] = 5.f; interleaved[0][2] = 2.f; interleaved[0][3] = 6.f; interleaved[0][4] = 3.f; interleaved[0][5] = 7.f; interleaved[0][6] = 4.f; interleaved[0][7] = 8.f; BufferF32 stereo(2, 4); stereo.deinterleaveFrom(interleaved); REQUIRE(stereo[0][0] == 1.f); REQUIRE(stereo[0][1] == 2.f); REQUIRE(stereo[0][2] == 3.f); REQUIRE(stereo[0][3] == 4.f); REQUIRE(stereo[1][0] == 5.f); REQUIRE(stereo[1][1] == 6.f); REQUIRE(stereo[1][2] == 7.f); REQUIRE(stereo[1][3] == 8.f); } // Test round-trip: interleaveTo then deinterleaveFrom { BufferF32 original(2, 10); original.noise(); BufferF32 interleaved(1, 20); original.interleaveTo(interleaved); BufferF32 restored(2, 10); restored.deinterleaveFrom(interleaved); for (int channel = 0; channel < 2; channel++) { for (int frame = 0; frame < 10; frame++) { REQUIRE(restored[channel][frame] == original[channel][frame]); } } } } TEST_CASE("buffer interleave mono", "[buffer]") { BufferF32 monoIn(1, 4); BufferF32 monoOut(1, 4); monoIn[0][0] = 1.f; monoIn[0][1] = 2.f; monoIn[0][2] = 3.f; monoIn[0][3] = 4.f; monoOut.fill(0.f); // SECTION lets us take every possible branch combination SECTION("interleaveTo") { monoIn.interleaveTo(monoOut); } SECTION("deinterleaveFrom") { monoOut.deinterleaveFrom(monoIn); } REQUIRE(monoOut[0][0] == 1.f); REQUIRE(monoOut[0][1] == 2.f); REQUIRE(monoOut[0][2] == 3.f); REQUIRE(monoOut[0][3] == 4.f); } template concept CanConstructOwning = requires { { Buffer(2, 1024) } -> std::same_as>; }; template concept CanModifyContents = requires(Buffer buffer, typename Buffer::sampleType value) { { buffer[0][0] = value } -> std::convertible_to::sampleType &>; }; template concept SupportsOwnershipTransfer = requires { requires std::is_move_constructible_v>; requires std::is_move_assignable_v>; }; template concept CanConvertToConstBuffer = requires(Buffer buffer) { { static_cast>>(buffer) } -> std::same_as>>; }; template concept CanPerformDataOperations = requires(Buffer buffer) { { buffer.fill(0.0f) } -> std::same_as; { buffer.slice(0, 10) } -> std::same_as>; }; template concept ConstMethodsReturnConstBuffers = requires(const Buffer buffer) { { buffer.slice(0, 10) } -> std::same_as>>; { buffer.sliceChannel(0) } -> std::same_as>>; }; static_assert(CanConstructOwning, "Buffer should support memory-owning construction"); static_assert(CanModifyContents, "Buffer should allow modifying its contents"); static_assert(SupportsOwnershipTransfer, "Buffer should support move operations"); static_assert(CanConvertToConstBuffer, "Buffer should be convertible to Buffer"); static_assert(CanPerformDataOperations, "Buffer should support data manipulation operations"); static_assert(ConstMethodsReturnConstBuffers, "Const methods should return const buffers for Buffer"); // Const buffer should have restricted capabilities static_assert( !CanConstructOwning, "Buffer should not support memory-owning construction"); static_assert(!CanModifyContents, "Buffer should not allow modifying its contents"); static_assert(!SupportsOwnershipTransfer, "Buffer should not support ownership transfer in " "move operations"); static_assert( ConstMethodsReturnConstBuffers, "Const methods should return const buffers for Buffer");