From ac7982c501a6b09865f00e1a7ac180b7497f9e44 Mon Sep 17 00:00:00 2001 From: Daniel Jones Date: Mon, 5 Aug 2024 13:11:14 +0100 Subject: [PATCH] Add FFTBuffer class --- source/include/signalflow/buffer/fftbuffer.h | 148 +++++++++++++++++++ source/include/signalflow/core/graph.h | 1 + source/include/signalflow/python/python.h | 1 + source/include/signalflow/signalflow.h | 18 ++- source/src/CMakeLists.txt | 1 + source/src/buffer/fftbuffer.cpp | 139 +++++++++++++++++ source/src/python/buffer.cpp | 39 +++++ 7 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 source/include/signalflow/buffer/fftbuffer.h create mode 100644 source/src/buffer/fftbuffer.cpp diff --git a/source/include/signalflow/buffer/fftbuffer.h b/source/include/signalflow/buffer/fftbuffer.h new file mode 100644 index 00000000..19decca1 --- /dev/null +++ b/source/include/signalflow/buffer/fftbuffer.h @@ -0,0 +1,148 @@ +#pragma once + +/**------------------------------------------------------------------------- + * @file fftbuffer.h + * @brief Stores one or more spectra of FFT mag/phase pairs. + * + *-----------------------------------------------------------------------*/ + +#include "signalflow/core/constants.h" +#include "signalflow/core/util.h" + +#include +#include +#include +#include +#include + +namespace signalflow +{ + +template +class FFTBufferRefTemplate; +class FFTBuffer; +typedef FFTBufferRefTemplate FFTBufferRef; + +class FFTBuffer +{ +public: + /**------------------------------------------------------------------------ + * Construct an FFT buffer of specified dimensions. + * The contents are initialised to zero. + * + *------------------------------------------------------------------------*/ + FFTBuffer(int num_frames, int fft_size, int hop_size); + + /**------------------------------------------------------------------------ + * Load the contents of the spectra file `filename` into a new buffer. + * + *------------------------------------------------------------------------*/ + FFTBuffer(std::string filename, int fft_size, int hop_size); + + /**------------------------------------------------------------------------ + * Destroy the buffer. + * + *------------------------------------------------------------------------*/ + virtual ~FFTBuffer(); + + /**------------------------------------------------------------------------ + * Resize the FFT buffer. + * + * @param num_frames The number of FFT frames to allocate. + * + *------------------------------------------------------------------------*/ + void resize(int num_frames); + + /**------------------------------------------------------------------------ + * Get the magnitude array corresponding to the given frame. + * + * @param frame The frame index, between [0, num_frames]. + * @returns A sample value, between [-1, 1]. + * + *------------------------------------------------------------------------*/ + sample *get_magnitudes(int frame); + sample *get_phases(int frame); + + /**------------------------------------------------------------------------ + * Get the buffer's audio sample rate. + * + * @returns The sample rate, in Hz. + * + *------------------------------------------------------------------------*/ + float get_sample_rate(); + + /**------------------------------------------------------------------------ + * Set the buffer's audio sample rate. + * + * @param sample_rate The sample rate + * + *------------------------------------------------------------------------*/ + void set_sample_rate(float sample_rate); + + /**------------------------------------------------------------------------ + * Get the number of spectrum frames in the buffer. + * + * @returns The number of frames. + * + *------------------------------------------------------------------------*/ + unsigned long get_num_frames(); + + /**------------------------------------------------------------------------ + * Get the duration of the buffer, based on the hop_size. + * + * @returns The duration, in seconds. + * + *------------------------------------------------------------------------*/ + float get_duration(); + + /**------------------------------------------------------------------------ + * Get the buffer's FFT size. + * + * @returns The FFT size. + * + *------------------------------------------------------------------------*/ + unsigned int get_fft_size(); + + /**------------------------------------------------------------------------ + * Get the buffer's hop size. + * + * @returns The hop size. + * + *------------------------------------------------------------------------*/ + unsigned int get_hop_size(); + + /**------------------------------------------------------------------------ + * Get the filename that the buffer was loaded from / saved to, if set. + * + * @returns The filename, or an empty string. + * + *------------------------------------------------------------------------*/ + std::string get_filename(); + + sample **data = nullptr; + +protected: + unsigned long get_total_num_values(); + + std::string filename; + float sample_rate; + unsigned int num_frames; + unsigned int fft_size; + unsigned int num_bins; + unsigned int hop_size; + float duration; +}; + +template +class FFTBufferRefTemplate : public std::shared_ptr +{ +public: + using std::shared_ptr::shared_ptr; + + FFTBufferRefTemplate() + : std::shared_ptr(nullptr) {} + FFTBufferRefTemplate(T *ptr) + : std::shared_ptr(ptr) {} +}; + +} diff --git a/source/include/signalflow/core/graph.h b/source/include/signalflow/core/graph.h index 058a5f88..04ca0066 100644 --- a/source/include/signalflow/core/graph.h +++ b/source/include/signalflow/core/graph.h @@ -432,6 +432,7 @@ class AudioGraph int recording_num_channels; friend class Buffer; + friend class FFTBuffer; }; class AudioGraphRef : public std::shared_ptr diff --git a/source/include/signalflow/python/python.h b/source/include/signalflow/python/python.h index 5b40e61c..94abef76 100644 --- a/source/include/signalflow/python/python.h +++ b/source/include/signalflow/python/python.h @@ -21,6 +21,7 @@ using namespace pybind11::literals; *----------------------------------------------------------------------------*/ PYBIND11_DECLARE_HOLDER_TYPE(T, NodeRefTemplate, false) PYBIND11_DECLARE_HOLDER_TYPE(T, BufferRefTemplate, false) +PYBIND11_DECLARE_HOLDER_TYPE(T, FFTBufferRefTemplate, false) PYBIND11_DECLARE_HOLDER_TYPE(T, PatchRefTemplate, false) PYBIND11_DECLARE_HOLDER_TYPE(T, PatchSpecRefTemplate, false) PYBIND11_DECLARE_HOLDER_TYPE(T, PropertyRefTemplate, false) \ No newline at end of file diff --git a/source/include/signalflow/signalflow.h b/source/include/signalflow/signalflow.h index e9c2a19e..eb762d3e 100644 --- a/source/include/signalflow/signalflow.h +++ b/source/include/signalflow/signalflow.h @@ -12,16 +12,26 @@ #include #include -#include -#include +/*------------------------------------------------------------------------ + * Node + *-----------------------------------------------------------------------*/ +#include +#include +/*------------------------------------------------------------------------ + * Patch + *-----------------------------------------------------------------------*/ #include #include #include #include -#include -#include +/*------------------------------------------------------------------------ + * Buffers + *-----------------------------------------------------------------------*/ +#include +#include +#include /*------------------------------------------------------------------------ * Operators diff --git a/source/src/CMakeLists.txt b/source/src/CMakeLists.txt index 388dbed9..66f8f11c 100644 --- a/source/src/CMakeLists.txt +++ b/source/src/CMakeLists.txt @@ -1,6 +1,7 @@ set(SRC ${SRC} ${CMAKE_CURRENT_SOURCE_DIR}/buffer/buffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/buffer/buffer2d.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/buffer/fftbuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/graph.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/config.cpp ${CMAKE_CURRENT_SOURCE_DIR}/core/core.cpp diff --git a/source/src/buffer/fftbuffer.cpp b/source/src/buffer/fftbuffer.cpp new file mode 100644 index 00000000..767b672a --- /dev/null +++ b/source/src/buffer/fftbuffer.cpp @@ -0,0 +1,139 @@ +#include "signalflow/buffer/fftbuffer.h" +#include "signalflow/core/constants.h" +#include "signalflow/core/exceptions.h" +#include "signalflow/core/graph.h" + +#include +#include +#include + +#ifdef WIN32 +#include +#else +#include +#endif + +#include + +namespace signalflow +{ + +extern AudioGraph *shared_graph; + +FFTBuffer::FFTBuffer(std::string filename, int fft_size, int hop_size) + : fft_size(fft_size), hop_size(hop_size) +{ + this->num_bins = (fft_size / 2) + 1; + + FILE *fd = fopen(filename.c_str(), "r"); + if (fd == NULL) + { + throw std::runtime_error(std::string("Couldn't find file at path: ") + filename); + } + + fseek(fd, 0, SEEK_END); + size_t file_size = ftell(fd); + fseek(fd, 0, SEEK_SET); + double num_frames_frac = (double) file_size / (this->num_bins * 2 * sizeof(float)); + printf("FFTBuffer: File size %zu bytes, %.2f frames\n", file_size, num_frames_frac); + if (num_frames_frac != (int) num_frames_frac) + { + throw std::runtime_error("Error: Not an integer number of frames (found " + std::to_string(num_frames) + " frames)"); + } + this->num_frames = (unsigned int) num_frames_frac; + + /*-------------------------------------------------------------------------------- + * If the AudioGraph has been instantiated, populate the buffer's sample + * rate and duration. Otherwise, zero them. + *-------------------------------------------------------------------------------*/ + if (shared_graph) + { + this->sample_rate = shared_graph->get_sample_rate(); + this->duration = (this->num_frames * this->hop_size) / this->sample_rate; + } + else + { + this->sample_rate = 0; + this->duration = 0; + } + + this->resize(num_frames); +} + +FFTBuffer::~FFTBuffer() +{ + if (this->data) + { + delete this->data[0]; + delete this->data; + + if (shared_graph) + { + size_t num_bytes = this->get_total_num_values() * sizeof(sample); + shared_graph->register_memory_dealloc(num_bytes); + } + } +} + +void FFTBuffer::resize(int num_frames) +{ + if (this->data) + { + delete this->data[0]; + delete this->data; + + if (shared_graph) + { + size_t num_bytes = this->get_total_num_values() * sizeof(sample); + shared_graph->register_memory_dealloc(num_bytes); + } + } + + this->num_frames = num_frames; + + /*-------------------------------------------------------------------------------- + * For use in numpy, memory allocation needs to be contiguous with a fixed + * stride between vectors. Allocate as one block and set element indices + * accordingly. + *-------------------------------------------------------------------------------*/ + if (num_frames) + { + this->data = new sample *[this->num_frames](); + + sample *data_frames = new sample[this->get_total_num_values()](); + for (unsigned int frame = 0; frame < this->num_frames; frame++) + { + this->data[frame] = data_frames + frame * (this->num_bins * 2); + } + + if (shared_graph) + { + size_t num_bytes = this->get_total_num_values() * sizeof(sample); + shared_graph->register_memory_alloc(num_bytes); + } + } + else + { + this->data = nullptr; + } +} + +float FFTBuffer::get_sample_rate() { return this->sample_rate; } + +void FFTBuffer::set_sample_rate(float sample_rate) { this->sample_rate = sample_rate; } + +unsigned long FFTBuffer::get_num_frames() { return this->num_frames; } + +std::string FFTBuffer::get_filename() { return this->filename; } + +unsigned int FFTBuffer::get_fft_size() { return this->fft_size; } + +unsigned int FFTBuffer::get_hop_size() { return this->hop_size; } + +float FFTBuffer::get_duration() { return this->duration; } + +unsigned long FFTBuffer::get_total_num_values() { return this->num_frames * this->num_bins * 2; } + +template class FFTBufferRefTemplate; + +} diff --git a/source/src/python/buffer.cpp b/source/src/python/buffer.cpp index efb9df9b..507d6bfc 100644 --- a/source/src/python/buffer.cpp +++ b/source/src/python/buffer.cpp @@ -161,4 +161,43 @@ void init_python_buffer(py::module &m) R"pbdoc(Create an envelope buffer filled with the output of a given function.)pbdoc") .def_property_readonly("frame_offsets", &Buffer::get_frame_offsets, R"pbdoc(Returns a list containing the offset in the envelope buffer for each frame, ranging over 0..1.)pbdoc"); + + /*-------------------------------------------------------------------------------- + * Buffer + *-------------------------------------------------------------------------------*/ + py::class_>(m, "FFTBuffer", + "A buffer of audio spectra in magnitude/phase format") + /*-------------------------------------------------------------------------------- + * Constructors + *-------------------------------------------------------------------------------*/ + .def(py::init(), "filename"_a, "fft_size"_a, "hop_size"_a, R"pbdoc(Load an FFTBuffer from a .spectra file.)pbdoc") + + /*-------------------------------------------------------------------------------- + * Operators + *-------------------------------------------------------------------------------*/ + .def("__str__", + [](FFTBufferRef a) { + std::string filename = a->get_filename(); + if (filename.empty()) + { + return "FFTBuffer (" + std::to_string(a->get_num_frames()) + " frames)"; + } + else + { + return "FFTBuffer (" + filename + ", " + std::to_string(a->get_num_frames()) + " frames)"; + } + }) + + /*-------------------------------------------------------------------------------- + * Properties + *-------------------------------------------------------------------------------*/ + .def_property_readonly("num_frames", &FFTBuffer::get_num_frames, + R"pbdoc(Returns the number of spectral frames in the FFT buffer.)pbdoc") + .def_property_readonly("sample_rate", &FFTBuffer::get_sample_rate, + R"pbdoc(Returns the FFT buffer's sample rate.)pbdoc") + .def_property_readonly("duration", &FFTBuffer::get_duration, + R"pbdoc(Returns the FFT buffer's duration, in seconds.)pbdoc") + .def_property_readonly( + "filename", &FFTBuffer::get_filename, + R"pbdoc(Returns the FFT buffer's filename, if the buffer has been loaded from/saved to file.)pbdoc"); }