diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b3d765..96fe09a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ option(EAR_PACKAGE_AND_INSTALL "Package and install libear" ${IS_ROOT_PROJECT}) option(EAR_USE_INTERNAL_EIGEN "should we use our own version of Eigen, or find one with find_package?" TRUE) option(EAR_USE_INTERNAL_XSIMD "should we use our own version of xsimd, or find one with find_package?" TRUE) option(EAR_SIMD "try to use SIMD extensions" TRUE) +option(EAR_NO_EXCEPTIONS "abort instead of throwing exceptions" FALSE) set(INSTALL_LIB_DIR lib CACHE PATH "Installation directory for libraries") set(INSTALL_BIN_DIR bin CACHE PATH "Installation directory for executables") set(INSTALL_INCLUDE_DIR include CACHE PATH "Installation directory for header files") @@ -102,6 +103,7 @@ add_feature_info(EAR_PACKAGE_AND_INSTALL ${EAR_PACKAGE_AND_INSTALL} "Package and add_feature_info(EAR_USE_INTERNAL_EIGEN ${EAR_USE_INTERNAL_EIGEN} "use internal version of Eigen") add_feature_info(EAR_USE_INTERNAL_XSIMD ${EAR_USE_INTERNAL_XSIMD} "use internal version of xsimd") add_feature_info(EAR_SIMD ${EAR_SIMD} "try to use SIMD extensions") +add_feature_info(EAR_NO_EXCEPTIONS ${EAR_NO_EXCEPTIONS} "abort instead of throwing exceptions") feature_summary(WHAT ALL) if(EAR_PACKAGE_AND_INSTALL) diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake new file mode 100644 index 0000000..bd82357 --- /dev/null +++ b/cmake/wasm.cmake @@ -0,0 +1,67 @@ +# determine export flags to pass when linking a wasm module + +# we would like this to be as small as possible, but not all functions are +# needed all the time, so groups of exports are defined which can be set with +# EAR_WASM_EXPORT_GROUPS + +# wasm_add_export adds a function name the wasm_exports_[group] lists, which are +# then combined into one list (wasm_exports) and ultimately a list of flags +# (wasm_export_flags) + +# it would be possible to put this in the source instead, but would require some +# tricky macros + +macro(wasm_add_export group name) + list(APPEND wasm_exports_${group} ${name}) + list(APPEND wasm_exports_all ${name}) + + # append to wasm_export_groups if not present + list(FIND wasm_export_groups ${group} wasm_has_group) + if(wasm_has_group EQUAL -1) + list(APPEND wasm_export_groups ${group}) + endif() +endmacro() + +# for all functions in c_api.h, add a call here with a group to put it in + +wasm_add_export(objects ear_objects_type_metadata_new) +wasm_add_export(objects ear_objects_type_metadata_free) +wasm_add_export(objects ear_objects_type_metadata_reset) +wasm_add_export(objects ear_objects_type_metadata_set_polar_position) +wasm_add_export(objects ear_objects_type_metadata_set_extent) +wasm_add_export(objects ear_objects_type_metadata_set_gain) +wasm_add_export(objects ear_objects_type_metadata_set_diffuse) + +wasm_add_export(bs2051 ear_layout_get) +wasm_add_export(layout ear_layout_num_channels) +wasm_add_export(layout ear_layout_free) + +wasm_add_export(objects ear_gain_calculator_objects_new) +wasm_add_export(objects ear_gain_calculator_objects_free) +wasm_add_export(objects ear_gain_calculator_objects_calc_gains) + +wasm_add_export(decorrelators ear_decorrelator_compensation_delay) +wasm_add_export(decorrelators ear_design_decorrelator) + +set(EAR_WASM_EXPORT_GROUPS + "all" + CACHE + STRING + "groups of wasm functions to export; options: ${wasm_export_groups} or all" +) + +# always required +set(wasm_exports malloc free ear_init) + +foreach(group IN LISTS EAR_WASM_EXPORT_GROUPS) + foreach(export IN LISTS wasm_exports_${group}) + # append to wasm_exports if not present + list(FIND wasm_exports ${export} wasm_has_export) + if(wasm_has_export EQUAL -1) + list(APPEND wasm_exports ${export}) + endif() + endforeach() +endforeach() + +list(TRANSFORM wasm_exports PREPEND "-Wl,--export=" OUTPUT_VARIABLE + wasm_export_flags) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 22cb26b..3c9e227 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,2 @@ add_executable(objects_gains objects_gains.cpp) -target_link_libraries(objects_gains PRIVATE ear) +target_link_libraries(objects_gains PRIVATE ear ear_handle_exceptions) diff --git a/flake.nix b/flake.nix index 7903a76..cc69d25 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,11 @@ flake-utils.lib.eachDefaultSystem (system: let - pkgs = nixpkgs.legacyPackages.${system}; + pkgs = import nixpkgs { + system = system; + config.allowUnsupportedSystem = true; # for eigen wasm + }; + pkgs_wasm = pkgs.pkgsCross.wasi32; devtools = [ pkgs.clang-tools @@ -33,6 +37,30 @@ nativeBuildInputs = attrs.nativeBuildInputs ++ devtools; }); devShells.default = devShells.libear; + + # wasm versions of packages + + packages.xsimd_wasm = pkgs_wasm.xsimd.overrideAttrs { + # this should be an overlay + version = pkgs.xsimd.version; + src = pkgs.xsimd.src; + }; + packages.libear_wasm = (pkgs_wasm.callPackage ./nix/libear.nix { + src = ./.; + boost = pkgs.boost; # doesn't build for wasm, but we only use headers, so use system version + xsimd = packages.xsimd_wasm; + }).overrideAttrs (super: { + cmakeBuildType = "MinSizeRel"; + }); + + devShells.libear_wasm = packages.libear_wasm.overrideAttrs (attrs: { + nativeBuildInputs = attrs.nativeBuildInputs ++ devtools ++ [ + pkgs.wabt + pkgs.wasmtime + pkgs.nodejs + pkgs.nodePackages.prettier + ]; + }); } ); } diff --git a/include/ear/c_api.h b/include/ear/c_api.h new file mode 100644 index 0000000..efcabdb --- /dev/null +++ b/include/ear/c_api.h @@ -0,0 +1,93 @@ +#include +#include "export.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*ear_warning_cb)(const char *warning); + +////////////////////// +// ObjectsTypeMetadata +////////////////////// + +typedef struct ear_objects_type_metadata ear_objects_type_metadata; + +EAR_EXPORT +ear_objects_type_metadata *ear_objects_type_metadata_new(); +EAR_EXPORT +void ear_objects_type_metadata_free(ear_objects_type_metadata **); +EAR_EXPORT +void ear_objects_type_metadata_reset(ear_objects_type_metadata *); + +EAR_EXPORT +void ear_objects_type_metadata_set_polar_position(ear_objects_type_metadata *, + double azimuth, + double elevation, + double distance); +EAR_EXPORT +void ear_objects_type_metadata_set_extent(ear_objects_type_metadata *, + double width, double height, + double depth); +EAR_EXPORT +void ear_objects_type_metadata_set_gain(ear_objects_type_metadata *, + double gain); +EAR_EXPORT +void ear_objects_type_metadata_set_diffuse(ear_objects_type_metadata *, + double diffuse); + +///////// +// Layout +///////// + +typedef struct ear_layout ear_layout; + +/// get a layout by its BS.2051 name (e.g. 4+5+0); returns null if there is no +/// layout with the given name +EAR_EXPORT +ear_layout *ear_layout_get(const char *name); +EAR_EXPORT +void ear_layout_free(ear_layout **); + +EAR_EXPORT +size_t ear_layout_num_channels(ear_layout *); + +//////////////////////// +// GainCalculatorObjects +//////////////////////// + +typedef struct ear_gain_calculator_objects ear_gain_calculator_objects; + +EAR_EXPORT +ear_gain_calculator_objects *ear_gain_calculator_objects_new(ear_layout *); +EAR_EXPORT +void ear_gain_calculator_objects_free(ear_gain_calculator_objects **); +EAR_EXPORT +void ear_gain_calculator_objects_calc_gains(ear_gain_calculator_objects *, + ear_objects_type_metadata *, + size_t n_gains, + double *direct_gains, + double *diffuse_gains); +#ifndef __wasm__ +EAR_EXPORT +void ear_gain_calculator_objects_calc_gains_cb( + ear_gain_calculator_objects *, ear_objects_type_metadata *, size_t n_gains, + double *direct_gains, double *diffuse_gains, ear_warning_cb warning_cb); +#endif + +//////////////// +// decorrelators +//////////////// + +EAR_EXPORT int ear_decorrelator_compensation_delay(); + +/// design a decorrelation filter for channel_idx in layout +/// +/// the number of samples will be written to *length; the result should be +/// freed using free() +EAR_EXPORT float *ear_design_decorrelator(ear_layout *layout, + size_t channel_idx, size_t *length); + +#ifdef __cplusplus +} +#endif diff --git a/include/ear/dsp/gain_interpolator.hpp b/include/ear/dsp/gain_interpolator.hpp index 8311573..d27b856 100644 --- a/include/ear/dsp/gain_interpolator.hpp +++ b/include/ear/dsp/gain_interpolator.hpp @@ -121,7 +121,7 @@ namespace ear { while (cmp != 0) { last_block += cmp; if (cmp != first_cmp) - throw invalid_argument("interpolation points are not sorted"); + ear_throw(invalid_argument("interpolation points are not sorted")); cmp = block_cmp(last_block, sample_idx); } diff --git a/include/ear/exceptions.hpp b/include/ear/exceptions.hpp index 895ba0a..332ba2e 100644 --- a/include/ear/exceptions.hpp +++ b/include/ear/exceptions.hpp @@ -4,6 +4,38 @@ #include "export.hpp" namespace ear { + +/// support for building without exceptions +/// +/// in the rest of the code, the ear_throw macro is used to throw exceptions, +/// and normally this just throws the given exception +/// +/// when exceptions are disabled, it instead calls ear::handle_exception with +/// the exception object, and can be provided by users of the library +/// +/// this callback must return out of the library (e.g. by abort or longjmp), +/// and you can't assume that the library is in a reasonable state after this +/// has happened, because clean-up that would normally be done during unwinding +/// will not happen +/// +/// this is really unfortunate, but the library was designed with use of C++ +/// exceptions in mind, so a lot of code would need to change to fix this +/// +/// note that boost has a similar system which requires defining +/// boost::throw_exception +/// +/// an example of both is provided in src/handle_exceptions.cpp, which is used +/// to build examples and tests +#ifdef EAR_NO_EXCEPTIONS + + [[noreturn]] void handle_exception(const std::exception &e); + +#define ear_throw(exc) ::ear::handle_exception(exc); + +#else +#define ear_throw(exc) throw exc +#endif + /// thrown if features are used which are not yet implemented class EAR_EXPORT not_implemented : public std::runtime_error { public: diff --git a/include/ear/helpers/assert.hpp b/include/ear/helpers/assert.hpp index 6f4a5ff..171c3d4 100644 --- a/include/ear/helpers/assert.hpp +++ b/include/ear/helpers/assert.hpp @@ -5,11 +5,11 @@ namespace ear { // implementation for _assert_impl. This is wrapped in a macro so that we can // use __LINE__, __FILE__ etc. in the future. inline void _assert_impl(bool condition, const char *message) { - if (!condition) throw internal_error(message); + if (!condition) ear_throw(internal_error(message)); } inline void _assert_impl(bool condition, const std::string &message) { - if (!condition) throw internal_error(message); + if (!condition) ear_throw(internal_error(message)); } } // namespace ear diff --git a/include/ear/helpers/output_gains.hpp b/include/ear/helpers/output_gains.hpp index 0dd2675..bed5575 100644 --- a/include/ear/helpers/output_gains.hpp +++ b/include/ear/helpers/output_gains.hpp @@ -39,7 +39,7 @@ namespace ear { OutputGainsT(std::vector &vec) : vec(vec) {} virtual void check_size(size_t n) override { if (vec.size() != n) { - throw invalid_argument("incorrect size for output vector"); + ear_throw(invalid_argument("incorrect size for output vector")); } } virtual size_t size() override { return vec.size(); } @@ -84,11 +84,12 @@ namespace ear { OutputGainMatVecT(std::vector> &mat) : mat(mat) {} virtual void check_size(size_t rows, size_t cols) override { if (mat.size() != cols) - throw invalid_argument("incorrect number of cols in output matrix"); + ear_throw( + invalid_argument("incorrect number of cols in output matrix")); for (auto &col : mat) if (col.size() != rows) - throw invalid_argument( - "incorrect number of rows in output matrix column"); + ear_throw(invalid_argument( + "incorrect number of rows in output matrix column")); } virtual size_t rows() override { return mat[0].size(); } virtual size_t cols() override { return mat.size(); } diff --git a/nix/libear.nix b/nix/libear.nix index 75ed12e..a3d76e7 100644 --- a/nix/libear.nix +++ b/nix/libear.nix @@ -1,6 +1,7 @@ -{ lib, buildPackages, stdenv, cmake, src, ninja, boost, eigen, xsimd }: +{ lib, buildPackages, stdenv, cmake, src, ninja, boost, eigen, xsimd, binaryen, nodejs }: let isCross = stdenv.buildPlatform != stdenv.hostPlatform; + isWasm = stdenv.hostPlatform.isWasm; in (stdenv.mkDerivation { name = "libear"; @@ -8,7 +9,11 @@ in nativeBuildInputs = [ cmake ninja + ] ++ lib.optionals isWasm [ + binaryen # gets used automatically by clang-ld if in path + nodejs ]; + buildInputs = [ boost eigen xsimd ]; cmakeFlags = [ "-DEAR_USE_INTERNAL_EIGEN=OFF" @@ -17,7 +22,20 @@ in ] ++ lib.optionals isCross [ "-DCMAKE_CROSSCOMPILING_EMULATOR=${stdenv.hostPlatform.emulator buildPackages}" + ] ++ lib.optionals isWasm [ + "-DEAR_NO_EXCEPTIONS=ON" ]; + doCheck = true; + + preConfigure = lib.optionalString isWasm '' + # for wasmtime cache + HOME=$(pwd) + # forced off in make-derivation.nix when build platform can't execute host + # platform, but we have an emulator + doCheck=1 + ''; + + env.NIX_CFLAGS_COMPILE = lib.optionalString isWasm "-DEIGEN_HAS_CXX11_ATOMIC=0 -DCATCH_CONFIG_NO_POSIX_SIGNALS"; }) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 218a081..155740d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ generate_points_file( add_library(ear bs2051.cpp bs2051_layouts.cpp + c_api.cpp common/convex_hull.cpp common/facets.cpp common/geom.cpp @@ -136,6 +137,28 @@ list(APPEND XSIMD_ARCHS xsimd::generic_for_dispatch) list(JOIN XSIMD_ARCHS "," XSIMD_ARCHS_STR) target_compile_definitions(ear PRIVATE "XSIMD_ARCHS=${XSIMD_ARCHS_STR}") +if (EAR_NO_EXCEPTIONS) + target_compile_definitions(ear PUBLIC "EAR_NO_EXCEPTIONS") +endif() +# link executables to ear_handle_exceptions to support building without +# exceptions; the comment at the top of exceptions.hpp explains how this works +add_library(ear_handle_exceptions OBJECT handle_exceptions.cpp) +target_link_libraries(ear_handle_exceptions PRIVATE ear) + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32|wasm64") + include(wasm) + + add_library(ear_wasm SHARED wasm_api.cpp) + + target_link_libraries(ear_wasm PRIVATE ear) + + target_link_options(ear_wasm PRIVATE -nostartfiles -Wl,--no-entry) + target_link_options(ear_wasm PRIVATE ${wasm_export_flags}) + + set_target_properties(ear_wasm PROPERTIES LIBRARY_OUTPUT_NAME ear SUFFIX + ".wasm") +endif() + include(GenerateExportHeader) generate_export_header(ear EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/generated/export.hpp @@ -200,4 +223,8 @@ if(EAR_PACKAGE_AND_INSTALL) ) install(EXPORT earTargetsStatic DESTINATION ${INSTALL_CMAKE_DIR}) endif() + + if (CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32|wasm64") + install(TARGETS ear_wasm LIBRARY DESTINATION "${INSTALL_LIB_DIR}") + endif() endif() diff --git a/src/bs2051.cpp b/src/bs2051.cpp index 9e326cd..cabef2c 100644 --- a/src/bs2051.cpp +++ b/src/bs2051.cpp @@ -16,7 +16,7 @@ namespace ear { layouts.begin(), layouts.end(), [&name](const Layout& l) -> bool { return l.name() == name; }); if (it == layouts.end()) { - throw unknown_layout(name); + ear_throw(unknown_layout(name)); } return *it; } diff --git a/src/c_api.cpp b/src/c_api.cpp new file mode 100644 index 0000000..0de9764 --- /dev/null +++ b/src/c_api.cpp @@ -0,0 +1,185 @@ +#include "ear/c_api.h" +#include "ear/bs2051.hpp" +#include "ear/decorrelate.hpp" +#include "ear/layout.hpp" +#include "ear/metadata.hpp" +#include "ear/warnings.hpp" +#include "object_based/gain_calculator_objects.hpp" + +#ifdef __wasm__ +#include "wasm_api.hpp" +#define DEFAULT_WARNING_CB ::ear::wasm_warning_cb +#else +#define DEFAULT_WARNING_CB ::ear::default_warning_cb +#endif + +namespace { + /// delete *ptr and set to null if not already null + template + void do_free(T **ptr) { + assert(ptr != NULL); + if (*ptr) { + delete *ptr; + *ptr = NULL; + } + } + + // implementation of OutputGains for float /double pointers + template + class OutputGainsPtr : public ear::OutputGains { + private: + T *ptr; + size_t size_; + + public: + OutputGainsPtr(T *ptr, size_t size_) : ptr(ptr), size_(size_) {} + virtual void check_size(size_t n) override { + if (size_ != n) { + ear_throw(ear::invalid_argument("incorrect size for output vector")); + } + } + virtual size_t size() override { return size_; } + virtual void write(size_t i, double x) override { ptr[i] = (T)x; } + virtual void zero() override { std::fill(ptr, ptr + size_, (T)0.0); } + }; +} // namespace + +namespace ear { + void wasm_warning_cb(const Warning &warning); +} + +extern "C" { + +////////////////////// +// ObjectsTypeMetadata +////////////////////// + +struct ear_objects_type_metadata : ear::ObjectsTypeMetadata {}; + +ear_objects_type_metadata *ear_objects_type_metadata_new() { + return new ear_objects_type_metadata{}; +} + +void ear_objects_type_metadata_free(ear_objects_type_metadata **ptr) { + do_free(ptr); +} + +void ear_objects_type_metadata_reset(ear_objects_type_metadata *ptr) { + *ptr = {}; +} + +void ear_objects_type_metadata_set_polar_position( + ear_objects_type_metadata *ptr, double azimuth, double elevation, + double distance) { + ptr->position = ear::PolarPosition{azimuth, elevation, distance}; +} +void ear_objects_type_metadata_set_extent(ear_objects_type_metadata *ptr, + double width, double height, + double depth) { + ptr->width = width; + ptr->height = height; + ptr->depth = depth; +} +void ear_objects_type_metadata_set_gain(ear_objects_type_metadata *ptr, + double gain) { + ptr->gain = gain; +} +void ear_objects_type_metadata_set_diffuse(ear_objects_type_metadata *ptr, + double diffuse) { + ptr->diffuse = diffuse; +} + +///////// +// Layout +///////// + +struct ear_layout : ear::Layout { + using ear::Layout::Layout; + ear_layout(ear::Layout &&layout) : ear::Layout(std::move(layout)) {} + ear_layout(const ear::Layout &layout) : ear::Layout(layout) {} +}; + +ear_layout *ear_layout_get(const char *name) { + auto &layouts = ear::loadLayouts(); + for (auto &layout : layouts) + if (layout.name() == name) return new ear_layout{layout}; + return NULL; +} + +void ear_layout_free(ear_layout **ptr) { do_free(ptr); } + +size_t ear_layout_num_channels(ear_layout *ptr) { + return ptr->channels().size(); +} + +//////////////////////// +// GainCalculatorObjects +//////////////////////// + +// use Impl rather than C++ API to avoid another layer of PIMPL and vector +// copying +struct ear_gain_calculator_objects : ear::GainCalculatorObjectsImpl { + using ear::GainCalculatorObjectsImpl::GainCalculatorObjectsImpl; +}; + +ear_gain_calculator_objects *ear_gain_calculator_objects_new( + ear_layout *layout) { + return new ear_gain_calculator_objects{*layout}; +} + +void ear_gain_calculator_objects_free(ear_gain_calculator_objects **ptr) { + do_free(ptr); +} + +static void ear_gain_calculator_objects_calc_gains_impl( + ear_gain_calculator_objects *ptr, ear_objects_type_metadata *otm, + size_t n_gains, double *direct_gains, double *diffuse_gains, + const ear::WarningCB &warning_cb) { + OutputGainsPtr direct_wrap(direct_gains, n_gains); + OutputGainsPtr diffuse_wrap(diffuse_gains, n_gains); + + ptr->calculate(*otm, direct_wrap, diffuse_wrap, warning_cb); +} + +void ear_gain_calculator_objects_calc_gains_cb(ear_gain_calculator_objects *ptr, + ear_objects_type_metadata *otm, + size_t n_gains, + double *direct_gains, + double *diffuse_gains, + ear_warning_cb warning_cb) { + ear_gain_calculator_objects_calc_gains_impl( + ptr, otm, n_gains, direct_gains, diffuse_gains, + [&](auto warning) { warning_cb(warning.message.c_str()); }); +} + +void ear_gain_calculator_objects_calc_gains(ear_gain_calculator_objects *ptr, + ear_objects_type_metadata *otm, + size_t n_gains, + double *direct_gains, + double *diffuse_gains) { + ear_gain_calculator_objects_calc_gains_impl( + ptr, otm, n_gains, direct_gains, diffuse_gains, DEFAULT_WARNING_CB); +} + +//////////////// +// decorrelators +//////////////// + +int ear_decorrelator_compensation_delay() { + return ear::decorrelatorCompensationDelay(); +} + +float *ear_design_decorrelator(ear_layout *layout, size_t channel_idx, + size_t *length) { + assert(length != NULL); + + auto dec_vector = ear::designDecorrelator(*layout, channel_idx); + + *length = dec_vector.size(); + + float *dec_ptr = (float *)malloc(sizeof(*dec_ptr) * dec_vector.size()); + for (size_t s = 0; s < dec_vector.size(); s++) dec_ptr[s] = dec_vector[s]; + + return dec_ptr; +} +} diff --git a/src/common/point_source_panner.cpp b/src/common/point_source_panner.cpp index 856a5de..0abfcfd 100644 --- a/src/common/point_source_panner.cpp +++ b/src/common/point_source_panner.cpp @@ -547,8 +547,8 @@ namespace ear { regions.push_back( boost::make_unique(outputChannels, positions)); } else { - throw internal_error( - "facets with more than 4 vertices are not supported"); + ear_throw(internal_error( + "facets with more than 4 vertices are not supported")); } } return std::make_shared( @@ -562,15 +562,15 @@ namespace ear { double abs_az = std::abs(channel.polarPosition().azimuth); if (!((5.0 <= abs_az && abs_az < 25.0) || (35.0 <= abs_az && abs_az < 60.0))) { - throw invalid_argument( + ear_throw(invalid_argument( "M+SC or M-SC has azimuth not in the allowed ranges of 5 to 25 " - "and 35 to 60 degrees"); + "and 35 to 60 degrees")); } if (25.0 < abs_az) { - throw not_implemented( + ear_throw(not_implemented( "M+SC and M-SC with azimuths wider than 25 degrees are not " - "currently supported"); + "currently supported")); } } } @@ -586,7 +586,7 @@ namespace ear { const Layout& layout) { auto isLfe = layout.isLfe(); if (find(isLfe.begin(), isLfe.end(), true) != isLfe.end()) { - throw internal_error("lfe channel passed to point source panner"); + ear_throw(internal_error("lfe channel passed to point source panner")); } checkScreenSpeakers(layout); diff --git a/src/common/screen_edge_lock.hpp b/src/common/screen_edge_lock.hpp index 7d9ac2a..5c10009 100644 --- a/src/common/screen_edge_lock.hpp +++ b/src/common/screen_edge_lock.hpp @@ -14,7 +14,7 @@ namespace ear { std::pair handleAzimuthElevation( double azimuth, double elevation, ScreenEdgeLock screenEdgeLock) { if (screenEdgeLock.horizontal || screenEdgeLock.vertical) - throw not_implemented("screenEdgeLock"); + ear_throw(not_implemented("screenEdgeLock")); return std::make_pair(azimuth, elevation); } @@ -22,7 +22,7 @@ namespace ear { std::tuple handleVector( Eigen::Vector3d pos, ScreenEdgeLock screenEdgeLock) { if (screenEdgeLock.horizontal || screenEdgeLock.vertical) - throw not_implemented("screenEdgeLock"); + ear_throw(not_implemented("screenEdgeLock")); return std::make_tuple(pos(0), pos(1), pos(2)); } diff --git a/src/conversion.cpp b/src/conversion.cpp index 36e1adb..0a7ac13 100644 --- a/src/conversion.cpp +++ b/src/conversion.cpp @@ -81,7 +81,7 @@ namespace ear { if (insideAngleRange(az, sector.cart_end_az, sector.cart_start_az)) return sector; - throw internal_error("could not find sector"); + ear_throw(internal_error("could not find sector")); } static const Sector &find_polar_sector(double az) { @@ -89,7 +89,7 @@ namespace ear { if (insideAngleRange(az, sector.polar_end_az, sector.polar_start_az)) return sector; - throw internal_error("could not find sector"); + ear_throw(internal_error("could not find sector")); } double map_az_to_linear(double left_az, double right_az, double azimuth) { diff --git a/src/direct_speakers/gain_calculator_direct_speakers.cpp b/src/direct_speakers/gain_calculator_direct_speakers.cpp index 0eeb68f..93f2500 100644 --- a/src/direct_speakers/gain_calculator_direct_speakers.cpp +++ b/src/direct_speakers/gain_calculator_direct_speakers.cpp @@ -47,7 +47,7 @@ namespace ear { // throws an exception if the given component is not implemented struct throw_if_not_implemented : public boost::static_visitor { void operator()(const CartesianSpeakerPosition&) const { - throw not_implemented("Cartesian position"); + ear_throw(not_implemented("Cartesian position")); } template @@ -245,9 +245,9 @@ namespace ear { const DirectSpeakersTypeMetadata& metadata, OutputGains& direct, const WarningCB& warning_cb) { if (metadata.audioPackFormatID && metadata.speakerLabels.size() == 0) - throw adm_error( + ear_throw(adm_error( "common definitions audioPackFormatID specified without any " - "speakerLabels as specified in the common definitions file"); + "speakerLabels as specified in the common definitions file")); direct.check_size(_nChannels); boost::apply_visitor(throw_if_not_implemented(), metadata.position); diff --git a/src/dsp/block_convolver_impl.cpp b/src/dsp/block_convolver_impl.cpp index 3dc5a58..167d972 100644 --- a/src/dsp/block_convolver_impl.cpp +++ b/src/dsp/block_convolver_impl.cpp @@ -87,13 +87,13 @@ namespace ear { if (filter) { for (auto &block : filter->blocks) if ((size_t)block.size() != ctx->fd_size) { - throw invalid_argument( + ear_throw(invalid_argument( "Filter block size is not equal to BlockConvolver block " "size; " - "was this created using the same context?"); + "was this created using the same context?")); } if (filter->num_blocks() > num_blocks) - throw invalid_argument("too many blocks in given Filter"); + ear_throw(invalid_argument("too many blocks in given Filter")); } } @@ -143,10 +143,10 @@ namespace ear { void BlockConvolver::process(const Eigen::Ref &in, Eigen::Ref out) { if (in.data() != nullptr && (size_t)in.size() != ctx->block_size) - throw invalid_argument( - "in must be a null pointer or of size block_size"); + ear_throw(invalid_argument( + "in must be a null pointer or of size block_size")); if ((size_t)out.size() != ctx->block_size) - throw invalid_argument("out must be of size block_size"); + ear_throw(invalid_argument("out must be of size block_size")); auto first_half = Eigen::seqN(0, ctx->block_size); auto second_half = Eigen::seqN(ctx->block_size, ctx->block_size); diff --git a/src/handle_exceptions.cpp b/src/handle_exceptions.cpp new file mode 100644 index 0000000..c3737dd --- /dev/null +++ b/src/handle_exceptions.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include "ear/exceptions.hpp" + +#ifdef EAR_NO_EXCEPTIONS +namespace ear { + + [[noreturn]] void handle_exception(const std::exception &e) { + fputs(e.what(), stderr); + fputc('\n', stderr); + std::abort(); + } + +} // namespace ear +#endif + +#ifdef BOOST_NO_EXCEPTIONS +namespace boost { + + BOOST_NORETURN void throw_exception(std::exception const &e) { ear_throw(e); } + + BOOST_NORETURN void throw_exception(std::exception const &e, + boost::source_location const &loc) { + ear_throw(e); + } + +} // namespace boost +#endif diff --git a/src/hoa/gain_calculator_hoa.cpp b/src/hoa/gain_calculator_hoa.cpp index 79e05e1..7101a63 100644 --- a/src/hoa/gain_calculator_hoa.cpp +++ b/src/hoa/gain_calculator_hoa.cpp @@ -24,20 +24,20 @@ namespace ear { OutputGainMat &direct, const WarningCB &warning_cb) { if (metadata.orders.size() != metadata.degrees.size()) - throw invalid_argument("orders and degrees must be the same size"); + ear_throw(invalid_argument("orders and degrees must be the same size")); for (size_t i = 0; i < metadata.orders.size(); i++) { if (metadata.orders[i] < 0) - throw invalid_argument("orders must not be negative"); + ear_throw(invalid_argument("orders must not be negative")); if (std::abs(metadata.degrees[i]) > metadata.orders[i]) - throw invalid_argument( - "magnitude of degree must not be greater than order"); + ear_throw(invalid_argument( + "magnitude of degree must not be greater than order")); } auto norm_it = ADM_norm_types.find(metadata.normalization); if (norm_it == ADM_norm_types.end()) - throw adm_error("unknown normalization type: '" + metadata.normalization + - "'"); + ear_throw(adm_error("unknown normalization type: '" + + metadata.normalization + "'")); if (metadata.screenRef) warning_cb({Warning::Code::HOA_SCREENREF_NOT_IMPLEMENTED, diff --git a/src/hoa/hoa.hpp b/src/hoa/hoa.hpp index 2857a87..176c33a 100644 --- a/src/hoa/hoa.hpp +++ b/src/hoa/hoa.hpp @@ -73,7 +73,7 @@ namespace ear { case NormType::FuMa: return norm_FuMa; default: - throw ear::internal_error("invalid value for norm_type"); + ear_throw(ear::internal_error("invalid value for norm_type")); } } diff --git a/src/libear.mjs b/src/libear.mjs new file mode 100644 index 0000000..1b412e1 --- /dev/null +++ b/src/libear.mjs @@ -0,0 +1,188 @@ +/// JS equivalents of types used in the WASM module +let Gains = Float64Array; +let Samples = Float32Array; +let size_t = Uint32Array; + +/// library wrapper, create with from_buffer or from_stream with a buffer or stream of libear.wasm +export class EARLibrary { + constructor(module) { + this.module = module; + this.exp = module.instance.exports; + + this.text_encoder = new TextEncoder(); + + this.exp.ear_init(); + } + + static async from_buffer(wasm_buffer) { + const import_object = EARLibrary.#get_imports(); + + let module = await WebAssembly.instantiate(wasm_buffer, import_object); + + return new EARLibrary(module); + } + + static async from_stream(wasm_stream) { + const import_object = EARLibrary.#get_imports(); + + let module = await WebAssembly.instantiateStreaming( + wasm_stream, + import_object, + ); + + return new EARLibrary(module); + } + + /// get a Layout object for a BS.2051 layout with the given name + get_layout(name) { + return this.#with_str(name, (name_ptr) => { + let layout_ptr = this.exp.ear_layout_get(name_ptr); + if (layout_ptr == 0) throw RangeError("unknown layout: " + name); + return new Layout(this, layout_ptr); + }); + } + + /// get a decorrelation filter for a given channel index in a Layout + design_decorrelator(layout, channel_idx) { + let length_ptr = this.exp.malloc(size_t.BYTES_PER_ELEMENT); + let dec_ptr = this.exp.ear_design_decorrelator( + layout.ptr, + channel_idx, + length_ptr, + ); + + let length_arr = new size_t(this.exp.memory.buffer, length_ptr, 1); + let length = length_arr[0]; + + let dec_arr = new Samples(this.exp.memory.buffer, dec_ptr, length); + let dec_arr_copy = dec_arr.slice(); + + this.exp.free(dec_ptr); + this.exp.free(length_ptr); + + return dec_arr_copy; + } + + /// get the delay in samples to compensate for the decorrelation filters + decorrelator_compensation_delay() { + return this.exp.ear_decorrelator_compensation_delay(); + } + + static #get_imports() { + return { + env: { + ear_handle_exception(arg) { + throw arg; + }, + ear_handle_warning(arg) { + console.log(arg); + }, + }, + }; + } + + /// call cb with a pointer to the string s in memory, encoded as a c string, + /// and free it when cb returns + #with_str(s, cb) { + let encoded = this.text_encoder.encode(s); + let ptr = this.exp.malloc(encoded.length + 1); + let arr = new Uint8Array(this.exp.memory.buffer, ptr); + arr.set(encoded); + arr[encoded.length] = 0; + + try { + return cb(ptr); + } finally { + this.exp.free(ptr); + } + } +} + +export class Layout { + /// construct with a reference to an EARLibrary and a pointer -- use EARLibrary.get_layout + constructor(lib, ptr) { + this.lib = lib; + this.ptr = ptr; + } + + num_channels() { + return this.lib.exp.ear_layout_num_channels(this.ptr); + } +} + +export class ObjectsTypeMetadata { + /// construct with a reference to an EARLibrary + constructor(lib) { + this.lib = lib; + this.ptr = this.lib.exp.ear_objects_type_metadata_new(); + } + + set_polar_position(azimuth, elevation, distance) { + this.lib.exp.ear_objects_type_metadata_set_polar_position( + this.ptr, + azimuth, + elevation, + distance, + ); + } + + set_gain(gain) { + this.lib.exp.ear_objects_type_metadata_set_gain(this.ptr, gain); + } + + set_extent(width, height, depth) { + this.lib.exp.ear_objects_type_metadata_set_extent( + this.ptr, + width, + height, + depth, + ); + } + + set_diffuse(diffuse) { + this.lib.exp.ear_objects_type_metadata_set_diffuse(this.ptr, diffuse); + } +} + +export class GainCalculatorObjects { + /// construct with a reference to an EARLibrary, and a Layout (returned from + /// get_layout) + constructor(lib, layout) { + this.lib = lib; + + this.ptr = this.lib.exp.ear_gain_calculator_objects_new(layout.ptr); + + this.num_channels = layout.num_channels(); + this.direct = this.lib.exp.malloc( + this.num_channels * Gains.BYTES_PER_ELEMENT, + ); + this.diffuse = this.lib.exp.malloc( + this.num_channels * Gains.BYTES_PER_ELEMENT, + ); + } + + /// calculate gains for a given ObjectsTypeMetadata; returns an object with + //"direct" and "diffuse" Gains + calculate(otm) { + this.lib.exp.ear_gain_calculator_objects_calc_gains( + this.ptr, + otm.ptr, + this.num_channels, + this.direct, + this.diffuse, + ); + + let direct_arr = new Gains( + this.lib.exp.memory.buffer, + this.direct, + this.num_channels, + ); + let diffuse_arr = new Gains( + this.lib.exp.memory.buffer, + this.diffuse, + this.num_channels, + ); + + return { direct: direct_arr.slice(), diffuse: diffuse_arr.slice() }; + } +} diff --git a/src/object_based/gain_calculator_objects.cpp b/src/object_based/gain_calculator_objects.cpp index d551cac..05993b0 100644 --- a/src/object_based/gain_calculator_objects.cpp +++ b/src/object_based/gain_calculator_objects.cpp @@ -11,13 +11,15 @@ namespace ear { // throws an exception if the given component is not implemented struct throw_if_not_implemented : public boost::static_visitor { void operator()(const CartesianPosition&) const { - throw not_implemented("cartesian"); + ear_throw(not_implemented("cartesian")); } void operator()(const PolarObjectDivergence& divergence) const { - if (divergence.divergence != 0.0) throw not_implemented("divergence"); + if (divergence.divergence != 0.0) + ear_throw(not_implemented("divergence")); } void operator()(const CartesianObjectDivergence& divergence) const { - if (divergence.divergence != 0.0) throw not_implemented("divergence"); + if (divergence.divergence != 0.0) + ear_throw(not_implemented("divergence")); } template void operator()(const T&) const {} @@ -34,13 +36,13 @@ namespace ear { OutputGains& direct, OutputGains& diffuse, const WarningCB&) { - if (metadata.cartesian) throw not_implemented("cartesian"); + if (metadata.cartesian) ear_throw(not_implemented("cartesian")); boost::apply_visitor(throw_if_not_implemented(), metadata.position); boost::apply_visitor(throw_if_not_implemented(), metadata.objectDivergence); - if (metadata.channelLock.flag) throw not_implemented("channelLock"); + if (metadata.channelLock.flag) ear_throw(not_implemented("channelLock")); if (metadata.zoneExclusion.zones.size()) - throw not_implemented("zoneExclusion"); - if (metadata.screenRef) throw not_implemented("screenRef"); + ear_throw(not_implemented("zoneExclusion")); + if (metadata.screenRef) ear_throw(not_implemented("screenRef")); Eigen::Vector3d position = toCartesianVector3d(boost::get(metadata.position)); diff --git a/src/wasm_api.cpp b/src/wasm_api.cpp new file mode 100644 index 0000000..d66090a --- /dev/null +++ b/src/wasm_api.cpp @@ -0,0 +1,53 @@ +#include "wasm_api.hpp" +#include +#include +#include +#include "ear/exceptions.hpp" +#include "ear/warnings.hpp" + +extern "C" { +// run static constructors once -- the compiler detects this and doesn't run +// them inside every function as usual +extern void __wasm_call_ctors(); +void WASM_EXPORT(ear_init)() { __wasm_call_ctors(); } + +// override things in the system libraries to avoid having to implement WASI +// imports just to print assertions which should never happen anyway +// +// this is bad and presumably depends on link order + +// used in libcxxabi +[[noreturn]] void abort_message(const char *format, ...) { + ear_handle_exception(format); +} + +// used by assert in wasi-libc +[[noreturn]] void __assert_fail(const char *expr, const char *file, int line, + const char *func) { + ear_handle_exception(expr); +} +} + +namespace ear { + + [[noreturn]] void handle_exception(const std::exception &e) { + ear_handle_exception(e.what()); + } + + void wasm_warning_cb(const ear::Warning &warning) { + ear_handle_warning(warning.message.c_str()); + } +} // namespace ear + +#ifdef BOOST_NO_EXCEPTIONS +namespace boost { + + BOOST_NORETURN void throw_exception(std::exception const &e) { ear_throw(e); } + + BOOST_NORETURN void throw_exception(std::exception const &e, + boost::source_location const &loc) { + ear_throw(e); + } + +} // namespace boost +#endif diff --git a/src/wasm_api.hpp b/src/wasm_api.hpp new file mode 100644 index 0000000..f97e922 --- /dev/null +++ b/src/wasm_api.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "ear/warnings.hpp" + +#define WASM_EXPORT(f) __attribute__((export_name(#f))) f +#define WASM_IMPORT(f) __attribute__((import_name(#f))) f + +extern "C" { + +/// call to initialise the library (calls static constructors) +void WASM_EXPORT(ear_init)(); + +/// implement in JS to handle exceptions; must call abort or throw an exception +[[noreturn]] extern void WASM_IMPORT(ear_handle_exception)(const char *); +/// implement in JS to handle warnings +extern void WASM_IMPORT(ear_handle_warning)(const char *); +} + +namespace ear { + /// c++ warning callback which calls ear_handle_warning + /// + /// this is used because passing function pointers in wasm is tricky, and not + /// really necessary as calls are always in the same thread + void wasm_warning_cb(const ear::Warning &warning); +} // namespace ear diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 942f83d..81b0072 100755 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ function(add_ear_test name) ear catch2 Eigen3::Eigen + ear_handle_exceptions ) target_include_directories(${name} PRIVATE ${PROJECT_SOURCE_DIR}/submodules @@ -44,3 +45,9 @@ add_ear_test("gain_calculator_hoa_tests") add_ear_test("gain_calculator_objects_tests") add_ear_test("gain_interpolator_tests") add_ear_test("variable_block_size_tests") + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32|wasm64") + add_test( + NAME libear_js + COMMAND node ${CMAKE_CURRENT_SOURCE_DIR}/test_libear.mjs -- $) +endif() diff --git a/tests/bs2051_tests.cpp b/tests/bs2051_tests.cpp index 0d47da5..e8913fb 100644 --- a/tests/bs2051_tests.cpp +++ b/tests/bs2051_tests.cpp @@ -23,7 +23,9 @@ TEST_CASE("layout") { PolarPosition{-30.0, 0.0, 1.0}); } +#ifndef EAR_NO_EXCEPTIONS TEST_CASE("unkown_layout") { REQUIRE_THROWS(getLayout("wat")); } +#endif TEST_CASE("all_positions_in_range") { std::vector errors; diff --git a/tests/gain_calculator_direct_speakers_tests.cpp b/tests/gain_calculator_direct_speakers_tests.cpp index e1712e5..dd3e8f9 100644 --- a/tests/gain_calculator_direct_speakers_tests.cpp +++ b/tests/gain_calculator_direct_speakers_tests.cpp @@ -223,6 +223,8 @@ TEST_CASE("test_dist_bounds_polar") { REQUIRE_VECTOR_APPROX(actual, directPv(layout, "T+000")); } +#ifndef EAR_NO_EXCEPTIONS + TEST_CASE("test_dist_bounds_cart", "[!shouldfail]") { Layout layout = getLayout("9+10+3"); GainCalculatorDirectSpeakers p(layout); @@ -277,6 +279,8 @@ TEST_CASE("test_dist_bounds_cart", "[!shouldfail]") { REQUIRE_VECTOR_APPROX(actual, directPv(layout, "M+030")); } +#endif + TEST_CASE("mapping") { auto layout = getLayout("4+5+0").withoutLfe(); GainCalculatorDirectSpeakers p(layout); @@ -338,6 +342,8 @@ TEST_CASE("mapping_per_input") { } } +#ifndef EAR_NO_EXCEPTIONS + TEST_CASE("not implemented") { auto layout = getLayout("4+7+0").withoutLfe(); GainCalculatorDirectSpeakers p(layout); @@ -378,6 +384,8 @@ TEST_CASE("adm errors") { } } +#endif + TEST_CASE("warnings") { auto layout = getLayout("4+7+0").withoutLfe(); GainCalculatorDirectSpeakers p(layout); diff --git a/tests/gain_calculator_hoa_tests.cpp b/tests/gain_calculator_hoa_tests.cpp index 037ebd4..5061b60 100644 --- a/tests/gain_calculator_hoa_tests.cpp +++ b/tests/gain_calculator_hoa_tests.cpp @@ -36,6 +36,8 @@ TEST_CASE("warnings") { } } +#ifndef EAR_NO_EXCEPTIONS + TEST_CASE("exceptions") { Layout layout = getLayout("0+5+0"); GainCalculatorHOA gc(layout); @@ -71,3 +73,5 @@ TEST_CASE("exceptions") { REQUIRE_THROWS_AS(gc.calculate(tm, gains), invalid_argument); } } + +#endif diff --git a/tests/gain_calculator_objects_tests.cpp b/tests/gain_calculator_objects_tests.cpp index 70afd50..1bb825d 100644 --- a/tests/gain_calculator_objects_tests.cpp +++ b/tests/gain_calculator_objects_tests.cpp @@ -131,6 +131,8 @@ TEST_CASE("gain_value") { REQUIRE(gainMap.diffuse.size() == 0); } +#ifndef EAR_NO_EXCEPTIONS + TEST_CASE("not implemented") { auto layout = getLayout("4+7+0").withoutLfe(); GainCalculatorObjectsTester gainCalc(layout); @@ -167,3 +169,5 @@ TEST_CASE("not implemented") { REQUIRE_THROWS_AS(gainCalc.run(otm), not_implemented); } } + +#endif diff --git a/tests/point_source_panner_tests.cpp b/tests/point_source_panner_tests.cpp index ab3efed..e1028c1 100644 --- a/tests/point_source_panner_tests.cpp +++ b/tests/point_source_panner_tests.cpp @@ -302,8 +302,10 @@ TEST_CASE("test_polar_point_source_panner") { } // should fail if not given enough channels +#ifndef EAR_NO_EXCEPTIONS REQUIRE_THROWS(PolarPointSourcePanner( std::move(regions_1), static_cast(positions.rows() - 1))); +#endif std::vector> regions_2; for (const auto& outputChannels : outputChannelsVec) { @@ -518,6 +520,8 @@ TEST_CASE("configure_full_polar_panner") { }; }; +#ifndef EAR_NO_EXCEPTIONS + TEST_CASE("screen_loudspeaker_positions") { auto layout = getLayout("4+9+0").withoutLfe(); @@ -549,6 +553,8 @@ TEST_CASE("screen_loudspeaker_positions") { } } +#endif + TEST_CASE("hull") { for (auto& layoutFull : loadLayouts()) { if (layoutFull.name() == "0+2+0") continue; diff --git a/tests/test_libear.mjs b/tests/test_libear.mjs new file mode 100644 index 0000000..9023193 --- /dev/null +++ b/tests/test_libear.mjs @@ -0,0 +1,73 @@ +let [wasm_file] = process.argv.slice(process.argv.indexOf("--") + 1); + +import { + EARLibrary, + GainCalculatorObjects, + ObjectsTypeMetadata, +} from "../src/libear.mjs"; +import fs from "node:fs"; +import { test } from "node:test"; +import assert from "node:assert"; + +/// check that two arrays are the same size and the same within a small tolerance +function assertArraysClose(a, b) { + assert.strictEqual(a.length, b.length); + + for (let i = 0; i < a.length; i++) { + assert.ok(Math.abs(a[i] - b[i]) < 1e-6); + } +} + +function assertClose(a, b) { + assert.ok(Math.abs(a - b) < 1e-6); +} + +test("load library then", async (t) => { + const wasm_buf = fs.readFileSync(wasm_file); + let lib = await EARLibrary.from_buffer(wasm_buf); + + await test("layout", (t) => { + let layout = lib.get_layout("0+5+0"); + assert.equal(layout.num_channels(), 6); + }); + + await test("get_layout throw", (t) => { + assert.throws(() => lib.get_layout("0+5+1"), { + name: "RangeError", + message: "unknown layout: 0+5+1", + }); + }); + + await test("decorrelator", (t) => { + let layout = lib.get_layout("0+5+0"); + + assert.equal(lib.decorrelator_compensation_delay(), 255); + + let dec = lib.design_decorrelator(layout, 1); + assert.equal(dec.length, 512); + + assertClose(dec[0], 0.06860811); + assertClose(dec[256], 0.111747); + assertClose(dec[511], -0.01473256); + }); + + await test("calculate objects gains", (t) => { + let layout = lib.get_layout("0+5+0"); + let gain_calc = new GainCalculatorObjects(lib, layout); + + let otm = new ObjectsTypeMetadata(lib); + otm.set_polar_position(15.0, 0.0, 1.0); + otm.set_gain(0.5); + otm.set_diffuse(0.25); + + let gains = gain_calc.calculate(otm); + assertArraysClose( + gains["direct"], + [0.3061862178478973, 0, 0.3061862178478973, 0, 0, 0], + ); + assertArraysClose( + gains["diffuse"], + [0.1767766952966369, 0, 0.1767766952966369, 0, 0, 0], + ); + }); +});