From e32549e8eba28bc1a2c9dc7eb7cd11942a031306 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 20 Oct 2024 10:33:36 +0100 Subject: [PATCH 01/33] Initial integration of miniaudio --- .../signalflow/node/io/output/miniaudio.h | 42 ++++ source/include/signalflow/signalflow.h | 3 +- source/src/CMakeLists.txt | 3 +- source/src/core/graph.cpp | 6 +- source/src/node/io/input/soundio.cpp | 4 +- source/src/node/io/output/miniaudio.cpp | 208 ++++++++++++++++++ 6 files changed, 259 insertions(+), 7 deletions(-) create mode 100644 source/include/signalflow/node/io/output/miniaudio.h create mode 100644 source/src/node/io/output/miniaudio.cpp diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h new file mode 100644 index 00000000..31d04862 --- /dev/null +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -0,0 +1,42 @@ +#pragma once + +#define AudioOut AudioOut_MiniAudio + +#include +#include + +#include "abstract.h" + +#include "miniaudio-library.h" +#include "signalflow/core/graph.h" +#include "signalflow/node/node.h" + +namespace signalflow +{ + +class AudioOut_MiniAudio : public AudioOut_Abstract +{ +public: + AudioOut_MiniAudio(const std::string &backend_name = "", + const std::string &device_name = "", + unsigned int sample_rate = 0, + unsigned int buffer_size = 0); + + virtual int init() override; + virtual int start() override; + virtual int stop() override; + virtual int destroy() override; + + std::list get_output_device_names(); + std::list get_output_backend_names(); + int get_default_output_device_index(); + +private: + std::string backend_name; + std::string device_name; + ma_device device; +}; + +REGISTER(AudioOut_MiniAudio, "audioout-miniaudio") + +} // namespace signalflow diff --git a/source/include/signalflow/signalflow.h b/source/include/signalflow/signalflow.h index 59937362..8c1f4c53 100644 --- a/source/include/signalflow/signalflow.h +++ b/source/include/signalflow/signalflow.h @@ -68,7 +68,8 @@ #include #include #include -#include +#include +//#include #include #include diff --git a/source/src/CMakeLists.txt b/source/src/CMakeLists.txt index 7576c3d9..55813326 100644 --- a/source/src/CMakeLists.txt +++ b/source/src/CMakeLists.txt @@ -86,7 +86,8 @@ set(SRC ${SRC} ${CMAKE_CURRENT_SOURCE_DIR}/node/io/input/soundio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/abstract.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/dummy.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/soundio.cpp +# ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/soundio.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/miniaudio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/operators/add.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/operators/amplitude-to-decibels.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/operators/bus.cpp diff --git a/source/src/core/graph.cpp b/source/src/core/graph.cpp index 6b2c941d..ca93ec80 100644 --- a/source/src/core/graph.cpp +++ b/source/src/core/graph.cpp @@ -4,7 +4,7 @@ #include "signalflow/node/io/output/abstract.h" #include "signalflow/node/io/output/dummy.h" #include "signalflow/node/io/output/ios.h" -#include "signalflow/node/io/output/soundio.h" +#include "signalflow/node/io/output/miniaudio.h" #include "signalflow/node/node.h" #include "signalflow/node/oscillators/constant.h" #include "signalflow/patch/patch.h" @@ -590,13 +590,13 @@ std::list AudioGraph::get_outputs() std::list AudioGraph::get_output_device_names() { - AudioOut_SoundIO *output = (AudioOut_SoundIO *) (this->output.get()); + AudioOut_MiniAudio *output = (AudioOut_MiniAudio *) (this->output.get()); return output->get_output_device_names(); } std::list AudioGraph::get_output_backend_names() { - AudioOut_SoundIO *output = (AudioOut_SoundIO *) (this->output.get()); + AudioOut_MiniAudio *output = (AudioOut_MiniAudio *) (this->output.get()); return output->get_output_backend_names(); } diff --git a/source/src/node/io/input/soundio.cpp b/source/src/node/io/input/soundio.cpp index fa529c5d..030eb528 100644 --- a/source/src/node/io/input/soundio.cpp +++ b/source/src/node/io/input/soundio.cpp @@ -5,7 +5,7 @@ #define SIGNALFLOW_AUDIO_IN_DEFAULT_BUFFER_SIZE 1024 #include "signalflow/core/graph.h" -#include "signalflow/node/io/output/soundio.h" +#include "signalflow/node/io/output/miniaudio.h" #include #include @@ -92,7 +92,7 @@ int AudioIn_SoundIO::init() { int err; - this->soundio = ((AudioOut_SoundIO *) this->graph->get_output().get())->soundio; + this->soundio = NULL; // ((AudioOut_MiniAudio *) this->graph->get_output().get())->soundio; if (!this->soundio) throw audio_io_exception("libsoundio init error: No output node found in graph (initialising input before output?)"); diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp new file mode 100644 index 00000000..a1324921 --- /dev/null +++ b/source/src/node/io/output/miniaudio.cpp @@ -0,0 +1,208 @@ +#include "signalflow/node/io/output/miniaudio.h" + +#define MINIAUDIO_IMPLEMENTATION +#include "signalflow/node/io/output/miniaudio-library.h" + +#ifdef HAVE_SOUNDIO + +#include "signalflow/core/graph.h" + +#include +#include +#include +#include +#include +#include + +static bool is_processing = false; + +namespace signalflow +{ + +extern AudioGraph *shared_graph; + +void data_callback(ma_device *pDevice, + void *pOutput, + const void *pInput, + ma_uint32 frame_count) +{ + is_processing = true; + int channel_count = pDevice->playback.channels; + + /*-----------------------------------------------------------------------* + * Return if the shared_graph hasn't been initialized yet. + * (The libsoundio Pulse Audio driver calls the write_callback once + * on initialization, so this may happen legitimately.) + *-----------------------------------------------------------------------*/ + if (!shared_graph || !shared_graph->get_output()) + { + return; + } + + float *output_pointer = (float *) pOutput; + + try + { + shared_graph->render(frame_count); + } + catch (const std::exception &e) + { + std::cerr << "Exception in AudioGraph: " << e.what() << std::endl; + exit(1); + } + + NodeRef output = shared_graph->get_output(); + for (int frame = 0; frame < frame_count; frame++) + { + for (int channel = 0; channel < channel_count; channel += 1) + { + output_pointer[channel_count * frame + channel] = output->out[channel][frame]; + } + } + + is_processing = false; +} + +AudioOut_MiniAudio::AudioOut_MiniAudio(const std::string &backend_name, + const std::string &device_name, + unsigned int sample_rate, + unsigned int buffer_size) + : AudioOut_Abstract() +{ + this->backend_name = backend_name; + this->device_name = device_name; + this->sample_rate = sample_rate; + this->buffer_size = buffer_size; + this->name = "audioout-soundio"; + + this->init(); +} + +int AudioOut_MiniAudio::init() +{ + ma_device_config config = ma_device_config_init(ma_device_type_playback); + + ma_device_info *playback_devices; + ma_uint32 playback_device_count; + ma_device_info *capture_devices; + ma_uint32 capture_device_count; + ma_context context; + ma_result rv; + + if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) + { + // Error. + printf("Error initialising context\n"); + return -1; + } + + rv = ma_context_get_devices(&context, + &playback_devices, + &playback_device_count, + &capture_devices, + &capture_device_count); + int selected_device_index = -1; + if (!this->device_name.empty()) + { + for (int i = 0; i < playback_device_count; i++) + { + printf(" - (%d) %s\n", i, playback_devices[i].name); + if (strcmp(playback_devices[i].name, device_name.c_str()) == 0) + { + selected_device_index = i; + } + } + if (selected_device_index == -1) + { + printf("Couldn't find device\n"); + return -1; + } + + config.playback.pDeviceID = &playback_devices[selected_device_index].id; + } + + // Set to ma_format_unknown to use the device's native format. + config.playback.format = ma_format_f32; + + // Set to 0 to use the device's native channel count / buffer size. + config.playback.channels = 0; + config.periodSizeInFrames = buffer_size; + + // Note that the underlying connection always uses the device's native sample rate. + // Setting values other than zero instantiates miniaudio's internal resampler. + config.sampleRate = sample_rate; + config.dataCallback = data_callback; + + // Buffer blocks into a fixed number of frames + config.noFixedSizedCallback = 1; + + rv = ma_device_init(NULL, &config, &device); + if (rv != MA_SUCCESS) + { + printf("Error initialising device\n"); + return -1; + } + + this->sample_rate = device.playback.internalSampleRate; + this->set_channels(device.playback.internalChannels, 0); + + std::string s = device.playback.internalChannels == 1 ? "" : "s"; + + std::cerr << "[MINIAUDIO] Output device: " << std::string(device.playback.name) << " (" << device.playback.internalSampleRate << "Hz, " + << "buffer size " << device.playback.internalPeriodSizeInFrames << " samples, " << device.playback.internalChannels << " channel" << s << ")" + << std::endl; + + // do we need to set num_output channels to allocate the right number of output buffers? + this->set_channels(device.playback.internalChannels, 0); + + return 0; +} + +int AudioOut_MiniAudio::start() +{ + ma_result rv = ma_device_start(&device); + if (rv != MA_SUCCESS) + { + printf("Error starting device\n"); + return -1; + } + this->set_state(SIGNALFLOW_NODE_STATE_ACTIVE); + return 0; +} + +int AudioOut_MiniAudio::stop() +{ + // TODO + this->set_state(SIGNALFLOW_NODE_STATE_STOPPED); + return 0; +} + +int AudioOut_MiniAudio::destroy() +{ + while (is_processing) + { + } + + ma_device_uninit(&device); + + return 0; +} + +std::list AudioOut_MiniAudio::get_output_device_names() +{ + return {}; +} + +int AudioOut_MiniAudio::get_default_output_device_index() +{ + return 0; +} + +std::list AudioOut_MiniAudio::get_output_backend_names() +{ + return {}; +} + +} // namespace signalflow + +#endif From 348179d3ca8952a6cb5b731a5cc2ea171103959b Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 20 Oct 2024 13:15:22 +0100 Subject: [PATCH 02/33] More work on miniaudio: List device/backend names --- CMakeLists.txt | 24 ------- .../signalflow/node/io/output/miniaudio.h | 1 + source/src/CMakeLists.txt | 2 - source/src/node/io/output/miniaudio.cpp | 70 +++++++++++++++---- source/src/python/nodes.cpp | 3 - 5 files changed, 59 insertions(+), 41 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7fe20629..de6bdf03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,30 +129,6 @@ add_library(signalflow SHARED ${SRC}) #------------------------------------------------------------------------------- add_compile_definitions(SIGNALFLOW_VERSION="${SIGNALFLOW_VERSION}") -#------------------------------------------------------------------------------- -# Dependencies -#------------------------------------------------------------------------------- - -set(SOUNDIO_BUILD_DIR "" CACHE PATH "Path to built SoundIO library (will use find_library if blank)") - -if (SOUNDIO_BUILD_DIR) - set(SOUNDIO_INCLUDE_DIR "${SOUNDIO_BUILD_DIR}/.." CACHE PATH "Path to SoundIO include directory (ignored if SOUNDIO_BUILD_DIR is blank") - add_definitions(-DHAVE_SOUNDIO) - target_link_libraries(signalflow "${SOUNDIO_BUILD_DIR}/x64-Debug/soundio.lib") - include_directories(signalflow "${SOUNDIO_BUILD_DIR}/$/") - include_directories(signalflow "${SOUNDIO_INCLUDE_DIR}/") -else() - find_library(SOUNDIO soundio) - if (SOUNDIO) - message("Found libsoundio") - add_definitions(-DHAVE_SOUNDIO) - target_link_libraries(signalflow ${SOUNDIO}) - else() - message(SEND_ERROR "Couldn't find libsoundio") - endif() -endif() - - set(SNDFILE_BUILD_DIR "" CACHE PATH "Path to build sndfile library (will use find_library if blank)") if (SNDFILE_BUILD_DIR) diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index 31d04862..4731af36 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -34,6 +34,7 @@ class AudioOut_MiniAudio : public AudioOut_Abstract private: std::string backend_name; std::string device_name; + ma_context context; ma_device device; }; diff --git a/source/src/CMakeLists.txt b/source/src/CMakeLists.txt index 55813326..b14d86c9 100644 --- a/source/src/CMakeLists.txt +++ b/source/src/CMakeLists.txt @@ -83,10 +83,8 @@ set(SRC ${SRC} ${CMAKE_CURRENT_SOURCE_DIR}/node/processors/fold.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/processors/wetdry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/input/abstract.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/node/io/input/soundio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/abstract.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/dummy.cpp -# ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/soundio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/miniaudio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/operators/add.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/operators/amplitude-to-decibels.cpp diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index a1324921..f33f0f2e 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -3,8 +3,6 @@ #define MINIAUDIO_IMPLEMENTATION #include "signalflow/node/io/output/miniaudio-library.h" -#ifdef HAVE_SOUNDIO - #include "signalflow/core/graph.h" #include @@ -86,7 +84,6 @@ int AudioOut_MiniAudio::init() ma_uint32 playback_device_count; ma_device_info *capture_devices; ma_uint32 capture_device_count; - ma_context context; ma_result rv; if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) @@ -106,16 +103,18 @@ int AudioOut_MiniAudio::init() { for (int i = 0; i < playback_device_count; i++) { - printf(" - (%d) %s\n", i, playback_devices[i].name); - if (strcmp(playback_devices[i].name, device_name.c_str()) == 0) + if (strncmp(playback_devices[i].name, device_name.c_str(), strlen(device_name.c_str())) == 0) { + if (selected_device_index != -1) + { + throw std::runtime_error("More than one audio device found matching name '" + device_name + "'"); + } selected_device_index = i; } } if (selected_device_index == -1) { - printf("Couldn't find device\n"); - return -1; + throw std::runtime_error("No audio device found matching name '" + device_name + "'"); } config.playback.pDeviceID = &playback_devices[selected_device_index].id; @@ -190,19 +189,66 @@ int AudioOut_MiniAudio::destroy() std::list AudioOut_MiniAudio::get_output_device_names() { - return {}; + std::list device_names; + + ma_context context; + ma_result rv; + ma_device_info *playback_devices; + ma_uint32 playback_device_count; + ma_device_info *capture_devices; + ma_uint32 capture_device_count; + + rv = ma_context_init(NULL, 0, NULL, &context); + if (rv != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Failure initialising audio context"); + } + + rv = ma_context_get_devices(&context, + &playback_devices, + &playback_device_count, + &capture_devices, + &capture_device_count); + if (rv != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Failure querying audio devices"); + } + for (int i = 0; i < playback_device_count; i++) + { + device_names.push_back(std::string(playback_devices[i].name)); + } + + return device_names; } int AudioOut_MiniAudio::get_default_output_device_index() { - return 0; + // TODO: Is this even used? + return -1; } std::list AudioOut_MiniAudio::get_output_backend_names() { - return {}; + std::list backend_names; + ma_backend enabled_backends[MA_BACKEND_COUNT]; + size_t enabled_backend_count; + ma_result rv; + + rv = ma_get_enabled_backends(enabled_backends, MA_BACKEND_COUNT, &enabled_backend_count); + if (rv != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Failure querying backend devices"); + } + for (int i = 0; i < enabled_backend_count; i++) + { + std::string backend_name = std::string(ma_get_backend_name(enabled_backends[i])); + if (backend_name != "Custom" && backend_name != "Null") + { + backend_names.push_back(backend_name); + } + } + + return backend_names; } } // namespace signalflow - -#endif diff --git a/source/src/python/nodes.cpp b/source/src/python/nodes.cpp index 4bc0ff07..5b2f4519 100644 --- a/source/src/python/nodes.cpp +++ b/source/src/python/nodes.cpp @@ -5,9 +5,6 @@ void init_python_nodes(py::module &m) /*-------------------------------------------------------------------------------- * Node subclasses *-------------------------------------------------------------------------------*/ - py::class_>(m, "AudioIn", "Audio input") - .def(py::init(), "num_channels"_a = 1); - py::class_>(m, "AudioOut_Abstract", "Abstract audio output"); py::class_>(m, "AudioOut_Dummy", "Dummy audio output for offline processing") From 9e28baac4a2b62f8d2aee1fc2cf617022c9b12c7 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 20 Oct 2024 21:58:43 +0100 Subject: [PATCH 03/33] GitHub Actions: Download miniaudio.h --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e30841eb..ebca1c5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ jobs: # 2024-07-29: Require importlib_metadata for now due to this: # https://github.com/pypa/setuptools/issues/4478 sudo pip3 install -U pytest numpy scipy setuptools>=62.1.0 importlib_metadata + curl https://raw.githubusercontent.com/mackron/miniaudio/master/miniaudio.h -o source/include/signalflow/node/io/output/miniaudio-library.h - name: Configure run: mkdir build && cd build && cmake .. - name: Make From f439ce0fe860a3aedf6e2da8394b76c31adf21e1 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 08:12:10 +0100 Subject: [PATCH 04/33] miniaudio: Add backend selector --- .../signalflow/node/io/output/miniaudio.h | 2 + source/src/node/io/output/miniaudio.cpp | 64 ++++++++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index 4731af36..990caaa1 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -32,6 +32,8 @@ class AudioOut_MiniAudio : public AudioOut_Abstract int get_default_output_device_index(); private: + void init_context(ma_context *context); + std::string backend_name; std::string device_name; ma_context context; diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index f33f0f2e..bb75cd72 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -11,6 +11,7 @@ #include #include #include +#include static bool is_processing = false; @@ -71,11 +72,54 @@ AudioOut_MiniAudio::AudioOut_MiniAudio(const std::string &backend_name, this->device_name = device_name; this->sample_rate = sample_rate; this->buffer_size = buffer_size; - this->name = "audioout-soundio"; + this->name = "audioout-miniaudio"; + printf("backend name: %s\n", backend_name.c_str()); this->init(); } +void AudioOut_MiniAudio::init_context(ma_context *context) +{ + if (!this->backend_name.empty()) + { + std::unordered_map possible_backend_names = { + { "wasapi", ma_backend_wasapi }, + { "dsound", ma_backend_dsound }, + { "ma_backend_winmm", ma_backend_winmm }, + { "coreaudio", ma_backend_coreaudio }, + { "sndio", ma_backend_sndio }, + { "audio4", ma_backend_audio4 }, + { "oss", ma_backend_oss }, + { "pulseaudio", ma_backend_pulseaudio }, + { "alsa", ma_backend_alsa }, + { "jack", ma_backend_jack }, + { "aaudio", ma_backend_aaudio }, + { "opensl", ma_backend_opensl }, + { "webaudio", ma_backend_webaudio }, + { "null", ma_backend_null }, + }; + + if (possible_backend_names.find(this->backend_name) == possible_backend_names.end()) + { + throw std::runtime_error("miniaudio: Backend name not recognised: " + this->backend_name); + } + printf("Initialising context with backend %s\n", this->backend_name.c_str()); + ma_backend backend_name = possible_backend_names[this->backend_name]; + + if (ma_context_init(&backend_name, 1, NULL, context) != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Error initialising context"); + } + } + else + { + if (ma_context_init(NULL, 0, NULL, context) != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Error initialising context"); + } + } +} + int AudioOut_MiniAudio::init() { ma_device_config config = ma_device_config_init(ma_device_type_playback); @@ -86,14 +130,9 @@ int AudioOut_MiniAudio::init() ma_uint32 capture_device_count; ma_result rv; - if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) - { - // Error. - printf("Error initialising context\n"); - return -1; - } + this->init_context(&this->context); - rv = ma_context_get_devices(&context, + rv = ma_context_get_devices(&this->context, &playback_devices, &playback_device_count, &capture_devices, @@ -191,18 +230,13 @@ std::list AudioOut_MiniAudio::get_output_device_names() { std::list device_names; - ma_context context; ma_result rv; ma_device_info *playback_devices; ma_uint32 playback_device_count; ma_device_info *capture_devices; ma_uint32 capture_device_count; - - rv = ma_context_init(NULL, 0, NULL, &context); - if (rv != MA_SUCCESS) - { - throw std::runtime_error("miniaudio: Failure initialising audio context"); - } + ma_context context; + this->init_context(&context); rv = ma_context_get_devices(&context, &playback_devices, From e47a02ee9aa199fff12422cf821b0ffbaff87c31 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 09:36:36 +0100 Subject: [PATCH 05/33] miniaudio: Add AudioIn support --- .../signalflow/node/io/input/miniaudio.h | 35 ++++++ source/include/signalflow/signalflow.h | 6 +- source/src/CMakeLists.txt | 1 + source/src/node/io/input/miniaudio.cpp | 115 ++++++++++++++++++ source/src/node/io/input/soundio.cpp | 2 +- source/src/node/io/output/miniaudio.cpp | 14 +-- source/src/node/io/output/soundio.cpp | 2 +- source/src/python/nodes.cpp | 5 +- 8 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 source/include/signalflow/node/io/input/miniaudio.h create mode 100644 source/src/node/io/input/miniaudio.cpp diff --git a/source/include/signalflow/node/io/input/miniaudio.h b/source/include/signalflow/node/io/input/miniaudio.h new file mode 100644 index 00000000..8ee00902 --- /dev/null +++ b/source/include/signalflow/node/io/input/miniaudio.h @@ -0,0 +1,35 @@ +#pragma once + +#define AudioIn AudioIn_MiniAudio + +#include +#include + +#include "abstract.h" + +#include "../output/miniaudio-library.h" +#include "signalflow/core/graph.h" + +namespace signalflow +{ + +class AudioIn_MiniAudio : public AudioIn_Abstract +{ +public: + AudioIn_MiniAudio(unsigned int num_channels = 1); + virtual ~AudioIn_MiniAudio() override; + virtual int init() override; + virtual int start() override; + virtual int stop() override; + virtual int destroy() override; + virtual void process(Buffer &out, int num_samples) override; + +private: + std::string backend_name; + std::string device_name; + ma_context context; + ma_device device; + unsigned int num_channels; +}; + +} diff --git a/source/include/signalflow/signalflow.h b/source/include/signalflow/signalflow.h index 8c1f4c53..8d3ad2a0 100644 --- a/source/include/signalflow/signalflow.h +++ b/source/include/signalflow/signalflow.h @@ -65,14 +65,12 @@ /*------------------------------------------------------------------------ * I/O *-----------------------------------------------------------------------*/ +#include +#include #include #include #include #include -//#include - -#include -#include /*------------------------------------------------------------------------ * Oscillators diff --git a/source/src/CMakeLists.txt b/source/src/CMakeLists.txt index b14d86c9..36db5f25 100644 --- a/source/src/CMakeLists.txt +++ b/source/src/CMakeLists.txt @@ -83,6 +83,7 @@ set(SRC ${SRC} ${CMAKE_CURRENT_SOURCE_DIR}/node/processors/fold.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/processors/wetdry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/input/abstract.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/node/io/input/miniaudio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/abstract.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/dummy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/node/io/output/miniaudio.cpp diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp new file mode 100644 index 00000000..74dffd7b --- /dev/null +++ b/source/src/node/io/input/miniaudio.cpp @@ -0,0 +1,115 @@ +#include "signalflow/node/io/input/miniaudio.h" + +#include "signalflow/core/graph.h" +#include "signalflow/node/io/output/miniaudio.h" + +#include +#include +#include + +static bool is_processing = false; + +namespace signalflow +{ +extern AudioIn_Abstract *shared_in; + +void read_callback(ma_device *pDevice, + void *pOutput, + const void *pInput, + ma_uint32 frameCount) +{ + is_processing = true; + + AudioIn_MiniAudio *input_node = (AudioIn_MiniAudio *) shared_in; + if (!input_node) + return; + + float *input_samples = (float *) pInput; + + // TODO: the number of channels at the mic input might not be the same as the number of channels of this device + int num_channels = input_node->get_num_output_channels(); + for (int frame = 0; frame < frameCount; frame++) + { + for (int channel = 0; channel < num_channels; channel++) + { + input_node->out[channel][frame] = input_samples[frame * num_channels + channel]; + } + } + + is_processing = false; +} + +AudioIn_MiniAudio::AudioIn_MiniAudio(unsigned int num_channels) + : AudioIn_Abstract() +{ + this->name = "audioin-miniaudio"; + this->num_channels = num_channels; + this->init(); +} + +AudioIn_MiniAudio::~AudioIn_MiniAudio() +{ + // TODO: call superclass destructor to set shared_in to null + this->destroy(); +} + +int AudioIn_MiniAudio::init() +{ + ma_device_config config = ma_device_config_init(ma_device_type_capture); + config.capture.format = ma_format_f32; + config.capture.channels = this->num_channels; + config.periodSizeInFrames = 0; + config.sampleRate = this->get_graph()->get_sample_rate(); + config.dataCallback = read_callback; + + ma_result rv = ma_device_init(NULL, &config, &device); + if (rv != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Error initialising input device"); + } + + this->set_channels(0, device.capture.internalChannels); + + std::string s = device.capture.internalChannels == 1 ? "" : "s"; + std::cerr << "[miniaudio] Input device: " << std::string(device.capture.name) << " (" << device.capture.internalSampleRate << "Hz, " + << "buffer size " << device.capture.internalPeriodSizeInFrames << " samples, " << device.capture.internalChannels << " channel" << s << ")" + << std::endl; + + this->start(); +} + +int AudioIn_MiniAudio::start() +{ + ma_result rv = ma_device_start(&device); + if (rv != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Error starting device"); + } +} + +int AudioIn_MiniAudio::stop() +{ + ma_result rv = ma_device_stop(&device); + if (rv != MA_SUCCESS) + { + throw std::runtime_error("miniaudio: Error stopping device"); + } +} + +int AudioIn_MiniAudio::destroy() +{ + while (is_processing) + { + } + + this->stop(); + shared_in = nullptr; + + return 0; +} + +void AudioIn_MiniAudio::process(Buffer &out, int num_samples) +{ +} + +} diff --git a/source/src/node/io/input/soundio.cpp b/source/src/node/io/input/soundio.cpp index 030eb528..a9218bbc 100644 --- a/source/src/node/io/input/soundio.cpp +++ b/source/src/node/io/input/soundio.cpp @@ -131,7 +131,7 @@ int AudioIn_SoundIO::init() int buffer_size = this->instream->software_latency * this->instream->sample_rate; std::string s = num_output_channels == 1 ? "" : "s"; - std::cerr << "Input device: " << device->name << " (" << this->instream->sample_rate << "Hz, " + std::cerr << "[soundio] Input device: " << device->name << " (" << this->instream->sample_rate << "Hz, " << "buffer size " << buffer_size << " samples, " << num_output_channels << " channel" << s << ")" << std::endl; return 0; diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index bb75cd72..24da2bc0 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -73,7 +73,6 @@ AudioOut_MiniAudio::AudioOut_MiniAudio(const std::string &backend_name, this->sample_rate = sample_rate; this->buffer_size = buffer_size; this->name = "audioout-miniaudio"; - printf("backend name: %s\n", backend_name.c_str()); this->init(); } @@ -103,7 +102,6 @@ void AudioOut_MiniAudio::init_context(ma_context *context) { throw std::runtime_error("miniaudio: Backend name not recognised: " + this->backend_name); } - printf("Initialising context with backend %s\n", this->backend_name.c_str()); ma_backend backend_name = possible_backend_names[this->backend_name]; if (ma_context_init(&backend_name, 1, NULL, context) != MA_SUCCESS) @@ -177,22 +175,17 @@ int AudioOut_MiniAudio::init() rv = ma_device_init(NULL, &config, &device); if (rv != MA_SUCCESS) { - printf("Error initialising device\n"); - return -1; + throw std::runtime_error("miniaudio: Error initialising output device"); } this->sample_rate = device.playback.internalSampleRate; this->set_channels(device.playback.internalChannels, 0); std::string s = device.playback.internalChannels == 1 ? "" : "s"; - - std::cerr << "[MINIAUDIO] Output device: " << std::string(device.playback.name) << " (" << device.playback.internalSampleRate << "Hz, " + std::cerr << "[miniaudio] Output device: " << std::string(device.playback.name) << " (" << device.playback.internalSampleRate << "Hz, " << "buffer size " << device.playback.internalPeriodSizeInFrames << " samples, " << device.playback.internalChannels << " channel" << s << ")" << std::endl; - // do we need to set num_output channels to allocate the right number of output buffers? - this->set_channels(device.playback.internalChannels, 0); - return 0; } @@ -201,8 +194,7 @@ int AudioOut_MiniAudio::start() ma_result rv = ma_device_start(&device); if (rv != MA_SUCCESS) { - printf("Error starting device\n"); - return -1; + throw std::runtime_error("miniaudio: Error starting output device"); } this->set_state(SIGNALFLOW_NODE_STATE_ACTIVE); return 0; diff --git a/source/src/node/io/output/soundio.cpp b/source/src/node/io/output/soundio.cpp index 2dee13f3..bb8383de 100644 --- a/source/src/node/io/output/soundio.cpp +++ b/source/src/node/io/output/soundio.cpp @@ -256,7 +256,7 @@ int AudioOut_SoundIO::init() std::string s = num_output_channels == 1 ? "" : "s"; - std::cerr << "Output device: " << device->name << " (" << sample_rate << "Hz, " + std::cerr << "[soundio] Output device: " << device->name << " (" << sample_rate << "Hz, " << "buffer size " << buffer_size << " samples, " << num_output_channels << " channel" << s << ")" << std::endl; diff --git a/source/src/python/nodes.cpp b/source/src/python/nodes.cpp index 5b2f4519..c18a0368 100644 --- a/source/src/python/nodes.cpp +++ b/source/src/python/nodes.cpp @@ -5,6 +5,9 @@ void init_python_nodes(py::module &m) /*-------------------------------------------------------------------------------- * Node subclasses *-------------------------------------------------------------------------------*/ + py::class_>(m, "AudioIn", "Audio input") + .def(py::init(), "num_channels"_a = 1); + py::class_>(m, "AudioOut_Abstract", "Abstract audio output"); py::class_>(m, "AudioOut_Dummy", "Dummy audio output for offline processing") @@ -314,7 +317,7 @@ void init_python_nodes(py::module &m) py::class_>(m, "Wavetable2D", "Wavetable2D") .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "crossfade"_a = 0.0, "phase_offset"_a = 0.0, "sync"_a = 0); - py::class_>(m, "Maraca", "Model of maraca") + py::class_>(m, "Maraca", "Physically-inspired model of a maraca. Parameters: - `num_beans`: The number of simulated beans in the maraca (1-1024) - `shake_decay`: Decay constant for the energy injected per shake - `grain_decay`: Decay constant for the energy created per bean collision - `shake_duration`: Duration of each shake action, milliseconds - `shell_frequency`: Resonant frequency of the maraca's shell, hertz - `shell_resonance`: Resonanc of the maraca's shell (0-1) - `clock`: If specified, triggers shake actions - `energy`: If specified, injects energy into the maraca From Cook (1997), 'Physically Informed Sonic Modeling (PhISM): Synthesis of Percussive Sounds', Computer Music Journal.") .def(py::init(), "num_beans"_a = 64, "shake_decay"_a = 0.99, "grain_decay"_a = 0.99, "shake_duration"_a = 0.02, "shell_frequency"_a = 12000, "shell_resonance"_a = 0.9, "clock"_a = nullptr, "energy"_a = nullptr); py::class_>(m, "Clip", "Clip the input to `min`/`max`.") From 8d4c55e85ae4fb7b817ac2fe9c06c759964d6f17 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 09:44:17 +0100 Subject: [PATCH 06/33] Fix handling of newlines and quotes in pydocs --- auxiliary/scripts/auto-generator.py | 5 +- source/src/python/nodes.cpp | 274 ++++++++++++++-------------- 2 files changed, 141 insertions(+), 138 deletions(-) diff --git a/auxiliary/scripts/auto-generator.py b/auxiliary/scripts/auto-generator.py index 53a48484..137c7c91 100755 --- a/auxiliary/scripts/auto-generator.py +++ b/auxiliary/scripts/auto-generator.py @@ -140,7 +140,10 @@ def extract_docs(doxygen: str) -> str: if re.search(r"^\s*/\*", line) or re.search(r"\*/\s*$", line): continue line = re.sub(r"^\s*\*\s*", "", line) - output = output + line + " " + + # Escape quote marks to avoid breaking auto-generated pydocs + line = re.sub('"', '\\"', line) + output = output + line + "\\n" return output.strip() diff --git a/source/src/python/nodes.cpp b/source/src/python/nodes.cpp index c18a0368..8655e398 100644 --- a/source/src/python/nodes.cpp +++ b/source/src/python/nodes.cpp @@ -16,461 +16,461 @@ void init_python_nodes(py::module &m) py::class_>(m, "AudioOut", "Audio output") .def(py::init(), "backend_name"_a = "", "device_name"_a = "", "sample_rate"_a = 0, "buffer_size"_a = 0); - py::class_>(m, "CrossCorrelate", "Outputs the cross-correlation of the input signal with the given buffer. If hop_size is zero, calculates the cross-correlation every sample.") + py::class_>(m, "CrossCorrelate", "Outputs the cross-correlation of the input signal with the given buffer.\nIf hop_size is zero, calculates the cross-correlation every sample.\n") .def(py::init(), "input"_a = nullptr, "buffer"_a = nullptr, "hop_size"_a = 0); - py::class_>(m, "NearestNeighbour", "Nearest Neighbour.") + py::class_>(m, "NearestNeighbour", "Nearest Neighbour.\n") .def(py::init(), "buffer"_a = nullptr, "target"_a = 0.0); - py::class_>(m, "OnsetDetector", "Simple time-domain onset detector. Outputs an impulse when an onset is detected in the input. Maintains short-time and long-time averages. An onset is registered when the short-time average is threshold x the long-time average. min_interval is the minimum interval between onsets, in seconds.") + py::class_>(m, "OnsetDetector", "Simple time-domain onset detector. Outputs an impulse when an onset is detected\nin the input. Maintains short-time and long-time averages. An onset is registered\nwhen the short-time average is threshold x the long-time average.\nmin_interval is the minimum interval between onsets, in seconds.\n") .def(py::init(), "input"_a = 0.0, "threshold"_a = 2.0, "min_interval"_a = 0.1); #ifdef HAVE_VAMP - py::class_>(m, "VampAnalysis", "Feature extraction using the Vamp plugin toolkit.") + py::class_>(m, "VampAnalysis", "Feature extraction using the Vamp plugin toolkit.\n") .def(py::init(), "input"_a = 0.0, "plugin_id"_a = "vamp-example-plugins:spectralcentroid:linearcentroid") .def("list_plugins", &VampAnalysis::list_plugins, R"pbdoc(list[str]: List the available plugin names.)pbdoc"); #endif - py::class_>(m, "BeatCutter", "Cuts a buffer into segment_count segments, and stutters/jumps with the given probabilities.") + py::class_>(m, "BeatCutter", "Cuts a buffer into segment_count segments, and stutters/jumps with\nthe given probabilities.\n") .def(py::init(), "buffer"_a = nullptr, "segment_count"_a = 8, "stutter_probability"_a = 0.0, "stutter_count"_a = 1, "jump_probability"_a = 0.0, "duty_cycle"_a = 1.0, "rate"_a = 1.0, "segment_rate"_a = 1.0); - py::class_>(m, "BufferLooper", "Read and write from a buffer concurrently, with controllable overdub.") + py::class_>(m, "BufferLooper", "Read and write from a buffer concurrently, with controllable overdub.\n") .def(py::init(), "buffer"_a = nullptr, "input"_a = 0.0, "feedback"_a = 0.0, "loop_playback"_a = 0, "loop_record"_a = 0, "start_time"_a = nullptr, "end_time"_a = nullptr, "looper_level"_a = 1.0, "playthrough_level"_a = 0.0); - py::class_>(m, "BufferPlayer", "Plays the contents of the given buffer. `start_time`/`end_time` are in seconds. When a `clock` signal is received, rewinds to the `start_time`. Set `clock` to `0` to prevent the buffer from being triggered immediately.") + py::class_>(m, "BufferPlayer", "Plays the contents of the given buffer. `start_time`/`end_time` are in seconds.\nWhen a `clock` signal is received, rewinds to the `start_time`.\n\nSet `clock` to `0` to prevent the buffer from being triggered immediately.\n") .def(py::init(), "buffer"_a = nullptr, "rate"_a = 1.0, "loop"_a = 0, "start_time"_a = nullptr, "end_time"_a = nullptr, "clock"_a = nullptr); - py::class_>(m, "BufferRecorder", "Records the input to a buffer. feedback controls overdub.") + py::class_>(m, "BufferRecorder", "Records the input to a buffer. feedback controls overdub.\n") .def(py::init(), "buffer"_a = nullptr, "input"_a = 0.0, "feedback"_a = 0.0, "loop"_a = false); - py::class_>(m, "FeedbackBufferReader", "Counterpart to FeedbackBufferWriter.") + py::class_>(m, "FeedbackBufferReader", "Counterpart to FeedbackBufferWriter.\n") .def(py::init(), "buffer"_a = nullptr); - py::class_>(m, "FeedbackBufferWriter", "Counterpart to FeedbackBufferReader.") + py::class_>(m, "FeedbackBufferWriter", "Counterpart to FeedbackBufferReader.\n") .def(py::init(), "buffer"_a = nullptr, "input"_a = 0.0, "delay_time"_a = 0.1); - py::class_>(m, "HistoryBufferWriter", "Writes a rolling history buffer of a given duration. At a given moment in time, the contents of the buffer will be equal to the past N seconds of the audio generated by `input`. This is useful for (e.g.) a visual display of a rolling waveform or LFO window. `downsample` can be used to downsample the input; for example, with `downsample` of 10, a 1-second buffer can be used to display 10 seconds of historical audio.") + py::class_>(m, "HistoryBufferWriter", "Writes a rolling history buffer of a given duration. At a given moment in time,\nthe contents of the buffer will be equal to the past N seconds of the audio\ngenerated by `input`. This is useful for (e.g.) a visual display of a rolling\nwaveform or LFO window. `downsample` can be used to downsample the input;\nfor example, with `downsample` of 10, a 1-second buffer can be used to display\n10 seconds of historical audio.\n") .def(py::init(), "buffer"_a = nullptr, "input"_a = 0.0, "downsample"_a = 1); - py::class_>(m, "SegmentPlayer", "Trigger segments of `buffer` at the given list of `onsets` positions, in seconds. `index` determines the index of the onset to play back at, which can also be passed as an argument to trigger(). `rate` determines the playback rate, and `clock` can be used to retrigger based on the output of another Node. If `continue_after_segment` is non-zero, playback will continue after the subsequent onset.") + py::class_>(m, "SegmentPlayer", "Trigger segments of `buffer` at the given list of `onsets` positions, in\nseconds. `index` determines the index of the onset to play back at, which can\nalso be passed as an argument to trigger(). `rate` determines the playback rate,\nand `clock` can be used to retrigger based on the output of another Node.\nIf `continue_after_segment` is non-zero, playback will continue after the\nsubsequent onset.\n") .def(py::init, NodeRef, NodeRef, NodeRef, NodeRef, NodeRef>(), "buffer"_a = nullptr, "onsets"_a = 0, "index"_a = nullptr, "rate"_a = 1.0, "start_offset"_a = nullptr, "clock"_a = nullptr, "continue_after_segment"_a = 0); - py::class_>(m, "SegmentedGranulator", "Segmented Granulator.") + py::class_>(m, "SegmentedGranulator", "Segmented Granulator.\n") .def(py::init, std::vector, NodeRef, NodeRef, NodeRef, NodeRef>(), "buffer"_a = nullptr, "onset_times"_a = 0, "durations"_a = 0, "index"_a = 0.0, "rate"_a = 1.0, "clock"_a = 0, "max_grains"_a = 2048); - py::class_>(m, "Granulator", "Granulator. Generates a grain from the given buffer each time a trigger is received on the `clock` input. Each new grain uses the given `duration`, `amplitude`, `pan` and `rate` values presented at each input at the moment the grain is created. The input buffer can be mono or stereo. If `wrap` is true, grain playback can wrap around the end/start of the buffer.") + py::class_>(m, "Granulator", "Granulator. Generates a grain from the given buffer each time a trigger is\nreceived on the `clock` input. Each new grain uses the given `duration`,\n`amplitude`, `pan` and `rate` values presented at each input at the moment the\ngrain is created. The input buffer can be mono or stereo. If `wrap` is true,\ngrain playback can wrap around the end/start of the buffer.\n") .def(py::init(), "buffer"_a = nullptr, "clock"_a = 0, "pos"_a = 0, "duration"_a = 0.1, "amplitude"_a = 1.0, "pan"_a = 0.0, "rate"_a = 1.0, "max_grains"_a = 2048, "wrap"_a = false); #ifdef __APPLE__ - py::class_>(m, "MouseX", "Outputs the normalised cursor X position, from 0 to 1. Currently only supported on macOS.") + py::class_>(m, "MouseX", "Outputs the normalised cursor X position, from 0 to 1.\nCurrently only supported on macOS.\n") .def(py::init<>()); #endif #ifdef __APPLE__ - py::class_>(m, "MouseY", "Outputs the normalised cursor Y position, from 0 to 1. Currently only supported on macOS.") + py::class_>(m, "MouseY", "Outputs the normalised cursor Y position, from 0 to 1.\nCurrently only supported on macOS.\n") .def(py::init<>()); #endif #ifdef __APPLE__ - py::class_>(m, "MouseDown", "Outputs 1 if the left mouse button is down, 0 otherwise. Currently only supported on macOS.") + py::class_>(m, "MouseDown", "Outputs 1 if the left mouse button is down, 0 otherwise.\nCurrently only supported on macOS.\n") .def(py::init(), "button_index"_a = 0); #endif - py::class_>(m, "Accumulator", "Accumulator with decay.") + py::class_>(m, "Accumulator", "Accumulator with decay.\n") .def(py::init(), "strike_force"_a = 0.5, "decay_coefficient"_a = 0.9999, "trigger"_a = nullptr); - py::class_>(m, "ADSREnvelope", "Attack-decay-sustain-release envelope. Sustain portion is held until gate is zero.") + py::class_>(m, "ADSREnvelope", "Attack-decay-sustain-release envelope.\nSustain portion is held until gate is zero.\n") .def(py::init(), "attack"_a = 0.1, "decay"_a = 0.1, "sustain"_a = 0.5, "release"_a = 0.1, "gate"_a = 0); - py::class_>(m, "ASREnvelope", "Attack-sustain-release envelope.") + py::class_>(m, "ASREnvelope", "Attack-sustain-release envelope.\n") .def(py::init(), "attack"_a = 0.1, "sustain"_a = 0.5, "release"_a = 0.1, "curve"_a = 1.0, "clock"_a = nullptr); - py::class_>(m, "DetectSilence", "Detects blocks of silence below the threshold value. Used as an auto-free node to terminate a Patch after processing is complete.") + py::class_>(m, "DetectSilence", "Detects blocks of silence below the threshold value. Used as an auto-free\nnode to terminate a Patch after processing is complete.\n") .def(py::init(), "input"_a = nullptr, "threshold"_a = 0.00001); - py::class_>(m, "Envelope", "Generic envelope constructor, given an array of levels, times and curves.") + py::class_>(m, "Envelope", "Generic envelope constructor, given an array of levels, times and curves.\n") .def(py::init, std::vector, std::vector, NodeRef, bool>(), "levels"_a = std::vector(), "times"_a = std::vector(), "curves"_a = std::vector(), "clock"_a = nullptr, "loop"_a = false); - py::class_>(m, "Line", "Line segment with the given start/end values, and duration (in seconds). If loop is true, repeats indefinitely. Retriggers on a clock signal.") + py::class_>(m, "Line", "Line segment with the given start/end values, and duration (in seconds).\nIf loop is true, repeats indefinitely.\nRetriggers on a clock signal.\n") .def(py::init(), "start"_a = 0.0, "end"_a = 1.0, "time"_a = 1.0, "loop"_a = 0, "clock"_a = nullptr); - py::class_>(m, "RectangularEnvelope", "Rectangular envelope with the given sustain duration.") + py::class_>(m, "RectangularEnvelope", "Rectangular envelope with the given sustain duration.\n") .def(py::init(), "sustain_duration"_a = 1.0, "clock"_a = nullptr); - py::class_>(m, "FFTContinuousPhaseVocoder", "Continuous phase vocoder. Requires an FFT* input.") + py::class_>(m, "FFTContinuousPhaseVocoder", "Continuous phase vocoder.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = nullptr, "rate"_a = 1.0); #ifdef __APPLE__ - py::class_>(m, "FFTConvolve", "Frequency-domain convolution, using overlap-add. Useful for convolution reverb, with the input buffer containing an impulse response. Requires an FFT* input.") + py::class_>(m, "FFTConvolve", "Frequency-domain convolution, using overlap-add.\nUseful for convolution reverb, with the input buffer containing an impulse response.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = nullptr, "buffer"_a = nullptr); #endif - py::class_>(m, "FFTBufferPlayer", "FFTBufferPlayer. Plays from a buffer of audio spectra in mag/phase format.") + py::class_>(m, "FFTBufferPlayer", "FFTBufferPlayer.\nPlays from a buffer of audio spectra in mag/phase format.\n") .def(py::init(), "buffer"_a = nullptr, "rate"_a = 1.0); - py::class_>(m, "FFTContrast", "FFT Contrast. Requires an FFT* input.") + py::class_>(m, "FFTContrast", "FFT Contrast.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "contrast"_a = 1); - py::class_>(m, "FFTCrossFade", "FFT FFTCrossFade. Requires two FFT* inputs.") + py::class_>(m, "FFTCrossFade", "FFT FFTCrossFade.\nRequires two FFT* inputs.\n") .def(py::init(), "inputA"_a = 0, "inputB"_a = 0, "crossfade"_a = 0.0); - py::class_>(m, "FFTLFO", "FFT LFO. Requires an FFT* input.") + py::class_>(m, "FFTLFO", "FFT LFO.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "frequency"_a = 1.0, "spectral_cycles"_a = 1.0); - py::class_>(m, "FFTMagnitudePhaseArray", "Fixed mag/phase array.") + py::class_>(m, "FFTMagnitudePhaseArray", "Fixed mag/phase array.\n") .def(py::init, std::vector>(), "input"_a = 0, "magnitudes"_a = 0, "phases"_a = 0) .def("set_magnitudes", &FFTMagnitudePhaseArray::set_magnitudes); - py::class_>(m, "FFTRandomPhase", "Randomise phase values.") + py::class_>(m, "FFTRandomPhase", "Randomise phase values.\n") .def(py::init(), "input"_a = 0, "level"_a = 1.0); - py::class_>(m, "FFTScaleMagnitudes", "Randomise phase values.") + py::class_>(m, "FFTScaleMagnitudes", "Randomise phase values.\n") .def(py::init>(), "input"_a = 0, "scale"_a = 0); - py::class_>(m, "FFTTransform", "Transforms the FFT magnitude spectrum in the X axis. Requires an FFT* input.") + py::class_>(m, "FFTTransform", "Transforms the FFT magnitude spectrum in the X axis.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "flip"_a = 0, "rotate"_a = 0); - py::class_>(m, "FFT", "Fast Fourier Transform. Takes a time-domain input, and generates a frequency-domain (FFT) output.") + py::class_>(m, "FFT", "Fast Fourier Transform.\nTakes a time-domain input, and generates a frequency-domain (FFT) output.\n") .def(py::init(), "input"_a = 0.0, "fft_size"_a = SIGNALFLOW_DEFAULT_FFT_SIZE, "hop_size"_a = SIGNALFLOW_DEFAULT_FFT_HOP_SIZE, "window_size"_a = 0, "do_window"_a = true); - py::class_>(m, "FFTFindPeaks", "Find peaks in the FFT magnitude spectrum. Requires an FFT* input.") + py::class_>(m, "FFTFindPeaks", "Find peaks in the FFT magnitude spectrum.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "prominence"_a = 1, "threshold"_a = 0.000001, "count"_a = SIGNALFLOW_MAX_CHANNELS, "interpolate"_a = true); - py::class_>(m, "IFFT", "Inverse Fast Fourier Transform. Requires an FFT* input, generates a time-domain output.") + py::class_>(m, "IFFT", "Inverse Fast Fourier Transform.\nRequires an FFT* input, generates a time-domain output.\n") .def(py::init(), "input"_a = nullptr, "do_window"_a = false); - py::class_>(m, "FFTLPF", "FFT-based brick wall low pass filter. Requires an FFT* input.") + py::class_>(m, "FFTLPF", "FFT-based brick wall low pass filter.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "frequency"_a = 2000); - py::class_>(m, "FFTNoiseGate", "FFT-based noise gate. Requires an FFT* input.") + py::class_>(m, "FFTNoiseGate", "FFT-based noise gate.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "threshold"_a = 0.5, "invert"_a = 0.0); - py::class_>(m, "FFTPhaseVocoder", "Phase vocoder. Requires an FFT* input.") + py::class_>(m, "FFTPhaseVocoder", "Phase vocoder.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = nullptr); - py::class_>(m, "FFTTonality", "Tonality filter. Requires an FFT* input.") + py::class_>(m, "FFTTonality", "Tonality filter.\nRequires an FFT* input.\n") .def(py::init(), "input"_a = 0, "level"_a = 0.5, "smoothing"_a = 0.9); - py::class_>(m, "Add", "Add each sample of a to each sample of b. Can also be written as a + b") + py::class_>(m, "Add", "Add each sample of a to each sample of b.\nCan also be written as a + b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "AmplitudeToDecibels", "Map a linear amplitude value to decibels.") + py::class_>(m, "AmplitudeToDecibels", "Map a linear amplitude value to decibels.\n") .def(py::init(), "a"_a = 0); py::class_>(m, "DecibelsToAmplitude", "DecibelsToAmplitude") .def(py::init(), "a"_a = 0); - py::class_>(m, "Bus", "Bus is a node with a fixed number of input channels and arbitrary number of inputs, used to aggregate multiple sources. It is similar to Sum, but with a defined channel count that does not adapt to its inputs.") + py::class_>(m, "Bus", "Bus is a node with a fixed number of input channels and arbitrary number of\ninputs, used to aggregate multiple sources. It is similar to Sum, but with\na defined channel count that does not adapt to its inputs.\n") .def(py::init(), "num_channels"_a = 1); - py::class_>(m, "ChannelArray", "Takes an array of inputs and spreads them across multiple channels of output.") + py::class_>(m, "ChannelArray", "Takes an array of inputs and spreads them across multiple channels of output.\n") .def(py::init<>()) .def(py::init>(), "inputs"_a) .def(py::init>(), "inputs"_a) .def(py::init>(), "inputs"_a) .def(py::init>(), "inputs"_a); - py::class_>(m, "ChannelCrossfade", "Given a multichannel input, crossfades between channels based on the given position within the virtual array, producing a single-channel output.") + py::class_>(m, "ChannelCrossfade", "Given a multichannel input, crossfades between channels based on the given\nposition within the virtual array, producing a single-channel output.\n") .def(py::init(), "input"_a = nullptr, "index"_a = nullptr, "num_output_channels"_a = 1); - py::class_>(m, "ChannelMixer", "Downmix a multichannel input to a lower-channel output. If num_channels is greater than one, spreads the input channels across the field. If amplitude_compensation is enabled, scale down the amplitude based on the ratio of input to output channels.") + py::class_>(m, "ChannelMixer", "Downmix a multichannel input to a lower-channel output. If num_channels is\ngreater than one, spreads the input channels across the field.\nIf amplitude_compensation is enabled, scale down the amplitude based on the\nratio of input to output channels.\n") .def(py::init(), "num_channels"_a = 1, "input"_a = 0, "amplitude_compensation"_a = true); - py::class_>(m, "ChannelOffset", "Offsets the input by a specified number of channels. With an N-channel input and an offset of M, the output will have M+N channels.") + py::class_>(m, "ChannelOffset", "Offsets the input by a specified number of channels. With an N-channel input\nand an offset of M, the output will have M+N channels.\n") .def(py::init(), "offset"_a = 0, "input"_a = nullptr); - py::class_>(m, "ChannelSelect", "Select a subset of channels from a multichannel input, starting at offset, up to a maximum of maximum, with the given step.") + py::class_>(m, "ChannelSelect", "Select a subset of channels from a multichannel input, starting at offset,\nup to a maximum of maximum, with the given step.\n") .def(py::init(), "input"_a = nullptr, "offset"_a = 0, "maximum"_a = 0, "step"_a = 1); - py::class_>(m, "Equal", "Compares the output of a to the output of b. Outputs 1 when equal, 0 otherwise. Can also be written as a == b") + py::class_>(m, "Equal", "Compares the output of a to the output of b. Outputs 1 when equal, 0 otherwise.\nCan also be written as a == b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "NotEqual", "Compares the output of a to the output of b. Outputs 0 when equal, 1 otherwise. Can also be written as a != b") + py::class_>(m, "NotEqual", "Compares the output of a to the output of b. Outputs 0 when equal, 1 otherwise.\nCan also be written as a != b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "GreaterThan", "Compares the output of a to the output of b. Outputs 1 when a > b, 0 otherwise. Can also be written as a > b") + py::class_>(m, "GreaterThan", "Compares the output of a to the output of b. Outputs 1 when a > b, 0 otherwise.\nCan also be written as a > b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "GreaterThanOrEqual", "Compares the output of a to the output of b. Outputs 1 when a >= b, 0 otherwise. Can also be written as a >= b") + py::class_>(m, "GreaterThanOrEqual", "Compares the output of a to the output of b. Outputs 1 when a >= b, 0 otherwise.\nCan also be written as a >= b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "LessThan", "Compares the output of a to the output of b. Outputs 1 when a < b, 0 otherwise. Can also be written as a < b") + py::class_>(m, "LessThan", "Compares the output of a to the output of b. Outputs 1 when a < b, 0 otherwise.\nCan also be written as a < b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "LessThanOrEqual", "Compares the output of a to the output of b. Outputs 1 when a <= b, 0 otherwise. Can also be written as a <= b") + py::class_>(m, "LessThanOrEqual", "Compares the output of a to the output of b. Outputs 1 when a <= b, 0 otherwise.\nCan also be written as a <= b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "Modulo", "Outputs the value of a modulo b, per sample. Supports fractional values. Can also be written as a % b") + py::class_>(m, "Modulo", "Outputs the value of a modulo b, per sample. Supports fractional values.\nCan also be written as a % b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "Abs", "Outputs the absolute value of a, per sample. Can also be written as abs(a)") + py::class_>(m, "Abs", "Outputs the absolute value of a, per sample.\nCan also be written as abs(a)\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "If", "Outputs value_if_true for each non-zero value of a, value_if_false for all other values.") + py::class_>(m, "If", "Outputs value_if_true for each non-zero value of a, value_if_false for all\nother values.\n") .def(py::init(), "a"_a = 0, "value_if_true"_a = 0, "value_if_false"_a = 0); - py::class_>(m, "Divide", "Divide each sample of a by each sample of b. Can also be written as a / b") + py::class_>(m, "Divide", "Divide each sample of a by each sample of b.\nCan also be written as a / b\n") .def(py::init(), "a"_a = 1, "b"_a = 1); - py::class_>(m, "FrequencyToMidiNote", "Map a frequency to a MIDI note (where 440Hz = A4 = 69), with floating-point output.") + py::class_>(m, "FrequencyToMidiNote", "Map a frequency to a MIDI note (where 440Hz = A4 = 69), with floating-point\noutput.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "MidiNoteToFrequency", "Map a MIDI note to a frequency (where 440Hz = A4 = 69), supporting floating-point input.") + py::class_>(m, "MidiNoteToFrequency", "Map a MIDI note to a frequency (where 440Hz = A4 = 69), supporting floating-point\ninput.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Multiply", "Multiply each sample of a by each sample of b. Can also be written as a * b") + py::class_>(m, "Multiply", "Multiply each sample of a by each sample of b.\nCan also be written as a * b\n") .def(py::init(), "a"_a = 1.0, "b"_a = 1.0); - py::class_>(m, "Pow", "Outputs a to the power of b, per sample. Can also be written as a ** b") + py::class_>(m, "Pow", "Outputs a to the power of b, per sample.\nCan also be written as a ** b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "RoundToScale", "Given a frequency input, generates a frequency output that is rounded to the nearest MIDI note. (TODO: Not very well named)") + py::class_>(m, "RoundToScale", "Given a frequency input, generates a frequency output that is rounded to the nearest MIDI note.\n(TODO: Not very well named)\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Round", "Round the input to the nearest integer value.") + py::class_>(m, "Round", "Round the input to the nearest integer value.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "ScaleLinExp", "Scales the input from a linear range (between a and b) to an exponential range (between c and d).") + py::class_>(m, "ScaleLinExp", "Scales the input from a linear range (between a and b)\nto an exponential range (between c and d).\n") .def(py::init(), "input"_a = 0, "a"_a = 0, "b"_a = 1, "c"_a = 1, "d"_a = 10); - py::class_>(m, "ScaleLinLin", "Scales the input from a linear range (between a and b) to a linear range (between c and d).") + py::class_>(m, "ScaleLinLin", "Scales the input from a linear range (between a and b)\nto a linear range (between c and d).\n") .def(py::init(), "input"_a = 0, "a"_a = 0, "b"_a = 1, "c"_a = 1, "d"_a = 10); - py::class_>(m, "SelectInput", "Pass through the output of one or more `inputs`, based on the integer input index specified in `index`. Unlike `ChannelSelect`, inputs may be multichannel, and `index` can be modulated in real time.") + py::class_>(m, "SelectInput", "Pass through the output of one or more `inputs`, based on the integer input index\nspecified in `index`. Unlike `ChannelSelect`, inputs may be multichannel,\nand `index` can be modulated in real time.\n") .def(py::init(), "index"_a = 0) .def(py::init, NodeRef>(), "inputs"_a, "index"_a = 0) .def(py::init, NodeRef>(), "inputs"_a, "index"_a = 0) .def(py::init, NodeRef>(), "inputs"_a, "index"_a = 0) .def(py::init, NodeRef>(), "inputs"_a, "index"_a = 0); - py::class_>(m, "Subtract", "Subtract each sample of b from each sample of a. Can also be written as a - b") + py::class_>(m, "Subtract", "Subtract each sample of b from each sample of a.\nCan also be written as a - b\n") .def(py::init(), "a"_a = 0, "b"_a = 0); - py::class_>(m, "Sum", "Sums the output of all of the input nodes, by sample.") + py::class_>(m, "Sum", "Sums the output of all of the input nodes, by sample.\n") .def(py::init<>()) .def(py::init>(), "inputs"_a) .def(py::init>(), "inputs"_a) .def(py::init>(), "inputs"_a) .def(py::init>(), "inputs"_a); - py::class_>(m, "TimeShift", "TimeShift") + py::class_>(m, "TimeShift", "TimeShift\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Sin", "Outputs sin(a), per sample.") + py::class_>(m, "Sin", "Outputs sin(a), per sample.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Cos", "Outputs cos(a), per sample.") + py::class_>(m, "Cos", "Outputs cos(a), per sample.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Tan", "Outputs tan(a), per sample.") + py::class_>(m, "Tan", "Outputs tan(a), per sample.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Tanh", "Outputs tanh(a), per sample. Can be used as a soft clipper.") + py::class_>(m, "Tanh", "Outputs tanh(a), per sample.\nCan be used as a soft clipper.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "Constant", "Produces a constant value.") + py::class_>(m, "Constant", "Produces a constant value.\n") .def(py::init(), "value"_a = 0); - py::class_>(m, "Impulse", "Produces a value of 1 at the given `frequency`, with output of 0 at all other times. If frequency is 0, produces a single impulse.") + py::class_>(m, "Impulse", "Produces a value of 1 at the given `frequency`, with output of 0 at all other times.\nIf frequency is 0, produces a single impulse.\n") .def(py::init(), "frequency"_a = 1.0); py::class_>(m, "LFO", "LFO") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0); - py::class_>(m, "SawLFO", "Produces a sawtooth LFO at the given `frequency` and `phase` offset, with output ranging from `min` to `max`.") + py::class_>(m, "SawLFO", "Produces a sawtooth LFO at the given `frequency` and `phase` offset, with output ranging from `min` to `max`.\n") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0); - py::class_>(m, "SawOscillator", "Produces a (non-band-limited) sawtooth wave, with the given `frequency` and `phase` offset. When a `reset` or trigger is received, resets the phase to zero.") + py::class_>(m, "SawOscillator", "Produces a (non-band-limited) sawtooth wave, with the given `frequency` and\n`phase` offset. When a `reset` or trigger is received, resets the phase to zero.\n") .def(py::init(), "frequency"_a = 440, "phase_offset"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "SineLFO", "Produces a sinusoidal LFO at the given `frequency` and `phase` offset, with output ranging from `min` to `max`.") + py::class_>(m, "SineLFO", "Produces a sinusoidal LFO at the given `frequency` and `phase` offset,\nwith output ranging from `min` to `max`.\n") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0); - py::class_>(m, "SineOscillator", "Produces a sine wave at the given `frequency`.") + py::class_>(m, "SineOscillator", "Produces a sine wave at the given `frequency`.\n") .def(py::init(), "frequency"_a = 440, "phase_offset"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "SquareLFO", "Produces a pulse wave LFO with the given `frequency` and pulse `width`, ranging from `min` to `max`, where `width` of `0.5` is a square wave and other values produce a rectangular wave.") + py::class_>(m, "SquareLFO", "Produces a pulse wave LFO with the given `frequency` and pulse `width`, \nranging from `min` to `max`, where `width` of `0.5` is a square wave and\nother values produce a rectangular wave.\n") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "width"_a = 0.5, "phase"_a = 0.0); - py::class_>(m, "SquareOscillator", "Produces a pulse wave with the given `frequency` and pulse `width`, where `width` of `0.5` is a square wave and other values produce a rectangular wave.") + py::class_>(m, "SquareOscillator", "Produces a pulse wave with the given `frequency` and pulse `width`, \nwhere `width` of `0.5` is a square wave and other values produce a\nrectangular wave.\n") .def(py::init(), "frequency"_a = 440, "width"_a = 0.5); - py::class_>(m, "TriangleLFO", "Produces a triangle LFO with the given `frequency` and `phase` offset, ranging from `min` to `max`.") + py::class_>(m, "TriangleLFO", "Produces a triangle LFO with the given `frequency` and `phase` offset,\nranging from `min` to `max`.\n") .def(py::init(), "frequency"_a = 1.0, "min"_a = 0.0, "max"_a = 1.0, "phase"_a = 0.0); - py::class_>(m, "TriangleOscillator", "Produces a triangle wave with the given `frequency`.") + py::class_>(m, "TriangleOscillator", "Produces a triangle wave with the given `frequency`.\n") .def(py::init(), "frequency"_a = 440); - py::class_>(m, "Wavetable", "Plays the wavetable stored in buffer at the given `frequency` and `phase` offset. `sync` can be used to provide a hard sync input, which resets the wavetable's phase at each zero-crossing.") + py::class_>(m, "Wavetable", "Plays the wavetable stored in buffer at the given `frequency` and `phase` offset.\n`sync` can be used to provide a hard sync input, which resets the wavetable's\nphase at each zero-crossing.\n") .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "phase_offset"_a = 0, "sync"_a = 0, "phase_map"_a = nullptr); py::class_>(m, "Wavetable2D", "Wavetable2D") .def(py::init(), "buffer"_a = nullptr, "frequency"_a = 440, "crossfade"_a = 0.0, "phase_offset"_a = 0.0, "sync"_a = 0); - py::class_>(m, "Maraca", "Physically-inspired model of a maraca. Parameters: - `num_beans`: The number of simulated beans in the maraca (1-1024) - `shake_decay`: Decay constant for the energy injected per shake - `grain_decay`: Decay constant for the energy created per bean collision - `shake_duration`: Duration of each shake action, milliseconds - `shell_frequency`: Resonant frequency of the maraca's shell, hertz - `shell_resonance`: Resonanc of the maraca's shell (0-1) - `clock`: If specified, triggers shake actions - `energy`: If specified, injects energy into the maraca From Cook (1997), 'Physically Informed Sonic Modeling (PhISM): Synthesis of Percussive Sounds', Computer Music Journal.") + py::class_>(m, "Maraca", "Physically-inspired model of a maraca.\n\nParameters:\n- `num_beans`: The number of simulated beans in the maraca (1-1024)\n- `shake_decay`: Decay constant for the energy injected per shake\n- `grain_decay`: Decay constant for the energy created per bean collision\n- `shake_duration`: Duration of each shake action, milliseconds\n- `shell_frequency`: Resonant frequency of the maraca's shell, hertz\n- `shell_resonance`: Resonanc of the maraca's shell (0-1)\n- `clock`: If specified, triggers shake actions\n- `energy`: If specified, injects energy into the maraca\n\nFrom Cook (1997), \"Physically Informed Sonic Modeling (PhISM): Synthesis of\nPercussive Sounds\", Computer Music Journal.\n") .def(py::init(), "num_beans"_a = 64, "shake_decay"_a = 0.99, "grain_decay"_a = 0.99, "shake_duration"_a = 0.02, "shell_frequency"_a = 12000, "shell_resonance"_a = 0.9, "clock"_a = nullptr, "energy"_a = nullptr); - py::class_>(m, "Clip", "Clip the input to `min`/`max`.") + py::class_>(m, "Clip", "Clip the input to `min`/`max`.\n") .def(py::init(), "input"_a = nullptr, "min"_a = -1.0, "max"_a = 1.0); - py::class_>(m, "Fold", "Fold the input beyond `min`/`max`, reflecting the excess back.") + py::class_>(m, "Fold", "Fold the input beyond `min`/`max`, reflecting the excess back.\n") .def(py::init(), "input"_a = nullptr, "min"_a = -1.0, "max"_a = 1.0); - py::class_>(m, "Smooth", "Smooth the input with a given smoothing coefficient. When `smooth` = 0, applies no smoothing.") + py::class_>(m, "Smooth", "Smooth the input with a given smoothing coefficient.\nWhen `smooth` = 0, applies no smoothing.\n") .def(py::init(), "input"_a = nullptr, "smooth"_a = 0.99); - py::class_>(m, "WetDry", "Takes `wet` and `dry` inputs, and outputs a mix determined by `wetness`.") + py::class_>(m, "WetDry", "Takes `wet` and `dry` inputs, and outputs a mix determined by `wetness`.\n") .def(py::init(), "dry_input"_a = nullptr, "wet_input"_a = nullptr, "wetness"_a = 0.0); - py::class_>(m, "Wrap", "Wrap the input beyond `min`/`max`.") + py::class_>(m, "Wrap", "Wrap the input beyond `min`/`max`.\n") .def(py::init(), "input"_a = nullptr, "min"_a = -1.0, "max"_a = 1.0); - py::class_>(m, "AllpassDelay", "All-pass delay, with `feedback` between 0 and 1. `delay_time` must be less than or equal to `max_delay_time`.") + py::class_>(m, "AllpassDelay", "All-pass delay, with `feedback` between 0 and 1.\n`delay_time` must be less than or equal to `max_delay_time`.\n") .def(py::init(), "input"_a = 0.0, "delay_time"_a = 0.1, "feedback"_a = 0.5, "max_delay_time"_a = 0.5); - py::class_>(m, "CombDelay", "Comb delay, with `feedback` between 0 and 1. `delay_time` must be less than or equal to `max_delay_time`.") + py::class_>(m, "CombDelay", "Comb delay, with `feedback` between 0 and 1.\n`delay_time` must be less than or equal to `max_delay_time`.\n") .def(py::init(), "input"_a = 0.0, "delay_time"_a = 0.1, "feedback"_a = 0.5, "max_delay_time"_a = 0.5); - py::class_>(m, "OneTapDelay", "Single-tap delay line. `delay_time` must be less than or equal to `max_delay_time`.") + py::class_>(m, "OneTapDelay", "Single-tap delay line.\n`delay_time` must be less than or equal to `max_delay_time`.\n") .def(py::init(), "input"_a = 0.0, "delay_time"_a = 0.1, "max_delay_time"_a = 0.5); - py::class_>(m, "Stutter", "Stutters the input whenever a trigger is received on `clock`. Generates `stutter_count` repeats, with duration of `stutter_time`.") + py::class_>(m, "Stutter", "Stutters the input whenever a trigger is received on `clock`.\nGenerates `stutter_count` repeats, with duration of `stutter_time`.\n") .def(py::init(), "input"_a = 0.0, "stutter_time"_a = 0.1, "stutter_count"_a = 1, "stutter_probability"_a = 1.0, "stutter_advance_time"_a = 0.0, "clock"_a = nullptr, "max_stutter_time"_a = 1.0); - py::class_>(m, "Resample", "Resampler and bit crusher. `sample_rate` is in Hz, `bit_rate` is an integer between 0 and 16.") + py::class_>(m, "Resample", "Resampler and bit crusher. `sample_rate` is in Hz, `bit_rate` is an integer\nbetween 0 and 16.\n") .def(py::init(), "input"_a = 0, "sample_rate"_a = 44100, "bit_rate"_a = 16); - py::class_>(m, "SampleAndHold", "Samples and holds the input each time a trigger is received on `clock`.") + py::class_>(m, "SampleAndHold", "Samples and holds the input each time a trigger is received on `clock`.\n") .def(py::init(), "input"_a = nullptr, "clock"_a = nullptr); - py::class_>(m, "Squiz", "Implementation of Dan Stowell's Squiz algorithm, a kind of downsampler.") + py::class_>(m, "Squiz", "Implementation of Dan Stowell's Squiz algorithm, a kind of downsampler.\n") .def(py::init(), "input"_a = 0.0, "rate"_a = 2.0, "chunk_size"_a = 1); - py::class_>(m, "WaveShaper", "Applies wave-shaping as described in the WaveShaperBuffer `buffer`.") + py::class_>(m, "WaveShaper", "Applies wave-shaping as described in the WaveShaperBuffer `buffer`.\n") .def(py::init(), "input"_a = 0.0, "buffer"_a = nullptr); - py::class_>(m, "Compressor", "Dynamic range compression, with optional `sidechain` input. When the input amplitude is above `threshold`, compresses the amplitude with the given `ratio`, following the given `attack_time` and `release_time` in seconds.") + py::class_>(m, "Compressor", "Dynamic range compression, with optional `sidechain` input.\nWhen the input amplitude is above `threshold`, compresses the amplitude with\nthe given `ratio`, following the given `attack_time` and `release_time`\nin seconds.\n") .def(py::init(), "input"_a = 0.0, "threshold"_a = 0.1, "ratio"_a = 2, "attack_time"_a = 0.01, "release_time"_a = 0.1, "sidechain"_a = nullptr); - py::class_>(m, "Gate", "Outputs the input value when it is above the given `threshold`, otherwise zero.") + py::class_>(m, "Gate", "Outputs the input value when it is above the given `threshold`, otherwise zero.\n") .def(py::init(), "input"_a = 0.0, "threshold"_a = 0.1); - py::class_>(m, "Maximiser", "Gain maximiser.") + py::class_>(m, "Maximiser", "Gain maximiser.\n") .def(py::init(), "input"_a = 0.0, "ceiling"_a = 0.5, "attack_time"_a = 1.0, "release_time"_a = 1.0); - py::class_>(m, "RMS", "Outputs the root-mean-squared value of the input, in buffers equal to the graph's current buffer size.") + py::class_>(m, "RMS", "Outputs the root-mean-squared value of the input, in buffers equal to the\ngraph's current buffer size.\n") .def(py::init(), "input"_a = 0.0); - py::class_>(m, "BiquadFilter", "Biquad filter. filter_type can be 'low_pass', 'band_pass', 'high_pass', 'notch', 'peak', 'low_shelf', 'high_shelf'. Not recommended for real-time modulation; for this, use SVFilter.") + py::class_>(m, "BiquadFilter", "Biquad filter.\nfilter_type can be 'low_pass', 'band_pass', 'high_pass', 'notch', 'peak',\n'low_shelf', 'high_shelf'.\nNot recommended for real-time modulation; for this, use SVFilter.\n") .def(py::init(), "input"_a = 0.0, "filter_type"_a = SIGNALFLOW_FILTER_TYPE_LOW_PASS, "cutoff"_a = 440, "resonance"_a = 0.0, "peak_gain"_a = 0.0) .def(py::init(), "input"_a, "filter_type"_a, "cutoff"_a = 440, "resonance"_a = 0.0, "peak_gain"_a = 0.0); - py::class_>(m, "DCFilter", "Remove low-frequency and DC content from a signal.") + py::class_>(m, "DCFilter", "Remove low-frequency and DC content from a signal.\n") .def(py::init(), "input"_a = 0.0); - py::class_>(m, "EQ", "Three-band EQ.") + py::class_>(m, "EQ", "Three-band EQ.\n") .def(py::init(), "input"_a = 0.0, "low_gain"_a = 1.0, "mid_gain"_a = 1.0, "high_gain"_a = 1.0, "low_freq"_a = 500, "high_freq"_a = 5000); - py::class_>(m, "MoogVCF", "Simulation of the Moog ladder low-pass filter. `cutoff` sets the cutoff frequency; `resonance` should typically be between 0..1.") + py::class_>(m, "MoogVCF", "Simulation of the Moog ladder low-pass filter. `cutoff` sets the cutoff\nfrequency; `resonance` should typically be between 0..1.\n") .def(py::init(), "input"_a = 0.0, "cutoff"_a = 200.0, "resonance"_a = 0.0); - py::class_>(m, "SVFilter", "State variable filter. `filter_type` can be 'low_pass', 'band_pass', 'high_pass', 'notch', 'peak', 'low_shelf', 'high_shelf'. `resonance` should be between `[0..1]`.") + py::class_>(m, "SVFilter", "State variable filter.\n`filter_type` can be 'low_pass', 'band_pass', 'high_pass', 'notch', 'peak',\n'low_shelf', 'high_shelf'. `resonance` should be between `[0..1]`.\n") .def(py::init(), "input"_a = 0.0, "filter_type"_a = SIGNALFLOW_FILTER_TYPE_LOW_PASS, "cutoff"_a = 440, "resonance"_a = 0.0) .def(py::init(), "input"_a, "filter_type"_a, "cutoff"_a = 440, "resonance"_a = 0.0); - py::class_>(m, "AzimuthPanner", "Pan input around an equally-spaced ring of `num_channels` speakers. `pan` is the pan position from -1..+1, where 0 = centre front. `width` is the source's width, where 1.0 spans exactly between an adjacent pair of channels.") + py::class_>(m, "AzimuthPanner", "Pan input around an equally-spaced ring of `num_channels` speakers.\n`pan` is the pan position from -1..+1, where 0 = centre front.\n`width` is the source's width, where 1.0 spans exactly between an adjacent pair of channels.\n") .def(py::init(), "num_channels"_a = 2, "input"_a = 0, "pan"_a = 0.0, "width"_a = 1.0); - py::class_>(m, "ChannelPanner", "Pan the input between a linear series of channels, where `pan` 0 = channel 0, 1 = channel 1, etc. No wrapping is applied.") + py::class_>(m, "ChannelPanner", "Pan the input between a linear series of channels, where `pan` 0 = channel 0,\n1 = channel 1, etc. No wrapping is applied.\n") .def(py::init(), "num_channels"_a = 2, "input"_a = 0, "pan"_a = 0.0, "width"_a = 1.0); - py::class_>(m, "SpatialPanner", "Implements a spatial panning algorithm, applied to a given SpatialEnvironment. Currently, only DBAP is supported.") + py::class_>(m, "SpatialPanner", "Implements a spatial panning algorithm, applied to a given SpatialEnvironment.\nCurrently, only DBAP is supported.\n") .def(py::init, NodeRef, NodeRef, NodeRef, NodeRef, NodeRef, NodeRef, std::string>(), "env"_a = nullptr, "input"_a = 0.0, "x"_a = 0.0, "y"_a = 0.0, "z"_a = 0.0, "radius"_a = 1.0, "use_delays"_a = 1.0, "algorithm"_a = "dbap"); - py::class_>(m, "StereoBalance", "Takes a stereo input and rebalances it, where `balance` of `0` is unchanged, `-1` is hard left, and `1` is hard right.") + py::class_>(m, "StereoBalance", "Takes a stereo input and rebalances it, where `balance` of `0` is unchanged,\n`-1` is hard left, and `1` is hard right.\n") .def(py::init(), "input"_a = 0, "balance"_a = 0); - py::class_>(m, "StereoPanner", "Pans a mono input to a stereo output. `pan` should be between -1 (hard left) to +1 (hard right), with 0 = centre.") + py::class_>(m, "StereoPanner", "Pans a mono input to a stereo output. `pan` should be between -1 (hard left) to\n+1 (hard right), with 0 = centre.\n") .def(py::init(), "input"_a = 0, "pan"_a = 0.0); - py::class_>(m, "StereoWidth", "Reduces the width of a stereo signal. When `width` = 1, input is unchanged. When `width` = 0, outputs a pair of identical channels both containing L+R.") + py::class_>(m, "StereoWidth", "Reduces the width of a stereo signal.\nWhen `width` = 1, input is unchanged.\nWhen `width` = 0, outputs a pair of identical channels both containing L+R.\n") .def(py::init(), "input"_a = 0, "width"_a = 1); - py::class_>(m, "ClockDivider", "When given a `clock` input (e.g., an Impulse), divides the clock by the given `factor`. factor must be an integer greater than or equal to 1.") + py::class_>(m, "ClockDivider", "When given a `clock` input (e.g., an Impulse), divides the clock by the given\n`factor`. factor must be an integer greater than or equal to 1.\n") .def(py::init(), "clock"_a = 0, "factor"_a = 1); - py::class_>(m, "Counter", "Count upwards from `min` to `max`, driven by `clock`.") + py::class_>(m, "Counter", "Count upwards from `min` to `max`, driven by `clock`.\n") .def(py::init(), "clock"_a = 0, "min"_a = 0, "max"_a = 2147483647); - py::class_>(m, "Euclidean", "Euclidean rhythm as described by Toussaint, with `sequence_length` (n) and `num_events` (k), driven by `clock`.") + py::class_>(m, "Euclidean", "Euclidean rhythm as described by Toussaint, with `sequence_length` (n)\nand `num_events` (k), driven by `clock`.\n") .def(py::init(), "clock"_a = 0, "sequence_length"_a = 0, "num_events"_a = 0); - py::class_>(m, "FlipFlop", "Flips from 0/1 on each `clock`.") + py::class_>(m, "FlipFlop", "Flips from 0/1 on each `clock`.\n") .def(py::init(), "clock"_a = 0); - py::class_>(m, "ImpulseSequence", "Each time a `clock` or trigger is received, outputs the next value in `sequence`. At all other times, outputs zero.") + py::class_>(m, "ImpulseSequence", "Each time a `clock` or trigger is received, outputs the next value in\n`sequence`. At all other times, outputs zero.\n") .def(py::init, NodeRef>(), "sequence"_a = std::vector(), "clock"_a = nullptr) .def(py::init(), "sequence"_a, "clock"_a = nullptr); - py::class_>(m, "Index", "Outputs the value in `list` corresponding to `index`.") + py::class_>(m, "Index", "Outputs the value in `list` corresponding to `index`.\n") .def(py::init, NodeRef>(), "list"_a = 0, "index"_a = 0); - py::class_>(m, "Latch", "Initially outputs 0. When a trigger is received at `set`, outputs 1. When a trigger is subsequently received at `reset`, outputs 0, until the next `set`.") + py::class_>(m, "Latch", "Initially outputs 0.\nWhen a trigger is received at `set`, outputs 1.\nWhen a trigger is subsequently received at `reset`, outputs 0, until the next\n`set`.\n") .def(py::init(), "set"_a = 0, "reset"_a = 0); - py::class_>(m, "Sequence", "Outputs the elements in `sequence`, incrementing position on each `clock`.") + py::class_>(m, "Sequence", "Outputs the elements in `sequence`, incrementing position on each `clock`.\n") .def(py::init, NodeRef>(), "sequence"_a = std::vector(), "clock"_a = nullptr); - py::class_>(m, "TriggerMult", "Distribute any triggers to all output nodes.") + py::class_>(m, "TriggerMult", "Distribute any triggers to all output nodes.\n") .def(py::init(), "a"_a = 0); - py::class_>(m, "TriggerRoundRobin", "Relay trigger() events to a single node from the list of connected outputs, with `direction` determining the direction: 1 (or above) = move forwards by N, -1 = move backwards by N, 0 = stationary.") + py::class_>(m, "TriggerRoundRobin", "Relay trigger() events to a single node from the list of connected outputs,\nwith `direction` determining the direction: 1 (or above) = move forwards by N,\n-1 = move backwards by N, 0 = stationary.\n") .def(py::init(), "direction"_a = 1); - py::class_>(m, "Logistic", "Logistic noise.") + py::class_>(m, "Logistic", "Logistic noise.\n") .def(py::init(), "chaos"_a = 3.7, "frequency"_a = 0.0); - py::class_>(m, "PinkNoise", "Pink noise, with specified low/high cutoffs.") + py::class_>(m, "PinkNoise", "Pink noise, with specified low/high cutoffs.\n") .def(py::init(), "low_cutoff"_a = 20.0, "high_cutoff"_a = 20000.0, "reset"_a = nullptr); - py::class_>(m, "RandomBrownian", "Outputs Brownian noise between min/max, with a mean change of delta between samples. If a clock is passed, only generates a new value on a clock tick.") + py::class_>(m, "RandomBrownian", "Outputs Brownian noise between min/max, with a mean change of delta between samples.\nIf a clock is passed, only generates a new value on a clock tick.\n") .def(py::init(), "min"_a = -1.0, "max"_a = 1.0, "delta"_a = 0.01, "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomChoice", "Pick a random value from the given array. If a clock is passed, only picks a new value on a clock tick.") + py::class_>(m, "RandomChoice", "Pick a random value from the given array.\nIf a clock is passed, only picks a new value on a clock tick.\n") .def(py::init, NodeRef, NodeRef>(), "values"_a = std::vector(), "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomCoin", "Flip a coin with the given probability. If a clock is passed, only picks a new value on a clock tick.") + py::class_>(m, "RandomCoin", "Flip a coin with the given probability.\nIf a clock is passed, only picks a new value on a clock tick.\n") .def(py::init(), "probability"_a = 0.5, "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomExponentialDist", "Generate an random value following the exponential distribution. If a clock is passed, only picks a new value on a clock tick.") + py::class_>(m, "RandomExponentialDist", "Generate an random value following the exponential distribution.\nIf a clock is passed, only picks a new value on a clock tick.\n") .def(py::init(), "scale"_a = 0.0, "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomExponential", "Generate an random exponential value between min/max. If a clock is passed, only picks a new value on a clock tick.") + py::class_>(m, "RandomExponential", "Generate an random exponential value between min/max.\nIf a clock is passed, only picks a new value on a clock tick.\n") .def(py::init(), "min"_a = 0.001, "max"_a = 1.0, "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomGaussian", "Generate an random Gaussian value, with given mean and sigma. If a clock is passed, only picks a new value on a clock tick.") + py::class_>(m, "RandomGaussian", "Generate an random Gaussian value, with given mean and sigma.\nIf a clock is passed, only picks a new value on a clock tick.\n") .def(py::init(), "mean"_a = 0.0, "sigma"_a = 0.0, "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomImpulseSequence", "Generates a random sequence of 0/1 bits with the given length, and the given probability each each bit = 1. The position of the sequence is incremented on each clock signal. explore and generate are trigger inputs which cause the sequence to mutate and re-generate respectively.") + py::class_>(m, "RandomImpulseSequence", "Generates a random sequence of 0/1 bits with the given length, and the given\nprobability each each bit = 1. The position of the sequence is incremented\non each clock signal. explore and generate are trigger inputs which cause\nthe sequence to mutate and re-generate respectively.\n") .def(py::init(), "probability"_a = 0.5, "length"_a = 8, "clock"_a = nullptr, "explore"_a = nullptr, "generate"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "RandomImpulse", "Generate random impulses at the given frequency, with either uniform or poisson distribution.") + py::class_>(m, "RandomImpulse", "Generate random impulses at the given frequency, with either uniform\nor poisson distribution.\n") .def(py::init(), "frequency"_a = 1.0, "distribution"_a = SIGNALFLOW_EVENT_DISTRIBUTION_UNIFORM, "reset"_a = nullptr) .def(py::init(), "frequency"_a, "distribution"_a, "reset"_a = nullptr); - py::class_>(m, "RandomUniform", "Generates a uniformly random value between min/max. If a clock is passed, only picks a new value on a clock tick.") + py::class_>(m, "RandomUniform", "Generates a uniformly random value between min/max.\nIf a clock is passed, only picks a new value on a clock tick.\n") .def(py::init(), "min"_a = 0.0, "max"_a = 1.0, "clock"_a = nullptr, "reset"_a = nullptr); - py::class_>(m, "WhiteNoise", "Generates whitenoise between min/max. If frequency is zero, generates at audio rate. For frequencies lower than audio rate, interpolate applies linear interpolation between values, and random_interval specifies whether new random values should be equally-spaced or randomly-spaced.") + py::class_>(m, "WhiteNoise", "Generates whitenoise between min/max. If frequency is zero, generates at\naudio rate. For frequencies lower than audio rate, interpolate applies linear\ninterpolation between values, and random_interval specifies whether new\nrandom values should be equally-spaced or randomly-spaced.\n") .def(py::init(), "frequency"_a = 0.0, "min"_a = -1.0, "max"_a = 1.0, "interpolate"_a = true, "random_interval"_a = true, "reset"_a = nullptr); } From f6f827f439c221b91993fa38e81d2c0a3491f06c Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 09:48:25 +0100 Subject: [PATCH 07/33] miniaudio: Fix warnings --- source/src/node/io/input/miniaudio.cpp | 10 ++++++++-- source/src/node/io/output/miniaudio.cpp | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index 74dffd7b..48036622 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -28,9 +28,9 @@ void read_callback(ma_device *pDevice, // TODO: the number of channels at the mic input might not be the same as the number of channels of this device int num_channels = input_node->get_num_output_channels(); - for (int frame = 0; frame < frameCount; frame++) + for (unsigned int frame = 0; frame < frameCount; frame++) { - for (int channel = 0; channel < num_channels; channel++) + for (unsigned int channel = 0; channel < num_channels; channel++) { input_node->out[channel][frame] = input_samples[frame * num_channels + channel]; } @@ -76,6 +76,8 @@ int AudioIn_MiniAudio::init() << std::endl; this->start(); + + return 0; } int AudioIn_MiniAudio::start() @@ -85,6 +87,8 @@ int AudioIn_MiniAudio::start() { throw std::runtime_error("miniaudio: Error starting device"); } + + return 0; } int AudioIn_MiniAudio::stop() @@ -94,6 +98,8 @@ int AudioIn_MiniAudio::stop() { throw std::runtime_error("miniaudio: Error stopping device"); } + + return 0; } int AudioIn_MiniAudio::destroy() diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index 24da2bc0..0a121471 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -51,9 +51,9 @@ void data_callback(ma_device *pDevice, } NodeRef output = shared_graph->get_output(); - for (int frame = 0; frame < frame_count; frame++) + for (unsigned int frame = 0; frame < frame_count; frame++) { - for (int channel = 0; channel < channel_count; channel += 1) + for (unsigned int channel = 0; channel < channel_count; channel += 1) { output_pointer[channel_count * frame + channel] = output->out[channel][frame]; } @@ -138,7 +138,7 @@ int AudioOut_MiniAudio::init() int selected_device_index = -1; if (!this->device_name.empty()) { - for (int i = 0; i < playback_device_count; i++) + for (unsigned int i = 0; i < playback_device_count; i++) { if (strncmp(playback_devices[i].name, device_name.c_str(), strlen(device_name.c_str())) == 0) { @@ -239,7 +239,7 @@ std::list AudioOut_MiniAudio::get_output_device_names() { throw std::runtime_error("miniaudio: Failure querying audio devices"); } - for (int i = 0; i < playback_device_count; i++) + for (unsigned int i = 0; i < playback_device_count; i++) { device_names.push_back(std::string(playback_devices[i].name)); } @@ -265,7 +265,7 @@ std::list AudioOut_MiniAudio::get_output_backend_names() { throw std::runtime_error("miniaudio: Failure querying backend devices"); } - for (int i = 0; i < enabled_backend_count; i++) + for (unsigned int i = 0; i < enabled_backend_count; i++) { std::string backend_name = std::string(ma_get_backend_name(enabled_backends[i])); if (backend_name != "Custom" && backend_name != "Null") From 6ac2efad64e416b6516be53385be31c28f483acc Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 15:37:24 +0100 Subject: [PATCH 08/33] miniaudio: Further integration; retire old audio hardware layers --- .../signalflow/node/io/input/abstract.h | 10 +- .../signalflow/node/io/input/miniaudio.h | 17 +- .../signalflow/node/io/input/soundio.h | 40 -- .../signalflow/node/io/output/abstract.h | 8 +- .../include/signalflow/node/io/output/dummy.h | 8 +- .../include/signalflow/node/io/output/ios.h | 33 -- .../signalflow/node/io/output/miniaudio.h | 26 +- .../signalflow/node/io/output/soundio.h | 48 --- source/include/signalflow/signalflow.h | 1 - source/src/core/graph.cpp | 7 +- source/src/node/io/input/abstract.cpp | 6 - source/src/node/io/input/miniaudio.cpp | 35 +- source/src/node/io/output/abstract.cpp | 6 +- source/src/node/io/output/ios.mm | 63 ---- source/src/node/io/output/miniaudio.cpp | 131 +++---- source/src/node/io/output/soundio.cpp | 343 ------------------ 16 files changed, 124 insertions(+), 658 deletions(-) delete mode 100644 source/include/signalflow/node/io/input/soundio.h delete mode 100644 source/include/signalflow/node/io/output/ios.h delete mode 100644 source/include/signalflow/node/io/output/soundio.h delete mode 100644 source/src/node/io/output/ios.mm delete mode 100644 source/src/node/io/output/soundio.cpp diff --git a/source/include/signalflow/node/io/input/abstract.h b/source/include/signalflow/node/io/input/abstract.h index a1c28beb..500a8be4 100644 --- a/source/include/signalflow/node/io/input/abstract.h +++ b/source/include/signalflow/node/io/input/abstract.h @@ -7,16 +7,18 @@ namespace signalflow { + class AudioIn_Abstract : public Node { public: AudioIn_Abstract(); - virtual int init() = 0; - virtual int start() = 0; - virtual int stop() = 0; - virtual int destroy() = 0; + virtual void init() = 0; + virtual void start() = 0; + virtual void stop() = 0; + virtual void destroy() = 0; virtual void process(Buffer &out, int num_samples) = 0; }; + } diff --git a/source/include/signalflow/node/io/input/miniaudio.h b/source/include/signalflow/node/io/input/miniaudio.h index 8ee00902..ec836e05 100644 --- a/source/include/signalflow/node/io/input/miniaudio.h +++ b/source/include/signalflow/node/io/input/miniaudio.h @@ -1,8 +1,5 @@ #pragma once -#define AudioIn AudioIn_MiniAudio - -#include #include #include "abstract.h" @@ -13,15 +10,15 @@ namespace signalflow { -class AudioIn_MiniAudio : public AudioIn_Abstract +class AudioIn : public AudioIn_Abstract { public: - AudioIn_MiniAudio(unsigned int num_channels = 1); - virtual ~AudioIn_MiniAudio() override; - virtual int init() override; - virtual int start() override; - virtual int stop() override; - virtual int destroy() override; + AudioIn(unsigned int num_channels = 1); + virtual ~AudioIn() override; + virtual void init() override; + virtual void start() override; + virtual void stop() override; + virtual void destroy() override; virtual void process(Buffer &out, int num_samples) override; private: diff --git a/source/include/signalflow/node/io/input/soundio.h b/source/include/signalflow/node/io/input/soundio.h deleted file mode 100644 index 90e4e273..00000000 --- a/source/include/signalflow/node/io/input/soundio.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#ifdef HAVE_SOUNDIO - -#define AudioIn AudioIn_SoundIO - -#include -#include - -#include "abstract.h" - -#include "signalflow/core/graph.h" - -namespace signalflow -{ - -class AudioIn_SoundIO : public AudioIn_Abstract -{ -public: - AudioIn_SoundIO(unsigned int num_channels = 1); - virtual ~AudioIn_SoundIO() override; - virtual int init() override; - virtual int start() override; - virtual int stop() override; - virtual int destroy() override; - virtual void process(Buffer &out, int num_samples) override; - - struct SoundIo *soundio; - struct SoundIoDevice *device; - struct SoundIoInStream *instream; - - Buffer *buffer; - int read_pos; - int write_pos; - unsigned int num_channels_requested; -}; - -} - -#endif diff --git a/source/include/signalflow/node/io/output/abstract.h b/source/include/signalflow/node/io/output/abstract.h index a891e4a2..2b4e130c 100644 --- a/source/include/signalflow/node/io/output/abstract.h +++ b/source/include/signalflow/node/io/output/abstract.h @@ -13,10 +13,10 @@ class AudioOut_Abstract : public Node AudioOut_Abstract(); virtual void process(Buffer &out, int num_samples); - virtual int init() = 0; - virtual int start() = 0; - virtual int stop() = 0; - virtual int destroy() = 0; + virtual void init() = 0; + virtual void start() = 0; + virtual void stop() = 0; + virtual void destroy() = 0; virtual void add_input(NodeRef node); virtual void remove_input(NodeRef node); diff --git a/source/include/signalflow/node/io/output/dummy.h b/source/include/signalflow/node/io/output/dummy.h index 73e102c7..7d79f7b9 100644 --- a/source/include/signalflow/node/io/output/dummy.h +++ b/source/include/signalflow/node/io/output/dummy.h @@ -11,10 +11,10 @@ class AudioOut_Dummy : public AudioOut_Abstract public: AudioOut_Dummy(int num_channels = 2, int buffer_size = 256); - virtual int init() { return 0; } - virtual int start() { return 0; } - virtual int stop() { return 0; } - virtual int destroy() { return 0; } + virtual void init() {} + virtual void start() {} + virtual void stop() {} + virtual void destroy() {} }; REGISTER(AudioOut_Dummy, "audioout-dummy") diff --git a/source/include/signalflow/node/io/output/ios.h b/source/include/signalflow/node/io/output/ios.h deleted file mode 100644 index bdc83f01..00000000 --- a/source/include/signalflow/node/io/output/ios.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "signalflow/core/platform.h" - -#if __APPLE__ - -#if TARGET_OS_IPHONE - -#define AudioOut AudioOut_iOS - -#include "abstract.h" - -#include "signalflow/core/graph.h" -#include "signalflow/node/node.h" - -namespace signalflow -{ - -class AudioOut_iOS : public AudioOut_Abstract -{ -public: - AudioOut_iOS(AudioGraph *graph); - - virtual int init() override; - virtual int start() override; - virtual int close() override; -}; - -} // namespace signalflow - -#endif - -#endif diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index 990caaa1..e242209c 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -1,7 +1,5 @@ #pragma once -#define AudioOut AudioOut_MiniAudio - #include #include @@ -14,24 +12,28 @@ namespace signalflow { -class AudioOut_MiniAudio : public AudioOut_Abstract +class AudioOut : public AudioOut_Abstract { public: - AudioOut_MiniAudio(const std::string &backend_name = "", - const std::string &device_name = "", - unsigned int sample_rate = 0, - unsigned int buffer_size = 0); + AudioOut(const std::string &backend_name = "", + const std::string &device_name = "", + unsigned int sample_rate = 0, + unsigned int buffer_size = 0); - virtual int init() override; - virtual int start() override; - virtual int stop() override; - virtual int destroy() override; + virtual void init() override; + virtual void start() override; + virtual void stop() override; + virtual void destroy() override; std::list get_output_device_names(); std::list get_output_backend_names(); int get_default_output_device_index(); private: + /*-------------------------------------------------------------------------------- + * Initialise a new miniaudio context, using the specified backend name if + * present, or the default backend otherwise. + *-------------------------------------------------------------------------------*/ void init_context(ma_context *context); std::string backend_name; @@ -40,6 +42,6 @@ class AudioOut_MiniAudio : public AudioOut_Abstract ma_device device; }; -REGISTER(AudioOut_MiniAudio, "audioout-miniaudio") +REGISTER(AudioOut, "audioout") } // namespace signalflow diff --git a/source/include/signalflow/node/io/output/soundio.h b/source/include/signalflow/node/io/output/soundio.h deleted file mode 100644 index 518cc988..00000000 --- a/source/include/signalflow/node/io/output/soundio.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#ifdef HAVE_SOUNDIO - -#define AudioOut AudioOut_SoundIO - -#include -#include - -#include "abstract.h" - -#include "signalflow/core/graph.h" -#include "signalflow/node/node.h" - -namespace signalflow -{ - -class AudioOut_SoundIO : public AudioOut_Abstract -{ -public: - AudioOut_SoundIO(const std::string &backend_name = "", - const std::string &device_name = "", - unsigned int sample_rate = 0, - unsigned int buffer_size = 0); - - virtual int init() override; - virtual int start() override; - virtual int stop() override; - virtual int destroy() override; - - std::list get_output_device_names(); - std::list get_output_backend_names(); - int get_default_output_device_index(); - - struct SoundIo *soundio; - struct SoundIoDevice *device; - struct SoundIoOutStream *outstream; - -private: - std::string backend_name; - std::string device_name; -}; - -REGISTER(AudioOut_SoundIO, "audioout-soundio") - -} // namespace signalflow - -#endif diff --git a/source/include/signalflow/signalflow.h b/source/include/signalflow/signalflow.h index 8d3ad2a0..64b20819 100644 --- a/source/include/signalflow/signalflow.h +++ b/source/include/signalflow/signalflow.h @@ -69,7 +69,6 @@ #include #include #include -#include #include /*------------------------------------------------------------------------ diff --git a/source/src/core/graph.cpp b/source/src/core/graph.cpp index ca93ec80..3627fb19 100644 --- a/source/src/core/graph.cpp +++ b/source/src/core/graph.cpp @@ -3,7 +3,6 @@ #include "signalflow/core/graph.h" #include "signalflow/node/io/output/abstract.h" #include "signalflow/node/io/output/dummy.h" -#include "signalflow/node/io/output/ios.h" #include "signalflow/node/io/output/miniaudio.h" #include "signalflow/node/node.h" #include "signalflow/node/oscillators/constant.h" @@ -49,7 +48,7 @@ AudioGraph::AudioGraph(AudioGraphConfig *config, std::string output_device, bool this->config = *config; } - if (output_device == "dummy") + if (output_device == "dummy" || this->config.get_output_device_name() == "dummy") { this->output = new AudioOut_Dummy(); } @@ -590,13 +589,13 @@ std::list AudioGraph::get_outputs() std::list AudioGraph::get_output_device_names() { - AudioOut_MiniAudio *output = (AudioOut_MiniAudio *) (this->output.get()); + AudioOut *output = (AudioOut *) (this->output.get()); return output->get_output_device_names(); } std::list AudioGraph::get_output_backend_names() { - AudioOut_MiniAudio *output = (AudioOut_MiniAudio *) (this->output.get()); + AudioOut *output = (AudioOut *) (this->output.get()); return output->get_output_backend_names(); } diff --git a/source/src/node/io/input/abstract.cpp b/source/src/node/io/input/abstract.cpp index 76a266e4..18d34b10 100644 --- a/source/src/node/io/input/abstract.cpp +++ b/source/src/node/io/input/abstract.cpp @@ -2,15 +2,9 @@ namespace signalflow { -AudioIn_Abstract *shared_in = nullptr; AudioIn_Abstract::AudioIn_Abstract() { - if (shared_in) - throw std::runtime_error("Multiple AudioIn nodes are not yet supported."); - - shared_in = this; - this->name = "audioin"; this->set_channels(0, 1); } diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index 48036622..47670f71 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -11,7 +11,8 @@ static bool is_processing = false; namespace signalflow { -extern AudioIn_Abstract *shared_in; + +AudioIn *shared_in; void read_callback(ma_device *pDevice, void *pOutput, @@ -20,7 +21,7 @@ void read_callback(ma_device *pDevice, { is_processing = true; - AudioIn_MiniAudio *input_node = (AudioIn_MiniAudio *) shared_in; + AudioIn *input_node = (AudioIn *) shared_in; if (!input_node) return; @@ -30,7 +31,7 @@ void read_callback(ma_device *pDevice, int num_channels = input_node->get_num_output_channels(); for (unsigned int frame = 0; frame < frameCount; frame++) { - for (unsigned int channel = 0; channel < num_channels; channel++) + for (int channel = 0; channel < num_channels; channel++) { input_node->out[channel][frame] = input_samples[frame * num_channels + channel]; } @@ -39,7 +40,7 @@ void read_callback(ma_device *pDevice, is_processing = false; } -AudioIn_MiniAudio::AudioIn_MiniAudio(unsigned int num_channels) +AudioIn::AudioIn(unsigned int num_channels) : AudioIn_Abstract() { this->name = "audioin-miniaudio"; @@ -47,13 +48,13 @@ AudioIn_MiniAudio::AudioIn_MiniAudio(unsigned int num_channels) this->init(); } -AudioIn_MiniAudio::~AudioIn_MiniAudio() +AudioIn::~AudioIn() { // TODO: call superclass destructor to set shared_in to null this->destroy(); } -int AudioIn_MiniAudio::init() +void AudioIn::init() { ma_device_config config = ma_device_config_init(ma_device_type_capture); config.capture.format = ma_format_f32; @@ -65,7 +66,7 @@ int AudioIn_MiniAudio::init() ma_result rv = ma_device_init(NULL, &config, &device); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error initialising input device"); + throw audio_io_exception("miniaudio: Error initialising input device"); } this->set_channels(0, device.capture.internalChannels); @@ -76,33 +77,27 @@ int AudioIn_MiniAudio::init() << std::endl; this->start(); - - return 0; } -int AudioIn_MiniAudio::start() +void AudioIn::start() { ma_result rv = ma_device_start(&device); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error starting device"); + throw audio_io_exception("miniaudio: Error starting device"); } - - return 0; } -int AudioIn_MiniAudio::stop() +void AudioIn::stop() { ma_result rv = ma_device_stop(&device); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error stopping device"); + throw audio_io_exception("miniaudio: Error stopping device"); } - - return 0; } -int AudioIn_MiniAudio::destroy() +void AudioIn::destroy() { while (is_processing) { @@ -110,11 +105,9 @@ int AudioIn_MiniAudio::destroy() this->stop(); shared_in = nullptr; - - return 0; } -void AudioIn_MiniAudio::process(Buffer &out, int num_samples) +void AudioIn::process(Buffer &out, int num_samples) { } diff --git a/source/src/node/io/output/abstract.cpp b/source/src/node/io/output/abstract.cpp index e766ada9..2862ad44 100644 --- a/source/src/node/io/output/abstract.cpp +++ b/source/src/node/io/output/abstract.cpp @@ -7,7 +7,6 @@ namespace signalflow AudioOut_Abstract::AudioOut_Abstract() { this->name = "audioout"; - // do we need to set num_output channels to allocate the right number of output buffers? this->set_channels(2, 0); this->no_input_upmix = true; this->has_variable_inputs = true; @@ -105,6 +104,11 @@ void AudioOut_Abstract::replace_input(NodeRef node, NodeRef other) void AudioOut_Abstract::set_channels(int num_input_channels, int num_output_channels) { Node::set_channels(num_input_channels, num_output_channels); + + /*-------------------------------------------------------------------------------- + * Typically, Node objects allocate an output buffer per output channel. + * In this unique case, allocate an output buffer per input channel. + *--------------------------------------------------------------------------------*/ this->resize_output_buffers(num_input_channels); } diff --git a/source/src/node/io/output/ios.mm b/source/src/node/io/output/ios.mm deleted file mode 100644 index 9eb2f2a0..00000000 --- a/source/src/node/io/output/ios.mm +++ /dev/null @@ -1,63 +0,0 @@ -#include "signalflow/node/io/output/ios.h" - -#if TARGET_OS_IPHONE - -#include "signalflow/core/graph.h" - -#ifdef __OBJC__ -#include "AudioIOManager.h" -#endif - -#include -#include -#include -#include -#include - - -namespace libsignal -{ - -extern AudioGraph *shared_graph; - -void audio_callback(float **data, int num_channels, int num_frames) -{ - shared_graph->pull_input(num_frames); - - for (int frame = 0; frame < num_frames; frame++) - { - for (int channel = 0; channel < num_channels; channel++) - { - data[channel][frame] = shared_graph->get_output()->out[channel][frame]; - } - } -} - -AudioOut_iOS::AudioOut_iOS(AudioGraph *graph) : AudioOut_Abstract(graph) -{ - this->init(); -} - -int AudioOut_iOS::init() -{ - AudioIOManager *ioManager = [[AudioIOManager alloc] initWithCallback:audio_callback]; - [ioManager start]; - - return 0; -} - -int AudioOut_iOS::start() -{ - return 0; -} - -int AudioOut_iOS::close() -{ - return 0; -} - - -} // namespace libsignal - -#endif /* TARGET_OS_IPHONE */ - diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index 0a121471..d4234bfc 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -20,29 +20,44 @@ namespace signalflow extern AudioGraph *shared_graph; -void data_callback(ma_device *pDevice, - void *pOutput, - const void *pInput, - ma_uint32 frame_count) +std::unordered_map possible_backend_names = { + { "wasapi", ma_backend_wasapi }, + { "dsound", ma_backend_dsound }, + { "ma_backend_winmm", ma_backend_winmm }, + { "coreaudio", ma_backend_coreaudio }, + { "sndio", ma_backend_sndio }, + { "audio4", ma_backend_audio4 }, + { "oss", ma_backend_oss }, + { "pulseaudio", ma_backend_pulseaudio }, + { "alsa", ma_backend_alsa }, + { "jack", ma_backend_jack }, + { "aaudio", ma_backend_aaudio }, + { "opensl", ma_backend_opensl }, + { "webaudio", ma_backend_webaudio }, + { "null", ma_backend_null }, +}; + +void data_callback(ma_device *ma_device_ptr, + void *ma_output_pointer, + const void *ma_input_pointer, + ma_uint32 ma_frame_count) { is_processing = true; - int channel_count = pDevice->playback.channels; + int channel_count = ma_device_ptr->playback.channels; /*-----------------------------------------------------------------------* - * Return if the shared_graph hasn't been initialized yet. - * (The libsoundio Pulse Audio driver calls the write_callback once - * on initialization, so this may happen legitimately.) + * Do nothing if the shared_graph hasn't been initialized yet. *-----------------------------------------------------------------------*/ if (!shared_graph || !shared_graph->get_output()) { return; } - float *output_pointer = (float *) pOutput; + float *output_pointer = (float *) ma_output_pointer; try { - shared_graph->render(frame_count); + shared_graph->render(ma_frame_count); } catch (const std::exception &e) { @@ -51,9 +66,9 @@ void data_callback(ma_device *pDevice, } NodeRef output = shared_graph->get_output(); - for (unsigned int frame = 0; frame < frame_count; frame++) + for (unsigned int frame = 0; frame < ma_frame_count; frame++) { - for (unsigned int channel = 0; channel < channel_count; channel += 1) + for (int channel = 0; channel < channel_count; channel += 1) { output_pointer[channel_count * frame + channel] = output->out[channel][frame]; } @@ -62,63 +77,46 @@ void data_callback(ma_device *pDevice, is_processing = false; } -AudioOut_MiniAudio::AudioOut_MiniAudio(const std::string &backend_name, - const std::string &device_name, - unsigned int sample_rate, - unsigned int buffer_size) +AudioOut::AudioOut(const std::string &backend_name, + const std::string &device_name, + unsigned int sample_rate, + unsigned int buffer_size) : AudioOut_Abstract() { this->backend_name = backend_name; this->device_name = device_name; this->sample_rate = sample_rate; this->buffer_size = buffer_size; - this->name = "audioout-miniaudio"; + this->name = "audioout"; this->init(); } -void AudioOut_MiniAudio::init_context(ma_context *context) +void AudioOut::init_context(ma_context *context) { if (!this->backend_name.empty()) { - std::unordered_map possible_backend_names = { - { "wasapi", ma_backend_wasapi }, - { "dsound", ma_backend_dsound }, - { "ma_backend_winmm", ma_backend_winmm }, - { "coreaudio", ma_backend_coreaudio }, - { "sndio", ma_backend_sndio }, - { "audio4", ma_backend_audio4 }, - { "oss", ma_backend_oss }, - { "pulseaudio", ma_backend_pulseaudio }, - { "alsa", ma_backend_alsa }, - { "jack", ma_backend_jack }, - { "aaudio", ma_backend_aaudio }, - { "opensl", ma_backend_opensl }, - { "webaudio", ma_backend_webaudio }, - { "null", ma_backend_null }, - }; - if (possible_backend_names.find(this->backend_name) == possible_backend_names.end()) { - throw std::runtime_error("miniaudio: Backend name not recognised: " + this->backend_name); + throw audio_io_exception("miniaudio: Backend name not recognised: " + this->backend_name); } ma_backend backend_name = possible_backend_names[this->backend_name]; if (ma_context_init(&backend_name, 1, NULL, context) != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error initialising context"); + throw audio_io_exception("miniaudio: Error initialising context"); } } else { if (ma_context_init(NULL, 0, NULL, context) != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error initialising context"); + throw audio_io_exception("miniaudio: Error initialising context"); } } } -int AudioOut_MiniAudio::init() +void AudioOut::init() { ma_device_config config = ma_device_config_init(ma_device_type_playback); @@ -144,14 +142,14 @@ int AudioOut_MiniAudio::init() { if (selected_device_index != -1) { - throw std::runtime_error("More than one audio device found matching name '" + device_name + "'"); + throw audio_io_exception("More than one audio device found matching name '" + device_name + "'"); } selected_device_index = i; } } if (selected_device_index == -1) { - throw std::runtime_error("No audio device found matching name '" + device_name + "'"); + throw audio_io_exception("No audio device found matching name '" + device_name + "'"); } config.playback.pDeviceID = &playback_devices[selected_device_index].id; @@ -160,13 +158,15 @@ int AudioOut_MiniAudio::init() // Set to ma_format_unknown to use the device's native format. config.playback.format = ma_format_f32; - // Set to 0 to use the device's native channel count / buffer size. + // Set to 0 to use the device's native channel count. config.playback.channels = 0; + + // Set to 0 to use the device's native buffer size. config.periodSizeInFrames = buffer_size; // Note that the underlying connection always uses the device's native sample rate. // Setting values other than zero instantiates miniaudio's internal resampler. - config.sampleRate = sample_rate; + config.sampleRate = this->sample_rate; config.dataCallback = data_callback; // Buffer blocks into a fixed number of frames @@ -175,50 +175,47 @@ int AudioOut_MiniAudio::init() rv = ma_device_init(NULL, &config, &device); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error initialising output device"); + throw audio_io_exception("miniaudio: Error initialising output device"); } - this->sample_rate = device.playback.internalSampleRate; this->set_channels(device.playback.internalChannels, 0); std::string s = device.playback.internalChannels == 1 ? "" : "s"; std::cerr << "[miniaudio] Output device: " << std::string(device.playback.name) << " (" << device.playback.internalSampleRate << "Hz, " << "buffer size " << device.playback.internalPeriodSizeInFrames << " samples, " << device.playback.internalChannels << " channel" << s << ")" << std::endl; - - return 0; } -int AudioOut_MiniAudio::start() +void AudioOut::start() { ma_result rv = ma_device_start(&device); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Error starting output device"); + throw audio_io_exception("miniaudio: Error starting output device"); } this->set_state(SIGNALFLOW_NODE_STATE_ACTIVE); - return 0; } -int AudioOut_MiniAudio::stop() +void AudioOut::stop() { - // TODO + ma_result rv = ma_device_stop(&device); + if (rv != MA_SUCCESS) + { + throw audio_io_exception("miniaudio: Error stopping output device"); + } this->set_state(SIGNALFLOW_NODE_STATE_STOPPED); - return 0; } -int AudioOut_MiniAudio::destroy() +void AudioOut::destroy() { while (is_processing) { } ma_device_uninit(&device); - - return 0; } -std::list AudioOut_MiniAudio::get_output_device_names() +std::list AudioOut::get_output_device_names() { std::list device_names; @@ -237,7 +234,7 @@ std::list AudioOut_MiniAudio::get_output_device_names() &capture_device_count); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Failure querying audio devices"); + throw audio_io_exception("miniaudio: Failure querying audio devices"); } for (unsigned int i = 0; i < playback_device_count; i++) { @@ -247,13 +244,13 @@ std::list AudioOut_MiniAudio::get_output_device_names() return device_names; } -int AudioOut_MiniAudio::get_default_output_device_index() +int AudioOut::get_default_output_device_index() { // TODO: Is this even used? return -1; } -std::list AudioOut_MiniAudio::get_output_backend_names() +std::list AudioOut::get_output_backend_names() { std::list backend_names; ma_backend enabled_backends[MA_BACKEND_COUNT]; @@ -263,14 +260,20 @@ std::list AudioOut_MiniAudio::get_output_backend_names() rv = ma_get_enabled_backends(enabled_backends, MA_BACKEND_COUNT, &enabled_backend_count); if (rv != MA_SUCCESS) { - throw std::runtime_error("miniaudio: Failure querying backend devices"); + throw audio_io_exception("miniaudio: Failure querying backend devices"); } for (unsigned int i = 0; i < enabled_backend_count; i++) { - std::string backend_name = std::string(ma_get_backend_name(enabled_backends[i])); - if (backend_name != "Custom" && backend_name != "Null") + for (auto pair : possible_backend_names) { - backend_names.push_back(backend_name); + if (pair.second == enabled_backends[i]) + { + std::string backend_name = pair.first; + if (backend_name != "null") + { + backend_names.push_back(backend_name); + } + } } } diff --git a/source/src/node/io/output/soundio.cpp b/source/src/node/io/output/soundio.cpp deleted file mode 100644 index bb8383de..00000000 --- a/source/src/node/io/output/soundio.cpp +++ /dev/null @@ -1,343 +0,0 @@ -#include "signalflow/node/io/output/soundio.h" - -#ifdef HAVE_SOUNDIO - -#include "signalflow/core/graph.h" - -#include -#include -#include -#include -#include -#include - -static bool is_processing = false; - -namespace signalflow -{ - -extern AudioGraph *shared_graph; - -void write_callback(struct SoundIoOutStream *outstream, int frame_count_min, int frame_count_max) -{ - is_processing = true; - - const struct SoundIoChannelLayout *layout = &outstream->layout; - struct SoundIoChannelArea *areas; - int frame_count = frame_count_max; - int frames_left = frame_count_max; - - /*-----------------------------------------------------------------------* - * Return if the shared_graph hasn't been initialized yet. - * (The libsoundio Pulse Audio driver calls the write_callback once - * on initialization, so this may happen legitimately.) - *-----------------------------------------------------------------------*/ - if (!shared_graph || !shared_graph->get_output()) - { - return; - } - - AudioOut_SoundIO *out_node = (AudioOut_SoundIO *) outstream->userdata; - - /*-----------------------------------------------------------------------* - * On some drivers (eg Linux), we cannot write all samples at once. - * Keep writing as many as we can until we have cleared the buffer. - *-----------------------------------------------------------------------*/ - while (frames_left > 0) - { - int err; - - if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count))) - { - throw audio_io_exception("libsoundio error on begin write: " + std::string(soundio_strerror(err))); - } - if (out_node->get_state() == SIGNALFLOW_NODE_STATE_ACTIVE) - { - try - { - shared_graph->render(frame_count); - } - catch (const std::exception &e) - { - std::cerr << "Exception in AudioGraph: " << e.what() << std::endl; - exit(1); - } - - NodeRef output = shared_graph->get_output(); - for (int frame = 0; frame < frame_count; frame++) - { - for (int channel = 0; channel < layout->channel_count; channel += 1) - { - if (outstream->format == SoundIoFormatFloat32NE) - { - float *ptr = reinterpret_cast(areas[channel].ptr + areas[channel].step * frame); - *ptr = output->out[channel][frame]; - /*-----------------------------------------------------------------------* - * Hard limiter. - *-----------------------------------------------------------------------*/ - if (*ptr > 1.0) - *ptr = 1.0; - if (*ptr < -1.0) - *ptr = -1.0; - } - else if (outstream->format == SoundIoFormatS16LE) - { - int16_t *ptr = reinterpret_cast(areas[channel].ptr + areas[channel].step * frame); - *ptr = (int16_t)(output->out[channel][frame] * 32768.0f); - } - } - } - } - else - { - for (int frame = 0; frame < frame_count; frame++) - { - for (int channel = 0; channel < layout->channel_count; channel += 1) - { - float *ptr = reinterpret_cast(areas[channel].ptr + areas[channel].step * frame); - *ptr = 0; - } - } - } - - if ((err = soundio_outstream_end_write(outstream))) - { - throw audio_io_exception("libsoundio error on end write: " + std::string(soundio_strerror(err))); - } - - frames_left -= frame_count; - } - - is_processing = false; -} - -int soundio_get_device_by_name(struct SoundIo *soundio, const char *name) -{ - int output_count = soundio_output_device_count(soundio); - for (int i = 0; i < output_count; i++) - { - struct SoundIoDevice *device = soundio_get_output_device(soundio, i); - if (strcmp(device->name, name) == 0) - { - return i; - } - } - std::cerr << "Couldn't find output device " << std::string(name) << std::endl; - - return -1; -} - -AudioOut_SoundIO::AudioOut_SoundIO(const std::string &backend_name, - const std::string &device_name, - unsigned int sample_rate, - unsigned int buffer_size) - : AudioOut_Abstract() -{ - this->backend_name = backend_name; - this->device_name = device_name; - this->sample_rate = sample_rate; - this->buffer_size = buffer_size; - this->name = "audioout-soundio"; - - this->init(); -} - -int AudioOut_SoundIO::init() -{ - int err; - - this->soundio = soundio_create(); - - if (!this->soundio) - throw audio_io_exception("libsoundio error: out of memory"); - - if (!this->backend_name.empty()) - { - // Backend name is specified; connect to the given backend - std::vector possible_backend_names = { - "none", - "jack", - "pulseaudio", - "alsa", - "coreaudio", - "wasapi", - "dummy" - }; - - auto location = std::find(possible_backend_names.begin(), - possible_backend_names.end(), - this->backend_name); - - if (location == possible_backend_names.end()) - { - throw audio_io_exception("libsoundio error: could not find backend name " + this->backend_name); - } - enum SoundIoBackend backend_index = (enum SoundIoBackend)(location - possible_backend_names.begin()); - if ((err = soundio_connect_backend(this->soundio, backend_index))) - throw audio_io_exception("libsoundio error: could not connect (" + std::string(soundio_strerror(err)) + ")"); - } - else - { - if ((err = soundio_connect(this->soundio))) - throw audio_io_exception("libsoundio error: could not connect (" + std::string(soundio_strerror(err)) + ")"); - } - - soundio_flush_events(this->soundio); - - int default_out_device_index = soundio_default_output_device_index(this->soundio); - if (default_out_device_index < 0) - throw device_not_found_exception("No audio devices were found. More information: https://signalflow.dev/troubleshooting/device_not_found_exception/"); - - if (!this->device_name.empty()) - { - int index = soundio_get_device_by_name(this->soundio, this->device_name.c_str()); - if (index == -1) - { - throw device_not_found_exception("Could not find device name: " + this->device_name + ". More information: https://signalflow.dev/troubleshooting/device_not_found_exception/"); - } - this->device = soundio_get_output_device(this->soundio, index); - } - else - { - this->device = soundio_get_output_device(this->soundio, default_out_device_index); - } - - if (!device) - throw audio_io_exception("libsoundio error: out of memory."); - - this->outstream = soundio_outstream_create(device); - if (soundio_device_supports_format(device, SoundIoFormatFloat32NE)) - { - this->outstream->format = SoundIoFormatFloat32NE; - } - else if (soundio_device_supports_format(device, SoundIoFormatS16LE)) - { - this->outstream->format = SoundIoFormatS16LE; - } - else - { - /*-----------------------------------------------------------------------* - * SignalFlow currently only supports float32 sample output - *-----------------------------------------------------------------------*/ - throw audio_io_exception("libsoundio error: Output device does not support float32 or int16le samples"); - } - this->outstream->write_callback = write_callback; - if (!this->sample_rate) - { - this->sample_rate = this->device->sample_rate_current; - } - this->outstream->sample_rate = this->sample_rate; - this->outstream->software_latency = (double) this->buffer_size / this->outstream->sample_rate; - this->outstream->userdata = (void *) this; - // With a device with multiple layouts, use the first. - // To check: is this always the layout with the most channels? - this->outstream->layout = device->layouts[0]; - - if ((err = soundio_outstream_open(this->outstream))) - { - throw audio_io_exception("libsoundio error: unable to open device: " + std::string(soundio_strerror(err))); - } - - if (this->outstream->layout_error) - { - /*-------------------------------------------------------------------------------- - * This should not be a fatal error (see example in libsoundio sio_sine.c). - * Should just generate a warning instead. - * Experienced on Raspberry Pi 4 with raspi-audio interface. - *-------------------------------------------------------------------------------*/ - std::cerr << "libsoundio warning: unable to set channel layout: " - << std::string(soundio_strerror(this->outstream->layout_error)) << std::endl; - } - - this->num_output_channels = this->outstream->layout.channel_count; - - // update based on the actual buffer size - this->buffer_size = (int) round(this->outstream->software_latency * this->outstream->sample_rate); - - std::string s = num_output_channels == 1 ? "" : "s"; - - std::cerr << "[soundio] Output device: " << device->name << " (" << sample_rate << "Hz, " - << "buffer size " << buffer_size << " samples, " << num_output_channels << " channel" << s << ")" - << std::endl; - - // do we need to set num_output channels to allocate the right number of output buffers? - this->set_channels(num_output_channels, 0); - - return 0; -} - -int AudioOut_SoundIO::start() -{ - int err; - if ((err = soundio_outstream_start(outstream))) - throw audio_io_exception("libsoundio error: unable to start device: " + std::string(soundio_strerror(err))); - this->set_state(SIGNALFLOW_NODE_STATE_ACTIVE); - - return 0; -} - -int AudioOut_SoundIO::stop() -{ - this->set_state(SIGNALFLOW_NODE_STATE_STOPPED); - return 0; -} - -int AudioOut_SoundIO::destroy() -{ - while (is_processing) - { - } - - soundio_outstream_destroy(this->outstream); - soundio_device_unref(this->device); - soundio_destroy(this->soundio); - - return 0; -} - -std::list AudioOut_SoundIO::get_output_device_names() -{ - int output_count = soundio_output_device_count(this->soundio); - - std::list device_names; - - for (int i = 0; i < output_count; i++) - { - struct SoundIoDevice *device = soundio_get_output_device(soundio, i); - device_names.push_back(std::string(device->name)); - } - - return device_names; -} - -int AudioOut_SoundIO::get_default_output_device_index() -{ - unsigned int default_output = soundio_default_output_device_index(this->soundio); - return default_output; -} - -std::list AudioOut_SoundIO::get_output_backend_names() -{ - std::list backend_names; - std::vector possible_backend_names = { - "none", - "jack", - "pulseaudio", - "alsa", - "coreaudio", - "wasapi", - "dummy" - }; - for (int i = 0; i < soundio_backend_count(this->soundio); i++) - { - int backend_index = soundio_get_backend(this->soundio, i); - std::string backend_name = possible_backend_names[backend_index]; - backend_names.push_back(backend_name); - } - - return backend_names; -} - -} // namespace signalflow - -#endif From bf474363a91fb7ac9ac2133307f1a907c60565eb Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 15:43:01 +0100 Subject: [PATCH 09/33] miniaudio: Further integration; retire old audio hardware layers --- auxiliary/libs/signalflow_cli/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auxiliary/libs/signalflow_cli/__init__.py b/auxiliary/libs/signalflow_cli/__init__.py index 904fb73b..32b1243e 100755 --- a/auxiliary/libs/signalflow_cli/__init__.py +++ b/auxiliary/libs/signalflow_cli/__init__.py @@ -62,7 +62,7 @@ def run_list_output_device_names(output_backend_name: str = None): config = AudioGraphConfig() if output_backend_name: config.output_backend_name = output_backend_name - config.output_device_name = "" + # config.output_device_name = "dummy" graph = AudioGraph(config=config, start=False) print("Available output device names:") for name in graph.output_device_names: @@ -71,8 +71,8 @@ def run_list_output_device_names(output_backend_name: str = None): def run_list_output_backend_names(): config = AudioGraphConfig() - config.output_backend_name = "dummy" - config.output_device_name = "" + config.output_backend_name = "null" + config.output_device_name = "dummy" graph = AudioGraph(config=config, start=False) print("Available output backend names:") for name in graph.output_backend_names: From 6b60469a1c3eff24afb586c1d59457ebd0a92bbf Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 16:45:31 +0100 Subject: [PATCH 10/33] miniaudio: Switch to using static methods for querying backends/devices --- auxiliary/libs/signalflow_cli/__init__.py | 17 +-- source/include/signalflow/core/graph.h | 4 +- .../signalflow/node/io/output/miniaudio.h | 7 +- source/src/core/graph.cpp | 10 +- source/src/node/io/output/miniaudio.cpp | 76 +++++++------ source/src/python/graph.cpp | 107 +++++++----------- 6 files changed, 102 insertions(+), 119 deletions(-) diff --git a/auxiliary/libs/signalflow_cli/__init__.py b/auxiliary/libs/signalflow_cli/__init__.py index 32b1243e..b84d5766 100755 --- a/auxiliary/libs/signalflow_cli/__init__.py +++ b/auxiliary/libs/signalflow_cli/__init__.py @@ -58,24 +58,17 @@ def run_version(): print(signalflow.__version__) -def run_list_output_device_names(output_backend_name: str = None): - config = AudioGraphConfig() - if output_backend_name: - config.output_backend_name = output_backend_name - # config.output_device_name = "dummy" - graph = AudioGraph(config=config, start=False) +def run_list_output_device_names(backend_name: str = None): + output_device_names = AudioGraph.get_output_device_names(backend_name) print("Available output device names:") - for name in graph.output_device_names: + for name in output_device_names: print(" - %s" % name) def run_list_output_backend_names(): - config = AudioGraphConfig() - config.output_backend_name = "null" - config.output_device_name = "dummy" - graph = AudioGraph(config=config, start=False) + output_backend_names = AudioGraph.get_output_backend_names() print("Available output backend names:") - for name in graph.output_backend_names: + for name in output_backend_names: print(" - %s" % name) diff --git a/source/include/signalflow/core/graph.h b/source/include/signalflow/core/graph.h index 9892aff3..b47c710b 100644 --- a/source/include/signalflow/core/graph.h +++ b/source/include/signalflow/core/graph.h @@ -199,7 +199,7 @@ class AudioGraph * @return The list of device names. * *--------------------------------------------------------------------------------*/ - std::list get_output_device_names(); + static std::list get_output_device_names(std::string backend_name = ""); /**-------------------------------------------------------------------------------- * Returns a list of available audio I/O output backends. @@ -207,7 +207,7 @@ class AudioGraph * @return The list of backend names. * *--------------------------------------------------------------------------------*/ - std::list get_output_backend_names(); + static std::list get_output_backend_names(); /**-------------------------------------------------------------------------------- * Schedule a node for rendering without connecting the node to the graph's output. diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index e242209c..9b79cc40 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -25,16 +25,15 @@ class AudioOut : public AudioOut_Abstract virtual void stop() override; virtual void destroy() override; - std::list get_output_device_names(); - std::list get_output_backend_names(); - int get_default_output_device_index(); + static std::list get_output_device_names(std::string backend_name = ""); + static std::list get_output_backend_names(); private: /*-------------------------------------------------------------------------------- * Initialise a new miniaudio context, using the specified backend name if * present, or the default backend otherwise. *-------------------------------------------------------------------------------*/ - void init_context(ma_context *context); + static void init_context(ma_context *context, std::string backend_name = ""); std::string backend_name; std::string device_name; diff --git a/source/src/core/graph.cpp b/source/src/core/graph.cpp index 3627fb19..65babff8 100644 --- a/source/src/core/graph.cpp +++ b/source/src/core/graph.cpp @@ -587,16 +587,16 @@ std::list AudioGraph::get_outputs() return output->get_inputs(); } -std::list AudioGraph::get_output_device_names() +// static +std::list AudioGraph::get_output_device_names(std::string backend_name) { - AudioOut *output = (AudioOut *) (this->output.get()); - return output->get_output_device_names(); + return AudioOut::get_output_device_names(backend_name); } +// static std::list AudioGraph::get_output_backend_names() { - AudioOut *output = (AudioOut *) (this->output.get()); - return output->get_output_backend_names(); + return AudioOut::get_output_backend_names(); } NodeRef AudioGraph::add_node(NodeRef node) diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index d4234bfc..258b0066 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -92,30 +92,6 @@ AudioOut::AudioOut(const std::string &backend_name, this->init(); } -void AudioOut::init_context(ma_context *context) -{ - if (!this->backend_name.empty()) - { - if (possible_backend_names.find(this->backend_name) == possible_backend_names.end()) - { - throw audio_io_exception("miniaudio: Backend name not recognised: " + this->backend_name); - } - ma_backend backend_name = possible_backend_names[this->backend_name]; - - if (ma_context_init(&backend_name, 1, NULL, context) != MA_SUCCESS) - { - throw audio_io_exception("miniaudio: Error initialising context"); - } - } - else - { - if (ma_context_init(NULL, 0, NULL, context) != MA_SUCCESS) - { - throw audio_io_exception("miniaudio: Error initialising context"); - } - } -} - void AudioOut::init() { ma_device_config config = ma_device_config_init(ma_device_type_playback); @@ -126,7 +102,7 @@ void AudioOut::init() ma_uint32 capture_device_count; ma_result rv; - this->init_context(&this->context); + AudioOut::init_context(&this->context, this->backend_name); rv = ma_context_get_devices(&this->context, &playback_devices, @@ -180,6 +156,18 @@ void AudioOut::init() this->set_channels(device.playback.internalChannels, 0); + /*-------------------------------------------------------------------------------- + * If no specified sample rate was given, update AudioOut's sample rate to + * reflect the actual underlying sample rate. + * + * Otherwise, SignalFlow will use the user-specified sample rate, and miniaudio + * will perform sample-rate conversion. + *-------------------------------------------------------------------------------*/ + if (this->sample_rate == 0) + { + this->sample_rate = device.playback.internalSampleRate; + } + std::string s = device.playback.internalChannels == 1 ? "" : "s"; std::cerr << "[miniaudio] Output device: " << std::string(device.playback.name) << " (" << device.playback.internalSampleRate << "Hz, " << "buffer size " << device.playback.internalPeriodSizeInFrames << " samples, " << device.playback.internalChannels << " channel" << s << ")" @@ -215,7 +203,32 @@ void AudioOut::destroy() ma_device_uninit(&device); } -std::list AudioOut::get_output_device_names() +// static +void AudioOut::init_context(ma_context *context, std::string backend_name) +{ + if (!backend_name.empty()) + { + if (possible_backend_names.find(backend_name) == possible_backend_names.end()) + { + throw audio_io_exception("miniaudio: Backend name not recognised: " + backend_name); + } + ma_backend backend = possible_backend_names[backend_name]; + + if (ma_context_init(&backend, 1, NULL, context) != MA_SUCCESS) + { + throw audio_io_exception("miniaudio: Error initialising context"); + } + } + else + { + if (ma_context_init(NULL, 0, NULL, context) != MA_SUCCESS) + { + throw audio_io_exception("miniaudio: Error initialising context"); + } + } +} + +std::list AudioOut::get_output_device_names(std::string backend_name) { std::list device_names; @@ -225,7 +238,8 @@ std::list AudioOut::get_output_device_names() ma_device_info *capture_devices; ma_uint32 capture_device_count; ma_context context; - this->init_context(&context); + + AudioOut::init_context(&context, backend_name); rv = ma_context_get_devices(&context, &playback_devices, @@ -241,13 +255,9 @@ std::list AudioOut::get_output_device_names() device_names.push_back(std::string(playback_devices[i].name)); } - return device_names; -} + ma_context_uninit(&context); -int AudioOut::get_default_output_device_index() -{ - // TODO: Is this even used? - return -1; + return device_names; } std::list AudioOut::get_output_backend_names() diff --git a/source/src/python/graph.cpp b/source/src/python/graph.cpp index a6c82393..70d3a2c0 100644 --- a/source/src/python/graph.cpp +++ b/source/src/python/graph.cpp @@ -36,10 +36,6 @@ void init_python_graph(py::module &m) .def_property_readonly( "outputs", &AudioGraph::get_outputs, R"pbdoc(int: Get the list of Node objects currently connected to the graph's output.)pbdoc") - .def_property_readonly("output_device_names", &AudioGraph::get_output_device_names, - R"pbdoc(list[str]: List the available output device names.)pbdoc") - .def_property_readonly("output_backend_names", &AudioGraph::get_output_backend_names, - R"pbdoc(list[str]: List the available output backend names.)pbdoc") .def_property_readonly( "status", &AudioGraph::get_status, R"pbdoc(int: Get a text representation of the AudioGraph's status (node count, patch count, CPU usage).)pbdoc") @@ -50,41 +46,40 @@ void init_python_graph(py::module &m) R"pbdoc(int: Get/set the graph's sample rate.)pbdoc") .def_property("output", &AudioGraph::get_output, &AudioGraph::set_output) + /*-------------------------------------------------------------------------------- + * Static methods + *-------------------------------------------------------------------------------*/ + .def_static( + "get_output_device_names", [](py::object backend_name) { + std::string backend_name_str = backend_name.is_none() ? "" : backend_name.cast(); + return AudioGraph::get_output_device_names(backend_name_str); + }, + "backend_name"_a = "", R"pbdoc(list[str]: List the available output device names.)pbdoc") + .def_static("get_output_backend_names", &AudioGraph::get_output_backend_names, R"pbdoc(list[str]: List the available output backend names.)pbdoc") + /*-------------------------------------------------------------------------------- * Methods *-------------------------------------------------------------------------------*/ .def("start", &AudioGraph::start, R"pbdoc(Start the AudioGraph processing.)pbdoc") .def( "stop", [](AudioGraph &graph) { graph.stop(); }, R"pbdoc(Stop the AudioGraph processing.)pbdoc") - .def("clear", &AudioGraph::clear, - R"pbdoc(Remove all Node and Patches objects currently in the processing graph.)pbdoc") - .def("destroy", &AudioGraph::destroy, - R"pbdoc(Clear the AudioGraph and deallocate its memory, ready to create a new AudioGraph.)pbdoc") + .def("clear", &AudioGraph::clear, R"pbdoc(Remove all Node and Patches objects currently in the processing graph.)pbdoc") + .def("destroy", &AudioGraph::destroy, R"pbdoc(Clear the AudioGraph and deallocate its memory, ready to create a new AudioGraph.)pbdoc") .def( - "show_structure", [](AudioGraph &graph) { graph.show_structure(); }, - R"pbdoc(Print the AudioGraph's node connectivity structure to stdout.)pbdoc") + "show_structure", [](AudioGraph &graph) { graph.show_structure(); }, R"pbdoc(Print the AudioGraph's node connectivity structure to stdout.)pbdoc") .def( - "poll", [](AudioGraph &graph, float frequency) { graph.poll(frequency); }, "frequency"_a, - R"pbdoc(Begin polling the AudioGraph's status every `frequency` seconds, printing it to stdout.)pbdoc") + "poll", [](AudioGraph &graph, float frequency) { graph.poll(frequency); }, "frequency"_a, R"pbdoc(Begin polling the AudioGraph's status every `frequency` seconds, printing it to stdout.)pbdoc") .def( - "poll", [](AudioGraph &graph) { graph.poll(); }, - R"pbdoc(Begin polling the AudioGraph's status every 1.0 seconds, printing it to stdout.)pbdoc") + "poll", [](AudioGraph &graph) { graph.poll(); }, R"pbdoc(Begin polling the AudioGraph's status every 1.0 seconds, printing it to stdout.)pbdoc") .def( - "render", [](AudioGraph &graph) { graph.render(); }, - R"pbdoc(Render a single block (of `output_buffer_size` frames) of the AudioGraph's output.)pbdoc") + "render", [](AudioGraph &graph) { graph.render(); }, R"pbdoc(Render a single block (of `output_buffer_size` frames) of the AudioGraph's output.)pbdoc") .def( - "render", [](AudioGraph &graph, int num_frames) { graph.render(num_frames); }, "num_frames"_a, - R"pbdoc(Render a specified number of samples of the AudioGraph's output.)pbdoc") + "render", [](AudioGraph &graph, int num_frames) { graph.render(num_frames); }, "num_frames"_a, R"pbdoc(Render a specified number of samples of the AudioGraph's output.)pbdoc") + .def("render_to_buffer", &AudioGraph::render_to_buffer, "buffer"_a, R"pbdoc(Render the graph's output to the specified buffer, for the same number of frames as the buffer's length.)pbdoc") + .def("render_to_new_buffer", &AudioGraph::render_to_new_buffer, "num_frames"_a, R"pbdoc(Render the graph's output for the specified number of frames, and return the resultant buffer.)pbdoc") .def( - "render_to_buffer", &AudioGraph::render_to_buffer, "buffer"_a, - R"pbdoc(Render the graph's output to the specified buffer, for the same number of frames as the buffer's length.)pbdoc") - .def( - "render_to_new_buffer", &AudioGraph::render_to_new_buffer, "num_frames"_a, - R"pbdoc(Render the graph's output for the specified number of frames, and return the resultant buffer.)pbdoc") - .def( - "render_subgraph", - [](AudioGraph &graph, NodeRef node, int num_frames, bool reset) { + "render_subgraph", [](AudioGraph &graph, NodeRef node, int num_frames, bool reset) { if (reset) { graph.reset_subgraph(node); @@ -98,58 +93,44 @@ void init_python_graph(py::module &m) graph.render_subgraph(node); } }, - "node"_a, "num_frames"_a = 0, "reset"_a = false, - R"pbdoc(Recursively render the nodes in the tree starting at `node`. If `reset` is true, call `reset_subgraph` first.)pbdoc") - .def("reset_subgraph", &AudioGraph::reset_subgraph, - R"pbdoc(Reset the `played` status of nodes in the tree starting at `node`.)pbdoc") + "node"_a, "num_frames"_a = 0, "reset"_a = false, R"pbdoc(Recursively render the nodes in the tree starting at `node`. If `reset` is true, call `reset_subgraph` first.)pbdoc") + .def("reset_subgraph", &AudioGraph::reset_subgraph, R"pbdoc(Reset the `played` status of nodes in the tree starting at `node`.)pbdoc") .def( - "play", [](AudioGraph &graph, NodeRef node) { graph.play(node); }, "node"_a, - R"pbdoc(Begin playback of `node` (by connecting it to the output of the graph))pbdoc") + "play", [](AudioGraph &graph, NodeRef node) { graph.play(node); }, "node"_a, R"pbdoc(Begin playback of `node` (by connecting it to the output of the graph))pbdoc") .def( - "play", [](AudioGraph &graph, PatchRef patch) { graph.play(patch); }, "patch"_a, - R"pbdoc(Begin playback of `patch` (by connecting it to the output of the graph))pbdoc") + "play", [](AudioGraph &graph, PatchRef patch) { graph.play(patch); }, "patch"_a, R"pbdoc(Begin playback of `patch` (by connecting it to the output of the graph))pbdoc") .def( - "stop", [](AudioGraph &graph, NodeRef node) { graph.stop(node); }, "node"_a, - R"pbdoc(Stop playback of `node` (by disconnecting it from the output of the graph))pbdoc") + "stop", [](AudioGraph &graph, NodeRef node) { graph.stop(node); }, "node"_a, R"pbdoc(Stop playback of `node` (by disconnecting it from the output of the graph))pbdoc") .def( - "stop", [](AudioGraph &graph, PatchRef patch) { graph.stop(patch); }, "patch"_a, - R"pbdoc(Stop playback of `patch]` (by disconnecting it from the output of the graph))pbdoc") + "stop", [](AudioGraph &graph, PatchRef patch) { graph.stop(patch); }, "patch"_a, R"pbdoc(Stop playback of `patch]` (by disconnecting it from the output of the graph))pbdoc") .def( - "replace", [](AudioGraph &graph, NodeRef node, NodeRef other) { graph.replace(node, other); }, "node"_a, - "other"_a, R"pbdoc(Replace `node` in the graph's output with `other`.)pbdoc") - .def( - "add_node", &AudioGraph::add_node, "node"_a, - R"pbdoc(Add `node` to the graph so that it is processed in future blocks, without connecting it to the graph's output. Useful for non-playback nodes (e.g. BufferRecorder).)pbdoc") - .def("remove_node", &AudioGraph::remove_node, "node"_a, - R"pbdoc(Remove a `node` that has previously been added with `add_node()`)pbdoc") + "replace", [](AudioGraph &graph, NodeRef node, NodeRef other) { graph.replace(node, other); }, "node"_a, "other"_a, R"pbdoc(Replace `node` in the graph's output with `other`.)pbdoc") + .def("add_node", &AudioGraph::add_node, "node"_a, R"pbdoc(Add `node` to the graph so that it is processed in future blocks, without connecting it to the graph's output. Useful for non-playback nodes (e.g. BufferRecorder).)pbdoc") + .def("remove_node", &AudioGraph::remove_node, "node"_a, R"pbdoc(Remove a `node` that has previously been added with `add_node()`)pbdoc") - .def( - "start_recording", &AudioGraph::start_recording, "filename"_a = "", "num_channels"_a = 0, - R"pbdoc(Start recording the graph's output to an audio file, with the same number of channels as the AudioGraph or `num_channels` if specified.)pbdoc") + .def("start_recording", &AudioGraph::start_recording, "filename"_a = "", "num_channels"_a = 0, R"pbdoc(Start recording the graph's output to an audio file, with the same number of channels as the AudioGraph or `num_channels` if specified.)pbdoc") .def("stop_recording", &AudioGraph::stop_recording, R"pbdoc(Stop recording the graph's output.)pbdoc") - .def("wait", - [](AudioGraph &graph) { - /*-------------------------------------------------------------------------------- + .def("wait", [](AudioGraph &graph) { + /*-------------------------------------------------------------------------------- * Interruptible wait * https://pybind11.readthedocs.io/en/stable/faq.html#how-can-i-properly-handle-ctrl-c-in-long-running-functions *-------------------------------------------------------------------------------*/ - for (;;) - { - if (PyErr_CheckSignals() != 0) - throw py::error_already_set(); - /*-------------------------------------------------------------------------------- + for (;;) + { + if (PyErr_CheckSignals() != 0) + throw py::error_already_set(); + /*-------------------------------------------------------------------------------- * Release the GIL so that other threads can do processing. *-------------------------------------------------------------------------------*/ - py::gil_scoped_release release; + py::gil_scoped_release release; - if (graph.has_raised_audio_thread_error()) - break; - } - }) + if (graph.has_raised_audio_thread_error()) + break; + } + }) .def( - "wait", - [](AudioGraph &graph, float timeout_seconds) { + "wait", [](AudioGraph &graph, float timeout_seconds) { timeval tv; gettimeofday(&tv, NULL); double t0 = tv.tv_sec + tv.tv_usec / 1000000.0; From 5312af432772a403d90bce50d30aaa377a9b81a6 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 21:09:40 +0100 Subject: [PATCH 11/33] Update CONTRIBUTING.md --- CONTRIBUTING.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe207e69..061662dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Building from source assumes that you have a working installation of Python 3, i To build on macOS from source, install dependencies with Homebrew: ``` -brew install cmake libsndfile libsoundio +brew install cmake libsndfile ``` Clone this repository, then build and install with `pip`: @@ -35,13 +35,8 @@ SignalFlow supports Linux (verified on Ubuntu 20.04 and Raspberry Pi OS buster) To build the Python library from source on Linux, install dependencies with apt: ``` -apt-get install -y git cmake g++ python3-pip libasound2-dev libsndfile1-dev libsoundio-dev fftw3-dev -``` - -If you experience an error on Raspberry Pi `libf77blas.so.3: cannot open shared object file`: - -``` -sudo apt-get install -y libatlas-base-dev +# If on Raspberry Pi: libfftw3-dev +apt-get install -y git cmake g++ python3-pip libasound2-dev libsndfile1-dev fftw3-dev ``` Clone this repository, then build and install with `pip`: @@ -60,8 +55,6 @@ This is work in progress. Currently, dependencies need to be downloaded and built by hand. These can be placed anywhere. -- https://github.com/timmb/libsoundio - check out the `fix-msvc` branch. - - Use CMake GUI to build libsoundio with Visual Studio 2019 with binaries in a subfolder of that repo named `build`. (Configure, Generate, Open project, Batch build all configurations) - https://github.com/libsndfile/libsndfile - Use CMake GUI to build libsndfile with Visual Studio 2019 with binaries in a subfolder of that repo named `build`. (Configure, Generate, Open project, Batch build all configurations) - Download Windows binaries of FFTW from http://fftw.org/install/windows.html. From 7f754c4473bfea3a8ec1b9ba823dd7ec572d97f5 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 22:20:05 +0100 Subject: [PATCH 12/33] miniaudio: Add config.coreaudio.allowNominalSampleRateChange --- source/src/node/io/input/miniaudio.cpp | 9 ++++++++- source/src/node/io/output/miniaudio.cpp | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index 47670f71..29dc5fab 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -43,6 +43,7 @@ void read_callback(ma_device *pDevice, AudioIn::AudioIn(unsigned int num_channels) : AudioIn_Abstract() { + shared_in = this; this->name = "audioin-miniaudio"; this->num_channels = num_channels; this->init(); @@ -59,7 +60,7 @@ void AudioIn::init() ma_device_config config = ma_device_config_init(ma_device_type_capture); config.capture.format = ma_format_f32; config.capture.channels = this->num_channels; - config.periodSizeInFrames = 0; + config.periodSizeInFrames = this->get_graph()->get_output_buffer_size(); config.sampleRate = this->get_graph()->get_sample_rate(); config.dataCallback = read_callback; @@ -71,6 +72,12 @@ void AudioIn::init() this->set_channels(0, device.capture.internalChannels); + /*-------------------------------------------------------------------------------- + * Note that the underlying sample rate used by the recording hardware + * (`device.capture.internalSampleRate`) may not be the same as that used + * by `AudioIn`: SignalFlow requires that the input and output streams are both + * on the same sample rate, so miniaudio's resampling is used to unify them. + *-------------------------------------------------------------------------------*/ std::string s = device.capture.internalChannels == 1 ? "" : "s"; std::cerr << "[miniaudio] Input device: " << std::string(device.capture.name) << " (" << device.capture.internalSampleRate << "Hz, " << "buffer size " << device.capture.internalPeriodSizeInFrames << " samples, " << device.capture.internalChannels << " channel" << s << ")" diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index 258b0066..c333ca3a 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -148,6 +148,9 @@ void AudioOut::init() // Buffer blocks into a fixed number of frames config.noFixedSizedCallback = 1; + // On Core Audio, let the application select a preferred sample rate. + config.coreaudio.allowNominalSampleRateChange = 1; + rv = ma_device_init(NULL, &config, &device); if (rv != MA_SUCCESS) { @@ -168,9 +171,14 @@ void AudioOut::init() this->sample_rate = device.playback.internalSampleRate; } + /*-------------------------------------------------------------------------------- + * Update AudioOut's buffer size to reflect the actual underlying buffer size. + *-------------------------------------------------------------------------------*/ + this->buffer_size = device.playback.internalPeriodSizeInFrames; + std::string s = device.playback.internalChannels == 1 ? "" : "s"; std::cerr << "[miniaudio] Output device: " << std::string(device.playback.name) << " (" << device.playback.internalSampleRate << "Hz, " - << "buffer size " << device.playback.internalPeriodSizeInFrames << " samples, " << device.playback.internalChannels << " channel" << s << ")" + << "buffer size " << this->buffer_size << " samples, " << device.playback.internalChannels << " channel" << s << ")" << std::endl; } From 231de5f56779eb119ca765141fe82cd6e4262424 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 22:20:29 +0100 Subject: [PATCH 13/33] miniaudio: Comments --- source/include/signalflow/node/io/output/abstract.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/include/signalflow/node/io/output/abstract.h b/source/include/signalflow/node/io/output/abstract.h index 2b4e130c..c6b6c6a4 100644 --- a/source/include/signalflow/node/io/output/abstract.h +++ b/source/include/signalflow/node/io/output/abstract.h @@ -26,10 +26,16 @@ class AudioOut_Abstract : public Node virtual void set_channels(int num_input_channels, int num_output_channels); + /**-------------------------------------------------------------------------------- + * Returns the audio output's sample rate. Note that this may not be the + * same as the audio hardware's sample rate if the user has specified + * a non-zero sample rate in AudioGraphConfig. + *-------------------------------------------------------------------------------*/ unsigned int get_sample_rate(); /**-------------------------------------------------------------------------------- - * Returns the buffer size required by the audio hardware. + * Returns the buffer size observed by the audio HAL. Note that this is + * served by miniaudio. * * @return The buffer size, in frames. *-------------------------------------------------------------------------------*/ From 0cdd3d8ea8034d66e5a31161197eb93e6ec99d9c Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 22:43:17 +0100 Subject: [PATCH 14/33] miniaudio: Add support for enumerating input devices --- auxiliary/libs/signalflow_cli/__init__.py | 32 ++++++++++++---- source/include/signalflow/core/graph.h | 10 ++++- .../signalflow/node/io/output/miniaudio.h | 3 +- source/src/core/graph.cpp | 10 ++++- source/src/node/io/output/miniaudio.cpp | 38 ++++++++++++++++--- source/src/python/graph.cpp | 8 +++- 6 files changed, 83 insertions(+), 18 deletions(-) diff --git a/auxiliary/libs/signalflow_cli/__init__.py b/auxiliary/libs/signalflow_cli/__init__.py index b84d5766..918e27e7 100755 --- a/auxiliary/libs/signalflow_cli/__init__.py +++ b/auxiliary/libs/signalflow_cli/__init__.py @@ -65,10 +65,16 @@ def run_list_output_device_names(backend_name: str = None): print(" - %s" % name) -def run_list_output_backend_names(): - output_backend_names = AudioGraph.get_output_backend_names() +def run_list_input_device_names(backend_name: str = None): + input_device_names = AudioGraph.get_input_device_names(backend_name) + print("Available input device names:") + for name in input_device_names: + print(" - %s" % name) + +def run_list_backend_names(): + backend_names = AudioGraph.get_backend_names() print("Available output backend names:") - for name in output_backend_names: + for name in backend_names: print(" - %s" % name) @@ -129,8 +135,16 @@ def main(): # Command: list-output-device-names # -------------------------------------------------------------------------------- list_output_device_names = subparsers.add_parser('list-output-device-names', help='list available output devices') - list_output_device_names.add_argument('--output-backend-name', type=str, - help='name of output backend to use (default: system default backend)', + list_output_device_names.add_argument('--backend-name', type=str, + help='name of audio backend to use (default: system default backend)', + default=None) + + # -------------------------------------------------------------------------------- + # Command: list-input-device-names + # -------------------------------------------------------------------------------- + list_input_device_names = subparsers.add_parser('list-input-device-names', help='list available input devices') + list_input_device_names.add_argument('--backend-name', type=str, + help='name of audio backend to use (default: system default backend)', default=None) # -------------------------------------------------------------------------------- @@ -168,9 +182,11 @@ def main(): elif args.command == 'test': run_test(args.frequency, args.gain, args.output_backend_name, args.output_device_name) elif args.command == 'list-output-device-names': - run_list_output_device_names(args.output_backend_name) - elif args.command == 'list-output-backend-names': - run_list_output_backend_names() + run_list_output_device_names(args.backend_name) + elif args.command == 'list-input-device-names': + run_list_input_device_names(args.backend_name) + elif args.command == 'list-backend-names': + run_list_backend_names() elif args.command == 'list-midi-output-device-names': run_list_midi_output_device_names() elif args.command == 'list-midi-input-device-names': diff --git a/source/include/signalflow/core/graph.h b/source/include/signalflow/core/graph.h index b47c710b..7b91d271 100644 --- a/source/include/signalflow/core/graph.h +++ b/source/include/signalflow/core/graph.h @@ -201,13 +201,21 @@ class AudioGraph *--------------------------------------------------------------------------------*/ static std::list get_output_device_names(std::string backend_name = ""); + /**-------------------------------------------------------------------------------- + * Returns a list of available audio I/O input devices. + * + * @return The list of device names. + * + *--------------------------------------------------------------------------------*/ + static std::list get_input_device_names(std::string backend_name = ""); + /**-------------------------------------------------------------------------------- * Returns a list of available audio I/O output backends. * * @return The list of backend names. * *--------------------------------------------------------------------------------*/ - static std::list get_output_backend_names(); + static std::list get_backend_names(); /**-------------------------------------------------------------------------------- * Schedule a node for rendering without connecting the node to the graph's output. diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index 9b79cc40..36bfa4ca 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -26,7 +26,8 @@ class AudioOut : public AudioOut_Abstract virtual void destroy() override; static std::list get_output_device_names(std::string backend_name = ""); - static std::list get_output_backend_names(); + static std::list get_input_device_names(std::string backend_name = ""); + static std::list get_backend_names(); private: /*-------------------------------------------------------------------------------- diff --git a/source/src/core/graph.cpp b/source/src/core/graph.cpp index 65babff8..0182159a 100644 --- a/source/src/core/graph.cpp +++ b/source/src/core/graph.cpp @@ -594,9 +594,15 @@ std::list AudioGraph::get_output_device_names(std::string backend_n } // static -std::list AudioGraph::get_output_backend_names() +std::list AudioGraph::get_input_device_names(std::string backend_name) { - return AudioOut::get_output_backend_names(); + return AudioOut::get_input_device_names(backend_name); +} + +// static +std::list AudioGraph::get_backend_names() +{ + return AudioOut::get_backend_names(); } NodeRef AudioGraph::add_node(NodeRef node) diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index c333ca3a..d69b291b 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -243,8 +243,6 @@ std::list AudioOut::get_output_device_names(std::string backend_nam ma_result rv; ma_device_info *playback_devices; ma_uint32 playback_device_count; - ma_device_info *capture_devices; - ma_uint32 capture_device_count; ma_context context; AudioOut::init_context(&context, backend_name); @@ -252,8 +250,8 @@ std::list AudioOut::get_output_device_names(std::string backend_nam rv = ma_context_get_devices(&context, &playback_devices, &playback_device_count, - &capture_devices, - &capture_device_count); + NULL, + NULL); if (rv != MA_SUCCESS) { throw audio_io_exception("miniaudio: Failure querying audio devices"); @@ -268,7 +266,37 @@ std::list AudioOut::get_output_device_names(std::string backend_nam return device_names; } -std::list AudioOut::get_output_backend_names() +std::list AudioOut::get_input_device_names(std::string backend_name) +{ + std::list device_names; + + ma_result rv; + ma_device_info *capture_devices; + ma_uint32 capture_device_count; + ma_context context; + + AudioOut::init_context(&context, backend_name); + + rv = ma_context_get_devices(&context, + NULL, + NULL, + &capture_devices, + &capture_device_count); + if (rv != MA_SUCCESS) + { + throw audio_io_exception("miniaudio: Failure querying audio devices"); + } + for (unsigned int i = 0; i < capture_device_count; i++) + { + device_names.push_back(std::string(capture_devices[i].name)); + } + + ma_context_uninit(&context); + + return device_names; +} + +std::list AudioOut::get_backend_names() { std::list backend_names; ma_backend enabled_backends[MA_BACKEND_COUNT]; diff --git a/source/src/python/graph.cpp b/source/src/python/graph.cpp index 70d3a2c0..e1ff39c1 100644 --- a/source/src/python/graph.cpp +++ b/source/src/python/graph.cpp @@ -55,7 +55,13 @@ void init_python_graph(py::module &m) return AudioGraph::get_output_device_names(backend_name_str); }, "backend_name"_a = "", R"pbdoc(list[str]: List the available output device names.)pbdoc") - .def_static("get_output_backend_names", &AudioGraph::get_output_backend_names, R"pbdoc(list[str]: List the available output backend names.)pbdoc") + .def_static( + "get_input_device_names", [](py::object backend_name) { + std::string backend_name_str = backend_name.is_none() ? "" : backend_name.cast(); + return AudioGraph::get_input_device_names(backend_name_str); + }, + "backend_name"_a = "", R"pbdoc(list[str]: List the available input device names.)pbdoc") + .def_static("get_backend_names", &AudioGraph::get_backend_names, R"pbdoc(list[str]: List the available audio backend names.)pbdoc") /*-------------------------------------------------------------------------------- * Methods From 5474da379de3830a2c3895612f151f6bc4792141 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 21 Oct 2024 22:52:55 +0100 Subject: [PATCH 15/33] Fix list-backend-names --- auxiliary/libs/signalflow_cli/__init__.py | 9 +- source/src/node/io/input/soundio.cpp | 177 ---------------------- 2 files changed, 5 insertions(+), 181 deletions(-) delete mode 100644 source/src/node/io/input/soundio.cpp diff --git a/auxiliary/libs/signalflow_cli/__init__.py b/auxiliary/libs/signalflow_cli/__init__.py index 918e27e7..7c432f1d 100755 --- a/auxiliary/libs/signalflow_cli/__init__.py +++ b/auxiliary/libs/signalflow_cli/__init__.py @@ -71,6 +71,7 @@ def run_list_input_device_names(backend_name: str = None): for name in input_device_names: print(" - %s" % name) + def run_list_backend_names(): backend_names = AudioGraph.get_backend_names() print("Available output backend names:") @@ -144,14 +145,14 @@ def main(): # -------------------------------------------------------------------------------- list_input_device_names = subparsers.add_parser('list-input-device-names', help='list available input devices') list_input_device_names.add_argument('--backend-name', type=str, - help='name of audio backend to use (default: system default backend)', - default=None) + help='name of audio backend to use (default: system default backend)', + default=None) # -------------------------------------------------------------------------------- # Command: list-output-backend-names # -------------------------------------------------------------------------------- - list_output_backend_names = subparsers.add_parser('list-output-backend-names', - help='list available output backends') + list_backend_names = subparsers.add_parser('list-backend-names', + help='list available output backends') help = subparsers.add_parser('help', help='show help') # -------------------------------------------------------------------------------- diff --git a/source/src/node/io/input/soundio.cpp b/source/src/node/io/input/soundio.cpp deleted file mode 100644 index a9218bbc..00000000 --- a/source/src/node/io/input/soundio.cpp +++ /dev/null @@ -1,177 +0,0 @@ -#include "signalflow/node/io/input/soundio.h" - -#ifdef HAVE_SOUNDIO - -#define SIGNALFLOW_AUDIO_IN_DEFAULT_BUFFER_SIZE 1024 - -#include "signalflow/core/graph.h" -#include "signalflow/node/io/output/miniaudio.h" - -#include -#include -#include -#include -#include - -static bool is_processing = false; - -namespace signalflow -{ -extern AudioIn_Abstract *shared_in; - -void read_callback(struct SoundIoInStream *instream, - int frame_count_min, int frame_count_max) -{ - is_processing = true; - - AudioIn_SoundIO *input = (AudioIn_SoundIO *) shared_in; - if (!shared_in) - return; - - struct SoundIoChannelArea *areas; - int frame_count = frame_count_max; - int frames_left = frame_count_max; - - /*-----------------------------------------------------------------------* - * On some drivers (eg Linux), we cannot write all samples at once. - * Keep reading as many as we can until we have cleared the buffer. - *-----------------------------------------------------------------------*/ - while (frames_left > 0) - { - int err; - - if ((err = soundio_instream_begin_read(instream, &areas, &frame_count))) - throw audio_io_exception("libsoundio error on begin read: " + std::string(soundio_strerror(err))); - - if (!input) - continue; - // throw std::runtime_error("libsoundio error: No global input created"); - - for (int frame = 0; frame < frame_count; frame++) - { - for (unsigned int channel = 0; channel < input->buffer->get_num_channels(); channel += 1) - { - float *ptr = reinterpret_cast(areas[channel].ptr + areas[channel].step * frame); - input->buffer->data[channel][input->write_pos] = *ptr; - } - input->write_pos = (input->write_pos + 1) % input->buffer->get_num_frames(); - } - - if ((err = soundio_instream_end_read(instream))) - throw audio_io_exception("libsoundio error on end read: " + std::string(soundio_strerror(err))); - - frames_left -= frame_count; - } - - is_processing = false; -} - -AudioIn_SoundIO::AudioIn_SoundIO(unsigned int num_channels) - : AudioIn_Abstract() -{ - // Allocate enough buffer for twice our block size, else - // we risk overwriting our input buffer from the audio in - // while it is still being read from. - // TODO: Bad hardcoded block size - - this->num_channels_requested = num_channels; - this->read_pos = 0; - this->write_pos = (int) (SIGNALFLOW_AUDIO_IN_DEFAULT_BUFFER_SIZE / 2); - this->name = "audioin_soundio"; - this->buffer = NULL; - - this->init(); -} - -AudioIn_SoundIO::~AudioIn_SoundIO() -{ - this->destroy(); -} - -int AudioIn_SoundIO::init() -{ - int err; - - this->soundio = NULL; // ((AudioOut_MiniAudio *) this->graph->get_output().get())->soundio; - - if (!this->soundio) - throw audio_io_exception("libsoundio init error: No output node found in graph (initialising input before output?)"); - - int default_in_device_index = soundio_default_input_device_index(this->soundio); - if (default_in_device_index < 0) - throw device_not_found_exception("No input devices found. More information: https://signalflow.dev/troubleshooting/device_not_found_exception/"); - - this->device = soundio_get_input_device(this->soundio, default_in_device_index); - if (!device) - throw audio_io_exception("libsoundio init error: out of memory."); - - this->instream = soundio_instream_create(device); - this->instream->format = SoundIoFormatFloat32NE; - this->instream->read_callback = read_callback; - this->instream->sample_rate = device->sample_rate_current; - this->instream->software_latency = 256.0 / this->instream->sample_rate; - - if ((err = soundio_instream_open(this->instream))) - { - throw audio_io_exception("libsoundio init error: unable to open device: " + std::string(soundio_strerror(err))); - } - - if ((err = soundio_instream_start(instream))) - { - throw audio_io_exception("libsoundio init error: unable to start device: " + std::string(soundio_strerror(err))); - } - - if (this->num_channels_requested > (unsigned int) this->instream->layout.channel_count) - { - throw audio_io_exception("AudioIn: Not enough input channels available (requested " + std::to_string(this->num_channels_requested) + ", available " + std::to_string(this->instream->layout.channel_count) + ")"); - } - this->set_channels(0, this->num_channels_requested); - this->buffer = new Buffer(this->num_output_channels, SIGNALFLOW_AUDIO_IN_DEFAULT_BUFFER_SIZE); - - int buffer_size = this->instream->software_latency * this->instream->sample_rate; - std::string s = num_output_channels == 1 ? "" : "s"; - - std::cerr << "[soundio] Input device: " << device->name << " (" << this->instream->sample_rate << "Hz, " - << "buffer size " << buffer_size << " samples, " << num_output_channels << " channel" << s << ")" << std::endl; - - return 0; -} - -int AudioIn_SoundIO::start() -{ - return 0; -} - -int AudioIn_SoundIO::stop() -{ - return 0; -} - -int AudioIn_SoundIO::destroy() -{ - while (is_processing) - { - } - - shared_in = nullptr; - soundio_instream_destroy(this->instream); - soundio_device_unref(this->device); - - return 0; -} - -void AudioIn_SoundIO::process(Buffer &out, int num_frames) -{ - for (int frame = 0; frame < num_frames; frame++) - { - for (int channel = 0; channel < num_output_channels; channel++) - { - out[channel][frame] = this->buffer->data[channel][this->read_pos]; - } - this->read_pos = (this->read_pos + 1) % this->buffer->get_num_frames(); - } -} - -} - -#endif From f10d489df94ed075c94511fde2b020af9b247c1a Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Tue, 22 Oct 2024 13:42:42 +0100 Subject: [PATCH 16/33] miniaudio: Support for selecting input device --- .../signalflow/node/io/output/miniaudio.h | 4 +- source/src/node/io/input/miniaudio.cpp | 43 ++++++++++++++++++- source/src/node/io/output/miniaudio.cpp | 14 +++--- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index 36bfa4ca..c2e03096 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -29,13 +29,15 @@ class AudioOut : public AudioOut_Abstract static std::list get_input_device_names(std::string backend_name = ""); static std::list get_backend_names(); -private: /*-------------------------------------------------------------------------------- * Initialise a new miniaudio context, using the specified backend name if * present, or the default backend otherwise. + * + * Public because AudioIn also uses this method. *-------------------------------------------------------------------------------*/ static void init_context(ma_context *context, std::string backend_name = ""); +private: std::string backend_name; std::string device_name; ma_context context; diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index 29dc5fab..75b62e39 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -57,6 +57,7 @@ AudioIn::~AudioIn() void AudioIn::init() { + ma_result rv; ma_device_config config = ma_device_config_init(ma_device_type_capture); config.capture.format = ma_format_f32; config.capture.channels = this->num_channels; @@ -64,7 +65,47 @@ void AudioIn::init() config.sampleRate = this->get_graph()->get_sample_rate(); config.dataCallback = read_callback; - ma_result rv = ma_device_init(NULL, &config, &device); + ma_device_info *capture_devices; + ma_uint32 capture_device_count; + + // TODO: Add get_input_backend_name + AudioOut::init_context(&this->context, this->get_graph()->get_config().get_output_backend_name()); + + rv = ma_context_get_devices(&this->context, + NULL, + NULL, + &capture_devices, + &capture_device_count); + int selected_device_index = -1; + std::string device_name = this->get_graph()->get_config().get_input_device_name(); + + if (!device_name.empty()) + { + for (unsigned int i = 0; i < capture_device_count; i++) + { + /*-----------------------------------------------------------------------* + * For ease of use, SignalFlow allows for partial matches so that only + * the first part of the device names needs to be specified. However, + * an errors is thrown if the match is ambiguous. + *-----------------------------------------------------------------------*/ + if (strncmp(capture_devices[i].name, device_name.c_str(), strlen(device_name.c_str())) == 0) + { + if (selected_device_index != -1) + { + throw audio_io_exception("More than one audio device found matching name '" + device_name + "'"); + } + selected_device_index = i; + } + } + if (selected_device_index == -1) + { + throw audio_io_exception("No audio device found matching name '" + device_name + "'"); + } + + config.capture.pDeviceID = &capture_devices[selected_device_index].id; + } + + rv = ma_device_init(NULL, &config, &device); if (rv != MA_SUCCESS) { throw audio_io_exception("miniaudio: Error initialising input device"); diff --git a/source/src/node/io/output/miniaudio.cpp b/source/src/node/io/output/miniaudio.cpp index d69b291b..9b0c257b 100644 --- a/source/src/node/io/output/miniaudio.cpp +++ b/source/src/node/io/output/miniaudio.cpp @@ -98,8 +98,6 @@ void AudioOut::init() ma_device_info *playback_devices; ma_uint32 playback_device_count; - ma_device_info *capture_devices; - ma_uint32 capture_device_count; ma_result rv; AudioOut::init_context(&this->context, this->backend_name); @@ -107,13 +105,18 @@ void AudioOut::init() rv = ma_context_get_devices(&this->context, &playback_devices, &playback_device_count, - &capture_devices, - &capture_device_count); + NULL, + NULL); int selected_device_index = -1; if (!this->device_name.empty()) { for (unsigned int i = 0; i < playback_device_count; i++) { + /*-----------------------------------------------------------------------* + * For ease of use, SignalFlow allows for partial matches so that only + * the first part of the device names needs to be specified. However, + * an errors is thrown if the match is ambiguous. + *-----------------------------------------------------------------------*/ if (strncmp(playback_devices[i].name, device_name.c_str(), strlen(device_name.c_str())) == 0) { if (selected_device_index != -1) @@ -145,9 +148,6 @@ void AudioOut::init() config.sampleRate = this->sample_rate; config.dataCallback = data_callback; - // Buffer blocks into a fixed number of frames - config.noFixedSizedCallback = 1; - // On Core Audio, let the application select a preferred sample rate. config.coreaudio.allowNominalSampleRateChange = 1; From 6be01ee386eabbb6bced747f7d1d802c7f55dd0e Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 24 Oct 2024 13:12:18 +0100 Subject: [PATCH 17/33] Input RingQueue tests --- source/include/signalflow/buffer/ringbuffer.h | 62 ++++++++++++++----- source/include/signalflow/core/constants.h | 1 + source/src/node/io/input/miniaudio.cpp | 15 ++++- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/source/include/signalflow/buffer/ringbuffer.h b/source/include/signalflow/buffer/ringbuffer.h index ee95d27e..ca2cc0c4 100644 --- a/source/include/signalflow/buffer/ringbuffer.h +++ b/source/include/signalflow/buffer/ringbuffer.h @@ -7,6 +7,7 @@ *--------------------------------------------------------------------------------*/ #include +#include #include #include #include @@ -20,27 +21,46 @@ template class RingBuffer { public: - RingBuffer(int size); + RingBuffer(unsigned int capacity); ~RingBuffer(); void append(T value); - void extend(T *ptr, int count); + void extend(T *ptr, unsigned int count); T get(double index); T operator[](double index) { return this->get(index); } -private: +protected: T *data = nullptr; - int size; - int position; + unsigned int capacity; + unsigned int write_position; signalflow_interpolation_mode_t interpolation_mode; }; template -RingBuffer::RingBuffer(int size) +class RingQueue : public RingBuffer { - this->data = new T[size](); - this->position = 0; - this->size = size; +public: + RingQueue(unsigned int capacity) + : RingBuffer(capacity) + { + read_position = capacity - 256; + } + T pop(); + +private: + unsigned int read_position; +}; + +template +RingBuffer::RingBuffer(unsigned int capacity) +{ + if (capacity == 0) + { + throw std::runtime_error("RingBuffer must have a capacity greater than zero"); + } + this->data = new T[capacity](); + this->write_position = 0; + this->capacity = capacity; } template @@ -52,12 +72,12 @@ RingBuffer::~RingBuffer() template void RingBuffer::append(T value) { - this->data[this->position] = value; - this->position = (this->position + 1) % this->size; + this->data[this->write_position] = value; + this->write_position = (this->write_position + 1) % this->capacity; } template -void RingBuffer::extend(T *ptr, int count) +void RingBuffer::extend(T *ptr, unsigned int count) { for (int i = 0; i < count; i++) this->append(ptr[i]); @@ -66,19 +86,27 @@ void RingBuffer::extend(T *ptr, int count) template T RingBuffer::get(double index) { - double frame = index + this->position; + double frame = index + this->write_position; while (frame < 0) { - frame += this->size; + frame += this->capacity; } - frame = fmod(frame, this->size); + frame = fmod(frame, this->capacity); double frame_frac = (frame - (int) frame); int frame_index = (int) frame; - int next_frame_index = ((int) ceil(frame)) % size; + int next_frame_index = ((int) ceil(frame)) % this->capacity; + + T rv = ((1.0 - frame_frac) * this->data[frame_index]) + (frame_frac * this->data[next_frame_index]); - T rv = ((1.0 - frame_frac) * data[frame_index]) + (frame_frac * data[next_frame_index]); + return rv; +} +template +T RingQueue::pop() +{ + T rv = this->data[this->read_position]; + this->read_position = (this->read_position + 1) % this->capacity; return rv; } diff --git a/source/include/signalflow/core/constants.h b/source/include/signalflow/core/constants.h index a7765dd8..656249c5 100644 --- a/source/include/signalflow/core/constants.h +++ b/source/include/signalflow/core/constants.h @@ -14,6 +14,7 @@ typedef float sample; typedef sample *frame; typedef RingBuffer SampleRingBuffer; +typedef RingQueue SampleRingQueue; #if defined(__APPLE__) && !defined(FFT_FFTW) #define FFT_ACCELERATE diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index 75b62e39..c57f8a16 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -13,6 +13,7 @@ namespace signalflow { AudioIn *shared_in; +std::vector input_queue; void read_callback(ma_device *pDevice, void *pOutput, @@ -33,7 +34,7 @@ void read_callback(ma_device *pDevice, { for (int channel = 0; channel < num_channels; channel++) { - input_node->out[channel][frame] = input_samples[frame * num_channels + channel]; + input_queue[channel]->append(input_samples[frame * num_channels + channel]); } } @@ -124,6 +125,11 @@ void AudioIn::init() << "buffer size " << device.capture.internalPeriodSizeInFrames << " samples, " << device.capture.internalChannels << " channel" << s << ")" << std::endl; + for (int channel = 0; channel < device.capture.internalChannels; channel++) + { + input_queue.push_back(new SampleRingQueue(device.capture.internalPeriodSizeInFrames * 4)); + } + this->start(); } @@ -157,6 +163,13 @@ void AudioIn::destroy() void AudioIn::process(Buffer &out, int num_samples) { + for (int channel = 0; channel < this->num_output_channels; channel++) + { + for (int frame = 0; frame < num_samples; frame++) + { + out[channel][frame] = input_queue[channel]->pop(); + } + } } } From 3f290ad04758c0eea9dad07cff4048f915dea426 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 24 Oct 2024 17:14:20 +0100 Subject: [PATCH 18/33] miniaudio: Fix RingBuffer glitching on Linux with mutexes --- examples/audio-through-example.py | 2 +- source/include/signalflow/buffer/ringbuffer.h | 21 ++++++++++++++++++- source/src/node/io/input/miniaudio.cpp | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/examples/audio-through-example.py b/examples/audio-through-example.py index a319006d..4b9b9626 100755 --- a/examples/audio-through-example.py +++ b/examples/audio-through-example.py @@ -23,7 +23,7 @@ def main(): #-------------------------------------------------------------------------------- # Add some delay, and play #-------------------------------------------------------------------------------- - output = audio_in + CombDelay(audio_in, 0.2, 0.8) * 0.3 + output = audio_in stereo = StereoPanner(output) graph.play(stereo) diff --git a/source/include/signalflow/buffer/ringbuffer.h b/source/include/signalflow/buffer/ringbuffer.h index ca2cc0c4..5697426b 100644 --- a/source/include/signalflow/buffer/ringbuffer.h +++ b/source/include/signalflow/buffer/ringbuffer.h @@ -11,6 +11,7 @@ #include #include #include +#include enum signalflow_interpolation_mode_t : unsigned int; @@ -43,12 +44,18 @@ class RingQueue : public RingBuffer RingQueue(unsigned int capacity) : RingBuffer(capacity) { - read_position = capacity - 256; + this->read_position = this->capacity - 826; + this->filled_count = 0; } T pop(); + int get_filled_count() { return this->filled_count; } + void append(T value); + private: unsigned int read_position; + int filled_count; + std::mutex mutex; }; template @@ -105,9 +112,21 @@ T RingBuffer::get(double index) template T RingQueue::pop() { + mutex.lock(); T rv = this->data[this->read_position]; this->read_position = (this->read_position + 1) % this->capacity; + this->filled_count--; + mutex.unlock(); return rv; } +template +void RingQueue::append(T value) +{ + mutex.lock(); + this->RingBuffer::append(value); + this->filled_count++; + mutex.unlock(); +} + } diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index c57f8a16..f8828527 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -127,7 +127,7 @@ void AudioIn::init() for (int channel = 0; channel < device.capture.internalChannels; channel++) { - input_queue.push_back(new SampleRingQueue(device.capture.internalPeriodSizeInFrames * 4)); + input_queue.push_back(new SampleRingQueue(device.capture.internalPeriodSizeInFrames * 8)); } this->start(); From 6ca7a8571ae099c981caa9bbfab9e0d0dced7260 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 24 Oct 2024 18:20:28 +0100 Subject: [PATCH 19/33] Add Python bindings and tests for RingBuffer; fix read/write head positions --- source/include/signalflow/buffer/ringbuffer.h | 20 ++++++++--- source/src/node/io/input/miniaudio.cpp | 5 ++- source/src/node/processors/delays/comb.cpp | 2 +- source/src/node/processors/delays/onetap.cpp | 2 +- source/src/python/buffer.cpp | 19 ++++++++++ tests/test_buffer.py | 36 ++++++++++++++++++- 6 files changed, 75 insertions(+), 9 deletions(-) diff --git a/source/include/signalflow/buffer/ringbuffer.h b/source/include/signalflow/buffer/ringbuffer.h index 5697426b..7220da67 100644 --- a/source/include/signalflow/buffer/ringbuffer.h +++ b/source/include/signalflow/buffer/ringbuffer.h @@ -7,11 +7,11 @@ *--------------------------------------------------------------------------------*/ #include +#include #include #include #include #include -#include enum signalflow_interpolation_mode_t : unsigned int; @@ -27,8 +27,11 @@ class RingBuffer void append(T value); void extend(T *ptr, unsigned int count); + void extend(std::vector vec); T get(double index); T operator[](double index) { return this->get(index); } + unsigned int get_capacity() { return this->capacity; } + unsigned int get_write_position() { return this->write_position; } protected: T *data = nullptr; @@ -44,7 +47,7 @@ class RingQueue : public RingBuffer RingQueue(unsigned int capacity) : RingBuffer(capacity) { - this->read_position = this->capacity - 826; + this->read_position = this->capacity - 1; this->filled_count = 0; } T pop(); @@ -66,7 +69,7 @@ RingBuffer::RingBuffer(unsigned int capacity) throw std::runtime_error("RingBuffer must have a capacity greater than zero"); } this->data = new T[capacity](); - this->write_position = 0; + this->write_position = capacity - 1; this->capacity = capacity; } @@ -79,8 +82,8 @@ RingBuffer::~RingBuffer() template void RingBuffer::append(T value) { - this->data[this->write_position] = value; this->write_position = (this->write_position + 1) % this->capacity; + this->data[this->write_position] = value; } template @@ -90,6 +93,13 @@ void RingBuffer::extend(T *ptr, unsigned int count) this->append(ptr[i]); } +template +void RingBuffer::extend(std::vector vec) +{ + for (auto item : vec) + this->append(item); +} + template T RingBuffer::get(double index) { @@ -113,9 +123,9 @@ template T RingQueue::pop() { mutex.lock(); - T rv = this->data[this->read_position]; this->read_position = (this->read_position + 1) % this->capacity; this->filled_count--; + T rv = this->data[this->read_position]; mutex.unlock(); return rv; } diff --git a/source/src/node/io/input/miniaudio.cpp b/source/src/node/io/input/miniaudio.cpp index f8828527..cfdc22be 100644 --- a/source/src/node/io/input/miniaudio.cpp +++ b/source/src/node/io/input/miniaudio.cpp @@ -127,7 +127,10 @@ void AudioIn::init() for (int channel = 0; channel < device.capture.internalChannels; channel++) { - input_queue.push_back(new SampleRingQueue(device.capture.internalPeriodSizeInFrames * 8)); + SampleRingQueue *queue = new SampleRingQueue(device.capture.internalPeriodSizeInFrames * 8); + std::vector silence(device.capture.internalPeriodSizeInFrames, 0); + queue->extend(silence); + input_queue.push_back(queue); } this->start(); diff --git a/source/src/node/processors/delays/comb.cpp b/source/src/node/processors/delays/comb.cpp index 1f7cde05..91f4d639 100644 --- a/source/src/node/processors/delays/comb.cpp +++ b/source/src/node/processors/delays/comb.cpp @@ -46,7 +46,7 @@ void CombDelay::process(Buffer &out, int num_frames) signalflow_audio_thread_error("CombDelay: Delay time exceeds maximum. Reduce the delay_time, or increase max_delay_time."); } - sample rv = input->out[channel][frame] + (feedback * buffers[channel]->get(-offset)); + sample rv = input->out[channel][frame] + (feedback * buffers[channel]->get(-offset + 1)); out[channel][frame] = rv; buffers[channel]->append(rv); } diff --git a/source/src/node/processors/delays/onetap.cpp b/source/src/node/processors/delays/onetap.cpp index b7826853..a9108824 100644 --- a/source/src/node/processors/delays/onetap.cpp +++ b/source/src/node/processors/delays/onetap.cpp @@ -42,7 +42,7 @@ void OneTapDelay::process(Buffer &out, int num_frames) * through the current frame immediately *-------------------------------------------------------------------------------*/ buffers[channel]->append(this->input->out[channel][frame]); - out[channel][frame] = buffers[channel]->get(-offset - 1); + out[channel][frame] = buffers[channel]->get(-offset); } } } diff --git a/source/src/python/buffer.cpp b/source/src/python/buffer.cpp index e2ae6c54..f887294f 100644 --- a/source/src/python/buffer.cpp +++ b/source/src/python/buffer.cpp @@ -2,6 +2,25 @@ void init_python_buffer(py::module &m) { + py::class_(m, "SampleRingBuffer", "A circular buffer of audio samples with a single read/write head") + .def(py::init(), "capacity"_a, R"pbdoc(Create a new ring buffer)pbdoc") + .def("append", &SampleRingBuffer::append, R"pbdoc(Append an item to the ring buffer.)pbdoc") + .def( + "extend", [](SampleRingBuffer &buf, std::vector vec) { buf.extend(vec); }, + R"pbdoc(Extend the ring buffer.)pbdoc") + .def("get", &SampleRingBuffer::get, R"pbdoc(Retrieve an item from the ring buffer, with offset relative to the read head.)pbdoc") + .def("get_capacity", &SampleRingBuffer::get_capacity, R"pbdoc(Returns the capacity of the ring buffer.)pbdoc"); + + py::class_(m, "SampleRingQueue", "A circular queue of audio samples with separate read/write heads") + .def(py::init(), "capacity"_a, R"pbdoc(Create a new ring queue)pbdoc") + .def("append", &SampleRingQueue::append, R"pbdoc(Append an item to the ring queue.)pbdoc") + .def( + "extend", [](SampleRingQueue &buf, std::vector vec) { buf.extend(vec); }, + R"pbdoc(Extend the ring queue.)pbdoc") + .def("pop", &SampleRingQueue::pop, R"pbdoc(Pop an item from the ring queue.)pbdoc") + .def("get_capacity", &SampleRingQueue::get_capacity, R"pbdoc(Returns the capacity of the ring queue.)pbdoc") + .def("get_filled_count", &SampleRingQueue::get_filled_count, R"pbdoc(Returns the number of items filled in the ring queue.)pbdoc"); + /*-------------------------------------------------------------------------------- * Buffer *-------------------------------------------------------------------------------*/ diff --git a/tests/test_buffer.py b/tests/test_buffer.py index f3795142..01cdc491 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -1,4 +1,4 @@ -from signalflow import Buffer, Buffer2D +from signalflow import Buffer, Buffer2D, SampleRingBuffer, SampleRingQueue from signalflow import SIGNALFLOW_INTERPOLATION_MODE_NONE, SIGNALFLOW_INTERPOLATION_MODE_LINEAR from signalflow import GraphNotCreatedException import numpy as np @@ -202,3 +202,37 @@ def test_buffer_2d(graph): assert b2d.get2D(1.5, 1.00) == 5 # TODO: Test with no interpolation + + +def test_ring_buffer(): + buf = SampleRingBuffer(128) + assert buf.get_capacity() == 128 + + assert buf.get(0) == 0.0 + buf.append(7) + buf.append(9) + buf.append(8) + assert buf.get(0) == 8 + assert buf.get(-1) == 9 + assert buf.get(-2) == 7 + + buf.extend([1, 2, 3]) + assert buf.get(0) == 3 + assert buf.get(-1) == 2 + assert buf.get(-2) == 1 + +def test_ring_queue(): + queue = SampleRingQueue(128) + assert queue.get_capacity() == 128 + assert queue.get_filled_count() == 0 + queue.append(7) + queue.append(8) + assert queue.get_filled_count() == 2 + assert queue.pop() == 7 + assert queue.pop() == 8 + assert queue.get_filled_count() == 0 + + queue.extend([1, 2, 3]) + assert queue.pop() == 1 + assert queue.pop() == 2 + assert queue.pop() == 3 From 99cd5e6fc1d58f80ddc99c7ef513cddbfaafbaf6 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Thu, 24 Oct 2024 18:26:25 +0100 Subject: [PATCH 20/33] Add Python bindings and tests for RingBuffer; fix read/write head positions --- source/include/signalflow/buffer/ringbuffer.h | 1 + 1 file changed, 1 insertion(+) diff --git a/source/include/signalflow/buffer/ringbuffer.h b/source/include/signalflow/buffer/ringbuffer.h index 7220da67..0c327f0a 100644 --- a/source/include/signalflow/buffer/ringbuffer.h +++ b/source/include/signalflow/buffer/ringbuffer.h @@ -12,6 +12,7 @@ #include #include #include +#include enum signalflow_interpolation_mode_t : unsigned int; From 963ccd2e678b753de93578963de96a2c1d3814d9 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 27 Oct 2024 12:01:52 +0000 Subject: [PATCH 21/33] AudioGraph: Convert to a singleton, and generate a warning if multiple AudioGraphs are created --- source/src/python/graph.cpp | 48 +++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/source/src/python/graph.cpp b/source/src/python/graph.cpp index e1ff39c1..4e16f51e 100644 --- a/source/src/python/graph.cpp +++ b/source/src/python/graph.cpp @@ -1,19 +1,47 @@ #include "signalflow/python/python.h" +void graph_created_warning() +{ + std::cerr << "AudioGraph: The global audio graph has already been created. To create a new graph, call .destroy() first." << std::endl; +} + void init_python_graph(py::module &m) { /*-------------------------------------------------------------------------------- - * Graph + * AudioGraph. + * + * This class is a singleton, which is handled by this block of constructors. + * If a shared_graph already exists, return it, with a warning. *-------------------------------------------------------------------------------*/ - py::class_(m, "AudioGraph", "The global audio signal processing graph") - .def(py::init(), "config"_a = nullptr, "output_device"_a = nullptr, - "start"_a = true) - .def(py::init(), "config"_a = nullptr, "output_device"_a = "", - "start"_a = true) - .def(py::init(), "config_name"_a = nullptr, "output_device"_a = nullptr, - "start"_a = true) - .def(py::init(), "config_name"_a = nullptr, "output_device"_a = "", - "start"_a = true) + py::class_>(m, "AudioGraph", "The global audio signal processing graph") + .def(py::init<>([](AudioGraphConfig *config, NodeRef output_device, bool start) { + AudioGraph *graph = AudioGraph::get_shared_graph(); + if (graph) + graph_created_warning(); + return graph ? graph : new AudioGraph(config, output_device, start); + }), + "config"_a = nullptr, "output_device"_a = nullptr, "start"_a = true) + .def(py::init<>([](AudioGraphConfig *config, std::string output_device, bool start) { + AudioGraph *graph = AudioGraph::get_shared_graph(); + if (graph) + graph_created_warning(); + return graph ? graph : new AudioGraph(config, output_device, start); + }), + "config"_a = nullptr, "output_device"_a = nullptr, "start"_a = true) + .def(py::init<>([](std::string config_name, NodeRef output_device, bool start) { + AudioGraph *graph = AudioGraph::get_shared_graph(); + if (graph) + graph_created_warning(); + return graph ? graph : new AudioGraph(config_name, output_device, start); + }), + "config"_a = nullptr, "output_device"_a = nullptr, "start"_a = true) + .def(py::init<>([](std::string config_name, NodeRef output_device, bool start) { + AudioGraph *graph = AudioGraph::get_shared_graph(); + if (graph) + graph_created_warning(); + return graph ? graph : new AudioGraph(config_name, output_device, start); + }), + "config"_a = nullptr, "output_device"_a = nullptr, "start"_a = true) .def_static("get_shared_graph", &AudioGraph::get_shared_graph) /*-------------------------------------------------------------------------------- From cf69360019cfac327e3ea7d5b004d014c8f866bb Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 27 Oct 2024 22:47:26 +0000 Subject: [PATCH 22/33] Build system updates for Windows --- CMakeLists.txt | 75 +++++++++++-------- examples/euclidean-rhythm-example.py | 2 +- examples/hello-world-example.py | 2 +- setup.cfg | 7 -- setup.py | 1 + .../signalflow/node/io/output/miniaudio.h | 1 - source/src/core/graph.cpp | 4 +- source/src/python/graph.cpp | 21 +++--- 8 files changed, 62 insertions(+), 51 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index de6bdf03..5f693c7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,28 +54,34 @@ cmake_print_variables(CMAKE_PREFIX_PATH) set(CMAKE_CXX_STANDARD 11) set(CMAKE_MACOSX_RPATH 1) -#------------------------------------------------------------------------------- -# Shared compiler flags. -#------------------------------------------------------------------------------- -if (NOT MSVC) +if (MSVC) + #------------------------------------------------------------------------------- + # Windows Visual C: Enable parallelisation + #------------------------------------------------------------------------------- + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") +else() + #------------------------------------------------------------------------------- + # GCC/Clang: Enable strict compiler warnings + #------------------------------------------------------------------------------- add_compile_options( -pedantic -fPIC -Wall ) -endif() -#------------------------------------------------------------------------------- -# Hide superfluous compiler warnings on macOS -#------------------------------------------------------------------------------- -if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - add_compile_options( - -Wno-gnu-zero-variadic-macro-arguments - -Wno-vla-extension - ) + #------------------------------------------------------------------------------- + # Hide superfluous compiler warnings on macOS + #------------------------------------------------------------------------------- + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + add_compile_options( + -Wno-gnu-zero-variadic-macro-arguments + -Wno-vla-extension + ) + endif() endif() + include_directories( /usr/local/include /opt/homebrew/include @@ -86,13 +92,25 @@ include_directories( if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") message("Building in debug mode") - add_compile_options(-ggdb3 -O0 -DDEBUG) + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_compile_options(-O1) + else() + add_compile_options(-ggdb3 -O0 -DDEBUG) + endif() elseif (${CMAKE_BUILD_TYPE} STREQUAL "Release") message("Building in release mode") - add_compile_options(-O3 -funroll-loops) + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_compile_options(-O2) + else() + add_compile_options(-O3 -funroll-loops) + endif() else() message("Building in dev mode") - add_compile_options(-O0) + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_compile_options(-O1) + else() + add_compile_options(-O0) + endif() endif() #------------------------------------------------------------------------------- @@ -129,34 +147,31 @@ add_library(signalflow SHARED ${SRC}) #------------------------------------------------------------------------------- add_compile_definitions(SIGNALFLOW_VERSION="${SIGNALFLOW_VERSION}") -set(SNDFILE_BUILD_DIR "" CACHE PATH "Path to build sndfile library (will use find_library if blank)") - -if (SNDFILE_BUILD_DIR) - set(SNDFILE_INCLUDE_DIR "${SNDFILE_BUILD_DIR}/../include" CACHE PATH "Path to sndfile include directory (ignored if SNDFILE_BUILD_DIR is blank") +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(SNDFILE_BINARY_DIR "${PROJECT_SOURCE_DIR}/../libsndfile-1.2.2-win64" CACHE PATH "For Windows, path to downloaded sndfile directory") add_definitions(-DHAVE_SNDFILE) - target_link_libraries(signalflow "${SNDFILE_BUILD_DIR}/sndfile") - include_directories(signalflow "${SNDFILE_BUILD_DIR}/include/") - include_directories(signalflow "${SNDFILE_INCLUDE_DIR}/") + target_link_libraries(signalflow "${SNDFILE_BINARY_DIR}/lib/sndfile.lib") + include_directories(signalflow "${SNDFILE_BINARY_DIR}/include/") else() find_library(SNDFILE sndfile) if (SNDFILE) message("Found sndfile") add_definitions(-DHAVE_SNDFILE) target_link_libraries(signalflow ${SNDFILE}) - else() - message(SEND_ERROR "Couldn't find libsndfile") + else() + message(FATAL_ERROR "Couldn't find libsndfile") endif() endif() if (NOT CMAKE_SYSTEM_NAME STREQUAL "Darwin") - set(FFTW_BUILD_DIR "" CACHE PATH "Path to prebuilt FFTW library (will use find_library if blank)") + set(FFTW_BUILD_DIR "${PROJECT_SOURCE_DIR}/../fftw-3.3.5-dll64" CACHE PATH "Path to prebuilt FFTW library (will use find_library if blank)") if (FFTW_BUILD_DIR) include_directories("${FFTW_BUILD_DIR}") add_definitions(-DFFT_FFTW) target_link_libraries(signalflow - "${FFTW_BUILD_DIR}/libfftw3-3" - "${FFTW_BUILD_DIR}/libfftw3f-3" - "${FFTW_BUILD_DIR}/libfftw3l-3" + "${FFTW_BUILD_DIR}/libfftw3-3.lib" + "${FFTW_BUILD_DIR}/libfftw3f-3.lib" + "${FFTW_BUILD_DIR}/libfftw3l-3.lib" ) else() find_library(FFTW3F fftw3f) @@ -201,4 +216,4 @@ endif() # Install shared lib and all includes #------------------------------------------------------------------------------- install(TARGETS signalflow DESTINATION lib) -install(DIRECTORY source/include/signalflow DESTINATION include) +install(DIRECTORY source/include/signalflow DESTINATION include) \ No newline at end of file diff --git a/examples/euclidean-rhythm-example.py b/examples/euclidean-rhythm-example.py index 4cc182ed..48a302af 100755 --- a/examples/euclidean-rhythm-example.py +++ b/examples/euclidean-rhythm-example.py @@ -68,7 +68,7 @@ def __init__(self, input=0, delay_time=1/8, feedback=0.7, wet=0.3): pingpong = PingPongDelayPatch(mix) pingpong.play() - graph.wait() + graph.wait(20) if __name__ == "__main__": diff --git a/examples/hello-world-example.py b/examples/hello-world-example.py index 33702f64..1412ffb4 100755 --- a/examples/hello-world-example.py +++ b/examples/hello-world-example.py @@ -24,7 +24,7 @@ def main(): # Play the #------------------------------------------------------------------------ graph.play(stereo) - graph.wait() + graph.wait(2) if __name__ == "__main__": main() \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index b845b0e4..7712808a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,13 +23,6 @@ install_requires = numpy package_dir = = auxiliary/libs -packages = - signalflow-stubs - signalflow_midi - signalflow_cli - signalflow_examples - signalflow_visualisation - signalflow_analysis include_package_data = true [options.extras_require] diff --git a/setup.py b/setup.py index cf15e948..9a90f222 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ def build_extension(self, ext): signalflow_package_data = ['*.pyd'] setup( + packages=signalflow_packages, ext_modules=[CMakeExtension('signalflow')], cmdclass=dict(build_ext=CMakeBuild), ) diff --git a/source/include/signalflow/node/io/output/miniaudio.h b/source/include/signalflow/node/io/output/miniaudio.h index c2e03096..460b37ec 100644 --- a/source/include/signalflow/node/io/output/miniaudio.h +++ b/source/include/signalflow/node/io/output/miniaudio.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include "abstract.h" diff --git a/source/src/core/graph.cpp b/source/src/core/graph.cpp index 0182159a..728b3eb1 100644 --- a/source/src/core/graph.cpp +++ b/source/src/core/graph.cpp @@ -16,7 +16,7 @@ #include #include -#include +//#include namespace signalflow { @@ -169,6 +169,7 @@ void AudioGraph::start() std::string recording_filename = recordings_dir + "/signalflow-" + timestamp_str + ".wav"; // TODO: This is all very POSIX-specific and won't work on Windows + /* struct stat st; if (stat(SIGNALFLOW_USER_DIR.c_str(), &st) == -1) { @@ -186,6 +187,7 @@ void AudioGraph::start() throw std::runtime_error("AudioGraph: Failed creating recordings directory for auto_record (" + recordings_dir + ")"); } } + */ this->start_recording(recording_filename, this->output->get_num_input_channels()); } } diff --git a/source/src/python/graph.cpp b/source/src/python/graph.cpp index e1ff39c1..678ae356 100644 --- a/source/src/python/graph.cpp +++ b/source/src/python/graph.cpp @@ -119,27 +119,27 @@ void init_python_graph(py::module &m) .def("wait", [](AudioGraph &graph) { /*-------------------------------------------------------------------------------- - * Interruptible wait - * https://pybind11.readthedocs.io/en/stable/faq.html#how-can-i-properly-handle-ctrl-c-in-long-running-functions - *-------------------------------------------------------------------------------*/ + * Interruptible wait + * https://pybind11.readthedocs.io/en/stable/faq.html#how-can-i-properly-handle-ctrl-c-in-long-running-functions + *-------------------------------------------------------------------------------*/ for (;;) { if (PyErr_CheckSignals() != 0) throw py::error_already_set(); /*-------------------------------------------------------------------------------- - * Release the GIL so that other threads can do processing. - *-------------------------------------------------------------------------------*/ + * Release the GIL so that other threads can do processing. + *-------------------------------------------------------------------------------*/ py::gil_scoped_release release; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + if (graph.has_raised_audio_thread_error()) break; } }) .def( "wait", [](AudioGraph &graph, float timeout_seconds) { - timeval tv; - gettimeofday(&tv, NULL); - double t0 = tv.tv_sec + tv.tv_usec / 1000000.0; + double t0 = signalflow_timestamp(); for (;;) { @@ -148,8 +148,7 @@ void init_python_graph(py::module &m) if (timeout_seconds) { - gettimeofday(&tv, NULL); - double t1 = tv.tv_sec + tv.tv_usec / 1000000.0; + double t1 = signalflow_timestamp(); if (t1 - t0 > timeout_seconds) { break; @@ -161,6 +160,8 @@ void init_python_graph(py::module &m) *-------------------------------------------------------------------------------*/ py::gil_scoped_release release; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + if (graph.has_raised_audio_thread_error()) break; } From 44a33737383175442940a15d28bfbe263bab3f08 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 27 Oct 2024 22:52:43 +0000 Subject: [PATCH 23/33] Fix graph destruction test by calling graph.destroy() --- tests/test_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_graph.py b/tests/test_graph.py index 80f8be54..53f87ad5 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -81,7 +81,7 @@ def test_graph_num_output_channels(): output = AudioOut_Dummy(5) graph = AudioGraph(output_device=output, start=False) assert graph.num_output_channels == 5 - del graph + graph.destroy() def test_graph_render_to_buffer(graph): From 397fcd606e7c3beda6c998592f58814607995968 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 27 Oct 2024 22:56:05 +0000 Subject: [PATCH 24/33] Add signalflow_msleep --- source/include/signalflow/core/util.h | 1 + source/src/core/util.cpp | 6 ++++++ source/src/python/graph.cpp | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/source/include/signalflow/core/util.h b/source/include/signalflow/core/util.h index 15d683ec..d3992ba0 100644 --- a/source/include/signalflow/core/util.h +++ b/source/include/signalflow/core/util.h @@ -18,6 +18,7 @@ namespace signalflow { double signalflow_timestamp(); +void signalflow_msleep(int millis); long signalflow_create_random_seed(); double signalflow_clip(double value, double min, double max); diff --git a/source/src/core/util.cpp b/source/src/core/util.cpp index 92caf3e5..7ba9c878 100644 --- a/source/src/core/util.cpp +++ b/source/src/core/util.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace signalflow { @@ -28,6 +29,11 @@ double signalflow_timestamp() / 1000000.0; } +void signalflow_msleep(int millis) +{ + std::this_thread::sleep_for(std::chrono::milliseconds(millis)); +} + long signalflow_create_random_seed() { /*--------------------------------------------------------------------* diff --git a/source/src/python/graph.cpp b/source/src/python/graph.cpp index a1c62ca8..b08cb540 100644 --- a/source/src/python/graph.cpp +++ b/source/src/python/graph.cpp @@ -159,7 +159,7 @@ void init_python_graph(py::module &m) *-------------------------------------------------------------------------------*/ py::gil_scoped_release release; - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + signalflow_msleep(5); if (graph.has_raised_audio_thread_error()) break; @@ -188,7 +188,7 @@ void init_python_graph(py::module &m) *-------------------------------------------------------------------------------*/ py::gil_scoped_release release; - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + signalflow_msleep(5); if (graph.has_raised_audio_thread_error()) break; From b682a5a5a14e6b57125c32a41a8381fa10af1891 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 27 Oct 2024 23:02:06 +0000 Subject: [PATCH 25/33] Fix Linux build --- CMakeLists.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f693c7b..684253a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,9 @@ add_library(signalflow SHARED ${SRC}) #------------------------------------------------------------------------------- add_compile_definitions(SIGNALFLOW_VERSION="${SIGNALFLOW_VERSION}") +#------------------------------------------------------------------------------- +# Dependency: libsndfile +#------------------------------------------------------------------------------- if (CMAKE_SYSTEM_NAME STREQUAL "Windows") set(SNDFILE_BINARY_DIR "${PROJECT_SOURCE_DIR}/../libsndfile-1.2.2-win64" CACHE PATH "For Windows, path to downloaded sndfile directory") add_definitions(-DHAVE_SNDFILE) @@ -163,9 +166,12 @@ else() endif() endif() +#------------------------------------------------------------------------------- +# Dependency: fftw3 +#------------------------------------------------------------------------------- if (NOT CMAKE_SYSTEM_NAME STREQUAL "Darwin") - set(FFTW_BUILD_DIR "${PROJECT_SOURCE_DIR}/../fftw-3.3.5-dll64" CACHE PATH "Path to prebuilt FFTW library (will use find_library if blank)") - if (FFTW_BUILD_DIR) + if (CMAKE_SYSTEM_NAME STREQUAL "Windows")) + set(FFTW_BUILD_DIR "${PROJECT_SOURCE_DIR}/../fftw-3.3.5-dll64" CACHE PATH "Path to prebuilt FFTW library (will use find_library if blank)") include_directories("${FFTW_BUILD_DIR}") add_definitions(-DFFT_FFTW) target_link_libraries(signalflow From 5c792e4093fc91b9fe1a59cd6456b8c2799a6266 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Sun, 27 Oct 2024 23:05:57 +0000 Subject: [PATCH 26/33] Fix Linux build --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 684253a0..786cb4a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,7 +170,7 @@ endif() # Dependency: fftw3 #------------------------------------------------------------------------------- if (NOT CMAKE_SYSTEM_NAME STREQUAL "Darwin") - if (CMAKE_SYSTEM_NAME STREQUAL "Windows")) + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") set(FFTW_BUILD_DIR "${PROJECT_SOURCE_DIR}/../fftw-3.3.5-dll64" CACHE PATH "Path to prebuilt FFTW library (will use find_library if blank)") include_directories("${FFTW_BUILD_DIR}") add_definitions(-DFFT_FFTW) From e33a73e16ecb10202a60da69bb8d06e667a2cd97 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 09:09:16 +0000 Subject: [PATCH 27/33] Remove libsoundio from GitHub actions; update Windows build docs --- .github/workflows/build.yml | 2 +- .github/workflows/wheels.yml | 5 ++--- CONTRIBUTING.md | 16 ++++++---------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebca1c5b..ff808dad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: run: | sudo rm -rf /home/linuxbrew sudo apt-get update -y - sudo apt-get install libasound2-dev libsoundio-dev libsndfile1-dev fftw3-dev -y + sudo apt-get install libasound2-dev libsndfile1-dev fftw3-dev -y sudo apt-get install python3 python3-setuptools python3-pip # Requires setuptools >= 62.1 for `python setup.py test`, as earlier versions # used a different build path to the .so file as located in tests/__init__.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 19d82286..7ce3cfc8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -28,9 +28,8 @@ jobs: yum install -y fftw-devel wget python3 sudo gcc && wget https://github.com/jackaudio/jack2/archive/v1.9.22.tar.gz && tar xzf v1.9.22.tar.gz && cd jack2-1.9.22 && python3 ./waf configure && /usr/bin/sudo python3 ./waf install && cd .. && wget https://www.alsa-project.org/files/pub/lib/alsa-lib-1.2.9.tar.bz2 && tar xjf alsa-lib-1.2.9.tar.bz2 && cd alsa-lib-1.2.9 && ./configure && make && /usr/bin/sudo make install && cd .. && - git clone https://github.com/libsndfile/libsndfile.git && cd libsndfile && cmake -DBUILD_SHARED_LIBS=1 . && make && make install && cd .. && - git clone https://github.com/andrewrk/libsoundio.git && cd libsoundio && cmake . && make && make install - CIBW_BEFORE_ALL_MACOS: brew install cmake python libsndfile libsoundio + git clone https://github.com/libsndfile/libsndfile.git && cd libsndfile && cmake -DBUILD_SHARED_LIBS=1 . && make && make install && cd .. + CIBW_BEFORE_ALL_MACOS: brew install cmake python libsndfile - uses: actions/upload-artifact@v3 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 061662dd..3041fd20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,17 +51,13 @@ pip3 install . ### Windows -This is work in progress. +The build process for SignalFlow on 64-bit Windows has been verified with Visual Studio 2022 and CMake. -Currently, dependencies need to be downloaded and built by hand. These can be placed anywhere. - -- https://github.com/libsndfile/libsndfile - - Use CMake GUI to build libsndfile with Visual Studio 2019 with binaries in a subfolder of that repo named `build`. (Configure, Generate, Open project, Batch build all configurations) -- Download Windows binaries of FFTW from http://fftw.org/install/windows.html. - -To build SignalFlow, use the CMake GUI. Press configure and you will see three empty fields to fill in with the path to the two build folders and the FFTW binaries folder (see above). Set these parameters then press Configure, then Generate then Open. Then build in Visual Studio 2019. - -As of 2021-03-03, only the signalflow project has been ported to build correctly on Windows. Only tested in x64 and for Debug builds. Tested using Visual Studio 2019. +- Download Windows binaries of [FFTW](http://fftw.org/install/windows.html) and [libsndfile](https://github.com/libsndfile/libsndfile/releases/), and unzip them in the same filesystem location as the `signalflow` source directory +- Install Python 3, and dependencies: `python -m pip install build delvewheel` +- Build the binary wheel: `python -m build --wheel` +- Copy the libsndfile and fftw binaries into `dlls` +- Bundle the DLL dependencies with the wheel: `python -m delvewheel repair --add-path=dlls *.whl` From 7474168bfbea5e4d250a16580c3ccdbd03176c7e Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 09:10:05 +0000 Subject: [PATCH 28/33] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e78446fd..476da572 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ dist .coverage .ipynb_checkpoints wheelhouse/ +.vscode +.vs From ed7e5656546a28873ebd4e15d1343bbc68dca31f Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 17:11:44 +0000 Subject: [PATCH 29/33] macOS build fixes --- CMakeLists.txt | 51 +++++++++++-------- .../cibuildwheel/make-macos-x86-arm64.sh | 20 +++++--- docs/installation/{macos => }/easy.md | 0 3 files changed, 44 insertions(+), 27 deletions(-) rename docs/installation/{macos => }/easy.md (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 786cb4a2..76b75122 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,42 +4,47 @@ #------------------------------------------------------------------------------- cmake_minimum_required(VERSION 3.15.0) +#-------------------------------------------------------------------------------- +# Allow deployment on older versions of macOS (back to 10.14 Mojave), +# and default to the include/lib paths of the current Python virtualenv +# (important for cross-compiling wheels) +#-------------------------------------------------------------------------------- +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum macOS deployment version" FORCE) +set(Python_FIND_VIRTUALENV STANDARD) +set(Python_FIND_FRAMEWORKS LAST) + +#------------------------------------------------------------------------------- +# Note that project() call should come after set CMAKE_OSX_DEPLOYMENT_TARGET, +# but CMAKE_SYSTEM_NAME is only available *after* project(), so any platform- +# dependent code should come later on. +#------------------------------------------------------------------------------- +project(SignalFlow C CXX) +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Develop) +endif() + if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") #------------------------------------------------------------------------------- # On Apple, build the current native system by default #------------------------------------------------------------------------------- if (NOT CMAKE_OSX_ARCHITECTURES) execute_process(COMMAND uname -m - OUTPUT_VARIABLE CMAKE_OSX_ARCHITECTURES - OUTPUT_STRIP_TRAILING_WHITESPACE) + OUTPUT_VARIABLE CMAKE_OSX_ARCHITECTURES + OUTPUT_STRIP_TRAILING_WHITESPACE) endif() #------------------------------------------------------------------------------- - # Select the appropriate homebrew prefix by architecture + # Select the appropriate homebrew prefix by architecture. + # This is necessary so that the library is correctly linked against + # dependencies later on. #------------------------------------------------------------------------------- if (CMAKE_OSX_ARCHITECTURES STREQUAL "arm64") set(CMAKE_PREFIX_PATH /opt/homebrew) else() set(CMAKE_PREFIX_PATH /usr/local) endif() - - #-------------------------------------------------------------------------------- - # Allow deployment on older versions of macOS (back to 10.14 Mojave), - # and default to the include/lib paths of the current Python virtualenv - # (important for cross-compiling wheels) - #-------------------------------------------------------------------------------- - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum macOS deployment version" FORCE) - set(Python_FIND_VIRTUALENV STANDARD) - set(Python_FIND_FRAMEWORKS LAST) endif() -# project call should come after set CMAKE_OSX_DEPLOYMENT_TARGET -project(SignalFlow C CXX) -if (NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Develop) -endif() - - #-------------------------------------------------------------------------------- # Print config setup to help with debugging #-------------------------------------------------------------------------------- @@ -54,7 +59,9 @@ cmake_print_variables(CMAKE_PREFIX_PATH) set(CMAKE_CXX_STANDARD 11) set(CMAKE_MACOSX_RPATH 1) - +#------------------------------------------------------------------------------- +# Compiler flags for optimisations, warnings, etc. +#------------------------------------------------------------------------------- if (MSVC) #------------------------------------------------------------------------------- # Windows Visual C: Enable parallelisation @@ -81,7 +88,6 @@ else() endif() endif() - include_directories( /usr/local/include /opt/homebrew/include @@ -90,6 +96,9 @@ include_directories( source/lib/pybind11/include ) +#------------------------------------------------------------------------------- +# Compiler flags for debug vs release vs dev mode +#------------------------------------------------------------------------------- if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") message("Building in debug mode") if (CMAKE_SYSTEM_NAME STREQUAL "Windows") diff --git a/auxiliary/cibuildwheel/make-macos-x86-arm64.sh b/auxiliary/cibuildwheel/make-macos-x86-arm64.sh index 1c1b4f55..b407b240 100755 --- a/auxiliary/cibuildwheel/make-macos-x86-arm64.sh +++ b/auxiliary/cibuildwheel/make-macos-x86-arm64.sh @@ -8,22 +8,30 @@ ROOT=auxiliary/cibuildwheel -for VERSION in 38 39 310 311 312 +for VERSION in 38 39 310 312 313 313 do rm -r build export CIBW_BUILD="cp${VERSION}-*" - export CIBW_BUILD_VERBOSITY=2 + export CIBW_BUILD_VERBOSITY=1 . $ROOT/venv-$VERSION/bin/activate - pip3 install cibuildwheel + pip3 install cibuildwheel delocate + + # For some reason, Python 3.13 seems to do additional validation on delocate which + # throws an exception when dependencies have a deployment target version set too high, + # and many of the dependencies on my build machine have a target of macOS 13 (Ventura). + # Need to verify whether the pre-3.13 builds are actually truly compatible with pre-Ventura! + if [ "$VERSION" == "313" ]; then + export MACOSX_DEPLOYMENT_TARGET=13.0 + fi #-------------------------------------------------------------------------------- - # Make x86 + # Make x86. #-------------------------------------------------------------------------------- export REPAIR_LIBRARY_PATH=/usr/local/lib export CIBW_ARCHS_MACOS="x86_64" export CMAKE_OSX_ARCHITECTURES=x86_64 - export CIBW_REPAIR_WHEEL_COMMAND_MACOS="DYLD_LIBRARY_PATH=$REPAIR_LIBRARY_PATH delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" + export CIBW_REPAIR_WHEEL_COMMAND_MACOS="DYLD_LIBRARY_PATH=$REPAIR_LIBRARY_PATH delocate-wheel -w {dest_dir} -v {wheel}" python3 -m cibuildwheel --output-dir wheelhouse --platform macos @@ -33,7 +41,7 @@ do export REPAIR_LIBRARY_PATH=/opt/homebrew/lib export CIBW_ARCHS_MACOS="arm64" export CMAKE_OSX_ARCHITECTURES=arm64 - export CIBW_REPAIR_WHEEL_COMMAND_MACOS="DYLD_LIBRARY_PATH=$REPAIR_LIBRARY_PATH delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" + export CIBW_REPAIR_WHEEL_COMMAND_MACOS="DYLD_LIBRARY_PATH=$REPAIR_LIBRARY_PATH delocate-wheel -w {dest_dir} -v {wheel}" python3 -m cibuildwheel --output-dir wheelhouse --platform macos done diff --git a/docs/installation/macos/easy.md b/docs/installation/easy.md similarity index 100% rename from docs/installation/macos/easy.md rename to docs/installation/easy.md From 667358f476b913b7fbd177e4de77ea3da7d98f16 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 17:11:58 +0000 Subject: [PATCH 30/33] Update installation docs --- docs/index.md | 6 ++---- docs/installation/easy.md | 24 ++++++++++++------------ docs/installation/index.md | 18 ++++++++---------- docs/installation/macos/buttons.md | 2 +- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/docs/index.md b/docs/index.md index fa2fbca8..12701fe7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,11 +2,9 @@ title: Explore sound synthesis and DSP with Python # SignalFlow: Explore sound synthesis and DSP with Python -SignalFlow is a sound synthesis framework whose goal is to make it quick and intuitive to explore complex sonic ideas. It has a simple Python API, allowing for rapid prototyping in Jupyter notebooks or on the command-line. It comes with over 100 signal processing classes for creative exploration, from filters and delays to FFT-based spectral processing and Euclidean rhythm generators. +SignalFlow is a sound synthesis framework designed for quick and intuitive expression of complex sonic ideas. It has a simple Python API, allowing for rapid prototyping in Jupyter notebooks or on the command-line. It comes with over 100 signal processing classes for creative exploration, from filters and delays to FFT-based spectral processing and Euclidean rhythm generators. -Its core is implemented in efficient C++11, with cross-platform hardware acceleration. - -SignalFlow has robust support for macOS and Linux (including Raspberry Pi), and has work-in-progress support for Windows. The overall project is currently in alpha status, and interfaces may change without warning. +Its core is implemented in efficient C++11, with cross-platform hardware acceleration, with cross-platform support for macOS, Linux (including Raspberry Pi) and Windows. --- diff --git a/docs/installation/easy.md b/docs/installation/easy.md index d36a248f..f6b88d05 100644 --- a/docs/installation/easy.md +++ b/docs/installation/easy.md @@ -1,14 +1,14 @@ -# SignalFlow: Easy install for macOS +# SignalFlow: Easy install with Visual Studio Code -The simplest way to start exploring SignalFlow is with the free [Visual Studio Code](https://code.visualstudio.com/) editor. Visual Studio Code can edit interactive "Jupyter" notebooks, which allow you to run and modify blocks of Python code in real-time, which is a great way to experiment live with audio synthesis. +The simplest way to start exploring SignalFlow is with the free [Visual Studio Code](https://code.visualstudio.com/) editor. Visual Studio Code can edit interactive Jupyter notebooks, allowing you to run and modify blocks of Python code in real-time, which is a great way to experiment live with audio synthesis. -You'll only need to do this installation process once. Once setup, experimenting with SignalFlow is as simple as opening Visual Studio Code. +You'll only need to do this installation process once. Once set up, experimenting with SignalFlow is as simple as opening Visual Studio Code. --- ## 1. Install Python -Download and install the latest version of Python (currently 3.12). +Download and install the latest version of Python. [Download Python](https://www.python.org/downloads/){ .md-button } @@ -20,7 +20,7 @@ Download and install the latest version of Visual Studio Code. [Download Visual Studio Code](https://code.visualstudio.com/Download){ .md-button } -Once installed, open `Applications` and run `Visual Studio Code`. +Once installed, open Visual Studio Code. --- @@ -28,7 +28,7 @@ Once installed, open `Applications` and run `Visual Studio Code`. Visual Studio Code requires extensions to be installed to handle Python and Jupyter files. -In Visual Studio Code, select the `Extensions` icon from in the far-left column (or press `⇧⌘X`), and install the `Python` and `Jupyter` extensions by searching for their names and clicking "Install" on each. +In Visual Studio Code, select the `Extensions` icon from in the far-left column, and install the `Python` and `Jupyter` extensions by searching for their names and clicking "Install" on each. Once installation has finished, close the `Extensions` tab. @@ -51,7 +51,7 @@ In Visual Studio code, create a new folder to contain your new SignalFlow projec ## 5. Create a notebook -Select `File → New File...` (`^⌥⌘N`), and select `Jupyter Notebook`. You should see the screen layout change to display an empty black text block (in Jupyter parlance, a "cell"). +Select `File → New File...`, and select `Jupyter Notebook`. You should see the screen layout change to display an empty black text block (in Jupyter parlance, a "cell"). --- @@ -62,14 +62,14 @@ Click the button marked `Select Kernel` in the top right. - Select `Python Environments...` - Select `Create Python Environment` - Select `Venv` - - Finally, select the version of Python you just installed (`3.12.x`). + - Finally, select the version of Python you just installed. !!! info "Multiple versions of Python?" If you already have one or more versions of Python installed, any version from Python 3.8 upwards is fine. Visual Studio Code will launch into some activity, in which it is installing necessary libraries and creating a Python "virtual environment", which is an isolated area of the filesystem containing all the packages needed for this working space. Working in different virtual environments for different projects is good practice to minimise the likelihood of conflicts and disruptions. -When the setup is complete, the button in the top right should change to say `.venv (Python 3.12.x)`. +When the setup is complete, the button in the top right should change to say `.venv (Python 3.x.x)`. !!! info New notebooks created within this workspace will share the same Python virtual environment. @@ -84,7 +84,7 @@ In the first block, copy and paste the below: %pip install signalflow ``` -To run the cell, press `^↵` (control-enter). After a minute, you should see some output saying `Successfully installed signalflow`. +To run the cell, press `Ctrl-Enter`. After a minute, you should see some output saying `Successfully installed signalflow`. !!! info "Running cells with '.venv' requires the ipykernel package." If you are given a prompt that the `ipykernel` package is required, press "Install" to install the package. @@ -102,7 +102,7 @@ print("Hello") print("world!") ``` -Press `^↵` (control-enter) to run the cell. You should see "Hello world!" appear below the cell. +Press `Ctrl-Enter` to run the cell. You should see "Hello world!" appear below the cell. !!! info "Keyboard shortcuts" - Navigate between cells with the arrow keys @@ -120,7 +120,7 @@ Clear the first cell, and replace it with: from signalflow import * ``` -Run the cell with `^↵`. This command imports all of the SignalFlow commands and classes, and only needs to be run once per session. +Run the cell with `Ctrl-Enter`. This command imports all of the SignalFlow commands and classes, and only needs to be run once per session. Create a new cell by pressing `b`, and in the new cell, run: diff --git a/docs/installation/index.md b/docs/installation/index.md index 96598412..f3714398 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -2,26 +2,24 @@ ## Requirements -SignalFlow supports macOS, Linux (including Raspberry Pi), and has alpha support for Windows. +SignalFlow supports macOS, Linux (including Raspberry Pi), and Windows (64-bit). It requires Python 3.8 or above. ## Installation ---- - -### macOS +If you're new to Python or getting started from scratch, the tutorial below will walk you through the setup process with Visual Studio Code. -{% - include-markdown "installation/macos/buttons.md" -%} +[Easy install with Visual Studio Code](easy.md){ .md-button } ---- +## Command-line installation -### Linux +If you are an existing Python user and confident with the command line: -{% include-markdown "installation/linux/buttons.md" %} +[Install from the command line](command-line-generic.md){ .md-button } --- ## Examples [Several example scripts](https://github.com/ideoforms/signalflow/tree/master/examples) are included within the repo, covering simple control and modulation, FM synthesis, sample granulation, MIDI control, chaotic functions, etc. + +--- diff --git a/docs/installation/macos/buttons.md b/docs/installation/macos/buttons.md index 1c4c26e0..6c8db519 100644 --- a/docs/installation/macos/buttons.md +++ b/docs/installation/macos/buttons.md @@ -1,6 +1,6 @@ If you're new to Python or getting started from scratch: -[macOS: Easy install with Visual Studio Code](easy.md){ .md-button } +[macOS: Easy install with Visual Studio Code](../easy.md){ .md-button } If you are an existing Python user and confident with the command line: From f0f79ec6ecc9b9353b866054ac630879ab7d81aa Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 17:12:07 +0000 Subject: [PATCH 31/33] Update version to 0.5.0 --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7712808a..0ea7d53d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [metadata] name = signalflow -version = 0.4.10 +version = 0.5.0 author = Daniel Jones author_email = dan@erase.net -description = SignalFlow is a sound synthesis library designed to make it quick and intuitive to explore complex sonic ideas +description = SignalFlow is a sound synthesis library designed for clear and concise expression of sonic ideas long_description = file: README.md long_description_content_type = text/markdown keywords = audio, sound, synthesis, dsp, sound-synthesis From 799d614c590a079f644283925c77ff5a8382978e Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 17:41:55 +0000 Subject: [PATCH 32/33] GitHub Actions: Add Python 3.13 to wheels build --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7ce3cfc8..6076f318 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -21,7 +21,7 @@ jobs: - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse env: - CIBW_BUILD: cp38-manylinux* cp39-manylinux* cp310-manylinux* cp311-manylinux* cp312-manylinux* + CIBW_BUILD: cp38-manylinux* cp39-manylinux* cp310-manylinux* cp311-manylinux* cp312-manylinux* cp313-manylinux* CIBW_ARCHS_MACOS: arm64 x86_64 CIBW_ARCHS_LINUX: x86_64 CIBW_BEFORE_ALL_LINUX: > From 4a0633e762c93c330f8e2fa33c2732ab9e70ef52 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 28 Oct 2024 17:42:37 +0000 Subject: [PATCH 33/33] Update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b7bc72..59c34c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # CHANGELOG +## [v0.5.0](https://github.com/ideoforms/signalflow/tree/v0.5.0) (2024-10-28) + +- Replaced the `libsoundio` audio abstraction layer with `miniaudio`, heralding first-class Windows and Linux support. +- Retired historical `AudioOut` classes for different operating systems, and refactored querying of inputs/outputs/backends +- `AudioGraphConfig`: Added `auto_record` flag, to automatically record all output in timestamped audio files +- Added support for instantiating `AudioGraph` and `AudioGraphConfig` with the path of a config file +- Modified `AudioGraph` to become a singleton, and throw a warning instead of an exception upon attempting to create a second `AudioGraph` +- Added Python bindings and added unit tests for `SampleRingBuffer` and `SampleRingQueue` classes +- Nodes: + - Added `Bus` node, to act as a fixed-channel summer with variable inputs + - Added `Maraca` node, a simple physically-inspired model of a shaker, after Cook (1997) + - Added `ChannelOffset` node to offset a node's output by `N` channels, and `node.play(output_channel=N)` syntax + - Added `SelectInput` node, to pass the output of an input whose index can be modulated at audio rate + - Added `HistoryBufferWriter` node to capture a rolling signal history window, useful for oscilloscope UI display + - Added `Accumulator` node, to accumulate energy with some leaky decay coefficient, and accompanying `calculate_decay_coefficient` function + - Added abstract `VariableInputNode` class + - Added `stutter_probability` and `stutter_advance_time` inputs to `Stutter` + ## [v0.4.10](https://github.com/ideoforms/signalflow/tree/v0.4.10) (2024-08-13) - Added `TriggerRoundRobin` node, to sequentially distribute triggers across outputs