From 5935d17ee0cdde1f2fda2444fc48d68fcb279b1f Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 29 Jun 2023 16:49:30 +0100 Subject: [PATCH 01/35] Introduce first cut of live shifter --- ladspa-lv2/RubberBandLivePitchShifter.cpp | 582 ++++++++ ladspa-lv2/RubberBandLivePitchShifter.h | 148 ++ ladspa-lv2/ladspa-rubberband.cat | 2 + ladspa-lv2/ladspa-rubberband.rdf | 4 + ladspa-lv2/libmain-ladspa.cpp | 5 +- ladspa-lv2/libmain-lv2.cpp | 5 +- ladspa-lv2/rubberband.lv2/lv2-rubberband.ttl | 89 ++ ladspa-lv2/rubberband.lv2/manifest.ttl | 10 + meson.build | 4 + rubberband/RubberBandLiveShifter.h | 310 +++++ src/RubberBandLiveShifter.cpp | 273 ++++ src/finer/R3LiveShifter.cpp | 1169 ++++++++++++++++ src/finer/R3LiveShifter.h | 413 ++++++ src/test/TestLiveShifter.cpp | 1272 ++++++++++++++++++ 14 files changed, 4284 insertions(+), 2 deletions(-) create mode 100644 ladspa-lv2/RubberBandLivePitchShifter.cpp create mode 100644 ladspa-lv2/RubberBandLivePitchShifter.h create mode 100644 rubberband/RubberBandLiveShifter.h create mode 100644 src/RubberBandLiveShifter.cpp create mode 100644 src/finer/R3LiveShifter.cpp create mode 100644 src/finer/R3LiveShifter.h create mode 100644 src/test/TestLiveShifter.cpp diff --git a/ladspa-lv2/RubberBandLivePitchShifter.cpp b/ladspa-lv2/RubberBandLivePitchShifter.cpp new file mode 100644 index 00000000..273700a3 --- /dev/null +++ b/ladspa-lv2/RubberBandLivePitchShifter.cpp @@ -0,0 +1,582 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#include "RubberBandLivePitchShifter.h" + +#include "RubberBandLiveShifter.h" + +#include +#include + +using namespace RubberBand; + +using std::cout; +using std::cerr; +using std::endl; +using std::min; + +#ifdef RB_PLUGIN_LADSPA + +const char *const +RubberBandLivePitchShifter::portNamesMono[PortCountMono] = +{ + "latency", + "Cents", + "Semitones", + "Octaves", + "Formant Preserving", + "Wet-Dry Mix", + "Input", + "Output" +}; + +const char *const +RubberBandLivePitchShifter::portNamesStereo[PortCountStereo] = +{ + "latency", + "Cents", + "Semitones", + "Octaves", + "Formant Preserving", + "Wet-Dry Mix", + "Input L", + "Output L", + "Input R", + "Output R" +}; + +const LADSPA_PortDescriptor +RubberBandLivePitchShifter::portsMono[PortCountMono] = +{ + LADSPA_PORT_OUTPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO, + LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO +}; + +const LADSPA_PortDescriptor +RubberBandLivePitchShifter::portsStereo[PortCountStereo] = +{ + LADSPA_PORT_OUTPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL, + LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO, + LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO, + LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO, + LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO +}; + +const LADSPA_PortRangeHint +RubberBandLivePitchShifter::hintsMono[PortCountMono] = +{ + { 0, 0, 0 }, // latency + { LADSPA_HINT_DEFAULT_0 | // cents + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE, + -100.0, 100.0 }, + { LADSPA_HINT_DEFAULT_0 | // semitones + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE | + LADSPA_HINT_INTEGER, + -12.0, 12.0 }, + { LADSPA_HINT_DEFAULT_0 | // octaves + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE | + LADSPA_HINT_INTEGER, + -2.0, 2.0 }, + { LADSPA_HINT_DEFAULT_0 | // formant preserving + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE | + LADSPA_HINT_TOGGLED, + 0.0, 1.0 }, + { LADSPA_HINT_DEFAULT_0 | // wet-dry mix + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE, + 0.0, 1.0 }, + { 0, 0, 0 }, + { 0, 0, 0 } +}; + +const LADSPA_PortRangeHint +RubberBandLivePitchShifter::hintsStereo[PortCountStereo] = +{ + { 0, 0, 0 }, // latency + { LADSPA_HINT_DEFAULT_0 | // cents + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE, + -100.0, 100.0 }, + { LADSPA_HINT_DEFAULT_0 | // semitones + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE | + LADSPA_HINT_INTEGER, + -12.0, 12.0 }, + { LADSPA_HINT_DEFAULT_0 | // octaves + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE | + LADSPA_HINT_INTEGER, + -2.0, 2.0 }, + { LADSPA_HINT_DEFAULT_0 | // formant preserving + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE | + LADSPA_HINT_TOGGLED, + 0.0, 1.0 }, + { LADSPA_HINT_DEFAULT_0 | // wet-dry mix + LADSPA_HINT_BOUNDED_BELOW | + LADSPA_HINT_BOUNDED_ABOVE, + 0.0, 1.0 }, + { 0, 0, 0 }, + { 0, 0, 0 }, + { 0, 0, 0 }, + { 0, 0, 0 } +}; + +const LADSPA_Properties +RubberBandLivePitchShifter::properties = LADSPA_PROPERTY_HARD_RT_CAPABLE; + +const LADSPA_Descriptor +RubberBandLivePitchShifter::ladspaDescriptorMono = +{ + 29791, // "Unique" ID + "rubberband-live-pitchshifter-mono", // Label + properties, + "Rubber Band Live Mono Pitch Shifter", // Name + "Breakfast Quay", + "GPL", + PortCountMono, + portsMono, + portNamesMono, + hintsMono, + nullptr, // Implementation data + instantiate, + connectPort, + activate, + run, + nullptr, // Run adding + nullptr, // Set run adding gain + deactivate, + cleanup +}; + +const LADSPA_Descriptor +RubberBandLivePitchShifter::ladspaDescriptorStereo = +{ + 97921, // "Unique" ID + "rubberband-live-pitchshifter-stereo", // Label + properties, + "Rubber Band Live Stereo Pitch Shifter", // Name + "Breakfast Quay", + "GPL", + PortCountStereo, + portsStereo, + portNamesStereo, + hintsStereo, + nullptr, // Implementation data + instantiate, + connectPort, + activate, + run, + nullptr, // Run adding + nullptr, // Set run adding gain + deactivate, + cleanup +}; + +const LADSPA_Descriptor * +RubberBandLivePitchShifter::getDescriptor(unsigned long index) +{ + if (index == 0) return &ladspaDescriptorMono; + if (index == 1) return &ladspaDescriptorStereo; + else return 0; +} + +#else + +const LV2_Descriptor +RubberBandLivePitchShifter::lv2DescriptorMono = +{ + "http://breakfastquay.com/rdf/lv2-rubberband-live#mono", + instantiate, + connectPort, + activate, + run, + deactivate, + cleanup, + nullptr +}; + +const LV2_Descriptor +RubberBandLivePitchShifter::lv2DescriptorStereo = +{ + "http://breakfastquay.com/rdf/lv2-rubberband-live#stereo", + instantiate, + connectPort, + activate, + run, + deactivate, + cleanup, + nullptr +}; + +const LV2_Descriptor * +RubberBandLivePitchShifter::getDescriptor(uint32_t index) +{ + if (index == 0) return &lv2DescriptorMono; + if (index == 1) return &lv2DescriptorStereo; + else return 0; +} + +#endif + +RubberBandLivePitchShifter::RubberBandLivePitchShifter(int sampleRate, size_t channels) : + m_latency(nullptr), + m_cents(nullptr), + m_semitones(nullptr), + m_octaves(nullptr), + m_formant(nullptr), + m_wetDry(nullptr), + m_ratio(1.0), + m_prevRatio(1.0), + m_currentFormant(false), + m_shifter(new RubberBandLiveShifter + (sampleRate, channels, + RubberBandLiveShifter::OptionWindowLong | + RubberBandLiveShifter::OptionPitchModeB | + RubberBandLiveShifter::OptionChannelsTogether)), + m_sampleRate(sampleRate), + m_channels(channels), + m_blockSize(0), + m_bufferSize(65536), + m_delay(0) +{ + m_input = new float *[m_channels]; + m_output = new float *[m_channels]; + + m_irb = new RingBuffer *[m_channels]; + m_orb = new RingBuffer *[m_channels]; + + m_ib = new float *[m_channels]; + m_ob = new float *[m_channels]; + + m_delayMixBuffer = new RingBuffer *[m_channels]; + + m_blockSize = m_shifter->getBlockSize(); + m_delay = m_shifter->getStartDelay(); + + for (int c = 0; c < m_channels; ++c) { + + m_irb[c] = new RingBuffer(m_bufferSize); + m_orb[c] = new RingBuffer(m_bufferSize); + m_irb[c]->zero(m_blockSize); + + m_ib[c] = new float[m_blockSize]; + m_ob[c] = new float[m_blockSize]; + + m_delayMixBuffer[c] = new RingBuffer(m_bufferSize + m_delay); + m_irb[c]->zero(m_delay); + } + + activateImpl(); +} + +RubberBandLivePitchShifter::~RubberBandLivePitchShifter() +{ + delete m_shifter; + for (int c = 0; c < m_channels; ++c) { + delete m_irb[c]; + delete m_orb[c]; + delete[] m_ib[c]; + delete[] m_ob[c]; + delete m_delayMixBuffer[c]; + } + delete[] m_irb; + delete[] m_orb; + delete[] m_ib; + delete[] m_ob; + delete[] m_delayMixBuffer; + delete[] m_output; + delete[] m_input; +} + +#ifdef RB_PLUGIN_LADSPA + +LADSPA_Handle +RubberBandLivePitchShifter::instantiate(const LADSPA_Descriptor *desc, unsigned long rate) +{ + if (desc->PortCount == ladspaDescriptorMono.PortCount) { + return new RubberBandLivePitchShifter(rate, 1); + } else if (desc->PortCount == ladspaDescriptorStereo.PortCount) { + return new RubberBandLivePitchShifter(rate, 2); + } + return nullptr; +} + +#else + +LV2_Handle +RubberBandLivePitchShifter::instantiate(const LV2_Descriptor *desc, double rate, + const char *, const LV2_Feature *const *) +{ + if (rate < 1.0) { + std::cerr << "RubberBandLivePitchShifter::instantiate: invalid sample rate " + << rate << " provided" << std::endl; + return nullptr; + } + size_t srate = size_t(round(rate)); + if (std::string(desc->URI) == lv2DescriptorMono.URI) { + return new RubberBandLivePitchShifter(srate, 1); + } else if (std::string(desc->URI) == lv2DescriptorStereo.URI) { + return new RubberBandLivePitchShifter(srate, 2); + } else { + std::cerr << "RubberBandLivePitchShifter::instantiate: unrecognised URI " + << desc->URI << " requested" << std::endl; + return nullptr; + } +} + +#endif + +#ifdef RB_PLUGIN_LADSPA +void +RubberBandLivePitchShifter::connectPort(LADSPA_Handle handle, + unsigned long port, LADSPA_Data *location) +#else +void +RubberBandLivePitchShifter::connectPort(LV2_Handle handle, + uint32_t port, void *location) +#endif +{ + RubberBandLivePitchShifter *shifter = (RubberBandLivePitchShifter *)handle; + + float **ports[PortCountStereo] = { + &shifter->m_latency, + &shifter->m_cents, + &shifter->m_semitones, + &shifter->m_octaves, + &shifter->m_formant, + &shifter->m_wetDry, + &shifter->m_input[0], + &shifter->m_output[0], + &shifter->m_input[1], + &shifter->m_output[1] + }; + + if (shifter->m_channels == 1) { + if (port >= PortCountMono) return; + } else { + if (port >= PortCountStereo) return; + } + + *ports[port] = (float *)location; + + if (shifter->m_latency) { + *(shifter->m_latency) = shifter->getLatency(); + } +} + +#ifdef RB_PLUGIN_LADSPA +void +RubberBandLivePitchShifter::activate(LADSPA_Handle handle) +#else +void +RubberBandLivePitchShifter::activate(LV2_Handle handle) +#endif +{ + RubberBandLivePitchShifter *shifter = (RubberBandLivePitchShifter *)handle; + shifter->activateImpl(); +} + +#ifdef RB_PLUGIN_LADSPA +void +RubberBandLivePitchShifter::run(LADSPA_Handle handle, unsigned long samples) +#else +void +RubberBandLivePitchShifter::run(LV2_Handle handle, uint32_t samples) +#endif +{ + RubberBandLivePitchShifter *shifter = (RubberBandLivePitchShifter *)handle; + shifter->runImpl(samples); +} + +#ifdef RB_PLUGIN_LADSPA +void +RubberBandLivePitchShifter::deactivate(LADSPA_Handle handle) +#else +void +RubberBandLivePitchShifter::deactivate(LV2_Handle handle) +#endif +{ + activate(handle); // both functions just reset the plugin +} + +#ifdef RB_PLUGIN_LADSPA +void +RubberBandLivePitchShifter::cleanup(LADSPA_Handle handle) +#else +void +RubberBandLivePitchShifter::cleanup(LV2_Handle handle) +#endif +{ + delete (RubberBandLivePitchShifter *)handle; +} + +void +RubberBandLivePitchShifter::activateImpl() +{ + updateRatio(); + m_prevRatio = m_ratio; + m_shifter->reset(); + m_shifter->setPitchScale(m_ratio); + + for (int c = 0; c < m_channels; ++c) { + m_irb[c]->reset(); + m_irb[c]->zero(m_blockSize); + m_orb[c]->reset(); + m_delayMixBuffer[c]->reset(); + m_delayMixBuffer[c]->zero(m_delay); + } +} + +void +RubberBandLivePitchShifter::updateRatio() +{ + // The octaves, semitones, and cents parameters are supposed to be + // integral: we want to enforce that, just to avoid + // inconsistencies between hosts if some respect the hints more + // than others + +#ifdef RB_PLUGIN_LADSPA + + // But we don't want to change the long-standing behaviour of the + // LADSPA plugin, so let's leave this as-is and only do "the right + // thing" for LV2 + double oct = (m_octaves ? *m_octaves : 0.0); + oct += (m_semitones ? *m_semitones : 0.0) / 12; + oct += (m_cents ? *m_cents : 0.0) / 1200; + m_ratio = pow(2.0, oct); + +#else + + // LV2 + + double octaves = round(m_octaves ? *m_octaves : 0.0); + if (octaves < -2.0) octaves = -2.0; + if (octaves > 2.0) octaves = 2.0; + + double semitones = round(m_semitones ? *m_semitones : 0.0); + if (semitones < -12.0) semitones = -12.0; + if (semitones > 12.0) semitones = 12.0; + + double cents = round(m_cents ? *m_cents : 0.0); + if (cents < -100.0) cents = -100.0; + if (cents > 100.0) cents = 100.0; + + m_ratio = pow(2.0, + octaves + + semitones / 12.0 + + cents / 1200.0); +#endif +} + +void +RubberBandLivePitchShifter::updateFormant() +{ + if (!m_formant) return; + + bool f = (*m_formant > 0.5f); + if (f == m_currentFormant) return; + + RubberBandLiveShifter *s = m_shifter; + + s->setFormantOption(f ? + RubberBandLiveShifter::OptionFormantPreserved : + RubberBandLiveShifter::OptionFormantShifted); + + m_currentFormant = f; +} + +int +RubberBandLivePitchShifter::getLatency() const +{ + return m_shifter->getStartDelay() + m_blockSize; +} + +void +RubberBandLivePitchShifter::runImpl(uint32_t insamples) +{ + updateRatio(); + if (m_ratio != m_prevRatio) { + m_shifter->setPitchScale(m_ratio); + m_prevRatio = m_ratio; + } + + updateFormant(); + + if (m_latency) { + *m_latency = getLatency(); + } + + for (int c = 0; c < m_channels; ++c) { + m_irb[c]->write(m_input[c], insamples); + m_delayMixBuffer[c]->write(m_input[c], insamples); + } + + while (m_irb[0]->getReadSpace() >= m_blockSize) { + + for (int c = 0; c < m_channels; ++c) { + m_irb[c]->read(m_ib[c], m_blockSize); + } + + m_shifter->shift(m_ib, m_ob); + + for (int c = 0; c < m_channels; ++c) { + m_orb[c]->write(m_ob[c], m_blockSize); + } + } + + for (int c = 0; c < m_channels; ++c) { + m_orb[c]->read(m_output[c], insamples); + } + + float mix = 0.0; + if (m_wetDry) mix = *m_wetDry; + + for (int c = 0; c < m_channels; ++c) { + if (mix > 0.0) { + for (uint32_t i = 0; i < insamples; ++i) { + float dry = m_delayMixBuffer[c]->readOne(); + m_output[c][i] *= (1.0 - mix); + m_output[c][i] += dry * mix; + } + } else { + m_delayMixBuffer[c]->skip(insamples); + } + } +} + diff --git a/ladspa-lv2/RubberBandLivePitchShifter.h b/ladspa-lv2/RubberBandLivePitchShifter.h new file mode 100644 index 00000000..5c9939b6 --- /dev/null +++ b/ladspa-lv2/RubberBandLivePitchShifter.h @@ -0,0 +1,148 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#ifndef RUBBER_BAND_LIVE_SHIFTER_H +#define RUBBER_BAND_LIVE_SHIFTER_H + +#ifdef RB_PLUGIN_LADSPA +#ifdef RB_PLUGIN_LV2 +#error "Only one of RB_PLUGIN_LADSPA and RB_PLUGIN_LV2 may be defined at once" +#endif +#else +#ifndef RB_PLUGIN_LV2 +#error "Including code must define either RB_PLUGIN_LADSPA or RB_PLUGIN_LV2" +#endif +#endif + +#ifdef RB_PLUGIN_LADSPA +#include +#else +#include +#endif + +#include "common/RingBuffer.h" + +namespace RubberBand { +class RubberBandLiveShifter; +} + +class RubberBandLivePitchShifter +{ +public: +#ifdef RB_PLUGIN_LADSPA + static const LADSPA_Descriptor *getDescriptor(unsigned long index); +#else + static const LV2_Descriptor *getDescriptor(uint32_t index); +#endif + +protected: + RubberBandLivePitchShifter(int sampleRate, size_t channels); + ~RubberBandLivePitchShifter(); + + enum { + LatencyPort = 0, + CentsPort = 1, + SemitonesPort = 2, + OctavesPort = 3, + FormantPort = 4, + WetDryPort = 5, + InputPort1 = 6, + OutputPort1 = 7, + PortCountMono = OutputPort1 + 1, + InputPort2 = 8, + OutputPort2 = 9, + PortCountStereo = OutputPort2 + 1 + }; + +#ifdef RB_PLUGIN_LADSPA + static const char *const portNamesMono[PortCountMono]; + static const LADSPA_PortDescriptor portsMono[PortCountMono]; + static const LADSPA_PortRangeHint hintsMono[PortCountMono]; + + static const char *const portNamesStereo[PortCountStereo]; + static const LADSPA_PortDescriptor portsStereo[PortCountStereo]; + static const LADSPA_PortRangeHint hintsStereo[PortCountStereo]; + + static const LADSPA_Properties properties; + + static const LADSPA_Descriptor ladspaDescriptorMono; + static const LADSPA_Descriptor ladspaDescriptorStereo; + + static LADSPA_Handle instantiate(const LADSPA_Descriptor *, unsigned long); + static void connectPort(LADSPA_Handle, unsigned long, LADSPA_Data *); + static void activate(LADSPA_Handle); + static void run(LADSPA_Handle, unsigned long); + static void deactivate(LADSPA_Handle); + static void cleanup(LADSPA_Handle); + +#else + + static const LV2_Descriptor lv2DescriptorMono; + static const LV2_Descriptor lv2DescriptorStereo; + + static LV2_Handle instantiate(const LV2_Descriptor *, double, + const char *, const LV2_Feature *const *); + static void connectPort(LV2_Handle, uint32_t, void *); + static void activate(LV2_Handle); + static void run(LV2_Handle, uint32_t); + static void deactivate(LV2_Handle); + static void cleanup(LV2_Handle); + +#endif + + void activateImpl(); + void runImpl(uint32_t count); + void process(); + void updateRatio(); + void updateFormant(); + + float **m_input; + float **m_output; + float *m_latency; + float *m_cents; + float *m_semitones; + float *m_octaves; + float *m_formant; + float *m_wetDry; + double m_ratio; + double m_prevRatio; + bool m_currentFormant; + + RubberBand::RubberBandLiveShifter *m_shifter; + RubberBand::RingBuffer **m_irb; + RubberBand::RingBuffer **m_orb; + float **m_ib; + float **m_ob; + RubberBand::RingBuffer **m_delayMixBuffer; + + int m_sampleRate; + int m_channels; + int m_blockSize; + int m_bufferSize; + int m_delay; + + int getLatency() const; +}; + + +#endif diff --git a/ladspa-lv2/ladspa-rubberband.cat b/ladspa-lv2/ladspa-rubberband.cat index 907081f1..ee794154 100644 --- a/ladspa-lv2/ladspa-rubberband.cat +++ b/ladspa-lv2/ladspa-rubberband.cat @@ -2,3 +2,5 @@ ladspa:ladspa-rubberband:rubberband-pitchshifter-mono::Frequency > Pitch shifter ladspa:ladspa-rubberband:rubberband-pitchshifter-stereo::Frequency > Pitch shifters ladspa:ladspa-rubberband:rubberband-r3-pitchshifter-mono::Frequency > Pitch shifters ladspa:ladspa-rubberband:rubberband-r3-pitchshifter-stereo::Frequency > Pitch shifters +ladspa:ladspa-rubberband:rubberband-live-pitchshifter-mono::Frequency > Pitch shifters +ladspa:ladspa-rubberband:rubberband-live-pitchshifter-stereo::Frequency > Pitch shifters diff --git a/ladspa-lv2/ladspa-rubberband.rdf b/ladspa-lv2/ladspa-rubberband.rdf index 23a3cefb..05c46c83 100644 --- a/ladspa-lv2/ladspa-rubberband.rdf +++ b/ladspa-lv2/ladspa-rubberband.rdf @@ -9,6 +9,10 @@ + + + + diff --git a/ladspa-lv2/libmain-ladspa.cpp b/ladspa-lv2/libmain-ladspa.cpp index ab9d42a1..a8fa946a 100644 --- a/ladspa-lv2/libmain-ladspa.cpp +++ b/ladspa-lv2/libmain-ladspa.cpp @@ -25,6 +25,7 @@ #undef RB_PLUGIN_LV2 #include "RubberBandPitchShifter.cpp" #include "RubberBandR3PitchShifter.cpp" +#include "RubberBandLivePitchShifter.cpp" #include @@ -34,8 +35,10 @@ const LADSPA_Descriptor *ladspa_descriptor(unsigned long index) { if (index < 2) { return RubberBandPitchShifter::getDescriptor(index); - } else { + } else if (index < 4) { return RubberBandR3PitchShifter::getDescriptor(index - 2); + } else { + return RubberBandLivePitchShifter::getDescriptor(index - 4); } } diff --git a/ladspa-lv2/libmain-lv2.cpp b/ladspa-lv2/libmain-lv2.cpp index b27f124a..5acafec1 100644 --- a/ladspa-lv2/libmain-lv2.cpp +++ b/ladspa-lv2/libmain-lv2.cpp @@ -25,6 +25,7 @@ #undef RB_PLUGIN_LADSPA #include "RubberBandPitchShifter.cpp" #include "RubberBandR3PitchShifter.cpp" +#include "RubberBandLivePitchShifter.cpp" #include @@ -35,8 +36,10 @@ const LV2_Descriptor *lv2_descriptor(uint32_t index) { if (index < 2) { return RubberBandPitchShifter::getDescriptor(index); - } else { + } else if (index < 4) { return RubberBandR3PitchShifter::getDescriptor(index - 2); + } else { + return RubberBandLivePitchShifter::getDescriptor(index - 4); } } diff --git a/ladspa-lv2/rubberband.lv2/lv2-rubberband.ttl b/ladspa-lv2/rubberband.lv2/lv2-rubberband.ttl index 6bc2b536..0621e10f 100644 --- a/ladspa-lv2/rubberband.lv2/lv2-rubberband.ttl +++ b/ladspa-lv2/rubberband.lv2/lv2-rubberband.ttl @@ -201,6 +201,43 @@ rubberband:r3mono lv2:designation pg:center ; ] . +rubberband:livemono + a doap:Project, lv2:Plugin, lv2:PitchPlugin ; + doap:name "Rubber Band Live Mono Pitch Shifter" ; + doap:license ; + foaf:maker :maker ; + doap:developer :maker ; + doap:maintainer :maker ; + # Minor version will be 2x the Rubber Band API minor version + lv2:minorVersion 4 ; + lv2:microVersion 1 ; + lv2:optionalFeature lv2:hardRTCapable ; + pg:mainInput rubberband:mono_in_group ; + pg:mainOutput rubberband:mono_out_group ; + dc:replaces ; + lv2:port :latencyPort , + :centsPort , + :semitonesPort , + :octavesPort , + :formantPortR3 , + :wetDryPortR3 , + [ a lv2:AudioPort, lv2:InputPort ; + lv2:index 6 ; + lv2:symbol "input" ; + lv2:name "Input" ; + lv2:shortName "Input" ; + pg:group rubberband:mono_in_group ; + lv2:designation pg:center ; + ], [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 7 ; + lv2:symbol "output" ; + lv2:name "Output" ; + lv2:shortName "Output" ; + pg:group rubberband:mono_out_group ; + lv2:designation pg:center ; + ] . + rubberband:stereo a doap:Project, lv2:Plugin, lv2:PitchPlugin ; doap:name "Rubber Band Stereo Pitch Shifter" ; @@ -306,3 +343,55 @@ rubberband:r3stereo lv2:designation pg:right ; ] . +rubberband:livestereo + a doap:Project, lv2:Plugin, lv2:PitchPlugin ; + doap:name "Rubber Band Live Stereo Pitch Shifter" ; + doap:license ; + foaf:maker :maker ; + doap:developer :maker ; + doap:maintainer :maker ; + # Minor version will be 2x the Rubber Band API minor version + lv2:minorVersion 4 ; + lv2:microVersion 1 ; + lv2:optionalFeature lv2:hardRTCapable ; + pg:mainInput rubberband:stereo_in_group ; + pg:mainOutput rubberband:stereo_out_group ; + dc:replaces ; + lv2:port :latencyPort , + :centsPort , + :semitonesPort , + :octavesPort , + :formantPortR3 , + :wetDryPortR3 , + [ a lv2:AudioPort, lv2:InputPort ; + lv2:index 6 ; + lv2:symbol "input_l" ; + lv2:name "Input L" ; + lv2:shortName "Input L" ; + pg:group rubberband:stereo_in_group ; + lv2:designation pg:left ; + ], [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 7 ; + lv2:symbol "output_l" ; + lv2:name "Output L" ; + lv2:shortName "Output L" ; + pg:group rubberband:stereo_out_group ; + lv2:designation pg:left ; + ], [ a lv2:AudioPort, lv2:InputPort ; + lv2:index 8 ; + lv2:symbol "input_r" ; + lv2:name "Input R" ; + lv2:shortName "Input R" ; + pg:group rubberband:stereo_in_group ; + lv2:designation pg:right ; + ], [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 9 ; + lv2:symbol "output_r" ; + lv2:name "Output R" ; + lv2:shortName "Output R" ; + pg:group rubberband:stereo_out_group ; + lv2:designation pg:right ; + ] . + diff --git a/ladspa-lv2/rubberband.lv2/manifest.ttl b/ladspa-lv2/rubberband.lv2/manifest.ttl index ccba9dd1..d71bcc79 100644 --- a/ladspa-lv2/rubberband.lv2/manifest.ttl +++ b/ladspa-lv2/rubberband.lv2/manifest.ttl @@ -12,6 +12,11 @@ rubberband:r3mono lv2:binary ; rdfs:seeAlso . +rubberband:livemono + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + rubberband:stereo a lv2:Plugin ; lv2:binary ; @@ -22,3 +27,8 @@ rubberband:r3stereo lv2:binary ; rdfs:seeAlso . +rubberband:livestereo + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + diff --git a/meson.build b/meson.build index 535b3db8..c21f0d3c 100644 --- a/meson.build +++ b/meson.build @@ -29,11 +29,13 @@ pkg = import('pkgconfig') public_headers = [ 'rubberband/rubberband-c.h', 'rubberband/RubberBandStretcher.h', + 'rubberband/RubberBandLiveShifter.h', ] library_sources = [ 'src/rubberband-c.cpp', 'src/RubberBandStretcher.cpp', + 'src/RubberBandLiveShifter.cpp', 'src/faster/AudioCurveCalculator.cpp', 'src/faster/CompoundAudioCurve.cpp', 'src/faster/HighFrequencyAudioCurve.cpp', @@ -52,6 +54,7 @@ library_sources = [ 'src/common/mathmisc.cpp', 'src/common/Thread.cpp', 'src/finer/R3Stretcher.cpp', + 'src/finer/R3LiveShifter.cpp', ] jni_sources = [ @@ -89,6 +92,7 @@ lv2_sources = [ unit_test_sources = [ 'src/test/TestAllocators.cpp', 'src/test/TestFFT.cpp', + 'src/test/TestLiveShifter.cpp', 'src/test/TestResampler.cpp', 'src/test/TestVectorOpsComplex.cpp', 'src/test/TestVectorOps.cpp', diff --git a/rubberband/RubberBandLiveShifter.h b/rubberband/RubberBandLiveShifter.h new file mode 100644 index 00000000..55c9d7fd --- /dev/null +++ b/rubberband/RubberBandLiveShifter.h @@ -0,0 +1,310 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Live Pitch Shifter obtained by agreement with the + copyright holders, you may redistribute and/or modify it under the + terms described in that licence. + + If you wish to distribute code using Rubber Band Live under terms + other than those of the GNU General Public License, you must + obtain a valid commercial licence before doing so. +*/ + +#ifndef RUBBERBAND_LIVE_SHIFTER_H +#define RUBBERBAND_LIVE_SHIFTER_H + +#define RUBBERBAND_LIVE_VERSION "0.0.1" +#define RUBBERBAND_LIVE_API_MAJOR_VERSION 0 +#define RUBBERBAND_LIVE_API_MINOR_VERSION 0 + +#undef RUBBERBAND_LIVE_DLLEXPORT +#ifdef _MSC_VER +#define RUBBERBAND_LIVE_DLLEXPORT __declspec(dllexport) +#else +#define RUBBERBAND_LIVE_DLLEXPORT +#endif + +#include +#include +#include +#include +#include + +namespace RubberBand +{ + +/** + * @mainpage RubberBandLiveShifter + */ +class RUBBERBAND_LIVE_DLLEXPORT +RubberBandLiveShifter +{ +public: + enum Option { + OptionWindowShort = 0x00000000, + OptionWindowMedium = 0x00100000, + OptionWindowLong = 0x00200000, + + OptionFormantShifted = 0x00000000, + OptionFormantPreserved = 0x01000000, + + OptionPitchModeA = 0x00000000, + OptionPitchModeB = 0x02000000, + + OptionChannelsApart = 0x00000000, + OptionChannelsTogether = 0x10000000, + + // n.b. Options is int, so we must stop before 0x80000000 + }; + + /** + * A bitwise OR of values from the RubberBandLiveShifter::Option + * enum. + */ + typedef int Options; + + enum PresetOption { + DefaultOptions = 0x00000000 + }; + + /** + * Interface for log callbacks that may optionally be provided to + * the shifter on construction. + * + * If a Logger is provided, the shifter will call one of these + * functions instead of sending output to \c cerr when there is + * something to report. This allows debug output to be diverted to + * an application's logging facilities, and/or handled in an + * RT-safe way. See setDebugLevel() for details about how and when + * RubberBandLiveShifter reports something in this way. + * + * The message text passed to each of these log functions is a + * C-style string with no particular guaranteed lifespan. If you + * need to retain it, copy it before returning. Do not free it. + * + * @see setDebugLevel + * @see setDefaultDebugLevel + */ + struct Logger { + /// Receive a log message with no numeric values. + virtual void log(const char *) = 0; + + /// Receive a log message and one accompanying numeric value. + virtual void log(const char *, double) = 0; + + /// Receive a log message and two accompanying numeric values. + virtual void log(const char *, double, double) = 0; + + virtual ~Logger() { } + }; + + /** + * Construct a pitch shifter object to run at the given sample + * rate, with the given number of channels. + */ + RubberBandLiveShifter(size_t sampleRate, size_t channels, + Options options); + + /** + * Construct a pitch shifter object with a custom debug + * logger. This may be useful for debugging if the default logger + * output (which simply goes to \c cerr) is not visible in the + * runtime environment, or if the application has a standard or + * more realtime-appropriate logging mechanism. + * + * See the documentation for the other constructor above for + * details of the arguments other than the logger. + * + * Note that although the supplied logger gets to decide what to + * do with log messages, the separately-set debug level (see + * setDebugLevel() and setDefaultDebugLevel()) still determines + * whether any given debug message is sent to the logger in the + * first place. + */ + RubberBandLiveShifter(size_t sampleRate, size_t channels, + std::shared_ptr logger, + Options options); + + ~RubberBandLiveShifter(); + + /** + * Reset the shifter's internal buffers. The shifter should + * subsequently behave as if it had just been constructed + * (although retaining the current pitch ratio). + */ + void reset(); + + /** + * Set the pitch scaling ratio for the shifter. This is the ratio + * of target frequency to source frequency. For example, a ratio + * of 2.0 would shift up by one octave; 0.5 down by one octave; or + * 1.0 leave the pitch unaffected. + * + * To put this in musical terms, a pitch scaling ratio + * corresponding to a shift of S equal-tempered semitones (where S + * is positive for an upwards shift and negative for downwards) is + * pow(2.0, S / 12.0). + * + * This function may be called at any time, so long as it is not + * called concurrently with process(). You should either call + * this function from the same thread as process(), or provide + * your own mutex or similar mechanism to ensure that + * setPitchScale and process() cannot be run at once (there is no + * internal mutex for this purpose). + */ + void setPitchScale(double scale); + + /** + * Set a pitch scale for the vocal formant envelope separately + * from the overall pitch scale. This is a ratio of target + * frequency to source frequency. For example, a ratio of 2.0 + * would shift the formant envelope up by one octave; 0.5 down by + * one octave; or 1.0 leave the formant unaffected. + * + * By default this is set to the special value of 0.0, which + * causes the scale to be calculated automatically. It will be + * treated as 1.0 / the pitch scale if OptionFormantPreserved is + * specified, or 1.0 for OptionFormantShifted. + * + * Conversely, if this is set to a value other than the default + * 0.0, formant shifting will happen regardless of the state of + * the OptionFormantPreserved/OptionFormantShifted option. + * + * This function is provided for special effects only. You do not + * need to call it for ordinary pitch shifting, with or without + * formant preservation - just specify or omit the + * OptionFormantPreserved option as appropriate. Use this function + * only if you want to shift formants by a distance other than + * that of the overall pitch shift. + */ + void setFormantScale(double scale); + + /** + * Return the last pitch scaling ratio value that was set (either + * on construction or with setPitchScale()). + */ + double getPitchScale() const; + + /** + * Return the last formant scaling ratio that was set with + * setFormantScale, or 0.0 if the default automatic scaling is in + * effect. + */ + double getFormantScale() const; + + /** + * Return the output delay of the shifter. This is the number of + * audio samples that one should discard at the start of the + * output, in order to ensure that the resulting audio has the + * expected time alignment with the input. + * + * Ensure you have set the pitch scale to its proper starting + * value before calling getStartDelay(). + */ + size_t getStartDelay() const; + + /** + * Return the number of channels this shifter was constructed + * with. + */ + size_t getChannelCount() const; + + /** + * Change an OptionFormant configuration setting. This may be + * called at any time in any mode. + * + * Note that if running multi-threaded in Offline mode, the change + * may not take effect immediately if processing is already under + * way when this function is called. + */ + void setFormantOption(Options options); + + /** + * Query the number of sample frames that must be passed to, and + * will be returned by, each process() call. This value is fixed + * for the lifetime of the shifter. + * + * Note that the blocksize refers to the number of audio sample + * frames, which may be multi-channel, not the number of + * individual samples. + */ + size_t getBlockSize() const; + + /** + * Pitch-shift a single block of sample frames. The number of + * sample frames (samples per channel) processed per call is + * constant. + * + * "input" should point to de-interleaved audio data with one + * float array per channel, with each array containing n samples + * where n is the value returned by getBlockSize(). + * + * "output" should point to a float array per channel, with each + * array having enough room to store n samples where n is the value + * returned by getBlockSize(). + * + * Sample values are conventionally expected to be in the range + * -1.0f to +1.0f. + */ + void shift(const float *const *input, float *const *output); + + /** + * Set the level of debug output. The supported values are: + * + * 0. Report errors only. + * + * 1. Report some information on construction and ratio + * change. Nothing is reported during normal processing unless + * something changes. + * + * 2. Report a significant amount of information about ongoing + * calculations during normal processing. + * + * The default is whatever has been set using + * setDefaultDebugLevel(), or 0 if that function has not been + * called. + * + * All output goes to \c cerr unless a custom + * RubberBandLiveShifter::Logger has been provided on + * construction. Because writing to \c cerr is not RT-safe, only + * debug level 0 is RT-safe in normal use by default. Debug levels + * 0 and 1 use only C-string constants as debug messages, so they + * are RT-safe if your custom logger is RT-safe. Levels 2 and 3 + * are not guaranteed to be RT-safe in any conditions as they may + * construct messages by allocation. + * + * @see Logger + * @see setDefaultDebugLevel + */ + void setDebugLevel(int level); + + /** + * Set the default level of debug output for subsequently + * constructed shifters. + * + * @see setDebugLevel + */ + static void setDefaultDebugLevel(int level); + +protected: + class Impl; + Impl *m_d; + + RubberBandLiveShifter(const RubberBandLiveShifter &) =delete; + RubberBandLiveShifter &operator=(const RubberBandLiveShifter &) =delete; +}; + +} + +#endif diff --git a/src/RubberBandLiveShifter.cpp b/src/RubberBandLiveShifter.cpp new file mode 100644 index 00000000..5b01cd4e --- /dev/null +++ b/src/RubberBandLiveShifter.cpp @@ -0,0 +1,273 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Live Pitch Shifter obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Live Pitch Shifter + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#include "../rubberband/RubberBandLiveShifter.h" +#include "finer/R3LiveShifter.h" + +#include + +namespace RubberBand { + +class RubberBandLiveShifter::Impl +{ + R3LiveShifter *m_s; + + class CerrLogger : public RubberBandLiveShifter::Logger { + public: + void log(const char *message) override { + std::cerr << "RubberBandLive: " << message << "\n"; + } + void log(const char *message, double arg0) override { + auto prec = std::cerr.precision(); + std::cerr.precision(10); + std::cerr << "RubberBandLive: " << message << ": " << arg0 << "\n"; + std::cerr.precision(prec); + } + void log(const char *message, double arg0, double arg1) override { + auto prec = std::cerr.precision(); + std::cerr.precision(10); + std::cerr << "RubberBandLive: " << message + << ": (" << arg0 << ", " << arg1 << ")" << "\n"; + std::cerr.precision(prec); + } + }; + + Log makeRBLog(std::shared_ptr logger) { + if (logger) { + return Log( + [=](const char *message) { + logger->log(message); + }, + [=](const char *message, double arg0) { + logger->log(message, arg0); + }, + [=](const char *message, double arg0, double arg1) { + logger->log(message, arg0, arg1); + } + ); + } else { + return makeRBLog(std::shared_ptr + (new CerrLogger())); + } + } + +public: + Impl(size_t sampleRate, size_t channels, + std::shared_ptr logger, + RubberBandLiveShifter::Options options) : + m_s (new R3LiveShifter + (R3LiveShifter::Parameters(double(sampleRate), channels, + options), + makeRBLog(logger))) + { + } + + ~Impl() + { + delete m_s; + } + + void reset() + { + m_s->reset(); + } + + RTENTRY__ + void + setPitchScale(double scale) + { + m_s->setPitchScale(scale); + } + + RTENTRY__ + void + setFormantScale(double scale) + { + m_s->setFormantScale(scale); + } + + RTENTRY__ + double + getPitchScale() const + { + return m_s->getPitchScale(); + } + + RTENTRY__ + double + getFormantScale() const + { + return m_s->getFormantScale(); + } + + RTENTRY__ + void + setFormantOption(RubberBandLiveShifter::Options options) + { + m_s->setFormantOption(options); + } + + RTENTRY__ + size_t + getStartDelay() const + { + return m_s->getStartDelay(); + } + + RTENTRY__ + size_t + getBlockSize() const + { + return m_s->getBlockSize(); + } + + RTENTRY__ + void + shift(const float *const *input, float *const *output) + { + m_s->shift(input, output); + } + + RTENTRY__ + size_t + getChannelCount() const + { + return m_s->getChannelCount(); + } + + void + setDebugLevel(int level) + { + m_s->setDebugLevel(level); + } + + static void + setDefaultDebugLevel(int level) + { + Log::setDefaultDebugLevel(level); + } +}; + +RubberBandLiveShifter::RubberBandLiveShifter(size_t sampleRate, + size_t channels, + Options options) : + m_d(new Impl(sampleRate, channels, nullptr, options)) +{ +} + +RubberBandLiveShifter::RubberBandLiveShifter(size_t sampleRate, + size_t channels, + std::shared_ptr logger, + Options options) : + m_d(new Impl(sampleRate, channels, logger, options)) +{ +} + +RubberBandLiveShifter::~RubberBandLiveShifter() +{ + delete m_d; +} + +void +RubberBandLiveShifter::reset() +{ + m_d->reset(); +} + +RTENTRY__ +void +RubberBandLiveShifter::setPitchScale(double scale) +{ + m_d->setPitchScale(scale); +} + +RTENTRY__ +void +RubberBandLiveShifter::setFormantScale(double scale) +{ + m_d->setFormantScale(scale); +} + +RTENTRY__ +double +RubberBandLiveShifter::getPitchScale() const +{ + return m_d->getPitchScale(); +} + +RTENTRY__ +double +RubberBandLiveShifter::getFormantScale() const +{ + return m_d->getFormantScale(); +} + +RTENTRY__ +size_t +RubberBandLiveShifter::getStartDelay() const +{ + return m_d->getStartDelay(); +} + +RTENTRY__ +void +RubberBandLiveShifter::setFormantOption(Options options) +{ + m_d->setFormantOption(options); +} + +RTENTRY__ +size_t +RubberBandLiveShifter::getBlockSize() const +{ + return m_d->getBlockSize(); +} + +RTENTRY__ +void +RubberBandLiveShifter::shift(const float *const *input, float *const *output) +{ + m_d->shift(input, output); +} + +RTENTRY__ +size_t +RubberBandLiveShifter::getChannelCount() const +{ + return m_d->getChannelCount(); +} + +void +RubberBandLiveShifter::setDebugLevel(int level) +{ + m_d->setDebugLevel(level); +} + +void +RubberBandLiveShifter::setDefaultDebugLevel(int level) +{ + Impl::setDefaultDebugLevel(level); +} + +} + diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp new file mode 100644 index 00000000..d17b470b --- /dev/null +++ b/src/finer/R3LiveShifter.cpp @@ -0,0 +1,1169 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#include "R3LiveShifter.h" + +#include "../common/VectorOpsComplex.h" +#include "../common/Profiler.h" + +#include + +namespace RubberBand { + +R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : + m_log(log), + m_parameters(validateSampleRate(parameters)), + m_limits(parameters.options, m_parameters.sampleRate), + m_pitchScale(1.0), + m_formantScale(0.0), + m_guide(Guide::Parameters + (m_parameters.sampleRate, + !(m_parameters.options & RubberBandLiveShifter::OptionWindowLong)), + m_log), + m_guideConfiguration(m_guide.getConfiguration()), + m_channelAssembly(m_parameters.channels), + m_useReadahead(false), + m_contractThenExpand(false), + m_prevInhop(m_limits.maxInhopWithReadahead / 2), + m_prevOuthop(m_prevInhop), + m_firstProcess(true), + m_unityCount(0) +{ + Profiler profiler("R3LiveShifter::R3LiveShifter"); + + initialise(); +} + +void +R3LiveShifter::initialise() +{ + m_log.log(1, "R3LiveShifter::R3LiveShifter: rate, options", + m_parameters.sampleRate, m_parameters.options); + + if (!isSingleWindowed()) { + m_log.log(1, "R3LiveShifter::R3LiveShifter: multi window enabled"); + } + + if ((m_parameters.options & RubberBandLiveShifter::OptionWindowMedium) || + (m_parameters.options & RubberBandLiveShifter::OptionWindowLong)) { + m_log.log(1, "R3LiveShifter::R3LiveShifter: readahead enabled"); + m_useReadahead = true; + } + + if ((m_parameters.options & RubberBandLiveShifter::OptionPitchModeB)) { + m_log.log(1, "R3LiveShifter::R3LiveShifter: contract-then-expand enabled"); + m_contractThenExpand = true; + } + + double maxClassifierFrequency = 16000.0; + if (maxClassifierFrequency > m_parameters.sampleRate/2) { + maxClassifierFrequency = m_parameters.sampleRate/2; + } + int classificationBins = + int(floor(m_guideConfiguration.classificationFftSize * + maxClassifierFrequency / m_parameters.sampleRate)); + + BinSegmenter::Parameters segmenterParameters + (m_guideConfiguration.classificationFftSize, + classificationBins, m_parameters.sampleRate, 18); + + BinClassifier::Parameters classifierParameters + (classificationBins, 9, 1, 10, 2.0, 2.0); + + if (isSingleWindowed()) { + classifierParameters.horizontalFilterLength = 7; + } + + int inRingBufferSize = getWindowSourceSize() * 4; + int outRingBufferSize = getWindowSourceSize() * 4; + + m_channelData.clear(); + + for (int c = 0; c < m_parameters.channels; ++c) { + m_channelData.push_back(std::make_shared + (segmenterParameters, + classifierParameters, + m_guideConfiguration.longestFftSize, + getWindowSourceSize(), + inRingBufferSize, + outRingBufferSize)); + for (int b = 0; b < m_guideConfiguration.fftBandLimitCount; ++b) { + const auto &band = m_guideConfiguration.fftBandLimits[b]; + int fftSize = band.fftSize; + m_channelData[c]->scales[fftSize] = + std::make_shared + (fftSize, m_guideConfiguration.longestFftSize); + } + } + + m_scaleData.clear(); + + for (int b = 0; b < m_guideConfiguration.fftBandLimitCount; ++b) { + const auto &band = m_guideConfiguration.fftBandLimits[b]; + int fftSize = band.fftSize; + GuidedPhaseAdvance::Parameters guidedParameters + (fftSize, m_parameters.sampleRate, m_parameters.channels, + isSingleWindowed()); + m_scaleData[fftSize] = std::make_shared + (guidedParameters, m_log); + } + + createResamplers(); + + if (!m_pitchScale.is_lock_free()) { + m_log.log(0, "R3LiveShifter: WARNING: std::atomic is not lock-free"); + } +} + +WindowType +R3LiveShifter::ScaleData::analysisWindowShape() +{ + if (singleWindowMode) { + return HannWindow; + } else { + if (fftSize < 1024 || fftSize > 2048) return HannWindow; + else return NiemitaloForwardWindow; + } +} + +int +R3LiveShifter::ScaleData::analysisWindowLength() +{ + return fftSize; +} + +WindowType +R3LiveShifter::ScaleData::synthesisWindowShape() +{ + if (singleWindowMode) { + return HannWindow; + } else { + if (fftSize < 1024 || fftSize > 2048) return HannWindow; + else return NiemitaloReverseWindow; + } +} + +int +R3LiveShifter::ScaleData::synthesisWindowLength() +{ + if (singleWindowMode) { + return fftSize; + } else { + if (fftSize > 2048) return fftSize/2; + else return fftSize; + } +} + +void +R3LiveShifter::setPitchScale(double scale) +{ + m_log.log(2, "R3LiveShifter::setPitchScale", scale); + if (scale == m_pitchScale) return; + m_pitchScale = scale; +} + +void +R3LiveShifter::setFormantScale(double scale) +{ + m_log.log(2, "R3LiveShifter::setFormantScale", scale); + m_formantScale = scale; +} + +void +R3LiveShifter::setFormantOption(RubberBandLiveShifter::Options options) +{ + int mask = (RubberBandLiveShifter::OptionFormantShifted | + RubberBandLiveShifter::OptionFormantPreserved); + m_parameters.options &= ~mask; + options &= mask; + m_parameters.options |= options; +} + +void +R3LiveShifter::createResamplers() +{ + Profiler profiler("R3LiveShifter::createResamplers"); + + Resampler::Parameters resamplerParameters; + resamplerParameters.quality = Resampler::FastestTolerable; + resamplerParameters.initialSampleRate = m_parameters.sampleRate; + resamplerParameters.maxBufferSize = m_guideConfiguration.longestFftSize; + resamplerParameters.dynamism = Resampler::RatioOftenChanging; + resamplerParameters.ratioChange = Resampler::SmoothRatioChange; + + m_inResampler = std::unique_ptr + (new Resampler(resamplerParameters, m_parameters.channels)); + + m_outResampler = std::unique_ptr + (new Resampler(resamplerParameters, m_parameters.channels)); +} + +double +R3LiveShifter::getPitchScale() const +{ + return m_pitchScale; +} + +double +R3LiveShifter::getFormantScale() const +{ + return m_formantScale; +} + +size_t +R3LiveShifter::getPreferredStartPad() const +{ + //!!!??? + return 0; +} + +size_t +R3LiveShifter::getStartDelay() const +{ + int resamplerDelay = 32; + int fixed = getWindowSourceSize() / 2 + resamplerDelay; + int variable = getWindowSourceSize() / 2; + if (m_contractThenExpand) { + if (m_pitchScale < 1.0) { + return size_t(fixed + ceil(variable / m_pitchScale)); + } else { + return size_t(fixed + ceil(variable * m_pitchScale)); + } + } else { + if (m_pitchScale < 1.0) { + return size_t(fixed + ceil(variable * m_pitchScale)); + } else { + return size_t(fixed + ceil(variable / m_pitchScale)); + } + } +} + +size_t +R3LiveShifter::getChannelCount() const +{ + return m_parameters.channels; +} + +void +R3LiveShifter::reset() +{ + m_inResampler->reset(); + m_outResampler->reset(); + + m_unityCount = 0; + + m_prevInhop = m_limits.maxInhopWithReadahead / 2; + m_prevOuthop = int(round(m_prevInhop * m_pitchScale)); + m_firstProcess = true; + + for (auto &it : m_scaleData) { + it.second->guided.reset(); + } + + for (auto &cd : m_channelData) { + cd->reset(); + } +} + +size_t +R3LiveShifter::getBlockSize() const +{ + return 512; +} + +void +R3LiveShifter::shift(const float *const *input, float *const *output) +{ + Profiler profiler("R3LiveShifter::shift"); + + int incount = int(getBlockSize()); + + m_log.log(2, "R3LiveShifter::shift: start of process with incount", incount); + m_log.log(2, "R3LiveShifter::shift: initially in inbuf", m_channelData[0]->inbuf->getReadSpace()); + m_log.log(2, "R3LiveShifter::shift: initially in outbuf", m_channelData[0]->outbuf->getReadSpace()); + + int pad = 0; + int resamplerDelay = 32; + if (m_firstProcess) { + if (m_contractThenExpand) { + pad = getWindowSourceSize(); + if (m_pitchScale > 1.0) { + pad = int(ceil(pad * m_pitchScale)); + } + } else { + pad = getWindowSourceSize() / 2; + } + pad += resamplerDelay; + m_log.log(2, "R3LiveShifter::shift: extending input with pre-pad", incount, pad); + for (int c = 0; c < m_parameters.channels; ++c) { + m_channelData[c]->inbuf->zero(pad); + } + } + + readIn(input); + + double outRatio = 1.0; + + if (m_contractThenExpand) { + if (m_pitchScale < 1.0) { + outRatio = 1.0 / m_pitchScale; + } + } else { + if (m_pitchScale > 1.0) { + outRatio = 1.0 / m_pitchScale; + } + } + + int requiredInOutbuf = int(ceil(incount / outRatio)) + resamplerDelay; + generate(requiredInOutbuf); + + int got = readOut(output, incount, 0); + + if (got < incount) { + m_log.log(0, "R3LiveShifter::shift: ERROR: internal error: insufficient data at output (wanted, got)", incount, got); + for (int c = 0; c < m_parameters.channels; ++c) { + for (int i = got; i < incount; ++i) { + if (i > 0) output[c][i] = output[c][i-1] * 0.9f; + else output[c][i] = 0.f; + } + } + } + + m_log.log(2, "R3LiveShifter::shift: end of process with incount", incount); + m_log.log(2, "R3LiveShifter::shift: remaining in inbuf", m_channelData[0]->inbuf->getReadSpace()); + m_log.log(2, "R3LiveShifter::shift: remaining in outbuf", m_channelData[0]->outbuf->getReadSpace()); + m_log.log(2, "R3LiveShifter::shift: returning", got); + + m_firstProcess = false; +} + +void +R3LiveShifter::readIn(const float *const *input) +{ + int incount = int(getBlockSize()); + + int ws = m_channelData[0]->inbuf->getWriteSpace(); + if (ws < incount) { + m_log.log(0, "R3LiveShifter::process: ERROR: internal error: insufficient space in inbuf (wanted, got)", incount, ws); + return; + } + + for (int c = 0; c < m_parameters.channels; ++c) { + auto &cd = m_channelData.at(c); + m_channelAssembly.resampled[c] = cd->resampled.data(); + } + + if (useMidSide()) { + auto &c0 = m_channelData.at(0)->mixdown; + auto &c1 = m_channelData.at(1)->mixdown; + for (int i = 0; i < incount; ++i) { + float l = input[0][i]; + float r = input[1][i]; + float m = (l + r) / 2.f; + float s = (l - r) / 2.f; + c0[i] = m; + c1[i] = s; + } + m_channelAssembly.input[0] = m_channelData.at(0)->mixdown.data(); + m_channelAssembly.input[1] = m_channelData.at(1)->mixdown.data(); + } else { + for (int c = 0; c < m_parameters.channels; ++c) { + m_channelAssembly.input[c] = input[c]; + } + } + + double inRatio = 1.0; + + if (m_contractThenExpand) { + if (m_pitchScale > 1.0) { + inRatio = 1.0 / m_pitchScale; + } + } else { + if (m_pitchScale < 1.0) { + inRatio = 1.0 / m_pitchScale; + } + } + + m_log.log(2, "R3LiveShifter::readIn: ratio", inRatio); + + int resampleBufSize = int(m_channelData.at(0)->resampled.size()); + + int resampleOutput = m_inResampler->resample + (m_channelAssembly.resampled.data(), + resampleBufSize, + m_channelAssembly.input.data(), + incount, + inRatio, + false); + + m_log.log(2, "R3LiveShifter::readIn: writing to inbuf from resampled data, former read space and samples being added", m_channelData[0]->inbuf->getReadSpace(), resampleOutput); + + for (int c = 0; c < m_parameters.channels; ++c) { + m_channelData[c]->inbuf->write + (m_channelData.at(c)->resampled.data(), + resampleOutput); + } +} + +void +R3LiveShifter::generate(int requiredInOutbuf) +{ + Profiler profiler("R3LiveShifter::generate"); + + auto &cd0 = m_channelData.at(0); + int alreadyGenerated = cd0->outbuf->getReadSpace(); + if (alreadyGenerated >= requiredInOutbuf) { + m_log.log(2, "R3LiveShifter::generate: have already generated required count", alreadyGenerated, requiredInOutbuf); + return; + } + + m_log.log(2, "R3LiveShifter::generate: alreadyGenerated, requiredInOutbuf", alreadyGenerated, requiredInOutbuf); + + int toGenerate = requiredInOutbuf - alreadyGenerated; + +// int longest = m_guideConfiguration.longestFftSize; + int channels = m_parameters.channels; + + int ws = getWindowSourceSize(); + + // We always leave at least ws in inbuf, and this function is + // called after some input has just been written to inbuf, so we + // must have more than ws samples in there. + + int atInput = cd0->inbuf->getReadSpace(); + if (atInput <= ws) { + m_log.log(0, "R3LiveShifter::generate: insufficient samples at input: have and require more than", atInput, ws); + return; + } + + m_log.log(2, "R3LiveShifter::generate: atInput, toLeave", atInput, ws); + + int toConsume = atInput - ws; + + m_log.log(2, "R3LiveShifter::generate: toConsume, toGenerate", toConsume, toGenerate); + + int indicativeInhop = m_limits.maxInhopWithReadahead; + int indicativeOuthop = m_limits.maxPreferredOuthop; + + int minHopsAtInput = 1 + (toConsume - 1) / indicativeInhop; + int minHopsAtOutput = 1 + (toGenerate - 1) / indicativeOuthop; + + int hops = std::max(minHopsAtInput, minHopsAtOutput); + + m_log.log(2, "R3LiveShifter::generate: indicative inhop, outhop", indicativeInhop, indicativeOuthop); + m_log.log(2, "R3LiveShifter::generate: minHopsAtInput, minHopsAtOutput", minHopsAtInput, minHopsAtOutput); + m_log.log(2, "R3LiveShifter::generate: hops", hops); + + for (int hop = 0; hop < hops; ++hop) { + + Profiler profiler("R3LiveShifter::generate/loop"); + + if (toConsume <= 0) { + m_log.log(2, "R3LiveShifter::generate: ERROR: toConsume is zero at top of loop, hop and hops", hop, hops); + break; + } + + int inhop = 1 + (toConsume - 1) / hops; + int outhop = 1 + (toGenerate - 1) / hops; + + if (inhop > toConsume) { + inhop = toConsume; + } + + m_log.log(2, "R3LiveShifter::generate: inhop, outhop", inhop, outhop); + + // Now inhop is the distance by which the input stream will be + // advanced after our current frame has been read, and outhop + // is the distance by which the output will be advanced after + // it has been emitted; m_prevInhop and m_prevOuthop are the + // corresponding values the last time a frame was processed. + // + // Our phase adjustments need to be based on the distances we + // have advanced the input and output since the previous + // frame, not the distances we are about to advance them, so + // they use the m_prev values. + + if (inhop != m_prevInhop) { + m_log.log(2, "R3LiveShifter::generate: change in inhop", m_prevInhop, inhop); + } + if (outhop != m_prevOuthop) { + m_log.log(2, "R3LiveShifter::generate: change in outhop", m_prevOuthop, outhop); + } + + m_log.log(2, "R3LiveShifter::generate: write space and outhop", cd0->outbuf->getWriteSpace(), outhop); + + // NB our ChannelData, ScaleData, and ChannelScaleData maps + // contain shared_ptrs; whenever we retain one of them in a + // variable, we do so by reference to avoid copying the + // shared_ptr (as that is not realtime safe). Same goes for + // the map iterators + + // Analysis + + for (int c = 0; c < channels; ++c) { + analyseChannel(c, inhop, m_prevInhop, m_prevOuthop); + } + + // Phase update. This is synchronised across all channels + + for (auto &it : m_channelData[0]->scales) { + int fftSize = it.first; + for (int c = 0; c < channels; ++c) { + auto &cd = m_channelData.at(c); + auto &scale = cd->scales.at(fftSize); + m_channelAssembly.mag[c] = scale->mag.data(); + m_channelAssembly.phase[c] = scale->phase.data(); + m_channelAssembly.prevMag[c] = scale->prevMag.data(); + m_channelAssembly.guidance[c] = &cd->guidance; + m_channelAssembly.outPhase[c] = scale->advancedPhase.data(); + } + m_scaleData.at(fftSize)->guided.advance + (m_channelAssembly.outPhase.data(), + m_channelAssembly.mag.data(), + m_channelAssembly.phase.data(), + m_channelAssembly.prevMag.data(), + m_guideConfiguration, + m_channelAssembly.guidance.data(), + useMidSide(), + m_prevInhop, + m_prevOuthop); + } + + for (int c = 0; c < channels; ++c) { + adjustPreKick(c); + } + + // Resynthesis + + for (int c = 0; c < channels; ++c) { + synthesiseChannel(c, outhop, false); + } + + // Emit + + int writeCount = outhop; + int advanceCount = inhop; + + for (int c = 0; c < channels; ++c) { + auto &cd = m_channelData.at(c); + cd->outbuf->write(cd->mixdown.data(), writeCount); + cd->inbuf->skip(advanceCount); + } + + m_log.log(2, "R3LiveShifter::generate: writing and advancing", writeCount, advanceCount); + + m_prevInhop = inhop; + m_prevOuthop = outhop; + } + + m_log.log(2, "R3LiveShifter::generate: returning, now have generated", cd0->outbuf->getReadSpace()); +} + +int +R3LiveShifter::readOut(float *const *output, int outcount, int origin) +{ + double outRatio = 1.0; + + if (m_contractThenExpand) { + if (m_pitchScale < 1.0) { + outRatio = 1.0 / m_pitchScale; + } + } else { + if (m_pitchScale > 1.0) { + outRatio = 1.0 / m_pitchScale; + } + } + + m_log.log(2, "R3LiveShifter::readOut: outcount and ratio", outcount, outRatio); + + int fromOutbuf = int(floor(outcount / outRatio)); + + if (fromOutbuf == 0) { + fromOutbuf = 1; + } + + int got = fromOutbuf; + + for (int c = 0; c < m_parameters.channels; ++c) { + auto &cd = m_channelData.at(c); + int gotHere = cd->outbuf->read(cd->resampled.data(), got); + if (gotHere < got) { + if (c > 0) { + m_log.log(0, "R3LiveShifter::readOut: WARNING: channel imbalance detected"); + } + got = std::min(got, std::max(gotHere, 0)); + } + + m_channelAssembly.resampled[c] = cd->resampled.data(); + m_channelAssembly.mixdown[c] = output[c] + origin; + } + + m_log.log(2, "R3LiveShifter::readOut: requested and got from outbufs", fromOutbuf, got); + m_log.log(2, "R3LiveShifter::readOut: leaving behind", m_channelData.at(0)->outbuf->getReadSpace()); + + int resampledCount = 0; + + if (got > 0) { + resampledCount = m_outResampler->resample + (m_channelAssembly.mixdown.data(), + outcount, + m_channelAssembly.resampled.data(), + got, + outRatio, + false); + } + + m_log.log(2, "R3LiveShifter::readOut: resampled to", resampledCount); + + if (resampledCount < outcount && + m_channelData.at(0)->outbuf->getReadSpace() > 0) { + int remaining = outcount - resampledCount; + m_log.log(2, "R3LiveShifter::readOut: recursing to try to get the remaining", + remaining); + resampledCount += readOut(output, remaining, origin + resampledCount); + } + + if (resampledCount < outcount) { + m_log.log(0, "R3LiveShifter::readOut: WARNING: Failed to obtain enough samples from resampler", resampledCount, outcount); + } + + if (useMidSide()) { + for (int i = 0; i < resampledCount; ++i) { + float m = output[0][i]; + float s = output[1][i]; + float l = m + s; + float r = m - s; + output[0][i] = l; + output[1][i] = r; + } + } + + return resampledCount; +} + +void +R3LiveShifter::analyseChannel(int c, int inhop, int prevInhop, int prevOuthop) +{ + Profiler profiler("R3LiveShifter::analyseChannel"); + + auto &cd = m_channelData.at(c); + + int sourceSize = cd->windowSource.size(); + process_t *buf = cd->windowSource.data(); + + int readSpace = cd->inbuf->getReadSpace(); + if (readSpace < sourceSize) { + cd->inbuf->peek(buf, readSpace); + v_zero(buf + readSpace, sourceSize - readSpace); + } else { + cd->inbuf->peek(buf, sourceSize); + } + + // We have an unwindowed time-domain frame in buf that is as long + // as required for the union of all FFT sizes and readahead + // hops. Populate the various sizes from it with aligned centres, + // windowing as we copy. The classification scale is handled + // separately because it has readahead, so skip it here. (In + // single-window mode that means we do nothing here, since the + // classification scale is the only one.) + + int longest = m_guideConfiguration.longestFftSize; + int classify = m_guideConfiguration.classificationFftSize; + + for (auto &it: cd->scales) { + int fftSize = it.first; + if (fftSize == classify) continue; + int offset = (longest - fftSize) / 2; + m_scaleData.at(fftSize)->analysisWindow.cut + (buf + offset, it.second->timeDomain.data()); + } + + auto &classifyScale = cd->scales.at(classify); + ClassificationReadaheadData &readahead = cd->readahead; + bool copyFromReadahead = false; + + if (m_useReadahead) { + + // The classification scale has a one-hop readahead, so + // populate the readahead from further down the long + // unwindowed frame. + + m_scaleData.at(classify)->analysisWindow.cut + (buf + (longest - classify) / 2 + inhop, + readahead.timeDomain.data()); + + // If inhop has changed since the previous frame, we must + // populate the classification scale (but for + // analysis/resynthesis rather than classification) anew + // rather than reuse the previous frame's readahead. + + copyFromReadahead = cd->haveReadahead; + if (inhop != prevInhop) copyFromReadahead = false; + } + + if (!copyFromReadahead) { + m_scaleData.at(classify)->analysisWindow.cut + (buf + (longest - classify) / 2, + classifyScale->timeDomain.data()); + } + + // FFT shift, forward FFT, and carry out cartesian-polar + // conversion for each FFT size. + + // For the classification scale we need magnitudes for the full + // range (polar only in a subset) and we operate in the readahead, + // pulling current values from the existing readahead (except + // where the inhop has changed as above, in which case we need to + // do both readahead and current) + + if (m_useReadahead) { + + if (copyFromReadahead) { + v_copy(classifyScale->mag.data(), + readahead.mag.data(), + classifyScale->bufSize); + v_copy(classifyScale->phase.data(), + readahead.phase.data(), + classifyScale->bufSize); + } + + v_fftshift(readahead.timeDomain.data(), classify); + m_scaleData.at(classify)->fft.forward(readahead.timeDomain.data(), + classifyScale->real.data(), + classifyScale->imag.data()); + + for (int b = 0; b < m_guideConfiguration.fftBandLimitCount; ++b) { + const auto &band = m_guideConfiguration.fftBandLimits[b]; + if (band.fftSize == classify) { + ToPolarSpec spec; + spec.magFromBin = 0; + spec.magBinCount = classify/2 + 1; + spec.polarFromBin = band.b0min; + spec.polarBinCount = band.b1max - band.b0min + 1; + convertToPolar(readahead.mag.data(), + readahead.phase.data(), + classifyScale->real.data(), + classifyScale->imag.data(), + spec); + + v_scale(classifyScale->mag.data(), + 1.0 / double(classify), + classifyScale->mag.size()); + break; + } + } + + cd->haveReadahead = true; + } + + // For the others (and the classify as well, if the inhop has + // changed or we aren't using readahead or haven't filled the + // readahead yet) we operate directly in the scale data and + // restrict the range for cartesian-polar conversion + + for (auto &it: cd->scales) { + int fftSize = it.first; + if (fftSize == classify && copyFromReadahead) { + continue; + } + + auto &scale = it.second; + + v_fftshift(scale->timeDomain.data(), fftSize); + + m_scaleData.at(fftSize)->fft.forward(scale->timeDomain.data(), + scale->real.data(), + scale->imag.data()); + + for (int b = 0; b < m_guideConfiguration.fftBandLimitCount; ++b) { + const auto &band = m_guideConfiguration.fftBandLimits[b]; + if (band.fftSize == fftSize) { + + ToPolarSpec spec; + + // For the classify scale we always want the full + // range, as all the magnitudes (though not + // necessarily all phases) are potentially relevant to + // classification and formant analysis. But this case + // here only happens if we don't copyFromReadahead - + // the normal case is above and, er, copies from the + // previous readahead. + if (fftSize == classify) { + spec.magFromBin = 0; + spec.magBinCount = classify/2 + 1; + spec.polarFromBin = band.b0min; + spec.polarBinCount = band.b1max - band.b0min + 1; + } else { + spec.magFromBin = band.b0min; + spec.magBinCount = band.b1max - band.b0min + 1; + spec.polarFromBin = spec.magFromBin; + spec.polarBinCount = spec.magBinCount; + } + + convertToPolar(scale->mag.data(), + scale->phase.data(), + scale->real.data(), + scale->imag.data(), + spec); + + v_scale(scale->mag.data() + spec.magFromBin, + 1.0 / double(fftSize), + spec.magBinCount); + + break; + } + } + } + + if (m_parameters.options & RubberBandLiveShifter::OptionFormantPreserved) { + analyseFormant(c); + adjustFormant(c); + } + + // Use the classification scale to get a bin segmentation and + // calculate the adaptive frequency guide for this channel + + v_copy(cd->classification.data(), cd->nextClassification.data(), + cd->classification.size()); + + if (m_useReadahead) { + cd->classifier->classify(readahead.mag.data(), + cd->nextClassification.data()); + } else { + cd->classifier->classify(classifyScale->mag.data(), + cd->nextClassification.data()); + } + + cd->prevSegmentation = cd->segmentation; + cd->segmentation = cd->nextSegmentation; + cd->nextSegmentation = cd->segmenter->segment(cd->nextClassification.data()); +/* + if (c == 0) { + double pb = cd->nextSegmentation.percussiveBelow; + double pa = cd->nextSegmentation.percussiveAbove; + double ra = cd->nextSegmentation.residualAbove; + int pbb = binForFrequency(pb, classify, m_parameters.sampleRate); + int pab = binForFrequency(pa, classify, m_parameters.sampleRate); + int rab = binForFrequency(ra, classify, m_parameters.sampleRate); + std::cout << "pb = " << pb << ", pbb = " << pbb << std::endl; + std::cout << "pa = " << pa << ", pab = " << pab << std::endl; + std::cout << "ra = " << ra << ", rab = " << rab << std::endl; + std::cout << "s:"; + for (int i = 0; i <= classify/2; ++i) { + if (i > 0) std::cout << ","; + if (i < pbb || (i >= pab && i <= rab)) { + std::cout << "1"; + } else { + std::cout << "0"; + } + } + std::cout << std::endl; + } +*/ + + double ratio = m_pitchScale; + + if (fabs(ratio - 1.0) < 1.0e-7) { + ++m_unityCount; + } else { + m_unityCount = 0; + } + + bool tighterChannelLock = + m_parameters.options & RubberBandLiveShifter::OptionChannelsTogether; + + double magMean = v_mean(classifyScale->mag.data() + 1, classify/2); + + bool resetOnSilence = true; + if (useMidSide() && c == 1) { + // Do not phase reset on silence in the side channel - the + // reset is propagated across to the mid channel, giving + // constant resets for e.g. mono material in a stereo + // configuration + resetOnSilence = false; + } + + if (m_useReadahead) { + m_guide.updateGuidance(ratio, + prevOuthop, + classifyScale->mag.data(), + classifyScale->prevMag.data(), + cd->readahead.mag.data(), + cd->segmentation, + cd->prevSegmentation, + cd->nextSegmentation, + magMean, + m_unityCount, + true, + tighterChannelLock, + resetOnSilence, + cd->guidance); + } else { + m_guide.updateGuidance(ratio, + prevOuthop, + classifyScale->prevMag.data(), + classifyScale->prevMag.data(), + classifyScale->mag.data(), + cd->segmentation, + cd->prevSegmentation, + cd->nextSegmentation, + magMean, + m_unityCount, + true, + tighterChannelLock, + resetOnSilence, + cd->guidance); + } + +/* + if (c == 0) { + if (cd->guidance.kick.present) { + std::cout << "k:2" << std::endl; + } else if (cd->guidance.preKick.present) { + std::cout << "k:1" << std::endl; + } else { + std::cout << "k:0" << std::endl; + } + } +*/ +} + +void +R3LiveShifter::analyseFormant(int c) +{ + Profiler profiler("R3LiveShifter::analyseFormant"); + + auto &cd = m_channelData.at(c); + auto &f = *cd->formant; + + int fftSize = f.fftSize; + int binCount = fftSize/2 + 1; + + auto &scale = cd->scales.at(fftSize); + auto &scaleData = m_scaleData.at(fftSize); + + scaleData->fft.inverseCepstral(scale->mag.data(), f.cepstra.data()); + + int cutoff = int(floor(m_parameters.sampleRate / 650.0)); + if (cutoff < 1) cutoff = 1; + + f.cepstra[0] /= 2.0; + f.cepstra[cutoff-1] /= 2.0; + for (int i = cutoff; i < fftSize; ++i) { + f.cepstra[i] = 0.0; + } + v_scale(f.cepstra.data(), 1.0 / double(fftSize), cutoff); + + scaleData->fft.forward(f.cepstra.data(), f.envelope.data(), f.spare.data()); + + v_exp(f.envelope.data(), binCount); + v_square(f.envelope.data(), binCount); + + for (int i = 0; i < binCount; ++i) { + if (f.envelope[i] > 1.0e10) f.envelope[i] = 1.0e10; + } +} + +void +R3LiveShifter::adjustFormant(int c) +{ + Profiler profiler("R3LiveShifter::adjustFormant"); + + auto &cd = m_channelData.at(c); + + for (auto &it : cd->scales) { + + int fftSize = it.first; + auto &scale = it.second; + + int highBin = int(floor(fftSize * 10000.0 / m_parameters.sampleRate)); + process_t targetFactor = process_t(cd->formant->fftSize) / process_t(fftSize); + process_t formantScale = m_formantScale; + if (formantScale == 0.0) formantScale = 1.0 / m_pitchScale; + process_t sourceFactor = targetFactor / formantScale; + process_t maxRatio = 60.0; + process_t minRatio = 1.0 / maxRatio; + + for (int b = 0; b < m_guideConfiguration.fftBandLimitCount; ++b) { + const auto &band = m_guideConfiguration.fftBandLimits[b]; + if (band.fftSize != fftSize) continue; + for (int i = band.b0min; i < band.b1max && i < highBin; ++i) { + process_t source = cd->formant->envelopeAt(i * sourceFactor); + process_t target = cd->formant->envelopeAt(i * targetFactor); + if (target > 0.0) { + process_t ratio = source / target; + if (ratio < minRatio) ratio = minRatio; + if (ratio > maxRatio) ratio = maxRatio; + scale->mag[i] *= ratio; + } + } + } + } +} + +void +R3LiveShifter::adjustPreKick(int c) +{ + if (isSingleWindowed()) return; + + Profiler profiler("R3LiveShifter::adjustPreKick"); + + auto &cd = m_channelData.at(c); + auto fftSize = cd->guidance.fftBands[0].fftSize; + if (cd->guidance.preKick.present) { + auto &scale = cd->scales.at(fftSize); + int from = binForFrequency(cd->guidance.preKick.f0, + fftSize, m_parameters.sampleRate); + int to = binForFrequency(cd->guidance.preKick.f1, + fftSize, m_parameters.sampleRate); + for (int i = from; i <= to; ++i) { + process_t diff = scale->mag[i] - scale->prevMag[i]; + if (diff > 0.0) { + scale->pendingKick[i] = diff; + scale->mag[i] -= diff; + } + } + } else if (cd->guidance.kick.present) { + auto &scale = cd->scales.at(fftSize); + int from = binForFrequency(cd->guidance.preKick.f0, + fftSize, m_parameters.sampleRate); + int to = binForFrequency(cd->guidance.preKick.f1, + fftSize, m_parameters.sampleRate); + for (int i = from; i <= to; ++i) { + scale->mag[i] += scale->pendingKick[i]; + scale->pendingKick[i] = 0.0; + } + } +} + +void +R3LiveShifter::synthesiseChannel(int c, int outhop, bool draining) +{ + Profiler profiler("R3LiveShifter::synthesiseChannel"); + + int longest = m_guideConfiguration.longestFftSize; + + auto &cd = m_channelData.at(c); + + for (int b = 0; b < cd->guidance.fftBandCount; ++b) { + + const auto &band = cd->guidance.fftBands[b]; + int fftSize = band.fftSize; + + auto &scale = cd->scales.at(fftSize); + auto &scaleData = m_scaleData.at(fftSize); + + // copy to prevMag before filtering + v_copy(scale->prevMag.data(), + scale->mag.data(), + scale->bufSize); + + process_t winscale = process_t(outhop) / scaleData->windowScaleFactor; + + // The frequency filter is applied naively in the frequency + // domain. Aliasing is reduced by the shorter resynthesis + // window. We resynthesise each scale individually, then sum - + // it's easier to manage scaling for in situations with a + // varying resynthesis hop + + int lowBin = binForFrequency(band.f0, fftSize, m_parameters.sampleRate); + int highBin = binForFrequency(band.f1, fftSize, m_parameters.sampleRate); + if (highBin % 2 == 0 && highBin > 0) --highBin; + + int n = scale->mag.size(); + if (lowBin >= n) lowBin = n - 1; + if (highBin >= n) highBin = n - 1; + if (highBin < lowBin) highBin = lowBin; + + if (lowBin > 0) { + v_zero(scale->real.data(), lowBin); + v_zero(scale->imag.data(), lowBin); + } + + v_scale(scale->mag.data() + lowBin, winscale, highBin - lowBin); + + v_polar_to_cartesian(scale->real.data() + lowBin, + scale->imag.data() + lowBin, + scale->mag.data() + lowBin, + scale->advancedPhase.data() + lowBin, + highBin - lowBin); + + if (highBin < scale->bufSize) { + v_zero(scale->real.data() + highBin, scale->bufSize - highBin); + v_zero(scale->imag.data() + highBin, scale->bufSize - highBin); + } + + scaleData->fft.inverse(scale->real.data(), + scale->imag.data(), + scale->timeDomain.data()); + + v_fftshift(scale->timeDomain.data(), fftSize); + + // Synthesis window may be shorter than analysis window, so + // copy and cut only from the middle of the time-domain frame; + // and the accumulator length always matches the longest FFT + // size, so as to make mixing straightforward, so there is an + // additional offset needed for the target + + int synthesisWindowSize = scaleData->synthesisWindow.getSize(); + int fromOffset = (fftSize - synthesisWindowSize) / 2; + int toOffset = (longest - synthesisWindowSize) / 2; + + scaleData->synthesisWindow.cutAndAdd + (scale->timeDomain.data() + fromOffset, + scale->accumulator.data() + toOffset); + } + + // Mix this channel and move the accumulator along + + float *mixptr = cd->mixdown.data(); + v_zero(mixptr, outhop); + + for (auto &it : cd->scales) { + auto &scale = it.second; + + process_t *accptr = scale->accumulator.data(); + for (int i = 0; i < outhop; ++i) { + mixptr[i] += float(accptr[i]); + } + + int n = scale->accumulator.size() - outhop; + v_move(accptr, accptr + outhop, n); + v_zero(accptr + n, outhop); + + if (draining) { + if (scale->accumulatorFill > outhop) { + auto newFill = scale->accumulatorFill - outhop; + m_log.log(2, "draining: reducing accumulatorFill from, to", scale->accumulatorFill, newFill); + scale->accumulatorFill = newFill; + } else { + scale->accumulatorFill = 0; + } + } else { + scale->accumulatorFill = scale->accumulator.size(); + } + } +} + +} + diff --git a/src/finer/R3LiveShifter.h b/src/finer/R3LiveShifter.h new file mode 100644 index 00000000..54239a39 --- /dev/null +++ b/src/finer/R3LiveShifter.h @@ -0,0 +1,413 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#ifndef RUBBERBAND_R3_LIVE_SHIFTERIMPL_H +#define RUBBERBAND_R3_LIVE_SHIFTERIMPL_H + +#include "BinSegmenter.h" +#include "Guide.h" +#include "Peak.h" +#include "PhaseAdvance.h" + +#include "../common/Resampler.h" +#include "../common/FFT.h" +#include "../common/FixedVector.h" +#include "../common/Allocators.h" +#include "../common/Window.h" +#include "../common/VectorOpsComplex.h" +#include "../common/Log.h" + +#include "../../rubberband/RubberBandLiveShifter.h" + +#include +#include +#include + +namespace RubberBand +{ + +class R3LiveShifter +{ +public: + struct Parameters { + double sampleRate; + int channels; + RubberBandLiveShifter::Options options; + Parameters(double _sampleRate, int _channels, + RubberBandLiveShifter::Options _options) : + sampleRate(_sampleRate), channels(_channels), options(_options) { } + }; + + R3LiveShifter(Parameters parameters, Log log); + ~R3LiveShifter() { } + + void reset(); + + void setPitchScale(double scale); + void setFormantScale(double scale); + + double getPitchScale() const; + double getFormantScale() const; + + void setFormantOption(RubberBandLiveShifter::Options); + + size_t getBlockSize() const; + void shift(const float *const *input, float *const *output); + + size_t getPreferredStartPad() const; + size_t getStartDelay() const; + + size_t getChannelCount() const; + + void setDebugLevel(int level) { + m_log.setDebugLevel(level); + for (auto &sd : m_scaleData) { + sd.second->guided.setDebugLevel(level); + } + m_guide.setDebugLevel(level); + } + +protected: + struct Limits { + int minPreferredOuthop; + int maxPreferredOuthop; + int minInhop; + int maxInhopWithReadahead; + int maxInhop; + Limits(RubberBandLiveShifter::Options options, double rate) : + // commented values are results when rate = 44100 or 48000 + minPreferredOuthop(roundUpDiv(rate, 512)), // 128 + maxPreferredOuthop(roundUpDiv(rate, 128)), // 512 + minInhop(1), + maxInhopWithReadahead(roundUpDiv(rate, 64)), // 1024 + maxInhop(roundUpDiv(rate, 32)) // 2048 + { + if (!(options & RubberBandLiveShifter::OptionWindowLong)) { + minPreferredOuthop = roundUpDiv(rate, 256); // 256 + maxPreferredOuthop = (roundUpDiv(rate, 128) * 5) / 4; // 640 + maxInhopWithReadahead = roundUpDiv(rate, 128); // 512 + maxInhop = (roundUpDiv(rate, 64) * 3) / 2; // 1536 + } + } + }; + + struct ClassificationReadaheadData { + FixedVector timeDomain; + FixedVector mag; + FixedVector phase; + ClassificationReadaheadData(int _fftSize) : + timeDomain(_fftSize, 0.f), + mag(_fftSize/2 + 1, 0.f), + phase(_fftSize/2 + 1, 0.f) + { } + + private: + ClassificationReadaheadData(const ClassificationReadaheadData &) =delete; + ClassificationReadaheadData &operator=(const ClassificationReadaheadData &) =delete; + }; + + struct ChannelScaleData { + int fftSize; + int bufSize; // size of every freq-domain array here: fftSize/2 + 1 + FixedVector timeDomain; + FixedVector real; + FixedVector imag; + FixedVector mag; + FixedVector phase; + FixedVector advancedPhase; + FixedVector prevMag; + FixedVector pendingKick; + FixedVector accumulator; + int accumulatorFill; + + ChannelScaleData(int _fftSize, int _longestFftSize) : + fftSize(_fftSize), + bufSize(fftSize/2 + 1), + timeDomain(fftSize, 0.f), + real(bufSize, 0.f), + imag(bufSize, 0.f), + mag(bufSize, 0.f), + phase(bufSize, 0.f), + advancedPhase(bufSize, 0.f), + prevMag(bufSize, 0.f), + pendingKick(bufSize, 0.f), + accumulator(_longestFftSize, 0.f), + accumulatorFill(0) + { } + + void reset() { + v_zero(prevMag.data(), prevMag.size()); + v_zero(pendingKick.data(), pendingKick.size()); + v_zero(accumulator.data(), accumulator.size()); + accumulatorFill = 0; + } + + private: + ChannelScaleData(const ChannelScaleData &) =delete; + ChannelScaleData &operator=(const ChannelScaleData &) =delete; + }; + + struct FormantData { + int fftSize; + FixedVector cepstra; + FixedVector envelope; + FixedVector spare; + + FormantData(int _fftSize) : + fftSize(_fftSize), + cepstra(_fftSize, 0.0), + envelope(_fftSize/2 + 1, 0.0), + spare(_fftSize/2 + 1, 0.0) { } + + process_t envelopeAt(process_t bin) const { + int b0 = int(floor(bin)), b1 = int(ceil(bin)); + if (b0 < 0 || b0 > fftSize/2) { + return 0.0; + } else if (b1 == b0 || b1 > fftSize/2) { + return envelope.at(b0); + } else { + process_t diff = bin - process_t(b0); + return envelope.at(b0) * (1.0 - diff) + envelope.at(b1) * diff; + } + } + }; + + struct ChannelData { + std::map> scales; + FixedVector windowSource; + ClassificationReadaheadData readahead; + bool haveReadahead; + std::unique_ptr classifier; + FixedVector classification; + FixedVector nextClassification; + std::unique_ptr segmenter; + BinSegmenter::Segmentation segmentation; + BinSegmenter::Segmentation prevSegmentation; + BinSegmenter::Segmentation nextSegmentation; + Guide::Guidance guidance; + FixedVector mixdown; + FixedVector resampled; + std::unique_ptr> inbuf; + std::unique_ptr> outbuf; + std::unique_ptr formant; + ChannelData(BinSegmenter::Parameters segmenterParameters, + BinClassifier::Parameters classifierParameters, + int longestFftSize, + int windowSourceSize, + int inRingBufferSize, + int outRingBufferSize) : + scales(), + windowSource(windowSourceSize, 0.0), + readahead(segmenterParameters.fftSize), + haveReadahead(false), + classifier(new BinClassifier(classifierParameters)), + classification(classifierParameters.binCount, + BinClassifier::Classification::Residual), + nextClassification(classifierParameters.binCount, + BinClassifier::Classification::Residual), + segmenter(new BinSegmenter(segmenterParameters)), + segmentation(), prevSegmentation(), nextSegmentation(), + mixdown(longestFftSize, 0.f), + resampled(outRingBufferSize, 0.f), + inbuf(new RingBuffer(inRingBufferSize)), + outbuf(new RingBuffer(outRingBufferSize)), + formant(new FormantData(segmenterParameters.fftSize)) { } + void reset() { + haveReadahead = false; + classifier->reset(); + segmentation = BinSegmenter::Segmentation(); + prevSegmentation = BinSegmenter::Segmentation(); + nextSegmentation = BinSegmenter::Segmentation(); + for (size_t i = 0; i < nextClassification.size(); ++i) { + nextClassification[i] = BinClassifier::Classification::Residual; + } + inbuf->reset(); + outbuf->reset(); + for (auto &s : scales) { + s.second->reset(); + } + } + }; + + struct ChannelAssembly { + // Vectors of bare pointers, used to package container data + // from different channels into arguments for PhaseAdvance + FixedVector input; + FixedVector mag; + FixedVector phase; + FixedVector prevMag; + FixedVector guidance; + FixedVector outPhase; + FixedVector mixdown; + FixedVector resampled; + ChannelAssembly(int channels) : + input(channels, nullptr), + mag(channels, nullptr), phase(channels, nullptr), + prevMag(channels, nullptr), guidance(channels, nullptr), + outPhase(channels, nullptr), mixdown(channels, nullptr), + resampled(channels, nullptr) { } + }; + + struct ScaleData { + int fftSize; + bool singleWindowMode; + FFT fft; + Window analysisWindow; + Window synthesisWindow; + process_t windowScaleFactor; + GuidedPhaseAdvance guided; + + ScaleData(GuidedPhaseAdvance::Parameters guidedParameters, + Log log) : + fftSize(guidedParameters.fftSize), + singleWindowMode(guidedParameters.singleWindowMode), + fft(fftSize), + analysisWindow(analysisWindowShape(), + analysisWindowLength()), + synthesisWindow(synthesisWindowShape(), + synthesisWindowLength()), + windowScaleFactor(0.0), + guided(guidedParameters, log) + { + int asz = analysisWindow.getSize(), ssz = synthesisWindow.getSize(); + int off = (asz - ssz) / 2; + for (int i = 0; i < ssz; ++i) { + windowScaleFactor += analysisWindow.getValue(i + off) * + synthesisWindow.getValue(i); + } + } + + WindowType analysisWindowShape(); + int analysisWindowLength(); + WindowType synthesisWindowShape(); + int synthesisWindowLength(); + }; + + Log m_log; + Parameters m_parameters; + const Limits m_limits; + + std::atomic m_pitchScale; + std::atomic m_formantScale; + + std::vector> m_channelData; + std::map> m_scaleData; + Guide m_guide; + Guide::Configuration m_guideConfiguration; + ChannelAssembly m_channelAssembly; + std::unique_ptr m_inResampler; + std::unique_ptr m_outResampler; + bool m_useReadahead; + int m_prevInhop; + int m_prevOuthop; + bool m_contractThenExpand; // otherwise expand then contract + bool m_firstProcess; + uint32_t m_unityCount; + + void initialise(); + + void readIn(const float *const *input); + void generate(int required); + int readOut(float *const *output, int outcount, int origin); + + void createResamplers(); + void analyseChannel(int channel, int inhop, int prevInhop, int prevOuthop); + void analyseFormant(int channel); + void adjustFormant(int channel); + void adjustPreKick(int channel); + void synthesiseChannel(int channel, int outhop, bool draining); + + struct ToPolarSpec { + int magFromBin; + int magBinCount; + int polarFromBin; + int polarBinCount; + }; + + Parameters validateSampleRate(const Parameters ¶ms) { + Parameters validated { params }; + double minRate = 8000.0, maxRate = 192000.0; + if (params.sampleRate < minRate) { + m_log.log(0, "R3LiveShifter: WARNING: Unsupported sample rate", params.sampleRate); + m_log.log(0, "R3LiveShifter: Minimum rate is", minRate); + validated.sampleRate = minRate; + } else if (params.sampleRate > maxRate) { + m_log.log(0, "R3LiveShifter: WARNING: Unsupported sample rate", params.sampleRate); + m_log.log(0, "R3LiveShifter: Maximum rate is", maxRate); + validated.sampleRate = maxRate; + } + return validated; + } + + void convertToPolar(process_t *mag, process_t *phase, + const process_t *real, const process_t *imag, + const ToPolarSpec &s) const { + v_cartesian_to_polar(mag + s.polarFromBin, + phase + s.polarFromBin, + real + s.polarFromBin, + imag + s.polarFromBin, + s.polarBinCount); + if (s.magFromBin < s.polarFromBin) { + v_cartesian_to_magnitudes(mag + s.magFromBin, + real + s.magFromBin, + imag + s.magFromBin, + s.polarFromBin - s.magFromBin); + } + if (s.magFromBin + s.magBinCount > s.polarFromBin + s.polarBinCount) { + v_cartesian_to_magnitudes(mag + s.polarFromBin + s.polarBinCount, + real + s.polarFromBin + s.polarBinCount, + imag + s.polarFromBin + s.polarBinCount, + s.magFromBin + s.magBinCount - + s.polarFromBin - s.polarBinCount); + } + } + + bool useMidSide() const { + return m_parameters.channels == 2 && + (m_parameters.options & + RubberBandLiveShifter::OptionChannelsTogether); + } + + bool isSingleWindowed() const { + return !(m_parameters.options & + RubberBandLiveShifter::OptionWindowLong); + } + + int getWindowSourceSize() const { + if (m_useReadahead) { + int sz = m_guideConfiguration.classificationFftSize + + m_limits.maxInhopWithReadahead; + if (m_guideConfiguration.longestFftSize > sz) { + return m_guideConfiguration.longestFftSize; + } else { + return sz; + } + } else { + return m_guideConfiguration.longestFftSize; + } + } +}; + +} + +#endif diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp new file mode 100644 index 00000000..794a32b6 --- /dev/null +++ b/src/test/TestLiveShifter.cpp @@ -0,0 +1,1272 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2023 Particular Programs Ltd. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#ifndef BOOST_TEST_DYN_LINK +#define BOOST_TEST_DYN_LINK +#endif +#include + +#include "../../rubberband/RubberBandLiveShifter.h" + +#include + +#include + +using namespace RubberBand; + +using std::vector; +using std::cerr; +using std::endl; + +namespace tt = boost::test_tools; + +BOOST_AUTO_TEST_SUITE(TestLiveShifter) + +BOOST_AUTO_TEST_CASE(sinusoid_unchanged) +{ + bool printDebug = true; +// bool printDebug = false; + + int n = 100000; + float freq = 440.f; + int rate = 44100; + RubberBandLiveShifter shifter + (rate, 1, RubberBandLiveShifter::OptionPitchModeB); + + //!!! + shifter.setPitchScale(2.0); + + if (printDebug) { + shifter.setDebugLevel(2); + } + + int blocksize = shifter.getBlockSize(); + BOOST_TEST(blocksize == 2560); + + n = (n / blocksize + 1) * blocksize; + + vector in(n), out(n); + for (int i = 0; i < n; ++i) { + in[i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); + } + + for (int i = 0; i < n; i += blocksize) { + float *inp = in.data() + i; + float *outp = out.data() + i; + shifter.shift(&inp, &outp); + } + + int delay = shifter.getStartDelay(); + + // We now have n samples of a simple sinusoid with stretch factor + // 1.0; obviously we expect the output to be essentially the same + // thing. It will have lower precision for a while at the start + // and end because of windowing factors, so we check those with a + // threshold of 0.1; in the middle we expect better + // precision. Note that these are relative tolerances, not + // absolute, i.e. 0.001 means 0.001x the smaller value - so they + // are tighter than they appear. + + // This syntax for comparing containers with a certain tolerance + // using BOOST_TEST is just bonkers. I can't find the << syntax to + // combine manipulators documented anywhere other than in a + // release note, but it does work. Well, sort of - it works this + // way around but not as per_element << tolerance. And + // tolerance(0.1) doesn't do what you'd expect if the things + // you're comparing are floats (it sets the tolerance for doubles, + // leaving float comparison unchanged). Clever... too clever. + +// BOOST_TEST(out == in, +// tt::tolerance(0.1f) << tt::per_element()); + + BOOST_TEST(vector(out.begin() + delay, out.begin() + n) == + vector(in.begin(), in.begin() + n - delay), + tt::tolerance(0.001f) << tt::per_element()); + + if (printDebug) { + // The initial # is to allow grep on the test output + std::cout << "#sample\tV" << std::endl; + for (int i = 0; i < out.size(); ++i) { + std::cout << "#" << i << "\t" << out[i] << std::endl; + } + } + + if (printDebug) { + // The initial @ is to allow grep on the test output + std::cout << "@sample\tV" << std::endl; + for (int i = 0; i + delay < in.size(); ++i) { + std::cout << "@" << i << "\t" << out[i + delay] - in[i] << std::endl; + } + } +} + +//!!! +#ifdef NOT_DEFINED + +BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_finer) +{ + int n = 10000; + float freq = 440.f; + int rate = 44100; + + RubberBandLiveShifter shifter + (rate, 1, RubberBandLiveShifter::OptionEngineFiner); + + vector in(n), out(n); + for (int i = 0; i < n; ++i) { + in[i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); + } + float *inp = in.data(), *outp = out.data(); + + shifter.setMaxProcessSize(n); + shifter.setExpectedInputDuration(n); + BOOST_TEST(shifter.available() == 0); + + shifter.study(&inp, n, true); + BOOST_TEST(shifter.available() == 0); + + shifter.process(&inp, n, true); + BOOST_TEST(shifter.available() == n); + + BOOST_TEST(shifter.getStartDelay() == 0); // offline mode + + size_t got = shifter.retrieve(&outp, n); + BOOST_TEST(got == n); + BOOST_TEST(shifter.available() == -1); + + // The R3 engine is actually less precise than R2 here because of + // its different windowing design, though see the note above about + // what these tolerances mean + + BOOST_TEST(out == in, + tt::tolerance(0.35f) << tt::per_element()); + + BOOST_TEST(vector(out.begin() + 1024, out.begin() + n - 1024) == + vector(in.begin() + 1024, in.begin() + n - 1024), + tt::tolerance(0.01f) << tt::per_element()); + +// std::cout << "ms\tV" << std::endl; +// for (int i = 0; i < n; ++i) { +// std::cout << i << "\t" << out[i] - in[i] << std::endl; +// } +} + +BOOST_AUTO_TEST_CASE(sinusoid_2x_offline_finer) +{ + int n = 10000; + float freq = 441.f; // so a cycle is an exact number of samples + int rate = 44100; + + RubberBandLiveShifter shifter + (rate, 1, RubberBandLiveShifter::OptionEngineFiner); + + shifter.setTimeRatio(2.0); + + vector in(n), out(n*2); + for (int i = 0; i < n*2; ++i) { + out[i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); + if (i < n) { + in[i] = out[i]; + } + } + float *inp = in.data(), *outp = out.data(); + + shifter.setMaxProcessSize(n); + shifter.setExpectedInputDuration(n); + BOOST_TEST(shifter.available() == 0); + + shifter.study(&inp, n, true); + BOOST_TEST(shifter.available() == 0); + + shifter.process(&inp, n, true); + BOOST_TEST(shifter.available() == n*2); + + BOOST_TEST(shifter.getStartDelay() == 0); // offline mode + + size_t got = shifter.retrieve(&outp, n*2); + BOOST_TEST(got == n*2); + BOOST_TEST(shifter.available() == -1); + + int period = -1; + for (int i = 1000; i < 2000; ++i) { + if (period >= 0) ++period; + if (out[i] <= 0.f && out[i+1] > 0.f) { + if (period == -1) period = 0; + else break; + } + } + BOOST_TEST(period == 100); + + int offset = 0; + for (int i = 0; i < 200; ++i) { + if (out[i] <= 0.f && out[i+1] > -0.01f) { + offset = i + 1; + break; + } + } + + // overall + + double rms = 0.0; + for (int i = 0; i < n - offset; ++i) { + double diff = out[i + offset] - in[i]; + rms += diff * diff; + } + rms = sqrt(rms / double(n - offset)); + BOOST_TEST(rms < 0.2); + + // steady state + + rms = 0.0; + for (int i = 1500; i < n - offset - 3000; ++i) { + double diff = out[i + offset + 1500] - in[i + 1500]; + rms += diff * diff; + } + rms = sqrt(rms / double(n - offset - 3000)); + BOOST_TEST(rms < 0.1); +} + +static vector process_realtime(RubberBandLiveShifter &shifter, + const vector &in, + int nOut, + int bs, + bool printDebug) +{ + int n = in.size(); + vector out(nOut, 0.f); + + // Prime the start + { + float *source = out.data(); // just reuse out because it's silent + shifter.process(&source, shifter.getPreferredStartPad(), false); + } + + int toSkip = shifter.getStartDelay(); + + int inOffset = 0, outOffset = 0; + + while (outOffset < nOut) { + + // Obtain a single block of size bs, simulating realtime + // playback. The following might be the content of a + // sound-producing callback function + + int needed = std::min(bs, nOut - outOffset); + int obtained = 0; + + while (obtained < needed) { + + int available = shifter.available(); + + if (available < 0) { // finished + for (int i = obtained; i < needed; ++i) { + out[outOffset++] = 0.f; + } + break; + + } else if (available == 0) { // need to provide more input + int required = shifter.getSamplesRequired(); + BOOST_TEST(required > 0); // because available == 0 + int toProcess = std::min(required, n - inOffset); + const float *const source = in.data() + inOffset; + bool final = (toProcess < required); + shifter.process(&source, toProcess, final); + inOffset += toProcess; + BOOST_TEST(shifter.available() > 0); + continue; + + } else if (toSkip > 0) { // available > 0 && toSkip > 0 + float *target = out.data() + outOffset; + int toRetrieve = std::min(toSkip, available); + int retrieved = shifter.retrieve(&target, toRetrieve); + BOOST_TEST(retrieved == toRetrieve); + toSkip -= retrieved; + + } else { // available > 0 + float *target = out.data() + outOffset; + int toRetrieve = std::min(needed - obtained, available); + int retrieved = shifter.retrieve(&target, toRetrieve); + BOOST_TEST(retrieved == toRetrieve); + obtained += retrieved; + outOffset += retrieved; + } + } + } + + if (printDebug) { + // The initial # is to allow grep on the test output + std::cout << "#sample\tV" << std::endl; + for (int i = 0; i < nOut; ++i) { + std::cout << "#" << i << "\t" << out[i] << std::endl; + } + } + + return out; +} + +static void sinusoid_realtime(RubberBandLiveShifter::Options options, + double timeRatio, + double pitchScale, + bool printDebug) +{ + int n = (timeRatio < 1.0 ? 80000 : 40000); + int nOut = int(ceil(n * timeRatio)); + float freq = 441.f; + int rate = 44100; + int bs = 512; + + // This test simulates block-by-block realtime processing with + // latency compensation, and checks that the output is all in the + // expected place + + RubberBandLiveShifter shifter(rate, 1, options, timeRatio, pitchScale); + shifter.setMaxProcessSize(bs); + + if (printDebug) { + shifter.setDebugLevel(2); + } + + // The input signal is a fixed frequency sinusoid that steps up in + // amplitude every 1/10 of the total duration - from 0.1 at the + // start, via increments of 0.1, to 1.0 at the end + + vector in(n); + for (int i = 0; i < n; ++i) { + float amplitude = float((i / (n/10)) + 1) / 10.f; + float sample = amplitude * + sinf(float(i) * freq * M_PI * 2.f / float(rate)); + in[i] = sample; + } + + vector out = process_realtime(shifter, in, nOut, bs, printDebug); + + // Step through the output signal in chunk of 1/20 of its duration + // (i.e. a rather arbitrary two per expected 0.1 increment in + // amplitude) and for each chunk, verify that the frequency is + // right and the amplitude is what we expect at that point + + for (int chunk = 0; chunk < 20; ++chunk) { + +// cerr << "chunk " << chunk << " of 20" << endl; + + int i0 = (nOut * chunk) / 20; + int i1 = (nOut * (chunk + 1)) / 20; + + // frequency + + int positiveCrossings = 0; + for (int i = i0; i + 1 < i1; ++i) { + if (out[i] <= 0.f && out[i+1] > 0.f) { + ++positiveCrossings; + } + } + + int expectedCrossings = int(round((freq * pitchScale * + double(i1 - i0)) / rate)); + + bool highSpeedPitch = + ! ((options & RubberBandLiveShifter::OptionPitchHighQuality) || + (options & RubberBandLiveShifter::OptionPitchHighConsistency)); + + // The check here has to depend on whether we are in Finer or + // Faster mode. In Finer mode, we expect to be generally exact + // but in the first and last chunks we can be out by one + // crossing if slowing, more if speeding up. In Faster mode we + // need to cut more slack + + int slack = 0; + + if (options & RubberBandLiveShifter::OptionEngineFiner) { + if (options & RubberBandLiveShifter::OptionWindowShort) { + slack = 2; + } else if (chunk == 0 || chunk == 19 || highSpeedPitch) { + slack = 1; + } + } else { + slack = 1; + if (chunk == 0) { + slack = (timeRatio < 1.0 ? 3 : 2); + } else if (chunk == 19) { + // all bets are off, practically + slack = expectedCrossings / 2; + } else { + slack = 1; + } + } + + BOOST_TEST(positiveCrossings <= expectedCrossings + slack); + BOOST_TEST(positiveCrossings >= expectedCrossings - slack); + + // amplitude + + double rms = 0.0; + for (int i = i0; i < i1; ++i) { + rms += out[i] * out[i]; + } + rms = sqrt(rms / double(i1 - i0)); + + double expected = (chunk/2 + 1) * 0.05 * sqrt(2.0); + + double maxOver = 0.01; + double maxUnder = 0.1; + + if (!(options & RubberBandLiveShifter::OptionEngineFiner) || + (options & RubberBandLiveShifter::OptionWindowShort)) { + maxUnder = 0.2; + } + + BOOST_TEST(rms - expected < maxOver); + BOOST_TEST(expected - rms < maxUnder); + } +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_finer) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 8.0, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_finer) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 0.5, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_hqpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_hqpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_hqpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_finer_short) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort, + 8.0, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_finer_short) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort, + 0.5, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_short) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_short_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort | + RubberBandLiveShifter::OptionPitchHighConsistency, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_short) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_hcpitch_short) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_short) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_short_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionWindowShort | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_faster) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime, + 8.0, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_faster) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime, + 0.5, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster_hqpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster_hqpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster_hqpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster_hcpitch) +{ + sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_2x_offline_faster) +{ + int n = 10000; + int rate = 44100; + RubberBandLiveShifter shifter + (rate, 1, RubberBandLiveShifter::OptionEngineFaster); + + shifter.setTimeRatio(2.0); + + vector in(n, 0.f), out(n * 2, 0.f); + + in[100] = 1.f; + in[101] = -1.f; + + in[5000] = 1.f; + in[5001] = -1.f; + + in[9900] = 1.f; + in[9901] = -1.f; + + float *inp = in.data(), *outp = out.data(); + + shifter.setMaxProcessSize(n); + shifter.setExpectedInputDuration(n); + BOOST_TEST(shifter.available() == 0); + + shifter.study(&inp, n, true); + BOOST_TEST(shifter.available() == 0); + + shifter.process(&inp, n, true); + BOOST_TEST(shifter.available() == n * 2); + + BOOST_TEST(shifter.getStartDelay() == 0); // offline mode + + size_t got = shifter.retrieve(&outp, n * 2); + BOOST_TEST(got == n * 2); + BOOST_TEST(shifter.available() == -1); + + int peak0 = -1, peak1 = -1, peak2 = -1; + float max; + + max = -2.f; + for (int i = 0; i < n/2; ++i) { + if (out[i] > max) { max = out[i]; peak0 = i; } + } + + max = -2.f; + for (int i = n/2; i < (n*3)/2; ++i) { + if (out[i] > max) { max = out[i]; peak1 = i; } + } + + max = -2.f; + for (int i = (n*3)/2; i < n*2; ++i) { + if (out[i] > max) { max = out[i]; peak2 = i; } + } + + BOOST_TEST(peak0 == 100); + BOOST_TEST(peak1 > n - 400); + BOOST_TEST(peak1 < n + 50); + BOOST_TEST(peak2 > n*2 - 600); + BOOST_TEST(peak2 < n*2); +/* + std::cout << "ms\tV" << std::endl; + for (int i = 0; i < n*2; ++i) { + std::cout << i << "\t" << out[i] << std::endl; + } +*/ +} + +BOOST_AUTO_TEST_CASE(impulses_2x_offline_finer) +{ + int n = 10000; + int rate = 44100; + RubberBandLiveShifter shifter + (rate, 1, RubberBandLiveShifter::OptionEngineFiner); + + shifter.setTimeRatio(2.0); + + vector in(n, 0.f), out(n * 2, 0.f); + + in[100] = 1.f; + in[101] = -1.f; + + in[5000] = 1.f; + in[5001] = -1.f; + + in[9900] = 1.f; + in[9901] = -1.f; + + float *inp = in.data(), *outp = out.data(); + + shifter.setMaxProcessSize(n); + shifter.setExpectedInputDuration(n); + BOOST_TEST(shifter.available() == 0); + + shifter.study(&inp, n, true); + BOOST_TEST(shifter.available() == 0); + + shifter.process(&inp, n, true); + BOOST_TEST(shifter.available() == n * 2); + + BOOST_TEST(shifter.getStartDelay() == 0); // offline mode + + size_t got = shifter.retrieve(&outp, n * 2); + BOOST_TEST(got == n * 2); + BOOST_TEST(shifter.available() == -1); + + int peak0 = -1, peak1 = -1, peak2 = -1; + float max; + + max = -2.f; + for (int i = 0; i < n/2; ++i) { + if (out[i] > max) { max = out[i]; peak0 = i; } + } + + max = -2.f; + for (int i = n/2; i < (n*3)/2; ++i) { + if (out[i] > max) { max = out[i]; peak1 = i; } + } + + max = -2.f; + for (int i = (n*3)/2; i < n*2; ++i) { + if (out[i] > max) { max = out[i]; peak2 = i; } + } + + BOOST_TEST(peak0 == 100); + BOOST_TEST(peak1 > n - 400); + BOOST_TEST(peak1 < n + 50); + BOOST_TEST(peak2 > n*2 - 600); + BOOST_TEST(peak2 < n*2); +/* + std::cout << "ms\tV" << std::endl; + for (int i = 0; i < n*2; ++i) { + std::cout << i << "\t" << out[i] << std::endl; + } +*/ +} + +BOOST_AUTO_TEST_CASE(impulses_2x_5up_offline_finer) +{ + int n = 10000; + int rate = 44100; + RubberBandLiveShifter shifter + (rate, 1, RubberBandLiveShifter::OptionEngineFiner); + + shifter.setTimeRatio(2.0); + shifter.setPitchScale(1.5); + + vector in(n, 0.f), out(n * 2, 0.f); + + in[100] = 1.f; + in[101] = -1.f; + + in[5000] = 1.f; + in[5001] = -1.f; + + in[9900] = 1.f; + in[9901] = -1.f; + + float *inp = in.data(), *outp = out.data(); + + shifter.setMaxProcessSize(n); + shifter.setExpectedInputDuration(n); + BOOST_TEST(shifter.available() == 0); + + shifter.study(&inp, n, true); + BOOST_TEST(shifter.available() == 0); + + shifter.process(&inp, n, true); + BOOST_TEST(shifter.available() == n * 2); + + BOOST_TEST(shifter.getStartDelay() == 0); // offline mode + + size_t got = shifter.retrieve(&outp, n * 2); + BOOST_TEST(got == n * 2); + BOOST_TEST(shifter.available() == -1); + + int peak0 = -1, peak1 = -1, peak2 = -1; + float max; + + max = -2.f; + for (int i = 0; i < n/2; ++i) { + if (out[i] > max) { max = out[i]; peak0 = i; } + } + + max = -2.f; + for (int i = n/2; i < (n*3)/2; ++i) { + if (out[i] > max) { max = out[i]; peak1 = i; } + } + + max = -2.f; + for (int i = (n*3)/2; i < n*2; ++i) { + if (out[i] > max) { max = out[i]; peak2 = i; } + } + + BOOST_TEST(peak0 < 100); + BOOST_TEST(peak1 > n - 400); + BOOST_TEST(peak1 < n + 50); + BOOST_TEST(peak2 > n*2 - 600); + BOOST_TEST(peak2 < n*2); +/* + std::cout << "ms\tV" << std::endl; + for (int i = 0; i < n*2; ++i) { + std::cout << i << "\t" << out[i] << std::endl; + } +*/ +} + +static void impulses_realtime(RubberBandLiveShifter::Options options, + double timeRatio, + double pitchScale, + bool printDebug) +{ + int n = 10000; + int nOut = int(ceil(n * timeRatio)); + int rate = 48000; + int bs = 1024; + + RubberBandLiveShifter shifter(rate, 1, options, timeRatio, pitchScale); + + if (printDebug) { + shifter.setDebugLevel(2); + } + + vector in(n, 0.f); + + in[100] = 1.f; + in[101] = -1.f; + + in[5000] = 1.f; + in[5001] = -1.f; + + in[9900] = 1.f; + in[9901] = -1.f; + + vector out = process_realtime(shifter, in, nOut, bs, printDebug); + + int peak0 = -1, peak1 = -1, peak2 = -1; + float max; + + max = -2.f; + for (int i = 0; i < nOut/4; ++i) { + if (out[i] > max) { max = out[i]; peak0 = i; } + } + + max = -2.f; + for (int i = nOut/4; i < (nOut*3)/4; ++i) { + if (out[i] > max) { max = out[i]; peak1 = i; } + } + + max = -2.f; + for (int i = (nOut*3)/4; i < nOut; ++i) { + if (out[i] > max) { max = out[i]; peak2 = i; } + } + + // These limits aren't alarming, but it be worth tightening them + // and and taking a look at the waveforms + + BOOST_TEST(peak0 < int(ceil(200 * timeRatio))); + BOOST_TEST(peak0 > int(ceil(50 * timeRatio))); + + BOOST_TEST(peak1 < int(ceil(5070 * timeRatio))); + BOOST_TEST(peak1 > int(ceil(4840 * timeRatio))); + + BOOST_TEST(peak2 < int(ceil(9970 * timeRatio))); + BOOST_TEST(peak2 > int(ceil(9770 * timeRatio))); + +/* + std::cout << "ms\tV" << std::endl; + for (int i = 0; i < n*2; ++i) { + std::cout << i << "\t" << out[i] << std::endl; + } +*/ +} + +BOOST_AUTO_TEST_CASE(impulses_slow_samepitch_realtime_finer) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 8.0, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_fast_samepitch_realtime_finer) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 0.5, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_slow_higher_realtime_finer) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_slow_higher_realtime_finer_hqpitch) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_slow_higher_realtime_finer_hcpitch) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_slow_lower_realtime_finer) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_slow_lower_realtime_finer_hqpitch) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighQuality, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(impulses_slow_lower_realtime_finer_hcpitch) +{ + impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 0.5, + false); +} + +static void final_realtime(RubberBandLiveShifter::Options options, + double timeRatio, + double pitchScale, + bool finalAfterFinishing, + bool printDebug) +{ + int n = 10000; + float freq = 440.f; + int rate = 44100; + int blocksize = 700; + RubberBandLiveShifter shifter(rate, 1, options); + + if (printDebug) { + shifter.setDebugLevel(2); + } + + shifter.setTimeRatio(timeRatio); + shifter.setPitchScale(pitchScale); + + int nOut = int(ceil(n * timeRatio)); + int excess = std::max(nOut, n); + vector in(n, 0.f), out(nOut + excess, 0.f); + + for (int i = 0; i < 100; ++i) { + in[n - 101 + i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); + } + + // Prime the start + { + float *source = out.data(); // just reuse out because it's silent + shifter.process(&source, shifter.getPreferredStartPad(), false); + } + + float *inp = in.data(), *outp = out.data(); + + shifter.setMaxProcessSize(blocksize); + BOOST_TEST(shifter.available() == 0); + + int toSkip = shifter.getStartDelay(); + + int incount = 0, outcount = 0; + while (true) { + + int inbs = std::min(blocksize, n - incount); + + bool final; + if (finalAfterFinishing) { + BOOST_TEST(inbs >= 0); + final = (inbs == 0); + } else { + BOOST_TEST(inbs > 0); + final = (incount + inbs >= n); + } + + float *in = inp + incount; + shifter.process(&in, inbs, final); + incount += inbs; + + int avail = shifter.available(); + BOOST_TEST(avail >= 0); + BOOST_TEST(outcount + avail < nOut + excess); + +// cerr << "in = " << inbs << ", incount now = " << incount << ", avail = " << avail << endl; + + float *out = outp + outcount; + + if (toSkip > 0) { + int skipHere = std::min(toSkip, avail); + size_t got = shifter.retrieve(&out, skipHere); + BOOST_TEST(got == size_t(skipHere)); + toSkip -= got; +// cerr << "got = " << got << ", toSkip now = " << toSkip << ", n = " << n << endl; + } + + avail = shifter.available(); + if (toSkip == 0 && avail > 0) { + size_t got = shifter.retrieve(&out, avail); + BOOST_TEST(got == size_t(avail)); + outcount += got; +// cerr << "got = " << got << ", outcount = " << outcount << ", n = " << n << endl; + if (final) { + BOOST_TEST(shifter.available() == -1); + } else { + BOOST_TEST(shifter.available() == 0); + } + } + + if (final) break; + } + + BOOST_TEST(outcount >= nOut); + + if (printDebug) { + // The initial # is to allow grep on the test output + std::cout << "#sample\tV" << std::endl; + for (int i = 0; i < outcount; ++i) { + std::cout << "#" << i << "\t" << out[i] << std::endl; + } + } + +} + +BOOST_AUTO_TEST_CASE(final_slow_samepitch_realtime_finer) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 1.0, + false, + false); +} + +BOOST_AUTO_TEST_CASE(final_slow_samepitch_realtime_finer_after) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 1.0, + true, + false); +} + +BOOST_AUTO_TEST_CASE(final_fast_samepitch_realtime_finer) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.2, 1.0, + false, + false); +} + +BOOST_AUTO_TEST_CASE(final_fast_samepitch_realtime_finer_after) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.2, 1.0, + true, + false); +} + +BOOST_AUTO_TEST_CASE(final_slow_higher_realtime_finer) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 1.5, + false, + false); +} + +BOOST_AUTO_TEST_CASE(final_slow_higher_realtime_finer_after) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 1.5, + true, + false); +} + +BOOST_AUTO_TEST_CASE(final_fast_higher_realtime_finer) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.2, 1.5, + false, + false); +} + +BOOST_AUTO_TEST_CASE(final_fast_higher_realtime_finer_after) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.2, 1.5, + true, + false); +} + +BOOST_AUTO_TEST_CASE(final_slow_lower_realtime_finer) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 0.5, + false, + false); +} + +BOOST_AUTO_TEST_CASE(final_slow_lower_realtime_finer_after) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 8.0, 0.5, + true, + false); +} + +BOOST_AUTO_TEST_CASE(final_fast_lower_realtime_finer) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.2, 0.5, + false, + false); +} + +BOOST_AUTO_TEST_CASE(final_fast_lower_realtime_finer_after) +{ + final_realtime(RubberBandLiveShifter::OptionEngineFiner | + RubberBandLiveShifter::OptionProcessRealTime | + RubberBandLiveShifter::OptionPitchHighConsistency, + 0.2, 0.5, + true, + false); +} +#endif // NOT_DEFINED + +BOOST_AUTO_TEST_SUITE_END() From d2d8e48c91e0161c090389edcb78a38b0dacc9fa Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 30 Jun 2023 16:43:59 +0100 Subject: [PATCH 02/35] Some docs --- rubberband/RubberBandLiveShifter.h | 60 ++++++++++++++++++++++++++---- src/finer/R3LiveShifter.cpp | 4 +- src/test/TestLiveShifter.cpp | 7 +++- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/rubberband/RubberBandLiveShifter.h b/rubberband/RubberBandLiveShifter.h index 55c9d7fd..c7f07a4a 100644 --- a/rubberband/RubberBandLiveShifter.h +++ b/rubberband/RubberBandLiveShifter.h @@ -41,11 +41,55 @@ #include #include +#pragma message("The RubberBandLiveShifter interface is in alpha test. It may fail to work correctly, or change at any time in the future. Use it at your own risk.") + namespace RubberBand { /** - * @mainpage RubberBandLiveShifter + * ### Summary + * + * RubberBand::RubberBandLiveShifter is an interface to the Rubber + * Band Library designed for applications that need to perform + * pitch-shifting only, without time-stretching, and to do so with the + * shortest available processing delay. + * + * RubberBandLiveShifter has a much simpler API than the general + * RubberBandStretcher. Its process function, called + * RubberBandLiveShifter::shift(), accepts a fixed number of sample + * frames on each call and always returns exactly the same number of + * sample frames. This is in contrast to the + * process/available/retrieve call sequence that RubberBandStretcher + * requires as a result of its variable output rate. + * + * The number of frames RubberBandLiveShifter::shift() accepts and + * returns is not under the caller's control: it always requires + * exactly the number given by RubberBandLiveShifter::getBlockSize(). + * However, that number is fixed for the lifetime of the shifter, so + * it only needs to be queried once and then fixed-size buffers may be + * passed. + * + * Using RubberBandLiveShifter also gives a substantially shorter + * processing delay than a typical buffering setup using + * RubberBandStretcher, making it a useful choice for some live + * situations, although it is still not a low-latency effect (and + * never will be) with a delay of 50ms or more between input and + * output signals depending on configuration. The actual value may be + * queried via RubberBandLiveShifter::getStartDelay(). The shifter is + * real-time safe in the sense of avoiding allocation, locking, or + * blocking operations in the processing path. + * + * ### Thread safety + * + * Multiple instances of RubberBandLiveShifter may be created and used + * in separate threads concurrently. However, for any single instance + * of RubberBandLiveShifter, you may not call + * RubberBandLiveShifter::shift() more than once concurrently, and you + * may not change the pitch scaling ratio using + * RubberBandLiveShifter::setPitchScale() while a + * RubberBandLiveShifter::shift() call is being executed. Changing the + * ratio is real-time safe, so when the pitch ratio is time-varying, + * it is normal to update the ratio before each shift call. */ class RUBBERBAND_LIVE_DLLEXPORT RubberBandLiveShifter @@ -157,11 +201,11 @@ RubberBandLiveShifter * pow(2.0, S / 12.0). * * This function may be called at any time, so long as it is not - * called concurrently with process(). You should either call - * this function from the same thread as process(), or provide - * your own mutex or similar mechanism to ensure that - * setPitchScale and process() cannot be run at once (there is no - * internal mutex for this purpose). + * called concurrently with shift(). You should either call this + * function from the same thread as shift(), or provide your own + * mutex or similar mechanism to ensure that setPitchScale and + * shift() cannot be run at once (there is no internal mutex for + * this purpose). */ void setPitchScale(double scale); @@ -232,8 +276,8 @@ RubberBandLiveShifter /** * Query the number of sample frames that must be passed to, and - * will be returned by, each process() call. This value is fixed - * for the lifetime of the shifter. + * will be returned by, each shift() call. This value is fixed for + * the lifetime of the shifter. * * Note that the blocksize refers to the number of audio sample * frames, which may be multi-channel, not the number of diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index d17b470b..faaacf96 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -43,14 +43,16 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_guideConfiguration(m_guide.getConfiguration()), m_channelAssembly(m_parameters.channels), m_useReadahead(false), - m_contractThenExpand(false), m_prevInhop(m_limits.maxInhopWithReadahead / 2), m_prevOuthop(m_prevInhop), + m_contractThenExpand(false), m_firstProcess(true), m_unityCount(0) { Profiler profiler("R3LiveShifter::R3LiveShifter"); + m_log.log(0, "WARNING: The RubberBandLiveShifter interface is in alpha test. It may fail to work correctly, or change at any time in the future. Use it at your own risk."); + initialise(); } diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 794a32b6..80969c84 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -44,6 +44,8 @@ BOOST_AUTO_TEST_SUITE(TestLiveShifter) BOOST_AUTO_TEST_CASE(sinusoid_unchanged) { +#ifdef TO_BE_CONTINUED + bool printDebug = true; // bool printDebug = false; @@ -106,7 +108,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) if (printDebug) { // The initial # is to allow grep on the test output std::cout << "#sample\tV" << std::endl; - for (int i = 0; i < out.size(); ++i) { + for (int i = 0; i < int(out.size()); ++i) { std::cout << "#" << i << "\t" << out[i] << std::endl; } } @@ -114,10 +116,11 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) if (printDebug) { // The initial @ is to allow grep on the test output std::cout << "@sample\tV" << std::endl; - for (int i = 0; i + delay < in.size(); ++i) { + for (int i = 0; i + delay < int(in.size()); ++i) { std::cout << "@" << i << "\t" << out[i + delay] - in[i] << std::endl; } } +#endif } //!!! From bf495b88ce1a2f7d99bd0f53aa022ebbe42e9f1b Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 30 Jun 2023 16:47:54 +0100 Subject: [PATCH 03/35] Small comment --- rubberband/RubberBandLiveShifter.h | 1 + 1 file changed, 1 insertion(+) diff --git a/rubberband/RubberBandLiveShifter.h b/rubberband/RubberBandLiveShifter.h index c7f07a4a..6c054778 100644 --- a/rubberband/RubberBandLiveShifter.h +++ b/rubberband/RubberBandLiveShifter.h @@ -103,6 +103,7 @@ RubberBandLiveShifter OptionFormantShifted = 0x00000000, OptionFormantPreserved = 0x01000000, + //!!! Rename and document OptionPitchModeA = 0x00000000, OptionPitchModeB = 0x02000000, From c2fbd6f2151d21864059e8f13ee6c7599063649d Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 23 Feb 2024 13:21:28 +0000 Subject: [PATCH 04/35] Some work on resampler delay --- src/finer/R3LiveShifter.cpp | 40 +++++++++++++++++++++++++++++------- src/finer/R3Stretcher.cpp | 4 ++++ src/test/TestLiveShifter.cpp | 34 +++++++++++++++++------------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index faaacf96..ed002899 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -212,6 +212,10 @@ R3LiveShifter::createResamplers() resamplerParameters.maxBufferSize = m_guideConfiguration.longestFftSize; resamplerParameters.dynamism = Resampler::RatioOftenChanging; resamplerParameters.ratioChange = Resampler::SmoothRatioChange; + + int debug = m_log.getDebugLevel(); + if (debug > 0) --debug; + resamplerParameters.debugLevel = debug; m_inResampler = std::unique_ptr (new Resampler(resamplerParameters, m_parameters.channels)); @@ -235,15 +239,18 @@ R3LiveShifter::getFormantScale() const size_t R3LiveShifter::getPreferredStartPad() const { - //!!!??? return 0; } size_t R3LiveShifter::getStartDelay() const { + //!!! need a principled way - measure it in ctor perhaps int resamplerDelay = 32; - int fixed = getWindowSourceSize() / 2 + resamplerDelay; +#ifdef HAVE_LIBSAMPLERATE + resamplerDelay = 47; +#endif + int fixed = getWindowSourceSize() / 2 + resamplerDelay * 2; int variable = getWindowSourceSize() / 2; if (m_contractThenExpand) { if (m_pitchScale < 1.0) { @@ -305,7 +312,6 @@ R3LiveShifter::shift(const float *const *input, float *const *output) m_log.log(2, "R3LiveShifter::shift: initially in outbuf", m_channelData[0]->outbuf->getReadSpace()); int pad = 0; - int resamplerDelay = 32; if (m_firstProcess) { if (m_contractThenExpand) { pad = getWindowSourceSize(); @@ -315,7 +321,6 @@ R3LiveShifter::shift(const float *const *input, float *const *output) } else { pad = getWindowSourceSize() / 2; } - pad += resamplerDelay; m_log.log(2, "R3LiveShifter::shift: extending input with pre-pad", incount, pad); for (int c = 0; c < m_parameters.channels; ++c) { m_channelData[c]->inbuf->zero(pad); @@ -336,7 +341,7 @@ R3LiveShifter::shift(const float *const *input, float *const *output) } } - int requiredInOutbuf = int(ceil(incount / outRatio)) + resamplerDelay; + int requiredInOutbuf = int(ceil(incount / outRatio)); generate(requiredInOutbuf); int got = readOut(output, incount, 0); @@ -417,8 +422,18 @@ R3LiveShifter::readIn(const float *const *input) incount, inRatio, false); - + m_log.log(2, "R3LiveShifter::readIn: writing to inbuf from resampled data, former read space and samples being added", m_channelData[0]->inbuf->getReadSpace(), resampleOutput); + + if (m_firstProcess) { + int expected = floor(incount * inRatio); + if (resampleOutput < expected) { + m_log.log(2, "R3LiveShifter::readIn: resampler left us short on first process, pre-padding output: expected and obtained", expected, resampleOutput); + for (int c = 0; c < m_parameters.channels; ++c) { + m_channelData[c]->inbuf->zero(expected - resampleOutput); + } + } + } for (int c = 0; c < m_parameters.channels; ++c) { m_channelData[c]->inbuf->write @@ -646,7 +661,18 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) } if (resampledCount < outcount) { - m_log.log(0, "R3LiveShifter::readOut: WARNING: Failed to obtain enough samples from resampler", resampledCount, outcount); + if (m_firstProcess) { + m_log.log(2, "R3LiveShifter::readOut: resampler left us short on first process, pre-padding output: expected and obtained", outcount, resampledCount); + int prepad = outcount - resampledCount; + for (int c = 0; c < m_parameters.channels; ++c) { + v_move(m_channelAssembly.mixdown.data()[c] + prepad, + m_channelAssembly.mixdown.data()[c], resampledCount); + v_zero(m_channelAssembly.mixdown.data()[c], prepad); + } + resampledCount = outcount; + } else { + m_log.log(0, "R3LiveShifter::readOut: WARNING: Failed to obtain enough samples from resampler", resampledCount, outcount); + } } if (useMidSide()) { diff --git a/src/finer/R3Stretcher.cpp b/src/finer/R3Stretcher.cpp index 81524b95..224038be 100644 --- a/src/finer/R3Stretcher.cpp +++ b/src/finer/R3Stretcher.cpp @@ -299,6 +299,10 @@ R3Stretcher::createResampler() resamplerParameters.dynamism = Resampler::RatioMostlyFixed; resamplerParameters.ratioChange = Resampler::SuddenRatioChange; } + + int debug = m_log.getDebugLevel(); + if (debug > 0) --debug; + resamplerParameters.debugLevel = debug; m_resampler = std::unique_ptr (new Resampler(resamplerParameters, m_parameters.channels)); diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 80969c84..359a25bc 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -52,18 +52,19 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) int n = 100000; float freq = 440.f; int rate = 44100; + + if (printDebug) { + RubberBandLiveShifter::setDefaultDebugLevel(2); + } + RubberBandLiveShifter shifter (rate, 1, RubberBandLiveShifter::OptionPitchModeB); //!!! - shifter.setPitchScale(2.0); - - if (printDebug) { - shifter.setDebugLevel(2); - } +// shifter.setPitchScale(2.0); int blocksize = shifter.getBlockSize(); - BOOST_TEST(blocksize == 2560); + BOOST_TEST(blocksize == 512); n = (n / blocksize + 1) * blocksize; @@ -79,6 +80,8 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) } int delay = shifter.getStartDelay(); + + std::cerr << "delay reported as " << delay << std::endl; // We now have n samples of a simple sinusoid with stretch factor // 1.0; obviously we expect the output to be essentially the same @@ -106,18 +109,21 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) tt::tolerance(0.001f) << tt::per_element()); if (printDebug) { - // The initial # is to allow grep on the test output - std::cout << "#sample\tV" << std::endl; + // The prefix is to allow grep on the test output + + std::cout << "IN,sample,V" << std::endl; + for (int i = 0; i < int(in.size()); ++i) { + std::cout << "IN," << i << "," << in[i] << std::endl; + } + + std::cout << "OUT,sample,V" << std::endl; for (int i = 0; i < int(out.size()); ++i) { - std::cout << "#" << i << "\t" << out[i] << std::endl; + std::cout << "OUT," << i << "," << out[i] << std::endl; } - } - if (printDebug) { - // The initial @ is to allow grep on the test output - std::cout << "@sample\tV" << std::endl; + std::cout << "DIFF,V" << std::endl; for (int i = 0; i + delay < int(in.size()); ++i) { - std::cout << "@" << i << "\t" << out[i + delay] - in[i] << std::endl; + std::cout << "DIFF," << i << "," << out[i + delay] - in[i] << std::endl; } } #endif From 62b4f458b8d7f19ed5625da2b646fc6f698ab4ba Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 7 Mar 2024 16:46:21 +0000 Subject: [PATCH 05/35] Having this here is just confusing, I think - want to make the Github repo link more obvious in comparison --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f0d5bf7d..0b344f85 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ tempo and pitch of an audio recording independently of one another. * About Rubber Band: https://breakfastquay.com/rubberband/ * Code repository: https://hg.sr.ht/~breakfastquay/rubberband -* Issue tracker: https://todo.sr.ht/~breakfastquay/rubberband or https://github.com/breakfastquay/rubberband/issues * Github mirror: https://github.com/breakfastquay/rubberband CI builds: From 68022c8dd92b0b4c2f73a905383169a0538cae0d Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 8 Mar 2024 15:56:52 +0000 Subject: [PATCH 06/35] Adjust tests --- src/test/TestLiveShifter.cpp | 1194 +--------------------------------- 1 file changed, 24 insertions(+), 1170 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 10a9c649..1e5843c0 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -42,27 +42,16 @@ namespace tt = boost::test_tools; BOOST_AUTO_TEST_SUITE(TestLiveShifter) -BOOST_AUTO_TEST_CASE(sinusoid_unchanged) +static void check_sinusoid_unchanged(int n, int rate, float freq, + RubberBandLiveShifter::Options options, + bool printDebug) { -#ifdef TO_BE_CONTINUED - - bool printDebug = true; -// bool printDebug = false; - - int n = 100000; - float freq = 440.f; - int rate = 44100; - if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(2); } - RubberBandLiveShifter shifter - (rate, 1, RubberBandLiveShifter::OptionPitchModeB); + RubberBandLiveShifter shifter(rate, 1, options); - //!!! -// shifter.setPitchScale(2.0); - int blocksize = shifter.getBlockSize(); BOOST_TEST(blocksize == 512); @@ -70,7 +59,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) vector in(n), out(n); for (int i = 0; i < n; ++i) { - in[i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); + in[i] = 0.5f * sinf(float(i) * freq * M_PI * 2.f / float(rate)); } for (int i = 0; i < n; i += blocksize) { @@ -92,23 +81,13 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) // absolute, i.e. 0.001 means 0.001x the smaller value - so they // are tighter than they appear. - // This syntax for comparing containers with a certain tolerance - // using BOOST_TEST is just bonkers. I can't find the << syntax to - // combine manipulators documented anywhere other than in a - // release note, but it does work. Well, sort of - it works this - // way around but not as per_element << tolerance. And - // tolerance(0.1) doesn't do what you'd expect if the things - // you're comparing are floats (it sets the tolerance for doubles, - // leaving float comparison unchanged). Clever... too clever. - -// BOOST_TEST(out == in, -// tt::tolerance(0.1f) << tt::per_element()); - BOOST_TEST(vector(out.begin() + delay, out.begin() + n) == vector(in.begin(), in.begin() + n - delay), tt::tolerance(0.001f) << tt::per_element()); if (printDebug) { + RubberBandLiveShifter::setDefaultDebugLevel(0); + // The prefix is to allow grep on the test output std::cout << "IN,sample,V" << std::endl; @@ -120,1162 +99,37 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) for (int i = 0; i < int(out.size()); ++i) { std::cout << "OUT," << i << "," << out[i] << std::endl; } + + std::cout << "SHIFTED,sample,V" << std::endl; + for (int i = 0; i + delay < int(out.size()); ++i) { + std::cout << "SHIFTED," << i << "," << out[i + delay] << std::endl; + } std::cout << "DIFF,V" << std::endl; for (int i = 0; i + delay < int(in.size()); ++i) { std::cout << "DIFF," << i << "," << out[i + delay] - in[i] << std::endl; } } -#endif -} - -//!!! -#ifdef NOT_DEFINED - -BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_finer) -{ - int n = 10000; - float freq = 440.f; - int rate = 44100; - - RubberBandLiveShifter shifter - (rate, 1, RubberBandLiveShifter::OptionEngineFiner); - - vector in(n), out(n); - for (int i = 0; i < n; ++i) { - in[i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); - } - float *inp = in.data(), *outp = out.data(); - - shifter.setMaxProcessSize(n); - shifter.setExpectedInputDuration(n); - BOOST_TEST(shifter.available() == 0); - - shifter.study(&inp, n, true); - BOOST_TEST(shifter.available() == 0); - - shifter.process(&inp, n, true); - BOOST_TEST(shifter.available() == n); - - BOOST_TEST(shifter.getStartDelay() == 0); // offline mode - - size_t got = shifter.retrieve(&outp, n); - BOOST_TEST(got == n); - BOOST_TEST(shifter.available() == -1); - - // The R3 engine is actually less precise than R2 here because of - // its different windowing design, though see the note above about - // what these tolerances mean - - BOOST_TEST(out == in, - tt::tolerance(0.35f) << tt::per_element()); - - BOOST_TEST(vector(out.begin() + 1024, out.begin() + n - 1024) == - vector(in.begin() + 1024, in.begin() + n - 1024), - tt::tolerance(0.01f) << tt::per_element()); - -// std::cout << "ms\tV" << std::endl; -// for (int i = 0; i < n; ++i) { -// std::cout << i << "\t" << out[i] - in[i] << std::endl; -// } -} - -BOOST_AUTO_TEST_CASE(sinusoid_2x_offline_finer) -{ - int n = 10000; - float freq = 441.f; // so a cycle is an exact number of samples - int rate = 44100; - - RubberBandLiveShifter shifter - (rate, 1, RubberBandLiveShifter::OptionEngineFiner); - - shifter.setTimeRatio(2.0); - - vector in(n), out(n*2); - for (int i = 0; i < n*2; ++i) { - out[i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); - if (i < n) { - in[i] = out[i]; - } - } - float *inp = in.data(), *outp = out.data(); - - shifter.setMaxProcessSize(n); - shifter.setExpectedInputDuration(n); - BOOST_TEST(shifter.available() == 0); - - shifter.study(&inp, n, true); - BOOST_TEST(shifter.available() == 0); - - shifter.process(&inp, n, true); - BOOST_TEST(shifter.available() == n*2); - - BOOST_TEST(shifter.getStartDelay() == 0); // offline mode - - size_t got = shifter.retrieve(&outp, n*2); - BOOST_TEST(got == n*2); - BOOST_TEST(shifter.available() == -1); - - int period = -1; - for (int i = 1000; i < 2000; ++i) { - if (period >= 0) ++period; - if (out[i] <= 0.f && out[i+1] > 0.f) { - if (period == -1) period = 0; - else break; - } - } - BOOST_TEST(period == 100); - - int offset = 0; - for (int i = 0; i < 200; ++i) { - if (out[i] <= 0.f && out[i+1] > -0.01f) { - offset = i + 1; - break; - } - } - - // overall - - double rms = 0.0; - for (int i = 0; i < n - offset; ++i) { - double diff = out[i + offset] - in[i]; - rms += diff * diff; - } - rms = sqrt(rms / double(n - offset)); - BOOST_TEST(rms < 0.2); - - // steady state - - rms = 0.0; - for (int i = 1500; i < n - offset - 3000; ++i) { - double diff = out[i + offset + 1500] - in[i + 1500]; - rms += diff * diff; - } - rms = sqrt(rms / double(n - offset - 3000)); - BOOST_TEST(rms < 0.1); -} - -static vector process_realtime(RubberBandLiveShifter &shifter, - const vector &in, - int nOut, - int bs, - bool printDebug) -{ - int n = in.size(); - vector out(nOut, 0.f); - - // Prime the start - { - float *source = out.data(); // just reuse out because it's silent - shifter.process(&source, shifter.getPreferredStartPad(), false); - } - - int toSkip = shifter.getStartDelay(); - - int inOffset = 0, outOffset = 0; - - while (outOffset < nOut) { - - // Obtain a single block of size bs, simulating realtime - // playback. The following might be the content of a - // sound-producing callback function - - int needed = std::min(bs, nOut - outOffset); - int obtained = 0; - - while (obtained < needed) { - - int available = shifter.available(); - - if (available < 0) { // finished - for (int i = obtained; i < needed; ++i) { - out[outOffset++] = 0.f; - } - break; - - } else if (available == 0) { // need to provide more input - int required = shifter.getSamplesRequired(); - BOOST_TEST(required > 0); // because available == 0 - int toProcess = std::min(required, n - inOffset); - const float *const source = in.data() + inOffset; - bool final = (toProcess < required); - shifter.process(&source, toProcess, final); - inOffset += toProcess; - BOOST_TEST(shifter.available() > 0); - continue; - - } else if (toSkip > 0) { // available > 0 && toSkip > 0 - float *target = out.data() + outOffset; - int toRetrieve = std::min(toSkip, available); - int retrieved = shifter.retrieve(&target, toRetrieve); - BOOST_TEST(retrieved == toRetrieve); - toSkip -= retrieved; - - } else { // available > 0 - float *target = out.data() + outOffset; - int toRetrieve = std::min(needed - obtained, available); - int retrieved = shifter.retrieve(&target, toRetrieve); - BOOST_TEST(retrieved == toRetrieve); - obtained += retrieved; - outOffset += retrieved; - } - } - } - - if (printDebug) { - // The initial # is to allow grep on the test output - std::cout << "#sample\tV" << std::endl; - for (int i = 0; i < nOut; ++i) { - std::cout << "#" << i << "\t" << out[i] << std::endl; - } - } - - return out; -} - -static void sinusoid_realtime(RubberBandLiveShifter::Options options, - double timeRatio, - double pitchScale, - bool printDebug) -{ - int n = (timeRatio < 1.0 ? 80000 : 40000); - int nOut = int(ceil(n * timeRatio)); - float freq = 441.f; - int rate = 44100; - int bs = 512; - - // This test simulates block-by-block realtime processing with - // latency compensation, and checks that the output is all in the - // expected place - - RubberBandLiveShifter shifter(rate, 1, options, timeRatio, pitchScale); - shifter.setMaxProcessSize(bs); - - if (printDebug) { - shifter.setDebugLevel(2); - } - - // The input signal is a fixed frequency sinusoid that steps up in - // amplitude every 1/10 of the total duration - from 0.1 at the - // start, via increments of 0.1, to 1.0 at the end - - vector in(n); - for (int i = 0; i < n; ++i) { - float amplitude = float((i / (n/10)) + 1) / 10.f; - float sample = amplitude * - sinf(float(i) * freq * M_PI * 2.f / float(rate)); - in[i] = sample; - } - - vector out = process_realtime(shifter, in, nOut, bs, printDebug); - - // Step through the output signal in chunk of 1/20 of its duration - // (i.e. a rather arbitrary two per expected 0.1 increment in - // amplitude) and for each chunk, verify that the frequency is - // right and the amplitude is what we expect at that point - - for (int chunk = 0; chunk < 20; ++chunk) { - -// cerr << "chunk " << chunk << " of 20" << endl; - - int i0 = (nOut * chunk) / 20; - int i1 = (nOut * (chunk + 1)) / 20; - - // frequency - - int positiveCrossings = 0; - for (int i = i0; i + 1 < i1; ++i) { - if (out[i] <= 0.f && out[i+1] > 0.f) { - ++positiveCrossings; - } - } - - int expectedCrossings = int(round((freq * pitchScale * - double(i1 - i0)) / rate)); - - bool highSpeedPitch = - ! ((options & RubberBandLiveShifter::OptionPitchHighQuality) || - (options & RubberBandLiveShifter::OptionPitchHighConsistency)); - - // The check here has to depend on whether we are in Finer or - // Faster mode. In Finer mode, we expect to be generally exact - // but in the first and last chunks we can be out by one - // crossing if slowing, more if speeding up. In Faster mode we - // need to cut more slack - - int slack = 0; - - if (options & RubberBandLiveShifter::OptionEngineFiner) { - if (options & RubberBandLiveShifter::OptionWindowShort) { - slack = 2; - } else if (chunk == 0 || chunk == 19 || highSpeedPitch) { - slack = 1; - } - } else { - slack = 1; - if (chunk == 0) { - slack = (timeRatio < 1.0 ? 3 : 2); - } else if (chunk == 19) { - // all bets are off, practically - slack = expectedCrossings / 2; - } else { - slack = 1; - } - } - - BOOST_TEST(positiveCrossings <= expectedCrossings + slack); - BOOST_TEST(positiveCrossings >= expectedCrossings - slack); - - // amplitude - - double rms = 0.0; - for (int i = i0; i < i1; ++i) { - rms += out[i] * out[i]; - } - rms = sqrt(rms / double(i1 - i0)); - - double expected = (chunk/2 + 1) * 0.05 * sqrt(2.0); - - double maxOver = 0.01; - double maxUnder = 0.1; - - if (!(options & RubberBandLiveShifter::OptionEngineFiner) || - (options & RubberBandLiveShifter::OptionWindowShort)) { - maxUnder = 0.2; - } - - BOOST_TEST(rms - expected < maxOver); - BOOST_TEST(expected - rms < maxUnder); - } -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_finer) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 8.0, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_finer) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 0.5, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_hqpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_hqpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_hqpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_finer_short) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort, - 8.0, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_finer_short) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort, - 0.5, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_short) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer_short_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort | - RubberBandLiveShifter::OptionPitchHighConsistency, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_short) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer_hcpitch_short) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_short) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer_short_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionWindowShort | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_faster) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime, - 8.0, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_faster) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime, - 0.5, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster_hqpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster_hqpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.5, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster_hqpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster_hcpitch) -{ - sinusoid_realtime(RubberBandLiveShifter::OptionEngineFaster | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_2x_offline_faster) -{ - int n = 10000; - int rate = 44100; - RubberBandLiveShifter shifter - (rate, 1, RubberBandLiveShifter::OptionEngineFaster); - - shifter.setTimeRatio(2.0); - - vector in(n, 0.f), out(n * 2, 0.f); - - in[100] = 1.f; - in[101] = -1.f; - - in[5000] = 1.f; - in[5001] = -1.f; - - in[9900] = 1.f; - in[9901] = -1.f; - - float *inp = in.data(), *outp = out.data(); - - shifter.setMaxProcessSize(n); - shifter.setExpectedInputDuration(n); - BOOST_TEST(shifter.available() == 0); - - shifter.study(&inp, n, true); - BOOST_TEST(shifter.available() == 0); - - shifter.process(&inp, n, true); - BOOST_TEST(shifter.available() == n * 2); - - BOOST_TEST(shifter.getStartDelay() == 0); // offline mode - - size_t got = shifter.retrieve(&outp, n * 2); - BOOST_TEST(got == n * 2); - BOOST_TEST(shifter.available() == -1); - - int peak0 = -1, peak1 = -1, peak2 = -1; - float max; - - max = -2.f; - for (int i = 0; i < n/2; ++i) { - if (out[i] > max) { max = out[i]; peak0 = i; } - } - - max = -2.f; - for (int i = n/2; i < (n*3)/2; ++i) { - if (out[i] > max) { max = out[i]; peak1 = i; } - } - - max = -2.f; - for (int i = (n*3)/2; i < n*2; ++i) { - if (out[i] > max) { max = out[i]; peak2 = i; } - } - - BOOST_TEST(peak0 == 100); - BOOST_TEST(peak1 > n - 400); - BOOST_TEST(peak1 < n + 50); - BOOST_TEST(peak2 > n*2 - 600); - BOOST_TEST(peak2 < n*2); -/* - std::cout << "ms\tV" << std::endl; - for (int i = 0; i < n*2; ++i) { - std::cout << i << "\t" << out[i] << std::endl; - } -*/ -} - -BOOST_AUTO_TEST_CASE(impulses_2x_offline_finer) -{ - int n = 10000; - int rate = 44100; - RubberBandLiveShifter shifter - (rate, 1, RubberBandLiveShifter::OptionEngineFiner); - - shifter.setTimeRatio(2.0); - - vector in(n, 0.f), out(n * 2, 0.f); - - in[100] = 1.f; - in[101] = -1.f; - - in[5000] = 1.f; - in[5001] = -1.f; - - in[9900] = 1.f; - in[9901] = -1.f; - - float *inp = in.data(), *outp = out.data(); - - shifter.setMaxProcessSize(n); - shifter.setExpectedInputDuration(n); - BOOST_TEST(shifter.available() == 0); - - shifter.study(&inp, n, true); - BOOST_TEST(shifter.available() == 0); - - shifter.process(&inp, n, true); - BOOST_TEST(shifter.available() == n * 2); - - BOOST_TEST(shifter.getStartDelay() == 0); // offline mode - - size_t got = shifter.retrieve(&outp, n * 2); - BOOST_TEST(got == n * 2); - BOOST_TEST(shifter.available() == -1); - - int peak0 = -1, peak1 = -1, peak2 = -1; - float max; - - max = -2.f; - for (int i = 0; i < n/2; ++i) { - if (out[i] > max) { max = out[i]; peak0 = i; } - } - - max = -2.f; - for (int i = n/2; i < (n*3)/2; ++i) { - if (out[i] > max) { max = out[i]; peak1 = i; } - } - - max = -2.f; - for (int i = (n*3)/2; i < n*2; ++i) { - if (out[i] > max) { max = out[i]; peak2 = i; } - } - - BOOST_TEST(peak0 == 100); - BOOST_TEST(peak1 > n - 400); - BOOST_TEST(peak1 < n + 50); - BOOST_TEST(peak2 > n*2 - 600); - BOOST_TEST(peak2 < n*2); -/* - std::cout << "ms\tV" << std::endl; - for (int i = 0; i < n*2; ++i) { - std::cout << i << "\t" << out[i] << std::endl; - } -*/ -} - -BOOST_AUTO_TEST_CASE(impulses_2x_5up_offline_finer) -{ - int n = 10000; - int rate = 44100; - RubberBandLiveShifter shifter - (rate, 1, RubberBandLiveShifter::OptionEngineFiner); - - shifter.setTimeRatio(2.0); - shifter.setPitchScale(1.5); - - vector in(n, 0.f), out(n * 2, 0.f); - - in[100] = 1.f; - in[101] = -1.f; - - in[5000] = 1.f; - in[5001] = -1.f; - - in[9900] = 1.f; - in[9901] = -1.f; - - float *inp = in.data(), *outp = out.data(); - - shifter.setMaxProcessSize(n); - shifter.setExpectedInputDuration(n); - BOOST_TEST(shifter.available() == 0); - - shifter.study(&inp, n, true); - BOOST_TEST(shifter.available() == 0); - - shifter.process(&inp, n, true); - BOOST_TEST(shifter.available() == n * 2); - - BOOST_TEST(shifter.getStartDelay() == 0); // offline mode - - size_t got = shifter.retrieve(&outp, n * 2); - BOOST_TEST(got == n * 2); - BOOST_TEST(shifter.available() == -1); - - int peak0 = -1, peak1 = -1, peak2 = -1; - float max; - - max = -2.f; - for (int i = 0; i < n/2; ++i) { - if (out[i] > max) { max = out[i]; peak0 = i; } - } - - max = -2.f; - for (int i = n/2; i < (n*3)/2; ++i) { - if (out[i] > max) { max = out[i]; peak1 = i; } - } - - max = -2.f; - for (int i = (n*3)/2; i < n*2; ++i) { - if (out[i] > max) { max = out[i]; peak2 = i; } - } - - BOOST_TEST(peak0 < 100); - BOOST_TEST(peak1 > n - 400); - BOOST_TEST(peak1 < n + 50); - BOOST_TEST(peak2 > n*2 - 600); - BOOST_TEST(peak2 < n*2); -/* - std::cout << "ms\tV" << std::endl; - for (int i = 0; i < n*2; ++i) { - std::cout << i << "\t" << out[i] << std::endl; - } -*/ -} - -static void impulses_realtime(RubberBandLiveShifter::Options options, - double timeRatio, - double pitchScale, - bool printDebug) -{ - int n = 10000; - int nOut = int(ceil(n * timeRatio)); - int rate = 48000; - int bs = 1024; - - RubberBandLiveShifter shifter(rate, 1, options, timeRatio, pitchScale); - - if (printDebug) { - shifter.setDebugLevel(2); - } - - vector in(n, 0.f); - - in[100] = 1.f; - in[101] = -1.f; - - in[5000] = 1.f; - in[5001] = -1.f; - - in[9900] = 1.f; - in[9901] = -1.f; - - vector out = process_realtime(shifter, in, nOut, bs, printDebug); - - int peak0 = -1, peak1 = -1, peak2 = -1; - float max; - - max = -2.f; - for (int i = 0; i < nOut/4; ++i) { - if (out[i] > max) { max = out[i]; peak0 = i; } - } - - max = -2.f; - for (int i = nOut/4; i < (nOut*3)/4; ++i) { - if (out[i] > max) { max = out[i]; peak1 = i; } - } - - max = -2.f; - for (int i = (nOut*3)/4; i < nOut; ++i) { - if (out[i] > max) { max = out[i]; peak2 = i; } - } - - // These limits aren't alarming, but it be worth tightening them - // and and taking a look at the waveforms - - BOOST_TEST(peak0 < int(ceil(200 * timeRatio))); - BOOST_TEST(peak0 > int(ceil(50 * timeRatio))); - - BOOST_TEST(peak1 < int(ceil(5070 * timeRatio))); - BOOST_TEST(peak1 > int(ceil(4840 * timeRatio))); - - BOOST_TEST(peak2 < int(ceil(9970 * timeRatio))); - BOOST_TEST(peak2 > int(ceil(9770 * timeRatio))); - -/* - std::cout << "ms\tV" << std::endl; - for (int i = 0; i < n*2; ++i) { - std::cout << i << "\t" << out[i] << std::endl; - } -*/ -} - -BOOST_AUTO_TEST_CASE(impulses_slow_samepitch_realtime_finer) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 8.0, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_fast_samepitch_realtime_finer) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 0.5, 1.0, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_slow_higher_realtime_finer) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_slow_higher_realtime_finer_hqpitch) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_slow_higher_realtime_finer_hcpitch) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 4.0, 1.5, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_slow_lower_realtime_finer) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_slow_lower_realtime_finer_hqpitch) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighQuality, - 8.0, 0.5, - false); -} - -BOOST_AUTO_TEST_CASE(impulses_slow_lower_realtime_finer_hcpitch) -{ - impulses_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 0.5, - false); -} - -static void final_realtime(RubberBandLiveShifter::Options options, - double timeRatio, - double pitchScale, - bool finalAfterFinishing, - bool printDebug) -{ - int n = 10000; - float freq = 440.f; - int rate = 44100; - int blocksize = 700; - RubberBandLiveShifter shifter(rate, 1, options); - - if (printDebug) { - shifter.setDebugLevel(2); - } - - shifter.setTimeRatio(timeRatio); - shifter.setPitchScale(pitchScale); - - int nOut = int(ceil(n * timeRatio)); - int excess = std::max(nOut, n); - vector in(n, 0.f), out(nOut + excess, 0.f); - - for (int i = 0; i < 100; ++i) { - in[n - 101 + i] = sinf(float(i) * freq * M_PI * 2.f / float(rate)); - } - - // Prime the start - { - float *source = out.data(); // just reuse out because it's silent - shifter.process(&source, shifter.getPreferredStartPad(), false); - } - - float *inp = in.data(), *outp = out.data(); - - shifter.setMaxProcessSize(blocksize); - BOOST_TEST(shifter.available() == 0); - - int toSkip = shifter.getStartDelay(); - - int incount = 0, outcount = 0; - while (true) { - - int inbs = std::min(blocksize, n - incount); - - bool final; - if (finalAfterFinishing) { - BOOST_TEST(inbs >= 0); - final = (inbs == 0); - } else { - BOOST_TEST(inbs > 0); - final = (incount + inbs >= n); - } - - float *in = inp + incount; - shifter.process(&in, inbs, final); - incount += inbs; - - int avail = shifter.available(); - BOOST_TEST(avail >= 0); - BOOST_TEST(outcount + avail < nOut + excess); - -// cerr << "in = " << inbs << ", incount now = " << incount << ", avail = " << avail << endl; - - float *out = outp + outcount; - - if (toSkip > 0) { - int skipHere = std::min(toSkip, avail); - size_t got = shifter.retrieve(&out, skipHere); - BOOST_TEST(got == size_t(skipHere)); - toSkip -= got; -// cerr << "got = " << got << ", toSkip now = " << toSkip << ", n = " << n << endl; - } - - avail = shifter.available(); - if (toSkip == 0 && avail > 0) { - size_t got = shifter.retrieve(&out, avail); - BOOST_TEST(got == size_t(avail)); - outcount += got; -// cerr << "got = " << got << ", outcount = " << outcount << ", n = " << n << endl; - if (final) { - BOOST_TEST(shifter.available() == -1); - } else { - BOOST_TEST(shifter.available() == 0); - } - } - - if (final) break; - } - - BOOST_TEST(outcount >= nOut); - - if (printDebug) { - // The initial # is to allow grep on the test output - std::cout << "#sample\tV" << std::endl; - for (int i = 0; i < outcount; ++i) { - std::cout << "#" << i << "\t" << out[i] << std::endl; - } - } - -} - -BOOST_AUTO_TEST_CASE(final_slow_samepitch_realtime_finer) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 1.0, - false, - false); -} - -BOOST_AUTO_TEST_CASE(final_slow_samepitch_realtime_finer_after) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 1.0, - true, - false); } -BOOST_AUTO_TEST_CASE(final_fast_samepitch_realtime_finer) +BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) { - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.2, 1.0, - false, - false); -} - -BOOST_AUTO_TEST_CASE(final_fast_samepitch_realtime_finer_after) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.2, 1.0, - true, - false); -} - -BOOST_AUTO_TEST_CASE(final_slow_higher_realtime_finer) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 1.5, - false, - false); -} - -BOOST_AUTO_TEST_CASE(final_slow_higher_realtime_finer_after) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 1.5, - true, - false); -} - -BOOST_AUTO_TEST_CASE(final_fast_higher_realtime_finer) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.2, 1.5, - false, - false); -} - -BOOST_AUTO_TEST_CASE(final_fast_higher_realtime_finer_after) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.2, 1.5, - true, - false); -} - -BOOST_AUTO_TEST_CASE(final_slow_lower_realtime_finer) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 0.5, - false, - false); -} + RubberBandLiveShifter::Options options = + RubberBandLiveShifter::OptionPitchModeA; + int n = 100000; -BOOST_AUTO_TEST_CASE(final_slow_lower_realtime_finer_after) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 8.0, 0.5, - true, - false); + check_sinusoid_unchanged(n, 44100, 440.f, options, false); + check_sinusoid_unchanged(n, 48000, 260.f, options, false); } -BOOST_AUTO_TEST_CASE(final_fast_lower_realtime_finer) +BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_b) { - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.2, 0.5, - false, - false); -} + RubberBandLiveShifter::Options options = + RubberBandLiveShifter::OptionPitchModeB; + int n = 100000; -BOOST_AUTO_TEST_CASE(final_fast_lower_realtime_finer_after) -{ - final_realtime(RubberBandLiveShifter::OptionEngineFiner | - RubberBandLiveShifter::OptionProcessRealTime | - RubberBandLiveShifter::OptionPitchHighConsistency, - 0.2, 0.5, - true, - false); + check_sinusoid_unchanged(n, 44100, 440.f, options, true); + check_sinusoid_unchanged(n, 48000, 260.f, options, false); } -#endif // NOT_DEFINED BOOST_AUTO_TEST_SUITE_END() From cbb8329057108cc76ebc12856e08623771c552a8 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 15 Mar 2024 16:44:38 +0000 Subject: [PATCH 07/35] Exploratory work --- ladspa-lv2/RubberBandLivePitchShifter.cpp | 2 +- src/common/BQResampler.cpp | 7 ++++ src/finer/Guide.h | 11 +++--- src/finer/R3LiveShifter.cpp | 45 ++++++++++++++--------- src/test/TestLiveShifter.cpp | 6 ++- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/ladspa-lv2/RubberBandLivePitchShifter.cpp b/ladspa-lv2/RubberBandLivePitchShifter.cpp index 273700a3..b3ac3ea2 100644 --- a/ladspa-lv2/RubberBandLivePitchShifter.cpp +++ b/ladspa-lv2/RubberBandLivePitchShifter.cpp @@ -267,7 +267,7 @@ RubberBandLivePitchShifter::RubberBandLivePitchShifter(int sampleRate, size_t ch m_shifter(new RubberBandLiveShifter (sampleRate, channels, RubberBandLiveShifter::OptionWindowLong | - RubberBandLiveShifter::OptionPitchModeB | + RubberBandLiveShifter::OptionPitchModeA | RubberBandLiveShifter::OptionChannelsTogether)), m_sampleRate(sampleRate), m_channels(channels), diff --git a/src/common/BQResampler.cpp b/src/common/BQResampler.cpp index 153f9cce..ab216f35 100644 --- a/src/common/BQResampler.cpp +++ b/src/common/BQResampler.cpp @@ -201,6 +201,13 @@ BQResampler::resampleInterleaved(float *const out, } } + if (i < incount_samples) { + std::cerr << "only used " << i << " of " << incount_samples + << " samples to generate output count " << o + << " (outspace_samples was " << outspace_samples << ")" + << std::endl; + } + int fbufsize = m_fade->buffer.size(); int fi = 0, fo = 0; while (fo < o && m_fade_count > 0) { diff --git a/src/finer/Guide.h b/src/finer/Guide.h index cf4f6862..93f7e400 100644 --- a/src/finer/Guide.h +++ b/src/finer/Guide.h @@ -552,11 +552,12 @@ class Guide if (guidance.phaseReset.f0 < 100.0) { guidance.phaseReset.f0 = 0.0; } - -// if (guidance.phaseReset.f0 > 0.0) { -// std::cout << unityCount << ": f0 = " << guidance.phaseReset.f0 -// << ", f1 = " << guidance.phaseReset.f1 << std::endl; -// } +/* + if (guidance.phaseReset.f0 > 0.0) { + std::cout << "unity: f0 = " << guidance.phaseReset.f0 + << ", f1 = " << guidance.phaseReset.f1 << std::endl; + } +*/ } bool checkPotentialKick(const process_t *const magnitudes, diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index cd203c62..565a7a84 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -341,7 +341,7 @@ R3LiveShifter::shift(const float *const *input, float *const *output) } } - int requiredInOutbuf = int(ceil(incount / outRatio)); + int requiredInOutbuf = 1 + int(ceil(incount / outRatio)); generate(requiredInOutbuf); int got = readOut(output, incount, 0); @@ -615,6 +615,8 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) int fromOutbuf = int(floor(outcount / outRatio)); + m_log.log(2, "R3LiveShifter::readOut: origin and fromOutbuf", origin, fromOutbuf); + if (fromOutbuf == 0) { fromOutbuf = 1; } @@ -630,9 +632,6 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) } got = std::min(got, std::max(gotHere, 0)); } - - m_channelAssembly.resampled[c] = cd->resampled.data(); - m_channelAssembly.mixdown[c] = output[c] + origin; } m_log.log(2, "R3LiveShifter::readOut: requested and got from outbufs", fromOutbuf, got); @@ -641,6 +640,13 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) int resampledCount = 0; if (got > 0) { + + for (int c = 0; c < m_parameters.channels; ++c) { + auto &cd = m_channelData.at(c); + m_channelAssembly.resampled[c] = cd->resampled.data(); + m_channelAssembly.mixdown[c] = output[c] + origin; + } + resampledCount = m_outResampler->resample (m_channelAssembly.mixdown.data(), outcount, @@ -648,6 +654,17 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) got, outRatio, false); + + if (useMidSide()) { + for (int i = 0; i < resampledCount; ++i) { + float m = output[0][origin + i]; + float s = output[1][origin + i]; + float l = m + s; + float r = m - s; + output[0][origin + i] = l; + output[1][origin + i] = r; + } + } } m_log.log(2, "R3LiveShifter::readOut: resampled to", resampledCount); @@ -665,26 +682,18 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) m_log.log(2, "R3LiveShifter::readOut: resampler left us short on first process, pre-padding output: expected and obtained", outcount, resampledCount); int prepad = outcount - resampledCount; for (int c = 0; c < m_parameters.channels; ++c) { - v_move(m_channelAssembly.mixdown.data()[c] + prepad, - m_channelAssembly.mixdown.data()[c], resampledCount); - v_zero(m_channelAssembly.mixdown.data()[c], prepad); + v_move(output[c] + origin + prepad, + output[c] + origin, + resampledCount); + v_zero(output[c] + origin, prepad); } resampledCount = outcount; } else { m_log.log(0, "R3LiveShifter::readOut: WARNING: Failed to obtain enough samples from resampler", resampledCount, outcount); } } - - if (useMidSide()) { - for (int i = 0; i < resampledCount; ++i) { - float m = output[0][i]; - float s = output[1][i]; - float l = m + s; - float r = m - s; - output[0][i] = l; - output[1][i] = r; - } - } + + m_log.log(2, "R3LiveShifter::readOut: returning", resampledCount); return resampledCount; } diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 1e5843c0..7e1cd23b 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -52,6 +52,8 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, RubberBandLiveShifter shifter(rate, 1, options); + shifter.setPitchScale(2.66968); + int blocksize = shifter.getBlockSize(); BOOST_TEST(blocksize == 512); @@ -118,7 +120,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) RubberBandLiveShifter::OptionPitchModeA; int n = 100000; - check_sinusoid_unchanged(n, 44100, 440.f, options, false); + check_sinusoid_unchanged(n, 44100, 440.f, options, true); check_sinusoid_unchanged(n, 48000, 260.f, options, false); } @@ -128,7 +130,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_b) RubberBandLiveShifter::OptionPitchModeB; int n = 100000; - check_sinusoid_unchanged(n, 44100, 440.f, options, true); + check_sinusoid_unchanged(n, 44100, 440.f, options, false); check_sinusoid_unchanged(n, 48000, 260.f, options, false); } From 59d0ff6abee72e6799e7267b73dcea92d0bb2c8e Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 15 Mar 2024 16:59:22 +0000 Subject: [PATCH 08/35] Rework output resampler handling to deal with case where fewer samples are consumed than available --- src/common/BQResampler.cpp | 7 --- src/finer/R3LiveShifter.cpp | 99 +++++++++++++++++++----------------- src/finer/R3LiveShifter.h | 2 +- src/test/TestLiveShifter.cpp | 4 +- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/common/BQResampler.cpp b/src/common/BQResampler.cpp index ab216f35..153f9cce 100644 --- a/src/common/BQResampler.cpp +++ b/src/common/BQResampler.cpp @@ -201,13 +201,6 @@ BQResampler::resampleInterleaved(float *const out, } } - if (i < incount_samples) { - std::cerr << "only used " << i << " of " << incount_samples - << " samples to generate output count " << o - << " (outspace_samples was " << outspace_samples << ")" - << std::endl; - } - int fbufsize = m_fade->buffer.size(); int fi = 0, fo = 0; while (fo < o && m_fade_count > 0) { diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 565a7a84..20c73e74 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -344,7 +344,7 @@ R3LiveShifter::shift(const float *const *input, float *const *output) int requiredInOutbuf = 1 + int(ceil(incount / outRatio)); generate(requiredInOutbuf); - int got = readOut(output, incount, 0); + int got = readOut(output, incount); if (got < incount) { m_log.log(0, "R3LiveShifter::shift: ERROR: internal error: insufficient data at output (wanted, got)", incount, got); @@ -597,7 +597,7 @@ R3LiveShifter::generate(int requiredInOutbuf) } int -R3LiveShifter::readOut(float *const *output, int outcount, int origin) +R3LiveShifter::readOut(float *const *output, int outcount) { double outRatio = 1.0; @@ -613,79 +613,86 @@ R3LiveShifter::readOut(float *const *output, int outcount, int origin) m_log.log(2, "R3LiveShifter::readOut: outcount and ratio", outcount, outRatio); - int fromOutbuf = int(floor(outcount / outRatio)); + int resampledCount = 0; + bool fillingTail = true; - m_log.log(2, "R3LiveShifter::readOut: origin and fromOutbuf", origin, fromOutbuf); - - if (fromOutbuf == 0) { - fromOutbuf = 1; - } + while (resampledCount < outcount) { + + int fromOutbuf; + + if (fillingTail) { + fromOutbuf = 1; + } else { + fromOutbuf = int(floor(outcount / outRatio)); + if (fromOutbuf == 0) { + fromOutbuf = 1; + } + } + + m_log.log(2, "R3LiveShifter::readOut: fromOutbuf", fromOutbuf); - int got = fromOutbuf; + int got = fromOutbuf; - for (int c = 0; c < m_parameters.channels; ++c) { - auto &cd = m_channelData.at(c); - int gotHere = cd->outbuf->read(cd->resampled.data(), got); - if (gotHere < got) { - if (c > 0) { - m_log.log(0, "R3LiveShifter::readOut: WARNING: channel imbalance detected"); + for (int c = 0; c < m_parameters.channels; ++c) { + auto &cd = m_channelData.at(c); + int gotHere = cd->outbuf->read(cd->resampled.data(), got); + if (gotHere < got) { + if (c > 0) { + m_log.log(0, "R3LiveShifter::readOut: WARNING: channel imbalance detected"); + } } got = std::min(got, std::max(gotHere, 0)); } - } - - m_log.log(2, "R3LiveShifter::readOut: requested and got from outbufs", fromOutbuf, got); - m_log.log(2, "R3LiveShifter::readOut: leaving behind", m_channelData.at(0)->outbuf->getReadSpace()); - - int resampledCount = 0; - if (got > 0) { + m_log.log(2, "R3LiveShifter::readOut: requested and got from outbufs", fromOutbuf, got); + m_log.log(2, "R3LiveShifter::readOut: leaving behind", m_channelData.at(0)->outbuf->getReadSpace()); for (int c = 0; c < m_parameters.channels; ++c) { auto &cd = m_channelData.at(c); m_channelAssembly.resampled[c] = cd->resampled.data(); - m_channelAssembly.mixdown[c] = output[c] + origin; + m_channelAssembly.mixdown[c] = output[c] + resampledCount; } - resampledCount = m_outResampler->resample + auto resampledHere = m_outResampler->resample (m_channelAssembly.mixdown.data(), - outcount, + outcount - resampledCount, m_channelAssembly.resampled.data(), got, outRatio, false); + + m_log.log(2, "R3LiveShifter::readOut: resampledHere", resampledHere); + + if (got == 0 && resampledHere == 0) { + m_log.log(2, "R3LiveShifter::readOut: made no progress, finishing"); + break; + } + + resampledCount += resampledHere; + + fillingTail = true; + } - if (useMidSide()) { - for (int i = 0; i < resampledCount; ++i) { - float m = output[0][origin + i]; - float s = output[1][origin + i]; - float l = m + s; - float r = m - s; - output[0][origin + i] = l; - output[1][origin + i] = r; - } + if (useMidSide()) { + for (int i = 0; i < resampledCount; ++i) { + float m = output[0][i]; + float s = output[1][i]; + float l = m + s; + float r = m - s; + output[0][i] = l; + output[1][i] = r; } } m_log.log(2, "R3LiveShifter::readOut: resampled to", resampledCount); - if (resampledCount < outcount && - m_channelData.at(0)->outbuf->getReadSpace() > 0) { - int remaining = outcount - resampledCount; - m_log.log(2, "R3LiveShifter::readOut: recursing to try to get the remaining", - remaining); - resampledCount += readOut(output, remaining, origin + resampledCount); - } - if (resampledCount < outcount) { if (m_firstProcess) { m_log.log(2, "R3LiveShifter::readOut: resampler left us short on first process, pre-padding output: expected and obtained", outcount, resampledCount); int prepad = outcount - resampledCount; for (int c = 0; c < m_parameters.channels; ++c) { - v_move(output[c] + origin + prepad, - output[c] + origin, - resampledCount); - v_zero(output[c] + origin, prepad); + v_move(output[c] + prepad, output[c], resampledCount); + v_zero(output[c], prepad); } resampledCount = outcount; } else { diff --git a/src/finer/R3LiveShifter.h b/src/finer/R3LiveShifter.h index 222146b8..a9224ef3 100644 --- a/src/finer/R3LiveShifter.h +++ b/src/finer/R3LiveShifter.h @@ -328,7 +328,7 @@ class R3LiveShifter void readIn(const float *const *input); void generate(int required); - int readOut(float *const *output, int outcount, int origin); + int readOut(float *const *output, int outcount); void createResamplers(); void analyseChannel(int channel, int inhop, int prevInhop, int prevOuthop); diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 7e1cd23b..3fbd168f 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -52,7 +52,7 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, RubberBandLiveShifter shifter(rate, 1, options); - shifter.setPitchScale(2.66968); +// shifter.setPitchScale(2.66968); int blocksize = shifter.getBlockSize(); BOOST_TEST(blocksize == 512); @@ -120,7 +120,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) RubberBandLiveShifter::OptionPitchModeA; int n = 100000; - check_sinusoid_unchanged(n, 44100, 440.f, options, true); + check_sinusoid_unchanged(n, 44100, 440.f, options, false); check_sinusoid_unchanged(n, 48000, 260.f, options, false); } From 53ccb1d60edd55ca7ec01b11d77e59fc5aed5ea7 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 19 Apr 2024 15:12:25 +0100 Subject: [PATCH 09/35] Measure rather than guess resampler delay --- src/finer/R3LiveShifter.cpp | 40 ++++++++++++++++++++++++++++--------- src/finer/R3LiveShifter.h | 2 ++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 20c73e74..ce119a51 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -42,6 +42,7 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_log), m_guideConfiguration(m_guide.getConfiguration()), m_channelAssembly(m_parameters.channels), + m_resamplerDelay(32), m_useReadahead(false), m_prevInhop(m_limits.maxInhopWithReadahead / 2), m_prevOuthop(m_prevInhop), @@ -222,6 +223,30 @@ R3LiveShifter::createResamplers() m_outResampler = std::unique_ptr (new Resampler(resamplerParameters, m_parameters.channels)); + + measureResamplerDelay(); +} + +void +R3LiveShifter::measureResamplerDelay() +{ + // Delay in the sense that the resampler at first returns fewer + // samples than requested, not that the samples are delayed within + // the returned buffer relative to their positions at input. We + // actually add the delay ourselves in the first block because we + // need a fixed block size and the resampler doesn't return one. + + int bs = getBlockSize(); + std::vector inbuf(bs * m_parameters.channels, 0.f); + auto outbuf = inbuf; + + int outcount = m_inResampler->resampleInterleaved + (outbuf.data(), bs, inbuf.data(), bs, 1.0, false); + + m_inResampler->reset(); + + m_resamplerDelay = bs - outcount; + m_log.log(1, "R3LiveShifter::measureResamplerDelay: measured delay and outcount ", m_resamplerDelay, outcount); } double @@ -245,12 +270,7 @@ R3LiveShifter::getPreferredStartPad() const size_t R3LiveShifter::getStartDelay() const { - //!!! need a principled way - measure it in ctor perhaps - int resamplerDelay = 32; -#ifdef HAVE_LIBSAMPLERATE - resamplerDelay = 47; -#endif - int fixed = getWindowSourceSize() / 2 + resamplerDelay * 2; + int fixed = getWindowSourceSize() / 2 + m_resamplerDelay * 2 - 1; int variable = getWindowSourceSize() / 2; if (m_contractThenExpand) { if (m_pitchScale < 1.0) { @@ -292,6 +312,8 @@ R3LiveShifter::reset() for (auto &cd : m_channelData) { cd->reset(); } + + measureResamplerDelay(); } size_t @@ -414,7 +436,7 @@ R3LiveShifter::readIn(const float *const *input) m_log.log(2, "R3LiveShifter::readIn: ratio", inRatio); int resampleBufSize = int(m_channelData.at(0)->resampled.size()); - + int resampleOutput = m_inResampler->resample (m_channelAssembly.resampled.data(), resampleBufSize, @@ -422,7 +444,7 @@ R3LiveShifter::readIn(const float *const *input) incount, inRatio, false); - + m_log.log(2, "R3LiveShifter::readIn: writing to inbuf from resampled data, former read space and samples being added", m_channelData[0]->inbuf->getReadSpace(), resampleOutput); if (m_firstProcess) { @@ -652,7 +674,7 @@ R3LiveShifter::readOut(float *const *output, int outcount) m_channelAssembly.resampled[c] = cd->resampled.data(); m_channelAssembly.mixdown[c] = output[c] + resampledCount; } - + auto resampledHere = m_outResampler->resample (m_channelAssembly.mixdown.data(), outcount - resampledCount, diff --git a/src/finer/R3LiveShifter.h b/src/finer/R3LiveShifter.h index a9224ef3..d2fed522 100644 --- a/src/finer/R3LiveShifter.h +++ b/src/finer/R3LiveShifter.h @@ -317,6 +317,7 @@ class R3LiveShifter ChannelAssembly m_channelAssembly; std::unique_ptr m_inResampler; std::unique_ptr m_outResampler; + int m_resamplerDelay; bool m_useReadahead; int m_prevInhop; int m_prevOuthop; @@ -331,6 +332,7 @@ class R3LiveShifter int readOut(float *const *output, int outcount); void createResamplers(); + void measureResamplerDelay(); void analyseChannel(int channel, int inhop, int prevInhop, int prevOuthop); void analyseFormant(int channel); void adjustFormant(int channel); From e0df952e5650da6487b7c3baf916fea162a4ed98 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Wed, 24 Apr 2024 14:17:48 +0100 Subject: [PATCH 10/35] Start with a phase reset; it gives better behaviour at start if we are initialised with ratio 1 --- src/finer/R3LiveShifter.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index ce119a51..96fb1f14 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -117,6 +117,9 @@ R3LiveShifter::initialise() std::make_shared (fftSize, m_guideConfiguration.longestFftSize); } + m_channelData[c]->guidance.phaseReset.present = true; + m_channelData[c]->guidance.phaseReset.f0 = 0.0; + m_channelData[c]->guidance.phaseReset.f1 = m_parameters.sampleRate / 2.0; } m_scaleData.clear(); From 47b2ff6698ac122c6849db6c26450abeba185664 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Wed, 24 Apr 2024 14:18:38 +0100 Subject: [PATCH 11/35] Correct previous (wrong) tweak to delay --- src/finer/R3LiveShifter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 96fb1f14..0df6c7b1 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -273,7 +273,7 @@ R3LiveShifter::getPreferredStartPad() const size_t R3LiveShifter::getStartDelay() const { - int fixed = getWindowSourceSize() / 2 + m_resamplerDelay * 2 - 1; + int fixed = getWindowSourceSize() / 2 + m_resamplerDelay * 2; int variable = getWindowSourceSize() / 2; if (m_contractThenExpand) { if (m_pitchScale < 1.0) { From a877385b8adb65b5107c63bf199b44dc223fe8fe Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Wed, 24 Apr 2024 14:19:05 +0100 Subject: [PATCH 12/35] We are not normally fillingTail --- src/finer/R3LiveShifter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 0df6c7b1..25a55f0a 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -639,7 +639,7 @@ R3LiveShifter::readOut(float *const *output, int outcount) m_log.log(2, "R3LiveShifter::readOut: outcount and ratio", outcount, outRatio); int resampledCount = 0; - bool fillingTail = true; + bool fillingTail = false; while (resampledCount < outcount) { @@ -654,7 +654,7 @@ R3LiveShifter::readOut(float *const *output, int outcount) } } - m_log.log(2, "R3LiveShifter::readOut: fromOutbuf", fromOutbuf); + m_log.log(2, "R3LiveShifter::readOut: fillingTail and fromOutbuf", fillingTail, fromOutbuf); int got = fromOutbuf; From 472081837e917b961f1be051fb9fb8bf180b67f4 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Wed, 24 Apr 2024 14:20:20 +0100 Subject: [PATCH 13/35] Debug output --- src/finer/Guide.h | 18 ++++++++---------- src/finer/R3LiveShifter.cpp | 3 +++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/finer/Guide.h b/src/finer/Guide.h index 93f7e400..5e1c50b7 100644 --- a/src/finer/Guide.h +++ b/src/finer/Guide.h @@ -457,7 +457,7 @@ class Guide << guidance.phaseReset.f0 << " to " << guidance.phaseReset.f1 << "]" << std::endl; - m_log.log(1, str.str().c_str()); + m_log.log(2, str.str().c_str()); */ } @@ -478,7 +478,9 @@ class Guide double m_maxHigher; void updateForSilence(Guidance &guidance) const { -// std::cout << "phase reset on silence" << std::endl; + + m_log.log(2, "Guide::updateForSilence"); + double nyquist = m_parameters.sampleRate / 2.0; if (!m_parameters.singleWindowMode) { guidance.fftBands[0].f0 = 0.0; @@ -498,7 +500,7 @@ class Guide const BinSegmenter::Segmentation &segmentation, bool realtime) const { -// std::cout << "unity" << std::endl; + m_log.log(2, "Guide::updateForUnity: realtime and single-window mode", (int)realtime, m_parameters.singleWindowMode); double nyquist = m_parameters.sampleRate / 2.0; @@ -533,9 +535,9 @@ class Guide if (!hadPhaseReset) { guidance.phaseReset.f0 = 16000.0; guidance.phaseReset.f1 = nyquist; -// std::cout << "f0 = " << guidance.phaseReset.f0 << std::endl; return; } else { + m_log.log(2, "Guide::updateForUnity: had phase reset"); guidance.phaseReset.f0 *= 0.9; guidance.phaseReset.f1 *= 1.1; } @@ -552,12 +554,8 @@ class Guide if (guidance.phaseReset.f0 < 100.0) { guidance.phaseReset.f0 = 0.0; } -/* - if (guidance.phaseReset.f0 > 0.0) { - std::cout << "unity: f0 = " << guidance.phaseReset.f0 - << ", f1 = " << guidance.phaseReset.f1 << std::endl; - } -*/ + + m_log.log(2, "Guide::updateForUnity: f0 and f1", guidance.phaseReset.f0, guidance.phaseReset.f1); } bool checkPotentialKick(const process_t *const magnitudes, diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 25a55f0a..faca038d 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -249,6 +249,7 @@ R3LiveShifter::measureResamplerDelay() m_inResampler->reset(); m_resamplerDelay = bs - outcount; + m_log.log(1, "R3LiveShifter::measureResamplerDelay: measured delay and outcount ", m_resamplerDelay, outcount); } @@ -1149,6 +1150,8 @@ R3LiveShifter::synthesiseChannel(int c, int outhop, bool draining) process_t winscale = process_t(outhop) / scaleData->windowScaleFactor; + m_log.log(2, "R3LiveShifter::synthesiseChannel: outhop and winscale", outhop, winscale); + // The frequency filter is applied naively in the frequency // domain. Aliasing is reduced by the shorter resynthesis // window. We resynthesise each scale individually, then sum - From 3a95b94087b28bea615c15fce91a611806b6ddf3 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 3 May 2024 13:38:23 +0100 Subject: [PATCH 14/35] Fix compiler warnings --- src/test/TestStretcher.cpp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/test/TestStretcher.cpp b/src/test/TestStretcher.cpp index fc72d995..f305217b 100644 --- a/src/test/TestStretcher.cpp +++ b/src/test/TestStretcher.cpp @@ -46,10 +46,10 @@ BOOST_AUTO_TEST_CASE(engine_version) { RubberBandStretcher s2(44100, 1, RubberBandStretcher::OptionEngineFaster); BOOST_TEST(s2.getEngineVersion() == 2); - BOOST_TEST(s2.getProcessSizeLimit() == 524288); + BOOST_TEST(s2.getProcessSizeLimit() == 524288u); RubberBandStretcher s3(44100, 1, RubberBandStretcher::OptionEngineFiner); BOOST_TEST(s3.getEngineVersion() == 3); - BOOST_TEST(s3.getProcessSizeLimit() == 524288); + BOOST_TEST(s3.getProcessSizeLimit() == 524288u); } BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_faster) @@ -76,10 +76,10 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_faster) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n); - BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0u); // offline mode size_t got = stretcher.retrieve(&outp, n); - BOOST_TEST(got == n); + BOOST_TEST(got == size_t(n)); BOOST_TEST(stretcher.available() == -1); // We now have n samples of a simple sinusoid with stretch factor @@ -133,10 +133,10 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n); - BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0u); // offline mode size_t got = stretcher.retrieve(&outp, n); - BOOST_TEST(got == n); + BOOST_TEST(got == size_t(n)); BOOST_TEST(stretcher.available() == -1); // The R3 engine is actually less precise than R2 here because of @@ -186,10 +186,10 @@ BOOST_AUTO_TEST_CASE(sinusoid_2x_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n*2); - BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0u); // offline mode size_t got = stretcher.retrieve(&outp, n*2); - BOOST_TEST(got == n*2); + BOOST_TEST(got == size_t(n)*2); BOOST_TEST(stretcher.available() == -1); int period = -1; @@ -774,10 +774,10 @@ BOOST_AUTO_TEST_CASE(impulses_2x_offline_faster) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n * 2); - BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0u); // offline mode size_t got = stretcher.retrieve(&outp, n * 2); - BOOST_TEST(got == n * 2); + BOOST_TEST(got == size_t(n) * 2); BOOST_TEST(stretcher.available() == -1); int peak0 = -1, peak1 = -1, peak2 = -1; @@ -843,10 +843,10 @@ BOOST_AUTO_TEST_CASE(impulses_2x_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n * 2); - BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0u); // offline mode size_t got = stretcher.retrieve(&outp, n * 2); - BOOST_TEST(got == n * 2); + BOOST_TEST(got == size_t(n) * 2); BOOST_TEST(stretcher.available() == -1); int peak0 = -1, peak1 = -1, peak2 = -1; @@ -913,10 +913,10 @@ BOOST_AUTO_TEST_CASE(impulses_2x_5up_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n * 2); - BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0u); // offline mode size_t got = stretcher.retrieve(&outp, n * 2); - BOOST_TEST(got == n * 2); + BOOST_TEST(got == size_t(n) * 2); BOOST_TEST(stretcher.available() == -1); int peak0 = -1, peak1 = -1, peak2 = -1; @@ -1424,7 +1424,7 @@ static void with_resets(RubberBandStretcher::Options options, stretcher->process(&inp, n, true); BOOST_TEST(stretcher->available() == nOut); - BOOST_TEST(stretcher->getStartDelay() == 0); // offline mode + BOOST_TEST(stretcher->getStartDelay() == 0u); // offline mode nActual = (int)stretcher->retrieve(&outp, nOut); BOOST_TEST(nActual == nOut); From 9998c26ea3b34e27926252503a218e78436f05a6 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 3 May 2024 17:52:14 +0100 Subject: [PATCH 15/35] Rename and reorder mode options; drop support for long window --- ladspa-lv2/RubberBandLivePitchShifter.cpp | 3 +- ladspa-lv2/RubberBandPitchShifter.cpp | 2 - ladspa-lv2/RubberBandR3PitchShifter.cpp | 2 - rubberband/RubberBandLiveShifter.h | 6 +-- src/finer/R3LiveShifter.cpp | 49 +++++++++++------------ src/finer/R3LiveShifter.h | 23 ++++------- src/test/TestLiveShifter.cpp | 4 +- 7 files changed, 36 insertions(+), 53 deletions(-) diff --git a/ladspa-lv2/RubberBandLivePitchShifter.cpp b/ladspa-lv2/RubberBandLivePitchShifter.cpp index b3ac3ea2..16bcaa5a 100644 --- a/ladspa-lv2/RubberBandLivePitchShifter.cpp +++ b/ladspa-lv2/RubberBandLivePitchShifter.cpp @@ -266,8 +266,7 @@ RubberBandLivePitchShifter::RubberBandLivePitchShifter(int sampleRate, size_t ch m_currentFormant(false), m_shifter(new RubberBandLiveShifter (sampleRate, channels, - RubberBandLiveShifter::OptionWindowLong | - RubberBandLiveShifter::OptionPitchModeA | + RubberBandLiveShifter::OptionPitchMethodStandard | RubberBandLiveShifter::OptionChannelsTogether)), m_sampleRate(sampleRate), m_channels(channels), diff --git a/ladspa-lv2/RubberBandPitchShifter.cpp b/ladspa-lv2/RubberBandPitchShifter.cpp index a2b6cca1..c98487f0 100644 --- a/ladspa-lv2/RubberBandPitchShifter.cpp +++ b/ladspa-lv2/RubberBandPitchShifter.cpp @@ -642,7 +642,6 @@ RubberBandPitchShifter::runImpl(uint32_t insamples, uint32_t offset) const int samples = insamples; int processed = 0; - size_t outTotal = 0; while (processed < samples) { @@ -671,7 +670,6 @@ RubberBandPitchShifter::runImpl(uint32_t insamples, uint32_t offset) } size_t actual = m_stretcher->retrieve(m_scratch, outchunk); - outTotal += actual; for (size_t c = 0; c < m_channels; ++c) { m_outputBuffer[c]->write(m_scratch[c], actual); diff --git a/ladspa-lv2/RubberBandR3PitchShifter.cpp b/ladspa-lv2/RubberBandR3PitchShifter.cpp index 91c804f1..746900ae 100644 --- a/ladspa-lv2/RubberBandR3PitchShifter.cpp +++ b/ladspa-lv2/RubberBandR3PitchShifter.cpp @@ -593,7 +593,6 @@ RubberBandR3PitchShifter::runImpl(uint32_t insamples, uint32_t offset) const int samples = insamples; int processed = 0; - size_t outTotal = 0; while (processed < samples) { @@ -622,7 +621,6 @@ RubberBandR3PitchShifter::runImpl(uint32_t insamples, uint32_t offset) } size_t actual = m_stretcher->retrieve(m_scratch, outchunk); - outTotal += actual; for (size_t c = 0; c < m_channels; ++c) { m_outputBuffer[c]->write(m_scratch[c], actual); diff --git a/rubberband/RubberBandLiveShifter.h b/rubberband/RubberBandLiveShifter.h index 0ecaed2f..a9b37ff8 100644 --- a/rubberband/RubberBandLiveShifter.h +++ b/rubberband/RubberBandLiveShifter.h @@ -98,14 +98,12 @@ RubberBandLiveShifter enum Option { OptionWindowShort = 0x00000000, OptionWindowMedium = 0x00100000, - OptionWindowLong = 0x00200000, OptionFormantShifted = 0x00000000, OptionFormantPreserved = 0x01000000, - //!!! Rename and document - OptionPitchModeA = 0x00000000, - OptionPitchModeB = 0x02000000, + OptionPitchMethodStandard = 0x00000000, + OptionPitchMethodAlternate = 0x02000000, OptionChannelsApart = 0x00000000, OptionChannelsTogether = 0x10000000, diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index faca038d..d80f6434 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -37,8 +37,7 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_pitchScale(1.0), m_formantScale(0.0), m_guide(Guide::Parameters - (m_parameters.sampleRate, - !(m_parameters.options & RubberBandLiveShifter::OptionWindowLong)), + (m_parameters.sampleRate, true), m_log), m_guideConfiguration(m_guide.getConfiguration()), m_channelAssembly(m_parameters.channels), @@ -46,7 +45,7 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_useReadahead(false), m_prevInhop(m_limits.maxInhopWithReadahead / 2), m_prevOuthop(m_prevInhop), - m_contractThenExpand(false), + m_expandThenContract(false), m_firstProcess(true), m_unityCount(0) { @@ -67,15 +66,14 @@ R3LiveShifter::initialise() m_log.log(1, "R3LiveShifter::R3LiveShifter: multi window enabled"); } - if ((m_parameters.options & RubberBandLiveShifter::OptionWindowMedium) || - (m_parameters.options & RubberBandLiveShifter::OptionWindowLong)) { + if (m_parameters.options & RubberBandLiveShifter::OptionWindowMedium) { m_log.log(1, "R3LiveShifter::R3LiveShifter: readahead enabled"); m_useReadahead = true; } - if ((m_parameters.options & RubberBandLiveShifter::OptionPitchModeB)) { - m_log.log(1, "R3LiveShifter::R3LiveShifter: contract-then-expand enabled"); - m_contractThenExpand = true; + if ((m_parameters.options & RubberBandLiveShifter::OptionPitchMethodAlternate)) { + m_log.log(1, "R3LiveShifter::R3LiveShifter: expand-then-contract enabled"); + m_expandThenContract = true; } double maxClassifierFrequency = 16000.0; @@ -276,17 +274,17 @@ R3LiveShifter::getStartDelay() const { int fixed = getWindowSourceSize() / 2 + m_resamplerDelay * 2; int variable = getWindowSourceSize() / 2; - if (m_contractThenExpand) { + if (m_expandThenContract) { if (m_pitchScale < 1.0) { - return size_t(fixed + ceil(variable / m_pitchScale)); - } else { return size_t(fixed + ceil(variable * m_pitchScale)); + } else { + return size_t(fixed + ceil(variable / m_pitchScale)); } } else { if (m_pitchScale < 1.0) { - return size_t(fixed + ceil(variable * m_pitchScale)); - } else { return size_t(fixed + ceil(variable / m_pitchScale)); + } else { + return size_t(fixed + ceil(variable * m_pitchScale)); } } } @@ -339,13 +337,13 @@ R3LiveShifter::shift(const float *const *input, float *const *output) int pad = 0; if (m_firstProcess) { - if (m_contractThenExpand) { + if (m_expandThenContract) { + pad = getWindowSourceSize() / 2; + } else { pad = getWindowSourceSize(); if (m_pitchScale > 1.0) { pad = int(ceil(pad * m_pitchScale)); } - } else { - pad = getWindowSourceSize() / 2; } m_log.log(2, "R3LiveShifter::shift: extending input with pre-pad", incount, pad); for (int c = 0; c < m_parameters.channels; ++c) { @@ -357,12 +355,12 @@ R3LiveShifter::shift(const float *const *input, float *const *output) double outRatio = 1.0; - if (m_contractThenExpand) { - if (m_pitchScale < 1.0) { + if (m_expandThenContract) { + if (m_pitchScale > 1.0) { outRatio = 1.0 / m_pitchScale; } } else { - if (m_pitchScale > 1.0) { + if (m_pitchScale < 1.0) { outRatio = 1.0 / m_pitchScale; } } @@ -427,12 +425,12 @@ R3LiveShifter::readIn(const float *const *input) double inRatio = 1.0; - if (m_contractThenExpand) { - if (m_pitchScale > 1.0) { + if (m_expandThenContract) { + if (m_pitchScale < 1.0) { inRatio = 1.0 / m_pitchScale; } } else { - if (m_pitchScale < 1.0) { + if (m_pitchScale > 1.0) { inRatio = 1.0 / m_pitchScale; } } @@ -484,7 +482,6 @@ R3LiveShifter::generate(int requiredInOutbuf) int toGenerate = requiredInOutbuf - alreadyGenerated; -// int longest = m_guideConfiguration.longestFftSize; int channels = m_parameters.channels; int ws = getWindowSourceSize(); @@ -627,12 +624,12 @@ R3LiveShifter::readOut(float *const *output, int outcount) { double outRatio = 1.0; - if (m_contractThenExpand) { - if (m_pitchScale < 1.0) { + if (m_expandThenContract) { + if (m_pitchScale > 1.0) { outRatio = 1.0 / m_pitchScale; } } else { - if (m_pitchScale > 1.0) { + if (m_pitchScale < 1.0) { outRatio = 1.0 / m_pitchScale; } } diff --git a/src/finer/R3LiveShifter.h b/src/finer/R3LiveShifter.h index d2fed522..814ea52f 100644 --- a/src/finer/R3LiveShifter.h +++ b/src/finer/R3LiveShifter.h @@ -94,20 +94,14 @@ class R3LiveShifter int minInhop; int maxInhopWithReadahead; int maxInhop; - Limits(RubberBandLiveShifter::Options options, double rate) : + Limits(RubberBandLiveShifter::Options, double rate) : // commented values are results when rate = 44100 or 48000 - minPreferredOuthop(roundUpDiv(rate, 512)), // 128 - maxPreferredOuthop(roundUpDiv(rate, 128)), // 512 - minInhop(1), - maxInhopWithReadahead(roundUpDiv(rate, 64)), // 1024 - maxInhop(roundUpDiv(rate, 32)) // 2048 + minInhop(1) { - if (!(options & RubberBandLiveShifter::OptionWindowLong)) { - minPreferredOuthop = roundUpDiv(rate, 256); // 256 - maxPreferredOuthop = (roundUpDiv(rate, 128) * 5) / 4; // 640 - maxInhopWithReadahead = roundUpDiv(rate, 128); // 512 - maxInhop = (roundUpDiv(rate, 64) * 3) / 2; // 1536 - } + minPreferredOuthop = roundUpDiv(rate, 256); // 256 + maxPreferredOuthop = (roundUpDiv(rate, 128) * 5) / 4; // 640 + maxInhopWithReadahead = roundUpDiv(rate, 128); // 512 + maxInhop = (roundUpDiv(rate, 64) * 3) / 2; // 1536 } }; @@ -321,7 +315,7 @@ class R3LiveShifter bool m_useReadahead; int m_prevInhop; int m_prevOuthop; - bool m_contractThenExpand; // otherwise expand then contract + bool m_expandThenContract; // otherwise contract then expand bool m_firstProcess; uint32_t m_unityCount; @@ -391,8 +385,7 @@ class R3LiveShifter } bool isSingleWindowed() const { - return !(m_parameters.options & - RubberBandLiveShifter::OptionWindowLong); + return true; } int getWindowSourceSize() const { diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 3fbd168f..884d1806 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -117,7 +117,7 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) { RubberBandLiveShifter::Options options = - RubberBandLiveShifter::OptionPitchModeA; + RubberBandLiveShifter::OptionPitchMethodStandard; int n = 100000; check_sinusoid_unchanged(n, 44100, 440.f, options, false); @@ -127,7 +127,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_b) { RubberBandLiveShifter::Options options = - RubberBandLiveShifter::OptionPitchModeB; + RubberBandLiveShifter::OptionPitchMethodAlternate; int n = 100000; check_sinusoid_unchanged(n, 44100, 440.f, options, false); From 87755ec9c4542c1d57b2592e939b7b5449fc488d Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 3 May 2024 17:52:38 +0100 Subject: [PATCH 16/35] Fix inexact resampler output at unity ratio --- src/finer/R3LiveShifter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index d80f6434..42f849ba 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -365,7 +365,7 @@ R3LiveShifter::shift(const float *const *input, float *const *output) } } - int requiredInOutbuf = 1 + int(ceil(incount / outRatio)); + int requiredInOutbuf = int(ceil(incount / outRatio)); generate(requiredInOutbuf); int got = readOut(output, incount); From 5b379121c7086a0ab8e938f3b11f3b2ab8fd0503 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 9 May 2024 17:15:42 +0100 Subject: [PATCH 17/35] Some work on warnings and tests --- src/finer/R3LiveShifter.cpp | 8 +++--- src/test/TestLiveShifter.cpp | 49 ++++++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 42f849ba..b615f98d 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -331,7 +331,7 @@ R3LiveShifter::shift(const float *const *input, float *const *output) int incount = int(getBlockSize()); - m_log.log(2, "R3LiveShifter::shift: start of process with incount", incount); + m_log.log(2, "R3LiveShifter::shift: start of shift with incount", incount); m_log.log(2, "R3LiveShifter::shift: initially in inbuf", m_channelData[0]->inbuf->getReadSpace()); m_log.log(2, "R3LiveShifter::shift: initially in outbuf", m_channelData[0]->outbuf->getReadSpace()); @@ -492,7 +492,7 @@ R3LiveShifter::generate(int requiredInOutbuf) int atInput = cd0->inbuf->getReadSpace(); if (atInput <= ws) { - m_log.log(0, "R3LiveShifter::generate: insufficient samples at input: have and require more than", atInput, ws); + m_log.log(2, "R3LiveShifter::generate: insufficient samples at input: have and require more than", atInput, ws); return; } @@ -658,7 +658,9 @@ R3LiveShifter::readOut(float *const *output, int outcount) for (int c = 0; c < m_parameters.channels; ++c) { auto &cd = m_channelData.at(c); - int gotHere = cd->outbuf->read(cd->resampled.data(), got); + int available = cd->outbuf->getReadSpace(); + int gotHere = cd->outbuf->read + (cd->resampled.data(), std::min(got, available)); if (gotHere < got) { if (c > 0) { m_log.log(0, "R3LiveShifter::readOut: WARNING: channel imbalance detected"); diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 884d1806..9e8d8f7a 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -76,16 +76,43 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, // We now have n samples of a simple sinusoid with stretch factor // 1.0; obviously we expect the output to be essentially the same - // thing. It will have lower precision for a while at the start - // and end because of windowing factors, so we check those with a - // threshold of 0.1; in the middle we expect better - // precision. Note that these are relative tolerances, not - // absolute, i.e. 0.001 means 0.001x the smaller value - so they - // are tighter than they appear. + // thing. It will have lower precision for a while at the start, + // so we check that with a threshold of 0.1; after that we expect + // better precision. - BOOST_TEST(vector(out.begin() + delay, out.begin() + n) == - vector(in.begin(), in.begin() + n - delay), - tt::tolerance(0.001f) << tt::per_element()); + int slackpart = 2048; + float slackeps = 1.0e-1f; + float eps = 1.0e-3f; + +#ifdef USE_BQRESAMPLER + eps = 1.0e-2f; +#endif + + for (int i = 0; i < slackpart; ++i) { + float fin = in[i]; + float fout = out[delay + i]; + float err = fabsf(fin - fout); + if (err > slackeps) { + std::cerr << "Error at index " << i << " exceeds slack eps " + << slackeps << ": output " << fout << " - input " + << fin << " = " << fout - fin << std::endl; + BOOST_TEST(err < eps); + break; + } + } + + for (int i = slackpart; i < n - delay; ++i) { + float fin = in[i]; + float fout = out[delay + i]; + float err = fabsf(fin - fout); + if (err > eps) { + std::cerr << "Error at index " << i << " exceeds tight eps " + << eps << ": output " << fout << " - input " + << fin << " = " << fout - fin << std::endl; + BOOST_TEST(err < eps); + break; + } + } if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(0); @@ -107,7 +134,7 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, std::cout << "SHIFTED," << i << "," << out[i + delay] << std::endl; } - std::cout << "DIFF,V" << std::endl; + std::cout << "DIFF,sample,V" << std::endl; for (int i = 0; i + delay < int(in.size()); ++i) { std::cout << "DIFF," << i << "," << out[i + delay] - in[i] << std::endl; } @@ -130,7 +157,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_b) RubberBandLiveShifter::OptionPitchMethodAlternate; int n = 100000; - check_sinusoid_unchanged(n, 44100, 440.f, options, false); + check_sinusoid_unchanged(n, 44100, 440.f, options, true); check_sinusoid_unchanged(n, 48000, 260.f, options, false); } From c6e8d079d91064b43073abd1f647a7d9c8915dd3 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 10 May 2024 11:51:31 +0100 Subject: [PATCH 18/35] Further tests --- src/finer/R3LiveShifter.cpp | 3 +- src/test/TestLiveShifter.cpp | 149 +++++++++++++++++++++++++++++------ 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index b615f98d..5affff92 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -184,6 +184,7 @@ R3LiveShifter::setPitchScale(double scale) m_log.log(2, "R3LiveShifter::setPitchScale", scale); if (scale == m_pitchScale) return; m_pitchScale = scale; + measureResamplerDelay(); } void @@ -242,7 +243,7 @@ R3LiveShifter::measureResamplerDelay() auto outbuf = inbuf; int outcount = m_inResampler->resampleInterleaved - (outbuf.data(), bs, inbuf.data(), bs, 1.0, false); + (outbuf.data(), bs, inbuf.data(), bs, m_pitchScale, false); m_inResampler->reset(); diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 9e8d8f7a..2cbede5c 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -42,6 +42,41 @@ namespace tt = boost::test_tools; BOOST_AUTO_TEST_SUITE(TestLiveShifter) +static void dump(const vector &in, + const vector &out, + const vector &expected, + int delay) +{ + std::cerr << "dump: delay reported as " << delay << std::endl; + + // The prefix is to allow grep on the test output + + std::cout << "IN,sample,V" << std::endl; + for (int i = 0; i < int(in.size()); ++i) { + std::cout << "IN," << i << "," << in[i] << std::endl; + } + + std::cout << "OUT,sample,V" << std::endl; + for (int i = 0; i < int(out.size()); ++i) { + std::cout << "OUT," << i << "," << out[i] << std::endl; + } + + std::cout << "SHIFTED,sample,V" << std::endl; + for (int i = 0; i + delay < int(out.size()); ++i) { + std::cout << "SHIFTED," << i << "," << out[i + delay] << std::endl; + } + + std::cout << "EXPECTED,sample,V" << std::endl; + for (int i = 0; i < int(expected.size()); ++i) { + std::cout << "EXPECTED," << i << "," << expected[i] << std::endl; + } + + std::cout << "DIFF,sample,V" << std::endl; + for (int i = 0; i + delay < int(expected.size()); ++i) { + std::cout << "DIFF," << i << "," << out[i + delay] - expected[i] << std::endl; + } +} + static void check_sinusoid_unchanged(int n, int rate, float freq, RubberBandLiveShifter::Options options, bool printDebug) @@ -51,8 +86,6 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, } RubberBandLiveShifter shifter(rate, 1, options); - -// shifter.setPitchScale(2.66968); int blocksize = shifter.getBlockSize(); BOOST_TEST(blocksize == 512); @@ -71,8 +104,6 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, } int delay = shifter.getStartDelay(); - - std::cerr << "delay reported as " << delay << std::endl; // We now have n samples of a simple sinusoid with stretch factor // 1.0; obviously we expect the output to be essentially the same @@ -116,28 +147,86 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(0); + dump(in, out, in, delay); + } +} - // The prefix is to allow grep on the test output - - std::cout << "IN,sample,V" << std::endl; - for (int i = 0; i < int(in.size()); ++i) { - std::cout << "IN," << i << "," << in[i] << std::endl; - } - - std::cout << "OUT,sample,V" << std::endl; - for (int i = 0; i < int(out.size()); ++i) { - std::cout << "OUT," << i << "," << out[i] << std::endl; +static void check_sinusoid_shifted(int n, int rate, float freq, float shift, + RubberBandLiveShifter::Options options, + bool printDebug) +{ + if (printDebug) { + RubberBandLiveShifter::setDefaultDebugLevel(2); + } + + RubberBandLiveShifter shifter(rate, 1, options); + + shifter.setPitchScale(shift); + + int blocksize = shifter.getBlockSize(); + BOOST_TEST(blocksize == 512); + + n = (n / blocksize + 1) * blocksize; + + vector in(n), out(n), expected(n); + for (int i = 0; i < n; ++i) { + in[i] = 0.5f * sinf(float(i) * freq * M_PI * 2.f / float(rate)); + expected[i] = 0.5f * sinf(float(i) * freq * shift * M_PI * 2.f / float(rate)); + } + + for (int i = 0; i < n; i += blocksize) { + float *inp = in.data() + i; + float *outp = out.data() + i; + shifter.shift(&inp, &outp); + } + + int delay = shifter.getStartDelay(); + + std::cerr << "delay reported as " << delay << std::endl; + + // We now have n samples of a simple sinusoid with stretch factor + // 1.0; obviously we expect the output to be essentially the same + // thing. It will have lower precision for a while at the start, + // so we check that with a threshold of 0.1; after that we expect + // better precision. + + int slackpart = 2048; + float slackeps = 1.0e-1f; + float eps = 1.0e-3f; + +#ifdef USE_BQRESAMPLER + eps = 1.0e-2f; +#endif + + for (int i = 0; i < slackpart; ++i) { + float fin = expected[i]; + float fout = out[delay + i]; + float err = fabsf(fin - fout); + if (err > slackeps) { + std::cerr << "Error at index " << i << " exceeds slack eps " + << slackeps << ": output " << fout << " - expected " + << fin << " = " << fout - fin << std::endl; + BOOST_TEST(err < eps); + break; } - - std::cout << "SHIFTED,sample,V" << std::endl; - for (int i = 0; i + delay < int(out.size()); ++i) { - std::cout << "SHIFTED," << i << "," << out[i + delay] << std::endl; + } + + for (int i = slackpart; i < n - delay; ++i) { + float fin = expected[i]; + float fout = out[delay + i]; + float err = fabsf(fin - fout); + if (err > eps) { + std::cerr << "Error at index " << i << " exceeds tight eps " + << eps << ": output " << fout << " - expected " + << fin << " = " << fout - fin << std::endl; + BOOST_TEST(err < eps); + break; } + } - std::cout << "DIFF,sample,V" << std::endl; - for (int i = 0; i + delay < int(in.size()); ++i) { - std::cout << "DIFF," << i << "," << out[i + delay] - in[i] << std::endl; - } + if (printDebug) { + RubberBandLiveShifter::setDefaultDebugLevel(0); + dump(in, out, expected, delay); } } @@ -145,7 +234,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) { RubberBandLiveShifter::Options options = RubberBandLiveShifter::OptionPitchMethodStandard; - int n = 100000; + int n = 20000; check_sinusoid_unchanged(n, 44100, 440.f, options, false); check_sinusoid_unchanged(n, 48000, 260.f, options, false); @@ -155,10 +244,20 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_b) { RubberBandLiveShifter::Options options = RubberBandLiveShifter::OptionPitchMethodAlternate; - int n = 100000; + int n = 20000; - check_sinusoid_unchanged(n, 44100, 440.f, options, true); + check_sinusoid_unchanged(n, 44100, 440.f, options, false); check_sinusoid_unchanged(n, 48000, 260.f, options, false); } +BOOST_AUTO_TEST_CASE(sinusoid_down_octave_mode_a) +{ + RubberBandLiveShifter::Options options = + RubberBandLiveShifter::OptionPitchMethodStandard; + int n = 20000; + + check_sinusoid_shifted(n, 44100, 440.f, 0.5f, options, true); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, options, false); +} + BOOST_AUTO_TEST_SUITE_END() From 2119c432878d42d3f0af315d0a7fcd2c5ca5bf5c Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 10 May 2024 13:09:58 +0100 Subject: [PATCH 19/35] Don't do this after we begin; correct ratio --- src/finer/R3LiveShifter.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 5affff92..6cd16925 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -182,9 +182,13 @@ void R3LiveShifter::setPitchScale(double scale) { m_log.log(2, "R3LiveShifter::setPitchScale", scale); + if (scale == m_pitchScale) return; m_pitchScale = scale; - measureResamplerDelay(); + + if (m_firstProcess) { + measureResamplerDelay(); + } } void @@ -241,9 +245,20 @@ R3LiveShifter::measureResamplerDelay() int bs = getBlockSize(); std::vector inbuf(bs * m_parameters.channels, 0.f); auto outbuf = inbuf; + + double inRatio = 1.0; + if (m_expandThenContract) { + if (m_pitchScale < 1.0) { + inRatio = 1.0 / m_pitchScale; + } + } else { + if (m_pitchScale > 1.0) { + inRatio = 1.0 / m_pitchScale; + } + } int outcount = m_inResampler->resampleInterleaved - (outbuf.data(), bs, inbuf.data(), bs, m_pitchScale, false); + (outbuf.data(), bs, inbuf.data(), bs, inRatio, false); m_inResampler->reset(); From 8ea77bb96dacc53b63bd2e405ceff20d80fcc1db Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 10 May 2024 13:22:09 +0100 Subject: [PATCH 20/35] This seems acceptable in practice --- src/finer/R3LiveShifter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 6cd16925..9fbd1baf 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -218,7 +218,7 @@ R3LiveShifter::createResamplers() resamplerParameters.initialSampleRate = m_parameters.sampleRate; resamplerParameters.maxBufferSize = m_guideConfiguration.longestFftSize; resamplerParameters.dynamism = Resampler::RatioOftenChanging; - resamplerParameters.ratioChange = Resampler::SmoothRatioChange; + resamplerParameters.ratioChange = Resampler::SuddenRatioChange; int debug = m_log.getDebugLevel(); if (debug > 0) --debug; From bbbba44774dd6ab21ef2a2622bfea00966c21dfb Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 10 May 2024 13:38:15 +0100 Subject: [PATCH 21/35] Remove the alternate pitch method. It isn't as reliable in avoiding artifacts and it's quite a bit of overhead to test. --- ladspa-lv2/RubberBandLivePitchShifter.cpp | 1 - rubberband/RubberBandLiveShifter.h | 3 - src/finer/R3LiveShifter.cpp | 72 +++++------------------ src/finer/R3LiveShifter.h | 1 - src/test/TestLiveShifter.cpp | 28 ++------- 5 files changed, 20 insertions(+), 85 deletions(-) diff --git a/ladspa-lv2/RubberBandLivePitchShifter.cpp b/ladspa-lv2/RubberBandLivePitchShifter.cpp index 16bcaa5a..74f3f40b 100644 --- a/ladspa-lv2/RubberBandLivePitchShifter.cpp +++ b/ladspa-lv2/RubberBandLivePitchShifter.cpp @@ -266,7 +266,6 @@ RubberBandLivePitchShifter::RubberBandLivePitchShifter(int sampleRate, size_t ch m_currentFormant(false), m_shifter(new RubberBandLiveShifter (sampleRate, channels, - RubberBandLiveShifter::OptionPitchMethodStandard | RubberBandLiveShifter::OptionChannelsTogether)), m_sampleRate(sampleRate), m_channels(channels), diff --git a/rubberband/RubberBandLiveShifter.h b/rubberband/RubberBandLiveShifter.h index a9b37ff8..bbf16e90 100644 --- a/rubberband/RubberBandLiveShifter.h +++ b/rubberband/RubberBandLiveShifter.h @@ -102,9 +102,6 @@ RubberBandLiveShifter OptionFormantShifted = 0x00000000, OptionFormantPreserved = 0x01000000, - OptionPitchMethodStandard = 0x00000000, - OptionPitchMethodAlternate = 0x02000000, - OptionChannelsApart = 0x00000000, OptionChannelsTogether = 0x10000000, diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 9fbd1baf..0dfb6e62 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -45,7 +45,6 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_useReadahead(false), m_prevInhop(m_limits.maxInhopWithReadahead / 2), m_prevOuthop(m_prevInhop), - m_expandThenContract(false), m_firstProcess(true), m_unityCount(0) { @@ -71,11 +70,6 @@ R3LiveShifter::initialise() m_useReadahead = true; } - if ((m_parameters.options & RubberBandLiveShifter::OptionPitchMethodAlternate)) { - m_log.log(1, "R3LiveShifter::R3LiveShifter: expand-then-contract enabled"); - m_expandThenContract = true; - } - double maxClassifierFrequency = 16000.0; if (maxClassifierFrequency > m_parameters.sampleRate/2) { maxClassifierFrequency = m_parameters.sampleRate/2; @@ -247,14 +241,8 @@ R3LiveShifter::measureResamplerDelay() auto outbuf = inbuf; double inRatio = 1.0; - if (m_expandThenContract) { - if (m_pitchScale < 1.0) { - inRatio = 1.0 / m_pitchScale; - } - } else { - if (m_pitchScale > 1.0) { - inRatio = 1.0 / m_pitchScale; - } + if (m_pitchScale > 1.0) { + inRatio = 1.0 / m_pitchScale; } int outcount = m_inResampler->resampleInterleaved @@ -290,18 +278,10 @@ R3LiveShifter::getStartDelay() const { int fixed = getWindowSourceSize() / 2 + m_resamplerDelay * 2; int variable = getWindowSourceSize() / 2; - if (m_expandThenContract) { - if (m_pitchScale < 1.0) { - return size_t(fixed + ceil(variable * m_pitchScale)); - } else { - return size_t(fixed + ceil(variable / m_pitchScale)); - } + if (m_pitchScale < 1.0) { + return size_t(fixed + ceil(variable / m_pitchScale)); } else { - if (m_pitchScale < 1.0) { - return size_t(fixed + ceil(variable / m_pitchScale)); - } else { - return size_t(fixed + ceil(variable * m_pitchScale)); - } + return size_t(fixed + ceil(variable * m_pitchScale)); } } @@ -353,13 +333,9 @@ R3LiveShifter::shift(const float *const *input, float *const *output) int pad = 0; if (m_firstProcess) { - if (m_expandThenContract) { - pad = getWindowSourceSize() / 2; - } else { - pad = getWindowSourceSize(); - if (m_pitchScale > 1.0) { - pad = int(ceil(pad * m_pitchScale)); - } + pad = getWindowSourceSize(); + if (m_pitchScale > 1.0) { + pad = int(ceil(pad * m_pitchScale)); } m_log.log(2, "R3LiveShifter::shift: extending input with pre-pad", incount, pad); for (int c = 0; c < m_parameters.channels; ++c) { @@ -371,14 +347,8 @@ R3LiveShifter::shift(const float *const *input, float *const *output) double outRatio = 1.0; - if (m_expandThenContract) { - if (m_pitchScale > 1.0) { - outRatio = 1.0 / m_pitchScale; - } - } else { - if (m_pitchScale < 1.0) { - outRatio = 1.0 / m_pitchScale; - } + if (m_pitchScale < 1.0) { + outRatio = 1.0 / m_pitchScale; } int requiredInOutbuf = int(ceil(incount / outRatio)); @@ -440,15 +410,8 @@ R3LiveShifter::readIn(const float *const *input) } double inRatio = 1.0; - - if (m_expandThenContract) { - if (m_pitchScale < 1.0) { - inRatio = 1.0 / m_pitchScale; - } - } else { - if (m_pitchScale > 1.0) { - inRatio = 1.0 / m_pitchScale; - } + if (m_pitchScale > 1.0) { + inRatio = 1.0 / m_pitchScale; } m_log.log(2, "R3LiveShifter::readIn: ratio", inRatio); @@ -639,15 +602,8 @@ int R3LiveShifter::readOut(float *const *output, int outcount) { double outRatio = 1.0; - - if (m_expandThenContract) { - if (m_pitchScale > 1.0) { - outRatio = 1.0 / m_pitchScale; - } - } else { - if (m_pitchScale < 1.0) { - outRatio = 1.0 / m_pitchScale; - } + if (m_pitchScale < 1.0) { + outRatio = 1.0 / m_pitchScale; } m_log.log(2, "R3LiveShifter::readOut: outcount and ratio", outcount, outRatio); diff --git a/src/finer/R3LiveShifter.h b/src/finer/R3LiveShifter.h index 814ea52f..044f2450 100644 --- a/src/finer/R3LiveShifter.h +++ b/src/finer/R3LiveShifter.h @@ -315,7 +315,6 @@ class R3LiveShifter bool m_useReadahead; int m_prevInhop; int m_prevOuthop; - bool m_expandThenContract; // otherwise contract then expand bool m_firstProcess; uint32_t m_unityCount; diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 2cbede5c..a7bb1945 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -230,34 +230,18 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, } } -BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_a) +BOOST_AUTO_TEST_CASE(sinusoid_unchanged) { - RubberBandLiveShifter::Options options = - RubberBandLiveShifter::OptionPitchMethodStandard; int n = 20000; - - check_sinusoid_unchanged(n, 44100, 440.f, options, false); - check_sinusoid_unchanged(n, 48000, 260.f, options, false); + check_sinusoid_unchanged(n, 44100, 440.f, 0, false); + check_sinusoid_unchanged(n, 48000, 260.f, 0, false); } -BOOST_AUTO_TEST_CASE(sinusoid_unchanged_mode_b) +BOOST_AUTO_TEST_CASE(sinusoid_down_octave) { - RubberBandLiveShifter::Options options = - RubberBandLiveShifter::OptionPitchMethodAlternate; int n = 20000; - - check_sinusoid_unchanged(n, 44100, 440.f, options, false); - check_sinusoid_unchanged(n, 48000, 260.f, options, false); -} - -BOOST_AUTO_TEST_CASE(sinusoid_down_octave_mode_a) -{ - RubberBandLiveShifter::Options options = - RubberBandLiveShifter::OptionPitchMethodStandard; - int n = 20000; - - check_sinusoid_shifted(n, 44100, 440.f, 0.5f, options, true); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, options, false); + check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0, true); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); } BOOST_AUTO_TEST_SUITE_END() From 900e0a03d59521e54b550354e91f49cdf31c8b9d Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 10 May 2024 16:52:42 +0100 Subject: [PATCH 22/35] Delay calculations --- src/finer/R3LiveShifter.cpp | 60 ++++++++++++++++-------------------- src/finer/R3LiveShifter.h | 18 ++++++++++- src/test/TestLiveShifter.cpp | 25 +++++++++++++-- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 0dfb6e62..b2846b07 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -41,7 +41,7 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_log), m_guideConfiguration(m_guide.getConfiguration()), m_channelAssembly(m_parameters.channels), - m_resamplerDelay(32), + m_initialResamplerDelays(32, 32), m_useReadahead(false), m_prevInhop(m_limits.maxInhopWithReadahead / 2), m_prevOuthop(m_prevInhop), @@ -240,19 +240,18 @@ R3LiveShifter::measureResamplerDelay() std::vector inbuf(bs * m_parameters.channels, 0.f); auto outbuf = inbuf; - double inRatio = 1.0; - if (m_pitchScale > 1.0) { - inRatio = 1.0 / m_pitchScale; - } - - int outcount = m_inResampler->resampleInterleaved - (outbuf.data(), bs, inbuf.data(), bs, inRatio, false); - + int incount = m_inResampler->resampleInterleaved + (outbuf.data(), bs, inbuf.data(), bs, getInRatio(), false); m_inResampler->reset(); - m_resamplerDelay = bs - outcount; + int outcount = m_outResampler->resampleInterleaved + (outbuf.data(), bs, inbuf.data(), bs, getOutRatio(), false); + m_outResampler->reset(); + + m_initialResamplerDelays = { bs - incount, bs - outcount }; - m_log.log(1, "R3LiveShifter::measureResamplerDelay: measured delay and outcount ", m_resamplerDelay, outcount); + m_log.log(1, "R3LiveShifter::measureResamplerDelay: inRatio, outRatio ", getInRatio(), getOutRatio()); + m_log.log(1, "R3LiveShifter::measureResamplerDelay: measured delays ", m_initialResamplerDelays.first, m_initialResamplerDelays.second); } double @@ -276,13 +275,21 @@ R3LiveShifter::getPreferredStartPad() const size_t R3LiveShifter::getStartDelay() const { - int fixed = getWindowSourceSize() / 2 + m_resamplerDelay * 2; - int variable = getWindowSourceSize() / 2; - if (m_pitchScale < 1.0) { - return size_t(fixed + ceil(variable / m_pitchScale)); - } else { - return size_t(fixed + ceil(variable * m_pitchScale)); + int inDelay = getWindowSourceSize() + m_initialResamplerDelays.first; + int outDelay = int(floor(inDelay * getOutRatio())) + m_initialResamplerDelays.second; + + int total = outDelay; + int bs = getBlockSize(); + if (m_pitchScale > 1.0) { + total += bs - 1; + } else if (m_pitchScale < 1.0) { + int scaled = int(ceil(bs / m_pitchScale)); + total -= bs * (scaled - bs) / bs; } + + m_log.log(2, "R3LiveShifter::getStartDelay: inDelay, outDelay", inDelay, outDelay); + m_log.log(1, "R3LiveShifter::getStartDelay", total); + return total; } size_t @@ -345,12 +352,7 @@ R3LiveShifter::shift(const float *const *input, float *const *output) readIn(input); - double outRatio = 1.0; - - if (m_pitchScale < 1.0) { - outRatio = 1.0 / m_pitchScale; - } - + double outRatio = getOutRatio(); int requiredInOutbuf = int(ceil(incount / outRatio)); generate(requiredInOutbuf); @@ -409,11 +411,7 @@ R3LiveShifter::readIn(const float *const *input) } } - double inRatio = 1.0; - if (m_pitchScale > 1.0) { - inRatio = 1.0 / m_pitchScale; - } - + double inRatio = getInRatio(); m_log.log(2, "R3LiveShifter::readIn: ratio", inRatio); int resampleBufSize = int(m_channelData.at(0)->resampled.size()); @@ -601,11 +599,7 @@ R3LiveShifter::generate(int requiredInOutbuf) int R3LiveShifter::readOut(float *const *output, int outcount) { - double outRatio = 1.0; - if (m_pitchScale < 1.0) { - outRatio = 1.0 / m_pitchScale; - } - + double outRatio = getOutRatio(); m_log.log(2, "R3LiveShifter::readOut: outcount and ratio", outcount, outRatio); int resampledCount = 0; diff --git a/src/finer/R3LiveShifter.h b/src/finer/R3LiveShifter.h index 044f2450..247416bb 100644 --- a/src/finer/R3LiveShifter.h +++ b/src/finer/R3LiveShifter.h @@ -311,7 +311,7 @@ class R3LiveShifter ChannelAssembly m_channelAssembly; std::unique_ptr m_inResampler; std::unique_ptr m_outResampler; - int m_resamplerDelay; + std::pair m_initialResamplerDelays; bool m_useReadahead; int m_prevInhop; int m_prevOuthop; @@ -387,6 +387,22 @@ class R3LiveShifter return true; } + double getInRatio() const { + if (m_pitchScale > 1.0) { + return 1.0 / m_pitchScale; + } else { + return 1.0; + } + } + + double getOutRatio() const { + if (m_pitchScale < 1.0) { + return 1.0 / m_pitchScale; + } else { + return 1.0; + } + } + int getWindowSourceSize() const { if (m_useReadahead) { int sz = m_guideConfiguration.classificationFftSize + diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index a7bb1945..d0dbfdac 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -182,8 +182,6 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, int delay = shifter.getStartDelay(); - std::cerr << "delay reported as " << delay << std::endl; - // We now have n samples of a simple sinusoid with stretch factor // 1.0; obviously we expect the output to be essentially the same // thing. It will have lower precision for a while at the start, @@ -240,7 +238,28 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) BOOST_AUTO_TEST_CASE(sinusoid_down_octave) { int n = 20000; - check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0, true); + check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0, false); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_down_2octave) +{ + int n = 20000; + check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0, false); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_up_octave) +{ + int n = 20000; + check_sinusoid_shifted(n, 44100, 440.f, 2.0f, 0, false); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_up_2octave) +{ + int n = 20000; + check_sinusoid_shifted(n, 44100, 440.f, 4.0f, 0, false); // check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); } From d0556ef4787d011dd1bb0c2b44bccd6b093f223a Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 17 May 2024 18:13:50 +0100 Subject: [PATCH 23/35] Dump enhancements --- src/test/TestLiveShifter.cpp | 131 +++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index d0dbfdac..767f04f7 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -29,6 +29,8 @@ #include "../../rubberband/RubberBandLiveShifter.h" #include +#include +#include #include @@ -37,50 +39,61 @@ using namespace RubberBand; using std::vector; using std::cerr; using std::endl; +using std::string; +using std::ofstream; namespace tt = boost::test_tools; BOOST_AUTO_TEST_SUITE(TestLiveShifter) -static void dump(const vector &in, +static void dumpTo(string basename, + const vector &data) +{ + string dir = "/tmp"; + string filename = dir + "/" + basename + ".csv"; + ofstream file(filename, std::ios::out | std::ios::binary); + if (!file) { + cerr << "dumpTo: failed to open file \"" << filename << "\" for writing" << endl; + return; + } + file << "sample,V" << endl; + for (int i = 0; i < int(data.size()); ++i) { + file << i << "," << data[i] << endl; + } +} + +static void dump(string prefix, + const vector &in, const vector &out, const vector &expected, int delay) { - std::cerr << "dump: delay reported as " << delay << std::endl; - - // The prefix is to allow grep on the test output - - std::cout << "IN,sample,V" << std::endl; - for (int i = 0; i < int(in.size()); ++i) { - std::cout << "IN," << i << "," << in[i] << std::endl; - } - - std::cout << "OUT,sample,V" << std::endl; - for (int i = 0; i < int(out.size()); ++i) { - std::cout << "OUT," << i << "," << out[i] << std::endl; - } + cerr << "dump: delay reported as " << delay << endl; - std::cout << "SHIFTED,sample,V" << std::endl; - for (int i = 0; i + delay < int(out.size()); ++i) { - std::cout << "SHIFTED," << i << "," << out[i + delay] << std::endl; + if (prefix != "") { + prefix += "-"; } - std::cout << "EXPECTED,sample,V" << std::endl; - for (int i = 0; i < int(expected.size()); ++i) { - std::cout << "EXPECTED," << i << "," << expected[i] << std::endl; - } - - std::cout << "DIFF,sample,V" << std::endl; - for (int i = 0; i + delay < int(expected.size()); ++i) { - std::cout << "DIFF," << i << "," << out[i + delay] - expected[i] << std::endl; + dumpTo(prefix + "in", in); + dumpTo(prefix + "out", out); + dumpTo(prefix + "expected", expected); + + vector shifted; + vector diff; + for (int i = 0; i + delay < int(out.size()); ++i) { + shifted.push_back(out[i + delay]); + diff.push_back(out[i + delay] - expected[i]); } + dumpTo(prefix + "shifted", shifted); + dumpTo(prefix + "diff", diff); } static void check_sinusoid_unchanged(int n, int rate, float freq, RubberBandLiveShifter::Options options, - bool printDebug) + string debugPrefix = {}) { + bool printDebug = (debugPrefix != ""); + if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(2); } @@ -124,9 +137,9 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, float fout = out[delay + i]; float err = fabsf(fin - fout); if (err > slackeps) { - std::cerr << "Error at index " << i << " exceeds slack eps " - << slackeps << ": output " << fout << " - input " - << fin << " = " << fout - fin << std::endl; + cerr << "Error at index " << i << " exceeds slack eps " + << slackeps << ": output " << fout << " - input " + << fin << " = " << fout - fin << endl; BOOST_TEST(err < eps); break; } @@ -137,9 +150,9 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, float fout = out[delay + i]; float err = fabsf(fin - fout); if (err > eps) { - std::cerr << "Error at index " << i << " exceeds tight eps " - << eps << ": output " << fout << " - input " - << fin << " = " << fout - fin << std::endl; + cerr << "Error at index " << i << " exceeds tight eps " + << eps << ": output " << fout << " - input " + << fin << " = " << fout - fin << endl; BOOST_TEST(err < eps); break; } @@ -147,14 +160,16 @@ static void check_sinusoid_unchanged(int n, int rate, float freq, if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(0); - dump(in, out, in, delay); + dump(debugPrefix, in, out, in, delay); } } static void check_sinusoid_shifted(int n, int rate, float freq, float shift, RubberBandLiveShifter::Options options, - bool printDebug) + string debugPrefix = {}) { + bool printDebug = (debugPrefix != ""); + if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(2); } @@ -201,9 +216,9 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, float fout = out[delay + i]; float err = fabsf(fin - fout); if (err > slackeps) { - std::cerr << "Error at index " << i << " exceeds slack eps " - << slackeps << ": output " << fout << " - expected " - << fin << " = " << fout - fin << std::endl; + cerr << "Error at index " << i << " exceeds slack eps " + << slackeps << ": output " << fout << " - expected " + << fin << " = " << fout - fin << endl; BOOST_TEST(err < eps); break; } @@ -214,9 +229,9 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, float fout = out[delay + i]; float err = fabsf(fin - fout); if (err > eps) { - std::cerr << "Error at index " << i << " exceeds tight eps " - << eps << ": output " << fout << " - expected " - << fin << " = " << fout - fin << std::endl; + cerr << "Error at index " << i << " exceeds tight eps " + << eps << ": output " << fout << " - expected " + << fin << " = " << fout - fin << endl; BOOST_TEST(err < eps); break; } @@ -224,43 +239,53 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(0); - dump(in, out, expected, delay); + dump(debugPrefix, in, out, expected, delay); } } BOOST_AUTO_TEST_CASE(sinusoid_unchanged) { int n = 20000; - check_sinusoid_unchanged(n, 44100, 440.f, 0, false); - check_sinusoid_unchanged(n, 48000, 260.f, 0, false); + check_sinusoid_unchanged(n, 44100, 440.f, 0); + check_sinusoid_unchanged(n, 48000, 260.f, 0); } -BOOST_AUTO_TEST_CASE(sinusoid_down_octave) +BOOST_AUTO_TEST_CASE(sinusoid_down_octave_440) { int n = 20000; - check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0, false); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); + check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0); +} + +BOOST_AUTO_TEST_CASE(sinusoid_down_octave_260) +{ + int n = 20000; + check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_down_2octave) { int n = 20000; - check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0, false); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); +// check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0); +} + +BOOST_AUTO_TEST_CASE(sinusoid_up_octave_440) +{ + int n = 20000; + check_sinusoid_shifted(n, 44100, 440.f, 2.0f, 0); } -BOOST_AUTO_TEST_CASE(sinusoid_up_octave) +BOOST_AUTO_TEST_CASE(sinusoid_up_octave_260) { int n = 20000; - check_sinusoid_shifted(n, 44100, 440.f, 2.0f, 0, false); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); + check_sinusoid_shifted(n, 44100, 260.f, 2.0f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_up_2octave) { int n = 20000; - check_sinusoid_shifted(n, 44100, 440.f, 4.0f, 0, false); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0, false); +// check_sinusoid_shifted(n, 44100, 440.f, 4.0f, 0, true); +// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0); } BOOST_AUTO_TEST_SUITE_END() From 60d071171001b61e0e6d756f4d63cd9031596492 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 31 May 2024 12:07:20 +0100 Subject: [PATCH 24/35] First cut at CONTRIBUTING --- CONTRIBUTING.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..87fddf5c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ + +## Bug reports + +We welcome and encourage bug reports for the Rubber Band +Library. We're happy to receive them by almost any channel. The most +usual are [as Github issues](https://github.com/breakfastquay/rubberband/issues) +or [by contacting us directly](https://breakfastquay.com/contact.html). + +Please try to provide enough information for us to reproduce the +problem. At the minimum this includes details of platform, compiler, +and library configuration, as well as a description of the expected +and problematic behaviour. We appreciate that describing issues +relating to audio output can be tricky, and we may ask you to send or +provide further information such as audio examples. + +## Pull requests + +Github pull requests may be appropriate if you have small fixes, such +as fixes to the build system or documentation, that would be +convenient to upstream. We will gratefully read and consider PRs and +consider applying changes of this sort. + +However, please be aware that we cannot directly merge pull +requests. This is partly for technical reasons (the Github repository +for the library is a read-only mirror of an upstream repo) and partly +for reasons of copyright and relicensing. + +If you have any more substantial changes you would like to see +included, please contact us directly to discuss. + +## Other ways to contribute + +An excellent way to contribute to the continued development of Rubber +Band Library is to [buy a commercial licence](https://breakfastquay.com/technology/license.html). +Or, for enhancements and other new development work, it may be possible to +engage [Particular Programs Ltd](https://particularprograms.co.uk/) at +a commercial rate. Please contact us for details. + From da9c586745efe096f8913eaffcebbfd24b78f249 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 13 Jun 2024 09:07:47 +0100 Subject: [PATCH 25/35] More on CONTRIBUTING --- CONTRIBUTING.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87fddf5c..7fbceb50 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,9 @@ +Rubber Band is an open source library with a commercial licence +option. It is developed and maintained with care, prioritising +reliability, output quality, compatibility across releases and +platforms, and performance, in roughly that order. + ## Bug reports We welcome and encourage bug reports for the Rubber Band @@ -15,15 +20,15 @@ provide further information such as audio examples. ## Pull requests -Github pull requests may be appropriate if you have small fixes, such -as fixes to the build system or documentation, that would be -convenient to upstream. We will gratefully read and consider PRs and -consider applying changes of this sort. +Please be aware that we **cannot directly merge** pull requests. This +is partly for technical reasons (the Github repository for the library +is a read-only mirror of an upstream repo) and partly for control of +copyright and provenance. -However, please be aware that we cannot directly merge pull -requests. This is partly for technical reasons (the Github repository -for the library is a read-only mirror of an upstream repo) and partly -for reasons of copyright and relicensing. +Github pull requests may still be appropriate if you have small fixes, +particularly fixes to the build system or documentation, that would be +convenient to upstream. We will gratefully read and consider PRs, +especially of this sort. If you have any more substantial changes you would like to see included, please contact us directly to discuss. @@ -32,7 +37,8 @@ included, please contact us directly to discuss. An excellent way to contribute to the continued development of Rubber Band Library is to [buy a commercial licence](https://breakfastquay.com/technology/license.html). -Or, for enhancements and other new development work, it may be possible to + +For enhancements and other new development work, it may be possible to engage [Particular Programs Ltd](https://particularprograms.co.uk/) at a commercial rate. Please contact us for details. From b4b11d451f02389946d1a299d2953cb22f8a72d6 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 13 Jun 2024 13:16:34 +0100 Subject: [PATCH 26/35] Minor tidy --- ladspa-lv2/RubberBandLivePitchShifter.cpp | 36 +++++++---------------- ladspa-lv2/RubberBandPitchShifter.cpp | 13 ++++---- ladspa-lv2/RubberBandR3PitchShifter.cpp | 15 +++++----- 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/ladspa-lv2/RubberBandLivePitchShifter.cpp b/ladspa-lv2/RubberBandLivePitchShifter.cpp index 74f3f40b..9ff0432f 100644 --- a/ladspa-lv2/RubberBandLivePitchShifter.cpp +++ b/ladspa-lv2/RubberBandLivePitchShifter.cpp @@ -30,10 +30,9 @@ using namespace RubberBand; -using std::cout; using std::cerr; using std::endl; -using std::min; +using std::string; #ifdef RB_PLUGIN_LADSPA @@ -342,18 +341,18 @@ RubberBandLivePitchShifter::instantiate(const LV2_Descriptor *desc, double rate, const char *, const LV2_Feature *const *) { if (rate < 1.0) { - std::cerr << "RubberBandLivePitchShifter::instantiate: invalid sample rate " - << rate << " provided" << std::endl; + cerr << "RubberBandLivePitchShifter::instantiate: invalid sample rate " + << rate << " provided" << endl; return nullptr; } size_t srate = size_t(round(rate)); - if (std::string(desc->URI) == lv2DescriptorMono.URI) { + if (string(desc->URI) == lv2DescriptorMono.URI) { return new RubberBandLivePitchShifter(srate, 1); - } else if (std::string(desc->URI) == lv2DescriptorStereo.URI) { + } else if (string(desc->URI) == lv2DescriptorStereo.URI) { return new RubberBandLivePitchShifter(srate, 2); } else { - std::cerr << "RubberBandLivePitchShifter::instantiate: unrecognised URI " - << desc->URI << " requested" << std::endl; + cerr << "RubberBandLivePitchShifter::instantiate: unrecognised URI " + << desc->URI << " requested" << endl; return nullptr; } } @@ -367,11 +366,11 @@ RubberBandLivePitchShifter::connectPort(LADSPA_Handle handle, #else void RubberBandLivePitchShifter::connectPort(LV2_Handle handle, - uint32_t port, void *location) + uint32_t port, void *location) #endif { RubberBandLivePitchShifter *shifter = (RubberBandLivePitchShifter *)handle; - + float **ports[PortCountStereo] = { &shifter->m_latency, &shifter->m_cents, @@ -468,20 +467,6 @@ RubberBandLivePitchShifter::updateRatio() // integral: we want to enforce that, just to avoid // inconsistencies between hosts if some respect the hints more // than others - -#ifdef RB_PLUGIN_LADSPA - - // But we don't want to change the long-standing behaviour of the - // LADSPA plugin, so let's leave this as-is and only do "the right - // thing" for LV2 - double oct = (m_octaves ? *m_octaves : 0.0); - oct += (m_semitones ? *m_semitones : 0.0) / 12; - oct += (m_cents ? *m_cents : 0.0) / 1200; - m_ratio = pow(2.0, oct); - -#else - - // LV2 double octaves = round(m_octaves ? *m_octaves : 0.0); if (octaves < -2.0) octaves = -2.0; @@ -499,7 +484,6 @@ RubberBandLivePitchShifter::updateRatio() octaves + semitones / 12.0 + cents / 1200.0); -#endif } void @@ -533,7 +517,7 @@ RubberBandLivePitchShifter::runImpl(uint32_t insamples) m_shifter->setPitchScale(m_ratio); m_prevRatio = m_ratio; } - + updateFormant(); if (m_latency) { diff --git a/ladspa-lv2/RubberBandPitchShifter.cpp b/ladspa-lv2/RubberBandPitchShifter.cpp index c98487f0..5f311b07 100644 --- a/ladspa-lv2/RubberBandPitchShifter.cpp +++ b/ladspa-lv2/RubberBandPitchShifter.cpp @@ -34,6 +34,7 @@ using std::cout; using std::cerr; using std::endl; using std::min; +using std::string; #ifdef RB_PLUGIN_LADSPA @@ -356,18 +357,18 @@ RubberBandPitchShifter::instantiate(const LV2_Descriptor *desc, double rate, const char *, const LV2_Feature *const *) { if (rate < 1.0) { - std::cerr << "RubberBandPitchShifter::instantiate: invalid sample rate " - << rate << " provided" << std::endl; + cerr << "RubberBandPitchShifter::instantiate: invalid sample rate " + << rate << " provided" << endl; return nullptr; } size_t srate = size_t(round(rate)); - if (std::string(desc->URI) == lv2DescriptorMono.URI) { + if (string(desc->URI) == lv2DescriptorMono.URI) { return new RubberBandPitchShifter(srate, 1); - } else if (std::string(desc->URI) == lv2DescriptorStereo.URI) { + } else if (string(desc->URI) == lv2DescriptorStereo.URI) { return new RubberBandPitchShifter(srate, 2); } else { - std::cerr << "RubberBandPitchShifter::instantiate: unrecognised URI " - << desc->URI << " requested" << std::endl; + cerr << "RubberBandPitchShifter::instantiate: unrecognised URI " + << desc->URI << " requested" << endl; return nullptr; } } diff --git a/ladspa-lv2/RubberBandR3PitchShifter.cpp b/ladspa-lv2/RubberBandR3PitchShifter.cpp index 746900ae..bb980260 100644 --- a/ladspa-lv2/RubberBandR3PitchShifter.cpp +++ b/ladspa-lv2/RubberBandR3PitchShifter.cpp @@ -30,10 +30,9 @@ using namespace RubberBand; -using std::cout; using std::cerr; using std::endl; -using std::min; +using std::string; #ifdef RB_PLUGIN_LADSPA @@ -341,18 +340,18 @@ RubberBandR3PitchShifter::instantiate(const LV2_Descriptor *desc, double rate, const char *, const LV2_Feature *const *) { if (rate < 1.0) { - std::cerr << "RubberBandR3PitchShifter::instantiate: invalid sample rate " - << rate << " provided" << std::endl; + cerr << "RubberBandR3PitchShifter::instantiate: invalid sample rate " + << rate << " provided" << endl; return nullptr; } size_t srate = size_t(round(rate)); - if (std::string(desc->URI) == lv2DescriptorMono.URI) { + if (string(desc->URI) == lv2DescriptorMono.URI) { return new RubberBandR3PitchShifter(srate, 1); - } else if (std::string(desc->URI) == lv2DescriptorStereo.URI) { + } else if (string(desc->URI) == lv2DescriptorStereo.URI) { return new RubberBandR3PitchShifter(srate, 2); } else { - std::cerr << "RubberBandR3PitchShifter::instantiate: unrecognised URI " - << desc->URI << " requested" << std::endl; + cerr << "RubberBandR3PitchShifter::instantiate: unrecognised URI " + << desc->URI << " requested" << endl; return nullptr; } } From 26618585c631acbf69bf61e5dd1a72cb48f958f1 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 28 Jun 2024 18:08:57 +0100 Subject: [PATCH 27/35] Adjust start delays, put an end point on sinusoid --- src/finer/R3LiveShifter.cpp | 5 ++- src/test/TestLiveShifter.cpp | 69 ++++++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index b2846b07..7ab77983 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -281,10 +281,9 @@ R3LiveShifter::getStartDelay() const int total = outDelay; int bs = getBlockSize(); if (m_pitchScale > 1.0) { - total += bs - 1; + total += bs * (m_pitchScale - 1.0); } else if (m_pitchScale < 1.0) { - int scaled = int(ceil(bs / m_pitchScale)); - total -= bs * (scaled - bs) / bs; + total -= bs * (1.0 / m_pitchScale - 1.0); } m_log.log(2, "R3LiveShifter::getStartDelay: inDelay, outDelay", inDelay, outDelay); diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 767f04f7..26e675ca 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -184,11 +184,17 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, n = (n / blocksize + 1) * blocksize; vector in(n), out(n), expected(n); + int endpoint = n; + if (endpoint > 20000) endpoint -= 10000; for (int i = 0; i < n; ++i) { - in[i] = 0.5f * sinf(float(i) * freq * M_PI * 2.f / float(rate)); + float value = 0.5f * sinf(float(i) * freq * M_PI * 2.f / float(rate)); + if (i > endpoint && value > 0.f && in[i-1] <= 0.f) break; + in[i] = value; expected[i] = 0.5f * sinf(float(i) * freq * shift * M_PI * 2.f / float(rate)); } + in[1000] = 1.f; + for (int i = 0; i < n; i += blocksize) { float *inp = in.data() + i; float *outp = out.data() + i; @@ -246,46 +252,81 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, BOOST_AUTO_TEST_CASE(sinusoid_unchanged) { int n = 20000; - check_sinusoid_unchanged(n, 44100, 440.f, 0); + + // delay = 2112, correct + + check_sinusoid_unchanged(n, 44100, 440.f, 0, "unchanged-440"); check_sinusoid_unchanged(n, 48000, 260.f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_down_octave_440) { - int n = 20000; - check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0); + // Checked: delay = 3648, correct + + // or about 3160? + + int n = 30000; + check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0, "down-octave-440"); } BOOST_AUTO_TEST_CASE(sinusoid_down_octave_260) { - int n = 20000; + // Checked: delay = 3648, correct + + int n = 30000; check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_down_2octave) { - int n = 20000; -// check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0); + // Checked: delay = 6784, sound + + // I like about 5250 + + int n = 30000; + check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0, "down-2octave-440"); +// check_sinusoid_shifted(n, 48000, 260.f, 0.25f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_up_octave_440) { - int n = 20000; + // Checked: delay = 2879, correct + + int n = 30000; check_sinusoid_shifted(n, 44100, 440.f, 2.0f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_up_octave_260) { - int n = 20000; - check_sinusoid_shifted(n, 44100, 260.f, 2.0f, 0); + // Checked: delay = 2879, correct + + //!!! or 3380? + + int n = 30000; + check_sinusoid_shifted(n, 44100, 260.f, 2.0f, 0, "up-octave-260"); } BOOST_AUTO_TEST_CASE(sinusoid_up_2octave) { - int n = 20000; -// check_sinusoid_shifted(n, 44100, 440.f, 4.0f, 0, true); -// check_sinusoid_shifted(n, 48000, 260.f, 0.5f, 0); + // Checked: delay = 3006 -> highly implausible, must be higher + // 3670 ish? + + int n = 30000; + check_sinusoid_shifted(n, 44100, 440.f, 4.0f, 0, "up-2octave-440"); + check_sinusoid_shifted(n, 48000, 260.f, 4.0f, 0); +} + +BOOST_AUTO_TEST_CASE(sinusoid_down_0_99) +{ + + int n = 30000; + check_sinusoid_shifted(n, 44100, 440.f, 0.99f, 0, "down-0_99-440"); +} + +BOOST_AUTO_TEST_CASE(sinusoid_up_1_01) +{ + int n = 30000; + check_sinusoid_shifted(n, 44100, 440.f, 1.01f, 0, "up-1_01-440"); } BOOST_AUTO_TEST_SUITE_END() From 22c606db6fad94c2124a1c646ba210b01a00e739 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Wed, 3 Jul 2024 17:38:01 +0100 Subject: [PATCH 28/35] Adjust test check --- src/test/TestLiveShifter.cpp | 43 +++++++++++++++--------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 26e675ca..181482d9 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -193,49 +193,42 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, expected[i] = 0.5f * sinf(float(i) * freq * shift * M_PI * 2.f / float(rate)); } - in[1000] = 1.f; - for (int i = 0; i < n; i += blocksize) { float *inp = in.data() + i; float *outp = out.data() + i; shifter.shift(&inp, &outp); } - int delay = shifter.getStartDelay(); - - // We now have n samples of a simple sinusoid with stretch factor - // 1.0; obviously we expect the output to be essentially the same - // thing. It will have lower precision for a while at the start, - // so we check that with a threshold of 0.1; after that we expect - // better precision. + int reportedDelay = shifter.getStartDelay(); int slackpart = 2048; - float slackeps = 1.0e-1f; + int delay = reportedDelay + slackpart; + + // Align to the next zero-crossing in output, as phase may differ + + for (int i = delay; i < endpoint; ++i) { + if (out[i] < 0.f && out[i+1] >= 0.f) { + delay = i+1; + break; + } + } + + cerr << "Adjusted delay from reported value of " << reportedDelay + << " by adding slack of " << slackpart + << " and moving to next positive zero crossing at " << delay << endl; + float eps = 1.0e-3f; #ifdef USE_BQRESAMPLER eps = 1.0e-2f; #endif - for (int i = 0; i < slackpart; ++i) { - float fin = expected[i]; - float fout = out[delay + i]; - float err = fabsf(fin - fout); - if (err > slackeps) { - cerr << "Error at index " << i << " exceeds slack eps " - << slackeps << ": output " << fout << " - expected " - << fin << " = " << fout - fin << endl; - BOOST_TEST(err < eps); - break; - } - } - - for (int i = slackpart; i < n - delay; ++i) { + for (int i = 0; i + delay < endpoint; ++i) { float fin = expected[i]; float fout = out[delay + i]; float err = fabsf(fin - fout); if (err > eps) { - cerr << "Error at index " << i << " exceeds tight eps " + cerr << "Error at index " << i << " exceeds eps " << eps << ": output " << fout << " - expected " << fin << " = " << fout - fin << endl; BOOST_TEST(err < eps); From 666b5e43b56aa3f8096dca12cb6a1d8e3cf8c973 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 5 Jul 2024 15:39:01 +0100 Subject: [PATCH 29/35] More reporting --- src/test/TestLiveShifter.cpp | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 181482d9..092bd19b 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -201,6 +201,37 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, int reportedDelay = shifter.getStartDelay(); + int lastCrossing = -1; + int nCrossings = 0; + int accWavelength = 0; + int minWavelength = 0; + int maxWavelength = 0; + for (int i = reportedDelay; i < endpoint; ++i) { + if (out[i-1] < 0.f && out[i] >= 0.f) { + if (lastCrossing >= 0) { + int wavelength = i - lastCrossing; + accWavelength += wavelength; + if (minWavelength == 0 || wavelength < minWavelength) { + minWavelength = wavelength; + } + if (maxWavelength == 0 || wavelength > maxWavelength) { + maxWavelength = wavelength; + } + cerr << wavelength << " "; + nCrossings ++; + } + lastCrossing = i; + } + } + cerr << endl; + + int avgWavelength = 1; + if (nCrossings > 0) { + avgWavelength = accWavelength / nCrossings; + } + double detectedFreq = double(rate) / double(avgWavelength); + cerr << "nCrossings = " << nCrossings << ", minWavelength = " << minWavelength << ", maxWavelength = " << maxWavelength << ", avgWavelength = " << avgWavelength << ", detectedFreq = " << detectedFreq << " (expected " << freq * shift << ")" << endl; + int slackpart = 2048; int delay = reportedDelay + slackpart; @@ -208,6 +239,7 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, for (int i = delay; i < endpoint; ++i) { if (out[i] < 0.f && out[i+1] >= 0.f) { + cerr << "zc: at " << i << " we have " << out[i] << ", " << out[i+1] << endl; delay = i+1; break; } @@ -216,9 +248,9 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, cerr << "Adjusted delay from reported value of " << reportedDelay << " by adding slack of " << slackpart << " and moving to next positive zero crossing at " << delay << endl; - - float eps = 1.0e-3f; + float eps = 1.0e-3f; + #ifdef USE_BQRESAMPLER eps = 1.0e-2f; #endif From e82accd199d054cc1a61fa797d12f5138620afdd Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 18 Jul 2024 16:35:26 +0100 Subject: [PATCH 30/35] Correct FFTW description --- COMPILING.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 11e295bd..0e25ee0b 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -299,7 +299,7 @@ vDSP -Dfft=vdsp -DHAVE_VDSP Default on macOS/iOS (in FFTW3 -Dfft=fftw -DHAVE_FFTW3 A bit faster than built-in, a bit slower than vDSP. - GPL licence. + GPL with commercial option. SLEEF -Dfft=sleef -DHAVE_SLEEF Usually very fast. Not as widely distributed as FFTW3. Requires diff --git a/README.md b/README.md index 0b344f85..9307e230 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ licences for some relevant library code are as follows, to the best of our knowledge. See also the file [COMPILING.md](COMPILING.md) for more details. - * FFTW3 - GPL; proprietary licence needed for redistribution - * Intel IPP - Proprietary; licence needed for redistribution + * FFTW3 - GPL with commercial proprietary option + * Intel IPP - Proprietary of some nature * SLEEF - BSD-like * KissFFT - BSD-like * libsamplerate - BSD-like from version 0.1.9 onwards From 35f0872867ce0a50bd0fcb2b302baaf3ea522088 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 1 Aug 2024 13:56:09 +0100 Subject: [PATCH 31/35] Attempt to fix CI problem --- cross/ios-simulator.txt | 3 +++ cross/ios.txt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cross/ios-simulator.txt b/cross/ios-simulator.txt index b03149c2..bfbc88c6 100644 --- a/cross/ios-simulator.txt +++ b/cross/ios-simulator.txt @@ -8,6 +8,9 @@ cpu = 'x86_64' system = 'darwin' endian = 'little' +[properties] +needs_exe_wrapper = true + [binaries] c = 'cc' cpp = 'c++' diff --git a/cross/ios.txt b/cross/ios.txt index c9a913a4..6e4a9d5c 100644 --- a/cross/ios.txt +++ b/cross/ios.txt @@ -8,6 +8,9 @@ cpu = 'aarch64' system = 'darwin' endian = 'little' +[properties] +needs_exe_wrapper = true + [binaries] c = 'cc' cpp = 'c++' From a0099e63bfd246a7dd287ca800d18cca39cd16c3 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 2 Aug 2024 15:22:54 +0100 Subject: [PATCH 32/35] Further messing in the tests --- src/test/TestLiveShifter.cpp | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 092bd19b..2d140551 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -186,12 +186,23 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, vector in(n), out(n), expected(n); int endpoint = n; if (endpoint > 20000) endpoint -= 10000; + double sumSquares = 0; + double peakIn = 0; for (int i = 0; i < n; ++i) { float value = 0.5f * sinf(float(i) * freq * M_PI * 2.f / float(rate)); + if (i < endpoint) { + sumSquares += value * value; + } if (i > endpoint && value > 0.f && in[i-1] <= 0.f) break; + if (fabs(value) > peakIn) { + peakIn = fabs(value); + } in[i] = value; expected[i] = 0.5f * sinf(float(i) * freq * shift * M_PI * 2.f / float(rate)); } + double rmsIn = sqrt(sumSquares / endpoint); + cerr << "rmsIn = " << rmsIn << endl; + cerr << "peakIn = " << peakIn << endl; for (int i = 0; i < n; i += blocksize) { float *inp = in.data() + i; @@ -201,15 +212,20 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, int reportedDelay = shifter.getStartDelay(); - int lastCrossing = -1; + double lastCrossing = -1; int nCrossings = 0; - int accWavelength = 0; - int minWavelength = 0; - int maxWavelength = 0; + double accWavelength = 0; + double minWavelength = 0; + double maxWavelength = 0; + sumSquares = 0; + double peakOut = 0; for (int i = reportedDelay; i < endpoint; ++i) { + sumSquares += out[i] * out[i]; + if (fabs(out[i]) > peakOut) peakOut = fabs(out[i]); if (out[i-1] < 0.f && out[i] >= 0.f) { + double crossing = (i-1) + (out[i-1] / (out[i-1] - out[i])); if (lastCrossing >= 0) { - int wavelength = i - lastCrossing; + double wavelength = crossing - lastCrossing; accWavelength += wavelength; if (minWavelength == 0 || wavelength < minWavelength) { minWavelength = wavelength; @@ -217,21 +233,25 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, if (maxWavelength == 0 || wavelength > maxWavelength) { maxWavelength = wavelength; } - cerr << wavelength << " "; + cerr << "wavelength = " << wavelength << " (freq " << rate / wavelength << ")" << endl; nCrossings ++; } - lastCrossing = i; + lastCrossing = crossing; } } cerr << endl; - int avgWavelength = 1; + double avgWavelength = 1; if (nCrossings > 0) { avgWavelength = accWavelength / nCrossings; } double detectedFreq = double(rate) / double(avgWavelength); cerr << "nCrossings = " << nCrossings << ", minWavelength = " << minWavelength << ", maxWavelength = " << maxWavelength << ", avgWavelength = " << avgWavelength << ", detectedFreq = " << detectedFreq << " (expected " << freq * shift << ")" << endl; + double rms = sqrt(sumSquares / (endpoint - reportedDelay)); + cerr << "rms = " << rms << endl; + cerr << "peak = " << peakOut << endl; + int slackpart = 2048; int delay = reportedDelay + slackpart; From 14344944b7f129e923e62b5a483ec5a267445a5a Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Fri, 2 Aug 2024 16:20:21 +0100 Subject: [PATCH 33/35] Further investigation --- src/test/TestLiveShifter.cpp | 58 +++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 2d140551..1f5e113c 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -211,36 +211,40 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, } int reportedDelay = shifter.getStartDelay(); + int slackpart = 2048; - double lastCrossing = -1; - int nCrossings = 0; - double accWavelength = 0; - double minWavelength = 0; - double maxWavelength = 0; - sumSquares = 0; - double peakOut = 0; - for (int i = reportedDelay; i < endpoint; ++i) { - sumSquares += out[i] * out[i]; - if (fabs(out[i]) > peakOut) peakOut = fabs(out[i]); - if (out[i-1] < 0.f && out[i] >= 0.f) { - double crossing = (i-1) + (out[i-1] / (out[i-1] - out[i])); - if (lastCrossing >= 0) { - double wavelength = crossing - lastCrossing; - accWavelength += wavelength; - if (minWavelength == 0 || wavelength < minWavelength) { - minWavelength = wavelength; - } - if (maxWavelength == 0 || wavelength > maxWavelength) { - maxWavelength = wavelength; + for (int section = 0; section < 2; ++section) { + + double lastCrossing = -1; + double freqeps = (section == 1 ? 0.15 : 5.0) * fabs(shift); + + sumSquares = 0; + + int i0 = reportedDelay; + int i1 = reportedDelay + slackpart; + if (section == 1) { + i0 = i1; + i1 = endpoint; + } + + for (int i = i0; i < i1; ++i) { + sumSquares += out[i] * out[i]; + if (out[i-1] < 0.f && out[i] >= 0.f) { + double crossing = (i-1) + (out[i-1] / (out[i-1] - out[i])); + if (lastCrossing >= 0) { + double f = rate / (crossing - lastCrossing); + double diff = freq * shift - f; + if (fabs(diff) > freqeps) { + cerr << "i = " << i << " (from " << i0 << " to " << i1 << "), f = " << f << ", freq = " << freq * shift << ", diff = " << diff << ", freqeps = " << freqeps << ", shift = " << shift << ", factor = " << fabs(diff)/freqeps << endl; + } } - cerr << "wavelength = " << wavelength << " (freq " << rate / wavelength << ")" << endl; - nCrossings ++; + lastCrossing = crossing; } - lastCrossing = crossing; } - } - cerr << endl; + // check rms + } +/* double avgWavelength = 1; if (nCrossings > 0) { avgWavelength = accWavelength / nCrossings; @@ -287,10 +291,10 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, break; } } - +*/ if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(0); - dump(debugPrefix, in, out, expected, delay); + dump(debugPrefix, in, out, expected, reportedDelay); } } From 7c38c0b19683d69b95afb058c686be10b8cbbd71 Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 8 Aug 2024 11:58:12 +0100 Subject: [PATCH 34/35] Ok, tidy this up --- src/test/TestLiveShifter.cpp | 133 +++++++---------------------------- 1 file changed, 25 insertions(+), 108 deletions(-) diff --git a/src/test/TestLiveShifter.cpp b/src/test/TestLiveShifter.cpp index 1f5e113c..5128329a 100644 --- a/src/test/TestLiveShifter.cpp +++ b/src/test/TestLiveShifter.cpp @@ -186,23 +186,12 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, vector in(n), out(n), expected(n); int endpoint = n; if (endpoint > 20000) endpoint -= 10000; - double sumSquares = 0; - double peakIn = 0; for (int i = 0; i < n; ++i) { float value = 0.5f * sinf(float(i) * freq * M_PI * 2.f / float(rate)); - if (i < endpoint) { - sumSquares += value * value; - } if (i > endpoint && value > 0.f && in[i-1] <= 0.f) break; - if (fabs(value) > peakIn) { - peakIn = fabs(value); - } in[i] = value; expected[i] = 0.5f * sinf(float(i) * freq * shift * M_PI * 2.f / float(rate)); } - double rmsIn = sqrt(sumSquares / endpoint); - cerr << "rmsIn = " << rmsIn << endl; - cerr << "peakIn = " << peakIn << endl; for (int i = 0; i < n; i += blocksize) { float *inp = in.data() + i; @@ -213,85 +202,30 @@ static void check_sinusoid_shifted(int n, int rate, float freq, float shift, int reportedDelay = shifter.getStartDelay(); int slackpart = 2048; - for (int section = 0; section < 2; ++section) { + double lastCrossing = -1; - double lastCrossing = -1; - double freqeps = (section == 1 ? 0.15 : 5.0) * fabs(shift); + double eps = 2.0; - sumSquares = 0; + int i0 = reportedDelay + slackpart; + int i1 = endpoint; - int i0 = reportedDelay; - int i1 = reportedDelay + slackpart; - if (section == 1) { - i0 = i1; - i1 = endpoint; - } - - for (int i = i0; i < i1; ++i) { - sumSquares += out[i] * out[i]; - if (out[i-1] < 0.f && out[i] >= 0.f) { - double crossing = (i-1) + (out[i-1] / (out[i-1] - out[i])); - if (lastCrossing >= 0) { - double f = rate / (crossing - lastCrossing); - double diff = freq * shift - f; - if (fabs(diff) > freqeps) { - cerr << "i = " << i << " (from " << i0 << " to " << i1 << "), f = " << f << ", freq = " << freq * shift << ", diff = " << diff << ", freqeps = " << freqeps << ", shift = " << shift << ", factor = " << fabs(diff)/freqeps << endl; - } + for (int i = i0; i < i1; ++i) { + if (out[i-1] < 0.f && out[i] >= 0.f) { + double crossing = (i-1) + (out[i-1] / (out[i-1] - out[i])); + if (lastCrossing >= 0) { + double f = rate / (crossing - lastCrossing); + double diff = freq * shift - f; + double ratio = f / (freq * shift); + double cents = 1200.0 * (log(ratio)/log(2.0)); + if (fabs(cents) >= eps) { + cerr << "i = " << i << " (from " << i0 << " to " << i1 << ", out[i-1] = " << out[i-1] << ", out[i] = " << out[i] << "), f = " << f << ", in freq = " << freq << ", out freq = " << freq * shift << ", ratio = " << ratio << ", cents = " << cents << ", diff = " << diff << ", eps = " << eps << ", shift = " << shift << ", factor = " << fabs(cents)/eps << endl; + BOOST_TEST(fabs(cents) < eps); } - lastCrossing = crossing; } - } - - // check rms - } -/* - double avgWavelength = 1; - if (nCrossings > 0) { - avgWavelength = accWavelength / nCrossings; - } - double detectedFreq = double(rate) / double(avgWavelength); - cerr << "nCrossings = " << nCrossings << ", minWavelength = " << minWavelength << ", maxWavelength = " << maxWavelength << ", avgWavelength = " << avgWavelength << ", detectedFreq = " << detectedFreq << " (expected " << freq * shift << ")" << endl; - - double rms = sqrt(sumSquares / (endpoint - reportedDelay)); - cerr << "rms = " << rms << endl; - cerr << "peak = " << peakOut << endl; - - int slackpart = 2048; - int delay = reportedDelay + slackpart; - - // Align to the next zero-crossing in output, as phase may differ - - for (int i = delay; i < endpoint; ++i) { - if (out[i] < 0.f && out[i+1] >= 0.f) { - cerr << "zc: at " << i << " we have " << out[i] << ", " << out[i+1] << endl; - delay = i+1; - break; + lastCrossing = crossing; } } - cerr << "Adjusted delay from reported value of " << reportedDelay - << " by adding slack of " << slackpart - << " and moving to next positive zero crossing at " << delay << endl; - - float eps = 1.0e-3f; - -#ifdef USE_BQRESAMPLER - eps = 1.0e-2f; -#endif - - for (int i = 0; i + delay < endpoint; ++i) { - float fin = expected[i]; - float fout = out[delay + i]; - float err = fabsf(fin - fout); - if (err > eps) { - cerr << "Error at index " << i << " exceeds eps " - << eps << ": output " << fout << " - expected " - << fin << " = " << fout - fin << endl; - BOOST_TEST(err < eps); - break; - } - } -*/ if (printDebug) { RubberBandLiveShifter::setDefaultDebugLevel(0); dump(debugPrefix, in, out, expected, reportedDelay); @@ -304,18 +238,16 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged) // delay = 2112, correct - check_sinusoid_unchanged(n, 44100, 440.f, 0, "unchanged-440"); + check_sinusoid_unchanged(n, 44100, 440.f, 0); check_sinusoid_unchanged(n, 48000, 260.f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_down_octave_440) { - // Checked: delay = 3648, correct - - // or about 3160? + // Checked: delay = 3648, seems ok int n = 30000; - check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0, "down-octave-440"); + check_sinusoid_shifted(n, 44100, 440.f, 0.5f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_down_octave_260) @@ -329,12 +261,10 @@ BOOST_AUTO_TEST_CASE(sinusoid_down_octave_260) BOOST_AUTO_TEST_CASE(sinusoid_down_2octave) { // Checked: delay = 6784, sound - - // I like about 5250 int n = 30000; - check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0, "down-2octave-440"); -// check_sinusoid_shifted(n, 48000, 260.f, 0.25f, 0); + check_sinusoid_shifted(n, 44100, 440.f, 0.25f, 0); + check_sinusoid_shifted(n, 48000, 260.f, 0.25f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_up_octave_440) @@ -347,35 +277,22 @@ BOOST_AUTO_TEST_CASE(sinusoid_up_octave_440) BOOST_AUTO_TEST_CASE(sinusoid_up_octave_260) { - // Checked: delay = 2879, correct - - //!!! or 3380? - - int n = 30000; - check_sinusoid_shifted(n, 44100, 260.f, 2.0f, 0, "up-octave-260"); -} + // Checked: delay = 2879, seems ok -BOOST_AUTO_TEST_CASE(sinusoid_up_2octave) -{ - // Checked: delay = 3006 -> highly implausible, must be higher - // 3670 ish? - int n = 30000; - check_sinusoid_shifted(n, 44100, 440.f, 4.0f, 0, "up-2octave-440"); - check_sinusoid_shifted(n, 48000, 260.f, 4.0f, 0); + check_sinusoid_shifted(n, 44100, 260.f, 2.0f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_down_0_99) { - int n = 30000; - check_sinusoid_shifted(n, 44100, 440.f, 0.99f, 0, "down-0_99-440"); + check_sinusoid_shifted(n, 44100, 440.f, 0.99f, 0); } BOOST_AUTO_TEST_CASE(sinusoid_up_1_01) { int n = 30000; - check_sinusoid_shifted(n, 44100, 440.f, 1.01f, 0, "up-1_01-440"); + check_sinusoid_shifted(n, 44100, 440.f, 1.01f, 0); } BOOST_AUTO_TEST_SUITE_END() From 091ef63ff03f6f751ef3566cb565c1fbdf34b8bb Mon Sep 17 00:00:00 2001 From: Chris Cannam Date: Thu, 8 Aug 2024 12:01:04 +0100 Subject: [PATCH 35/35] Remove alpha warnings --- rubberband/RubberBandLiveShifter.h | 2 -- src/finer/R3LiveShifter.cpp | 3 --- 2 files changed, 5 deletions(-) diff --git a/rubberband/RubberBandLiveShifter.h b/rubberband/RubberBandLiveShifter.h index bbf16e90..3276a7c8 100644 --- a/rubberband/RubberBandLiveShifter.h +++ b/rubberband/RubberBandLiveShifter.h @@ -41,8 +41,6 @@ #include #include -#pragma message("The RubberBandLiveShifter interface is in alpha test. It may fail to work correctly, or change at any time in the future. Use it at your own risk.") - namespace RubberBand { diff --git a/src/finer/R3LiveShifter.cpp b/src/finer/R3LiveShifter.cpp index 7ab77983..d3f0b467 100644 --- a/src/finer/R3LiveShifter.cpp +++ b/src/finer/R3LiveShifter.cpp @@ -49,9 +49,6 @@ R3LiveShifter::R3LiveShifter(Parameters parameters, Log log) : m_unityCount(0) { Profiler profiler("R3LiveShifter::R3LiveShifter"); - - m_log.log(0, "WARNING: The RubberBandLiveShifter interface is in alpha test. It may fail to work correctly, or change at any time in the future. Use it at your own risk."); - initialise(); }