Skip to content

Commit

Permalink
improved, static polymorphic API
Browse files Browse the repository at this point in the history
  • Loading branch information
kupix committed Apr 14, 2024
1 parent 5e8d5cc commit 90959e5
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 95 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ 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<Basic>` object and initialise a `Request` object:

``` C++
#include "Bungee.h"
#include <cmath>

const int sampleRate = 44100;

Bungee::Stretcher stretcher({sampleRate, sampleRate}, 2);
Bungee::Stretcher<Bungee::Basic> stretcher({sampleRate, sampleRate}, 2);

Bungee::Request request{};

Expand All @@ -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<Basic>::specifiyGrain`, `Stretcher<Basic>::analyseGain` and `Stretcher<Basic>::synthesiseGrain` should be called in sequence.
```C++
while (true)
{
Expand Down Expand Up @@ -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<Basic>` instance reads in the input chunk data when `Stretcher<Basic>::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<Basic>` instance owns the output audio buffer. It is valid from when `Stretcher<Basic>::synthesiseGrain` returns up until `Stretcher<Basic>::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`.

Expand Down
56 changes: 24 additions & 32 deletions bungee/Bungee.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@

#pragma once

#include "Modes.h"

#include <cmath>
#include <cstdint>

#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.
Expand All @@ -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;
}
Expand All @@ -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 <class Implementation>
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<Basic> is the open-source implementation contained in this repository
struct Basic;

// Stretcher<Pro> is an enhanced and optimised implementation that is available under commercial license
struct Pro;

} // namespace Bungee
2 changes: 1 addition & 1 deletion bungee/CommandLine.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct Options :
{
std::vector<std::string> 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<Basic>::version() + "\n") :
cxxopts::Options(program_name, help_string)
{
add_options() //
Expand Down
2 changes: 1 addition & 1 deletion cmd/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Basic> stretcher(processor.sampleRates, processor.channelCount);

processor.restart(request);
stretcher.preroll(request);
Expand Down
94 changes: 52 additions & 42 deletions src/Stretcher.cpp → src/Basic.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -10,57 +10,31 @@ namespace Bungee {

extern const char *versionDescription;

const char *version()
template <>
const char *Stretcher<Basic>::version()
{
return versionDescription;
}

Stretcher::Stretcher(SampleRates sampleRates, int channelCount) :
state(new Implementation(sampleRates, channelCount))
template <>
void Stretcher<Basic>::analyseGrain(const float *data, intptr_t channelStride)
{
implementation->analyseGrain(data, channelStride);
}

Stretcher::~Stretcher()
template <>
void Stretcher<Basic>::synthesiseGrain(OutputChunk &outputChunk)
{
delete state;
implementation->synthesiseGrain(outputChunk);
}

InputChunk Stretcher::specifyGrain(const Request &request)
template <>
bool Stretcher<Basic>::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),
Expand All @@ -70,7 +44,7 @@ Stretcher::Implementation::Implementation(SampleRates sampleRates, int channelCo
grain = std::make_unique<Grain>(log2SynthesisHop, channelCount);
}

InputChunk Stretcher::Implementation::specifyGrain(const Request &request)
InputChunk Basic::specifyGrain(const Request &request)
{
const Assert::FloatingPointExceptions floatingPointExceptions(0);

Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -155,4 +129,40 @@ void Stretcher::Implementation::synthesiseGrain(OutputChunk &outputChunk)
outputChunk.request[OutputChunk::end] = &grains[1].request;
}

template <>
Stretcher<Basic>::Stretcher(SampleRates sampleRates, int channelCount) :
implementation(new Basic(sampleRates, channelCount))
{
}

template <>
Stretcher<Basic>::~Stretcher()
{
delete implementation;
}

template <>
InputChunk Stretcher<Basic>::specifyGrain(const Request &request)
{
return implementation->specifyGrain(request);
}

template <>
int Stretcher<Basic>::maxInputFrameCount() const
{
return implementation->maxInputFrameCount(true);
}

template <>
void Stretcher<Basic>::preroll(Request &request) const
{
implementation->preroll(request);
}

template <>
void Stretcher<Basic>::next(Request &request) const
{
implementation->next(request);
}

} // namespace Bungee
4 changes: 2 additions & 2 deletions src/Stretcher.h → src/Basic.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 4 additions & 2 deletions src/Fourier.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#include <vector>

namespace Bungee {
const char *version();
extern const char *versionDescription;
}

namespace Bungee::Fourier {
Expand All @@ -36,7 +36,9 @@ inline constexpr int binCount(int log2TransformLength)
template <typename Scalar>
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 <>
Expand Down
2 changes: 1 addition & 1 deletion src/Grain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 90959e5

Please sign in to comment.