|
| 1 | +/* |
| 2 | + * Python bindings for libsamplerate using nanobind |
| 3 | + * Copyright (C) 2025 LedFx Team |
| 4 | + * |
| 5 | + * Permission is hereby granted, free of charge, to any person obtaining a copy |
| 6 | + * of this software and associated documentation files (the "Software"), to deal |
| 7 | + * in the Software without restriction, including without limitation the rights |
| 8 | + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 9 | + * copies of the Software, and to permit persons to whom the Software is |
| 10 | + * furnished to do so, subject to the following conditions: |
| 11 | + * |
| 12 | + * The above copyright notice and this permission notice shall be included in |
| 13 | + * all copies or substantial portions of the Software. |
| 14 | + * |
| 15 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 16 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 17 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 18 | + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 19 | + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 21 | + * SOFTWARE. |
| 22 | + * |
| 23 | + * You should have received a copy of the MIT License along with this program. |
| 24 | + * If not, see <https://opensource.org/licenses/MIT>. |
| 25 | + */ |
| 26 | + |
| 27 | +#include <nanobind/nanobind.h> |
| 28 | +#include <nanobind/ndarray.h> |
| 29 | +#include <nanobind/stl/function.h> |
| 30 | +#include <nanobind/stl/string.h> |
| 31 | +#include <samplerate.h> |
| 32 | + |
| 33 | +#include <cmath> |
| 34 | +#include <iostream> |
| 35 | +#include <string> |
| 36 | +#include <typeinfo> |
| 37 | +#include <vector> |
| 38 | + |
| 39 | +#ifndef VERSION_INFO |
| 40 | +#define VERSION_INFO "nightly" |
| 41 | +#endif |
| 42 | + |
| 43 | +// This value was empirically and somewhat arbitrarily chosen; increase it for further safety. |
| 44 | +#define END_OF_INPUT_EXTRA_OUTPUT_FRAMES 10000 |
| 45 | + |
| 46 | +namespace nb = nanobind; |
| 47 | +using namespace nb::literals; |
| 48 | + |
| 49 | +// Type aliases for nanobind arrays |
| 50 | +using nb_array_f32 = nb::ndarray<nb::numpy, float, nb::c_contig>; |
| 51 | +using callback_t = std::function<nb_array_f32(void)>; |
| 52 | + |
| 53 | +namespace samplerate { |
| 54 | + |
| 55 | +enum class ConverterType { |
| 56 | + sinc_best, |
| 57 | + sinc_medium, |
| 58 | + sinc_fastest, |
| 59 | + zero_order_hold, |
| 60 | + linear |
| 61 | +}; |
| 62 | + |
| 63 | +class ResamplingException : public std::exception { |
| 64 | + public: |
| 65 | + explicit ResamplingException(int err_num) : message{src_strerror(err_num)} {} |
| 66 | + const char *what() const noexcept override { return message.c_str(); } |
| 67 | + |
| 68 | + private: |
| 69 | + std::string message = ""; |
| 70 | +}; |
| 71 | + |
| 72 | +int get_converter_type(const nb::object &obj) { |
| 73 | + if (nb::isinstance<nb::str>(obj)) { |
| 74 | + std::string s = nb::cast<std::string>(obj); |
| 75 | + if (s.compare("sinc_best") == 0) { |
| 76 | + return 0; |
| 77 | + } else if (s.compare("sinc_medium") == 0) { |
| 78 | + return 1; |
| 79 | + } else if (s.compare("sinc_fastest") == 0) { |
| 80 | + return 2; |
| 81 | + } else if (s.compare("zero_order_hold") == 0) { |
| 82 | + return 3; |
| 83 | + } else if (s.compare("linear") == 0) { |
| 84 | + return 4; |
| 85 | + } |
| 86 | + } else if (nb::isinstance<nb::int_>(obj)) { |
| 87 | + return nb::cast<int>(obj); |
| 88 | + } else if (nb::isinstance<ConverterType>(obj)) { |
| 89 | + nb::int_ c = obj.attr("value"); |
| 90 | + return nb::cast<int>(c); |
| 91 | + } |
| 92 | + |
| 93 | + throw std::domain_error("Unsupported converter type"); |
| 94 | + return -1; |
| 95 | +} |
| 96 | + |
| 97 | +void error_handler(int errnum) { |
| 98 | + if (errnum > 0 && errnum < 24) { |
| 99 | + throw ResamplingException(errnum); |
| 100 | + } else if (errnum != 0) { // the zero case is excluded as it is not an error |
| 101 | + // this will throw a segmentation fault if we call src_strerror here |
| 102 | + // also, these should never happen |
| 103 | + throw std::runtime_error("libsamplerate raised an unknown error code"); |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +} // namespace samplerate |
| 108 | + |
| 109 | +namespace sr = samplerate; |
| 110 | + |
| 111 | +NB_MODULE(samplerate, m) { |
| 112 | + m.doc() = "A simple python wrapper library around libsamplerate using nanobind"; |
| 113 | + m.attr("__version__") = VERSION_INFO; |
| 114 | + m.attr("__libsamplerate_version__") = LIBSAMPLERATE_VERSION; |
| 115 | + |
| 116 | + auto m_exceptions = m.def_submodule( |
| 117 | + "exceptions", "Sub-module containing sampling exceptions"); |
| 118 | + auto m_converters = m.def_submodule( |
| 119 | + "converters", "Sub-module containing the samplerate converters"); |
| 120 | + auto m_internals = m.def_submodule("_internals", "Internal helper functions"); |
| 121 | + |
| 122 | + // give access to this function for testing |
| 123 | + m_internals.def( |
| 124 | + "get_converter_type", &sr::get_converter_type, |
| 125 | + "Convert python object to integer of converter type or raise an error " |
| 126 | + "if illegal"); |
| 127 | + |
| 128 | + m_internals.def( |
| 129 | + "error_handler", &sr::error_handler, |
| 130 | + "A function to translate libsamplerate error codes into exceptions"); |
| 131 | + |
| 132 | + nb::register_exception_translator([](const std::exception_ptr &p, void *payload) { |
| 133 | + try { |
| 134 | + std::rethrow_exception(p); |
| 135 | + } catch (const sr::ResamplingException &e) { |
| 136 | + PyErr_SetString(PyExc_RuntimeError, e.what()); |
| 137 | + } |
| 138 | + }); |
| 139 | + |
| 140 | + // Create ResamplingError as an alias to the Python RuntimeError |
| 141 | + m_exceptions.attr("ResamplingError") = nb::handle(PyExc_RuntimeError); |
| 142 | + |
| 143 | + nb::enum_<sr::ConverterType>(m_converters, "ConverterType", R"mydelimiter( |
| 144 | + Enum of samplerate converter types. |
| 145 | +
|
| 146 | + Pass any of the members, or their string or value representation, as |
| 147 | + ``converter_type`` in the resamplers. |
| 148 | + )mydelimiter") |
| 149 | + .value("sinc_best", sr::ConverterType::sinc_best) |
| 150 | + .value("sinc_medium", sr::ConverterType::sinc_medium) |
| 151 | + .value("sinc_fastest", sr::ConverterType::sinc_fastest) |
| 152 | + .value("zero_order_hold", sr::ConverterType::zero_order_hold) |
| 153 | + .value("linear", sr::ConverterType::linear) |
| 154 | + .export_values(); |
| 155 | + |
| 156 | + // Convenience imports |
| 157 | + m.attr("ResamplingError") = m_exceptions.attr("ResamplingError"); |
| 158 | + m.attr("ConverterType") = m_converters.attr("ConverterType"); |
| 159 | +} |
0 commit comments