diff --git a/CMakeLists.txt b/CMakeLists.txt index c25fab3..2f492fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ project(bungee) add_library(libbungee STATIC src/Synthesis.cpp + src/Basic.cpp src/Fourier.cpp src/Fourier.cpp src/Grain.cpp @@ -18,7 +19,6 @@ add_library(libbungee STATIC src/Partials.cpp src/Resample.cpp src/Stretch.cpp - src/Stretcher.cpp src/Timing.cpp src/Window.cpp src/Assert.cpp diff --git a/README.md b/README.md index 1592e08..2b9ea0d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ For a working example of this API, see [cmd/main.cpp](./cmd/main.cpp). ### Instantiation -To instantiate, include the [bungee/Bungee.h](./bungee/Bungee.h) header file, create a `Stretcher` object and initialise a `Request` object: +To instantiate, include the [bungee/Bungee.h](./bungee/Bungee.h) header file, create a `Stretcher` object and initialise a `Request` object: ``` C++ #include "Bungee.h" @@ -55,7 +55,7 @@ To instantiate, include the [bungee/Bungee.h](./bungee/Bungee.h) header file, cr const int sampleRate = 44100; -Bungee::Stretcher stretcher({sampleRate, sampleRate}, 2); +Bungee::Stretcher stretcher({sampleRate, sampleRate}, 2); Bungee::Request request{}; @@ -75,7 +75,7 @@ stretcher.preroll(request); ### Granular loop -`Stretcher`'s processing functions are typically called from within a loop, each iteration of which corresponds to a grain of audio. For each grain, the functions `Stretcher::specifiyGrain`, `Stretcher::analyseGain` and `Stretcher::synthesiseGrain` should be called in sequence. +`Stretcher`'s processing functions are typically called from within a loop, each iteration of which corresponds to a grain of audio. For each grain, the functions `Stretcher::specifiyGrain`, `Stretcher::analyseGain` and `Stretcher::synthesiseGrain` should be called in sequence. ```C++ while (true) { @@ -110,9 +110,9 @@ while (true) * `Request::position` is a timestamp, it defines the grain centre point in terms of an input audio frame offset. It is the primary control for speed adjustments and is also the driver for seek and scrub operations. The caller is responsible for deciding `Request::position` for each grain. -* The caller owns the input audio buffer and must provide the audio segment indicated by `InputChunk`. Successive grains' input audio chunks may overlap. The `Stretcher` instance reads in the input chunk data when `Stretcher::analyseGrain` is called. +* The caller owns the input audio buffer and must provide the audio segment indicated by `InputChunk`. Successive grains' input audio chunks may overlap. The `Stretcher` instance reads in the input chunk data when `Stretcher::analyseGrain` is called. -* The `Stretcher` instance owns the output audio buffer. It is valid from when `Stretcher::synthesiseGrain` returns up until `Stretcher::synthesiseGrain` is called for the subsequent grain. Output audio chunks do not overlap: they should be concatenated to produce an output audio stream. +* The `Stretcher` instance owns the output audio buffer. It is valid from when `Stretcher::synthesiseGrain` returns up until `Stretcher::synthesiseGrain` is called for the subsequent grain. Output audio chunks do not overlap: they should be concatenated to produce an output audio stream. * Output audio is timestamped. The original `Request` objects corresponding to the start and end of the chunk are provided by `OutputChunk`. diff --git a/bungee/Bungee.h b/bungee/Bungee.h index 0c7de7a..2816191 100644 --- a/bungee/Bungee.h +++ b/bungee/Bungee.h @@ -3,17 +3,10 @@ #pragma once -#include "Modes.h" - -#include #include -#define BUNGEE_API __attribute__((visibility("default"))) - namespace Bungee { -BUNGEE_API const char *version(); - struct Request { // Frame-offset within the input audio of the centre-point of this audio grain. @@ -30,23 +23,15 @@ struct Request // Set to have the stretcher forget all previous grains and restart on this grain. bool reset; - -#define X_BEGIN(Type, type) Type##Mode type##Mode; -#define X_ITEM(Type, type, mode, description) -#define X_END(Type, type) - BUNGEE_MODES -#undef X_BEGIN -#undef X_ITEM -#undef X_END }; // Information to describe a chunk of audio required as input struct InputChunk { - // Sample positions relative to the start of the audio track + // Frame offsets relative to the start of the audio track int begin, end; - BUNGEE_API int frameCount() const + int frameCount() const { return end - begin; } @@ -56,58 +41,65 @@ struct InputChunk // Output chunks do not overlap and can be appended for seamless playback struct OutputChunk { - float *data; // audio output data, not aligned + float *data; // audio output data, not aligned and not interleaved int frameCount; - intptr_t channelStride; + intptr_t channelStride; // nth audio channel audio starts at data[n * channelStride] static constexpr int begin = 0, end = 1; Request *request[2 /* 0=begin, 1=end */]; }; +// Stretcher audio sample rates, in Hz struct SampleRates { int input; int output; }; -struct Configuration; - +template struct Stretcher { - struct Implementation; - Implementation *const state; + Implementation *const implementation; + + static const char *version(); - BUNGEE_API Stretcher(SampleRates sampleRates, int channelCount); + Stretcher(SampleRates sampleRates, int channelCount); - BUNGEE_API ~Stretcher(); + ~Stretcher(); // Returns the largest number of frames that might be requested by specifyGrain() // This helps the caller to allocate large enough buffers because it is guaranteed that // InputChunk::frameCount() will not exceed this number. - BUNGEE_API int maxInputFrameCount() const; + int maxInputFrameCount() const; // This function adjusts request.position so that the stretcher has a run in of a few // grains before hitting the requested position. Without preroll, the first milliseconds // of audio might sound weak or initial transients might be lost. - BUNGEE_API void preroll(Request &request) const; + void preroll(Request &request) const; // This function prepares request.position and request.reset for the subsequent grain. // Typically called within a granular loop where playback at constant request.speed is desired. - BUNGEE_API void next(Request &request) const; + void next(Request &request) const; // Specify a grain of audio and compute the necessary segment of input audio. // After calling this function, call analyseGrain. - BUNGEE_API InputChunk specifyGrain(const Request &request); + InputChunk specifyGrain(const Request &request); // Begins processing the grain. The audio data should correspond to the range // specified by specifyGrain's return value. After calling this function, call synthesiseGrain. - BUNGEE_API void analyseGrain(const float *data, intptr_t channelStride); + void analyseGrain(const float *data, intptr_t channelStride); // Complete processing of the grain of audio that was previously set up with calls to specifyGrain and analyseGrain. - BUNGEE_API void synthesiseGrain(OutputChunk &outputChunk); + void synthesiseGrain(OutputChunk &outputChunk); // Returns true if every grain in the stretcher's pipeline is invalid (its Request::position was NaN). - BUNGEE_API bool isFlushed() const; + bool isFlushed() const; }; +// Stretcher is the open-source implementation contained in this repository +struct Basic; + +// Stretcher is an enhanced and optimised implementation that is available under commercial license +struct Pro; + } // namespace Bungee diff --git a/bungee/CommandLine.h b/bungee/CommandLine.h index dbe8e31..46c9f58 100644 --- a/bungee/CommandLine.h +++ b/bungee/CommandLine.h @@ -30,7 +30,7 @@ struct Options : { std::vector helpGroups; - Options(std::string program_name = "bungee", std::string help_string = std::string("Bungee audio speed and pitch changer\n\nVersion: ") + Bungee::version() + "\n") : + Options(std::string program_name = "bungee", std::string help_string = std::string("Bungee audio speed and pitch changer\n\nVersion: ") + Bungee::Stretcher::version() + "\n") : cxxopts::Options(program_name, help_string) { add_options() // diff --git a/cmd/main.cpp b/cmd/main.cpp index 0e70065..5ac91c6 100644 --- a/cmd/main.cpp +++ b/cmd/main.cpp @@ -17,7 +17,7 @@ int main(int argc, const char *argv[]) CommandLine::Parameters parameters{options, argc, argv, request}; CommandLine::Processor processor{parameters, request}; - Stretcher stretcher(processor.sampleRates, processor.channelCount); + Bungee::Stretcher stretcher(processor.sampleRates, processor.channelCount); processor.restart(request); stretcher.preroll(request); diff --git a/src/Stretcher.cpp b/src/Basic.cpp similarity index 72% rename from src/Stretcher.cpp rename to src/Basic.cpp index 7d954ea..1fe3642 100644 --- a/src/Stretcher.cpp +++ b/src/Basic.cpp @@ -1,7 +1,7 @@ // Copyright (C) 2020-2024 Parabola Research Limited // SPDX-License-Identifier: MPL-2.0 -#include "Stretcher.h" +#include "Basic.h" #include "Resample.h" #include "Synthesis.h" #include "log2.h" @@ -10,57 +10,31 @@ namespace Bungee { extern const char *versionDescription; -const char *version() +template <> +const char *Stretcher::version() { return versionDescription; } -Stretcher::Stretcher(SampleRates sampleRates, int channelCount) : - state(new Implementation(sampleRates, channelCount)) +template <> +void Stretcher::analyseGrain(const float *data, intptr_t channelStride) { + implementation->analyseGrain(data, channelStride); } -Stretcher::~Stretcher() +template <> +void Stretcher::synthesiseGrain(OutputChunk &outputChunk) { - delete state; + implementation->synthesiseGrain(outputChunk); } -InputChunk Stretcher::specifyGrain(const Request &request) +template <> +bool Stretcher::isFlushed() const { - return state->specifyGrain(request); + return implementation->grains.flushed(); } -int Stretcher::maxInputFrameCount() const -{ - return state->maxInputFrameCount(true); -} - -void Stretcher::preroll(Request &request) const -{ - state->preroll(request); -} - -void Stretcher::next(Request &request) const -{ - state->next(request); -} - -void Stretcher::analyseGrain(const float *data, intptr_t channelStride) -{ - state->analyseGrain(data, channelStride); -} - -void Stretcher::synthesiseGrain(OutputChunk &outputChunk) -{ - state->synthesiseGrain(outputChunk); -} - -bool Stretcher::isFlushed() const -{ - return state->grains.flushed(); -} - -Stretcher::Implementation::Implementation(SampleRates sampleRates, int channelCount) : +Basic::Basic(SampleRates sampleRates, int channelCount) : Timing(sampleRates), input(log2SynthesisHop, channelCount), grains(4), @@ -70,7 +44,7 @@ Stretcher::Implementation::Implementation(SampleRates sampleRates, int channelCo grain = std::make_unique(log2SynthesisHop, channelCount); } -InputChunk Stretcher::Implementation::specifyGrain(const Request &request) +InputChunk Basic::specifyGrain(const Request &request) { const Assert::FloatingPointExceptions floatingPointExceptions(0); @@ -81,7 +55,7 @@ InputChunk Stretcher::Implementation::specifyGrain(const Request &request) return grain.specify(request, previous, sampleRates, log2SynthesisHop); } -void Stretcher::Implementation::analyseGrain(const float *data, std::ptrdiff_t stride) +void Basic::analyseGrain(const float *data, std::ptrdiff_t stride) { const Assert::FloatingPointExceptions floatingPointExceptions(FE_INEXACT | FE_UNDERFLOW | FE_DENORMALOPERAND); @@ -116,7 +90,7 @@ void Stretcher::Implementation::analyseGrain(const float *data, std::ptrdiff_t s } } -void Stretcher::Implementation::synthesiseGrain(OutputChunk &outputChunk) +void Basic::synthesiseGrain(OutputChunk &outputChunk) { const Assert::FloatingPointExceptions floatingPointExceptions(FE_INEXACT); @@ -155,4 +129,40 @@ void Stretcher::Implementation::synthesiseGrain(OutputChunk &outputChunk) outputChunk.request[OutputChunk::end] = &grains[1].request; } +template <> +Stretcher::Stretcher(SampleRates sampleRates, int channelCount) : + implementation(new Basic(sampleRates, channelCount)) +{ +} + +template <> +Stretcher::~Stretcher() +{ + delete implementation; +} + +template <> +InputChunk Stretcher::specifyGrain(const Request &request) +{ + return implementation->specifyGrain(request); +} + +template <> +int Stretcher::maxInputFrameCount() const +{ + return implementation->maxInputFrameCount(true); +} + +template <> +void Stretcher::preroll(Request &request) const +{ + implementation->preroll(request); +} + +template <> +void Stretcher::next(Request &request) const +{ + implementation->next(request); +} + } // namespace Bungee diff --git a/src/Stretcher.h b/src/Basic.h similarity index 83% rename from src/Stretcher.h rename to src/Basic.h index 04ee858..ab1010f 100644 --- a/src/Stretcher.h +++ b/src/Basic.h @@ -10,14 +10,14 @@ namespace Bungee { -struct Stretcher::Implementation : +struct Basic : Timing { Input input; Grains grains; Output output; - Implementation(SampleRates sampleRates, int channelCount); + Basic(SampleRates sampleRates, int channelCount); InputChunk specifyGrain(const Request &request); diff --git a/src/Fourier.h b/src/Fourier.h index b11003a..d8969da 100644 --- a/src/Fourier.h +++ b/src/Fourier.h @@ -15,7 +15,7 @@ #include namespace Bungee { -const char *version(); +extern const char *versionDescription; } namespace Bungee::Fourier { @@ -36,7 +36,9 @@ inline constexpr int binCount(int log2TransformLength) template inline Scalar uninitialisedValue() { - return *(Scalar *)version(); + // This value changes at every commit. + // So any usage of uninitialised values in computations should be detected as a change in output. + return *(Scalar *)versionDescription; } template <> diff --git a/src/Grain.cpp b/src/Grain.cpp index 1f0b0c8..771a1cd 100644 --- a/src/Grain.cpp +++ b/src/Grain.cpp @@ -32,7 +32,7 @@ InputChunk Grain::specify(const Request &r, Grain &previous, SampleRates sampleR const Assert::FloatingPointExceptions floatingPointExceptions(FE_INEXACT); - const auto unitHop = (1 << log2SynthesisHop) * resampleOperations.setup(sampleRates, request.resampleMode, request.pitch); + const auto unitHop = (1 << log2SynthesisHop) * resampleOperations.setup(sampleRates, request.pitch); requestHop = request.position - previous.request.position; if (std::isnan(requestHop) || request.reset) diff --git a/bungee/Modes.h b/src/Modes.h similarity index 91% rename from bungee/Modes.h rename to src/Modes.h index 864892b..c619267 100644 --- a/bungee/Modes.h +++ b/src/Modes.h @@ -15,19 +15,21 @@ X_END(Resample, resample) #define BUNGEE_MODES \ - BUNGEE_MODES_RESAMPLE \ - // + BUNGEE_MODES_RESAMPLE +// namespace Bungee { #define X_BEGIN(Type, type) \ - enum class Type##Mode \ - { + struct Type##Mode \ + { \ + enum Enum \ + { #define X_ITEM(Type, type, mode, description) \ mode, #define X_END(Type, type) \ - } \ - ; + }; \ + }; BUNGEE_MODES diff --git a/src/Resample.h b/src/Resample.h index 216c4a6..fc5b921 100644 --- a/src/Resample.h +++ b/src/Resample.h @@ -4,6 +4,8 @@ #pragma once #include "Assert.h" +#include "Modes.h" + #include "bungee/Bungee.h" #include @@ -178,7 +180,7 @@ struct Operations { Operation input, output; - double setup(const SampleRates &sampleRates, ResampleMode resampleMode, double pitch) + double setup(const SampleRates &sampleRates, double pitch, ResampleMode::Enum resampleMode = {}) { const double resampleRatio = pitch * sampleRates.input / sampleRates.output; input.ratio = 1.f / resampleRatio; diff --git a/src/Timing.cpp b/src/Timing.cpp index f516da9..4080ffe 100644 --- a/src/Timing.cpp +++ b/src/Timing.cpp @@ -34,7 +34,7 @@ int Timing::maxOutputFrameCount(bool mayUpsampleOutput) const double Timing::calculateInputHop(const Request &request) const { - const double unitHop = (1 << log2SynthesisHop) * Resample::Operations().setup(sampleRates, request.resampleMode, request.pitch); + const double unitHop = (1 << log2SynthesisHop) * Resample::Operations().setup(sampleRates, request.pitch); return unitHop * request.speed; }