diff --git a/main/main.cpp b/main/main.cpp index 3309490..523dad7 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -726,11 +726,11 @@ int main(int argc, char **argv) bool clipping = false; // The stretcher only pads the start in offline mode; to avoid - // a fade in at the start, we pad it manually in RT mode - int toDrop = 0; + // a fade in at the start, we pad it manually in RT mode. Both + // of these functions are defined to return zero in offline mode + int toDrop = ts.getStartDelay(); if (realtime) { - toDrop = int(ts.getLatency()); - int toPad = int(round(toDrop * frequencyshift)); + int toPad = ts.getPreferredStartPad(); if (debug > 0) { cerr << "padding start with " << toPad << " samples in RT mode, will drop " << toDrop diff --git a/rubberband/RubberBandStretcher.h b/rubberband/RubberBandStretcher.h index a300131..c750a3d 100644 --- a/rubberband/RubberBandStretcher.h +++ b/rubberband/RubberBandStretcher.h @@ -72,8 +72,8 @@ namespace RubberBand * study pass has begun and cannot be changed afterwards. (But see * RubberBandStretcher::setKeyFrameMap() for a way to do pre-planned * variable time stretching in offline mode.) Offline mode also - * performs latency compensation so that the stretched result has an - * exact start and duration. + * performs padding and delay compensation so that the stretched + * result has an exact start and duration. * * ### Real-time mode * @@ -85,8 +85,9 @@ namespace RubberBand * In real-time mode you can change the time and pitch ratios at any * time. * - * You may need to perform latency compensation in real-time mode; see - * RubberBandStretcher::getLatency() for details. + * You may need to perform signal padding and delay compensation in + * real-time mode; see RubberBandStretcher::getPreferredStartPad() and + * RubberBandStretcher::getStartDelay() for details. * * Rubber Band Library is RT-safe when used in real-time mode with * "normal" processing parameters. That is, it performs no allocation, @@ -623,27 +624,68 @@ public: * This function was added in Rubber Band Library v3.0. */ double getFormantScale() const; + + /** + * In RealTime mode (unlike in Offline mode) the stretcher + * performs no automatic padding or delay/latency compensation at + * the start of the signal. This permits applications to have + * their own custom requirements, but it also means that by + * default some samples will be lost or attenuated at the start of + * the output and the correct linear relationship between input + * and output sample counts may be lost. + * + * Most applications using RealTime mode should solve this by + * calling getPreferredStartPad() and supplying the returned + * number of (silent) samples at the start of their input, before + * their first "true" process() call; and then also calling + * getStartDelay() and trimming the returned number of samples + * from the start of their stretcher's output. + * + * Ensure you have set the time and pitch scale factors to their + * proper starting values before calling getRequiredStartPad() or + * getStartDelay(). + * + * In Offline mode, padding and delay compensation are handled + * internally and both functions always return zero. + * + * This function was added in Rubber Band Library v3.0. + * + * @see getStartDelay + */ + size_t getPreferredStartPad() const; /** - * Return the output delay or latency of the stretcher. This is - * the number of audio samples that one would have to discard at - * the start of the output in order to ensure that the resulting - * audio aligns with the input audio at the start. In Offline - * mode, this delay is automatically adjusted for and the result - * is zero. In RealTime mode, the delay may depend on the time - * and pitch ratio and other options. - * - * Note that this is not the same thing as the number of samples - * needed at input to cause a block of processing to happen (also - * sometimes referred to as latency). That value is reported by - * getSamplesRequired() and will vary, but typically will be - * higher than the output delay, at least at the start of - * processing. + * Return the output delay of the stretcher. This is the number + * of audio samples that one should discard at the start of the + * output, after padding the start of the input with + * getPreferredStartPad(), in order to ensure that the resulting + * audio has the expected time alignment with the input. * - * @see getSamplesRequired + * Ensure you have set the time and pitch scale factors to their + * proper starting values before calling getPreferredStartPad() or + * getStartDelay(). + * + * In Offline mode, padding and delay compensation are handled + * internally and both functions always return zero. + * + * This function was added in Rubber Band Library v3.0. Previously + * it was called getLatency(). It was renamed to avoid confusion + * with the number of samples needed at input to cause a block of + * processing to handle (returned by getSamplesRequired()) which + * is also sometimes referred to as latency. + * + * @see getPreferredStartPad + */ + size_t getStartDelay() const; + + /** + * Return the start delay of the stretcher. This is a deprecated + * alias for getStartDelay(). + * + * @deprecated */ size_t getLatency() const; - + /** * Return the number of channels this stretcher was constructed * with. @@ -765,7 +807,7 @@ public: * sampled at 44100Hz yields a value of 44100 sample frames, not * 88200.) This rule applies throughout the Rubber Band API. * - * @see getLatency + * @see getStartDelay */ size_t getSamplesRequired() const; diff --git a/src/RubberBandStretcher.cpp b/src/RubberBandStretcher.cpp index a2a6ee4..a278444 100644 --- a/src/RubberBandStretcher.cpp +++ b/src/RubberBandStretcher.cpp @@ -159,10 +159,18 @@ public: RTENTRY__ size_t - getLatency() const + getPreferredStartPad() const { - if (m_r2) return m_r2->getLatency(); - else return m_r3->getLatency(); + if (m_r2) return m_r2->getPreferredStartPad(); + else return m_r3->getPreferredStartPad(); + } + + RTENTRY__ + size_t + getStartDelay() const + { + if (m_r2) return m_r2->getStartDelay(); + else return m_r3->getStartDelay(); } //!!! review all these @@ -414,11 +422,26 @@ RubberBandStretcher::getFormantScale() const return m_d->getFormantScale(); } +RTENTRY__ +size_t +RubberBandStretcher::getPreferredStartPad() const +{ + return m_d->getPreferredStartPad(); +} + +RTENTRY__ +size_t +RubberBandStretcher::getStartDelay() const +{ + return m_d->getStartDelay(); +} + RTENTRY__ size_t RubberBandStretcher::getLatency() const { - return m_d->getLatency(); + // deprecated alias for getStartDelay + return m_d->getStartDelay(); } RTENTRY__ diff --git a/src/faster/R2Stretcher.cpp b/src/faster/R2Stretcher.cpp index 46e044c..4546895 100644 --- a/src/faster/R2Stretcher.cpp +++ b/src/faster/R2Stretcher.cpp @@ -829,7 +829,14 @@ R2Stretcher::reconfigure() } size_t -R2Stretcher::getLatency() const +R2Stretcher::getPreferredStartPad() const +{ + if (!m_realtime) return 0; + return m_aWindowSize/2; +} + +size_t +R2Stretcher::getStartDelay() const { if (!m_realtime) return 0; return lrint((m_aWindowSize/2) / m_pitchScale); diff --git a/src/faster/R2Stretcher.h b/src/faster/R2Stretcher.h index b9e1709..3f001eb 100644 --- a/src/faster/R2Stretcher.h +++ b/src/faster/R2Stretcher.h @@ -62,7 +62,8 @@ public: double getTimeRatio() const; double getPitchScale() const; - size_t getLatency() const; + size_t getPreferredStartPad() const; + size_t getStartDelay() const; void setTransientsOption(RubberBandStretcher::Options); void setDetectorOption(RubberBandStretcher::Options); diff --git a/src/finer/R3Stretcher.cpp b/src/finer/R3Stretcher.cpp index 1197764..bdbca02 100644 --- a/src/finer/R3Stretcher.cpp +++ b/src/finer/R3Stretcher.cpp @@ -367,13 +367,23 @@ R3Stretcher::getFormantScale() const } size_t -R3Stretcher::getLatency() const +R3Stretcher::getPreferredStartPad() const { if (!isRealTime()) { return 0; } else { - return size_t(ceil(m_guideConfiguration.longestFftSize - * 0.5 * m_pitchScale)); + return m_guideConfiguration.longestFftSize / 2; + } +} + +size_t +R3Stretcher::getStartDelay() const +{ + if (!isRealTime()) { + return 0; + } else { + double factor = 0.5 / m_pitchScale; + return size_t(ceil(m_guideConfiguration.longestFftSize * factor)); } } @@ -509,12 +519,7 @@ R3Stretcher::process(const float *const *input, size_t samples, bool final) // NB by the time we skip this later we may have resampled // as well as stretched m_startSkip = int(round(pad / m_pitchScale)); - if (m_startSkip < 0) { - m_log.log(0, "WARNING: calculated start skip < 0", m_startSkip); - m_startSkip = 0; - } else { - m_log.log(1, "start skip is", m_startSkip); - } + m_log.log(1, "start skip is", m_startSkip); } } diff --git a/src/finer/R3Stretcher.h b/src/finer/R3Stretcher.h index 3625518..7e69d50 100644 --- a/src/finer/R3Stretcher.h +++ b/src/finer/R3Stretcher.h @@ -84,7 +84,9 @@ public: int available() const; size_t retrieve(float *const *output, size_t samples) const; - size_t getLatency() const; + size_t getPreferredStartPad() const; + size_t getStartDelay() const; + size_t getChannelCount() const; void setMaxProcessSize(size_t samples); diff --git a/src/test/TestStretcher.cpp b/src/test/TestStretcher.cpp index 7ae6ef5..271dfec 100644 --- a/src/test/TestStretcher.cpp +++ b/src/test/TestStretcher.cpp @@ -70,7 +70,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_faster) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n); - BOOST_TEST(stretcher.getLatency() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode size_t got = stretcher.retrieve(&outp, n); BOOST_TEST(got == n); @@ -127,7 +127,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_unchanged_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n); - BOOST_TEST(stretcher.getLatency() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode size_t got = stretcher.retrieve(&outp, n); BOOST_TEST(got == n); @@ -180,7 +180,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_2x_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n*2); - BOOST_TEST(stretcher.getLatency() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode size_t got = stretcher.retrieve(&outp, n*2); BOOST_TEST(got == n*2); @@ -225,11 +225,13 @@ BOOST_AUTO_TEST_CASE(sinusoid_2x_offline_finer) BOOST_TEST(rms < 0.1); } -BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) +static void sinusoid_realtime(RubberBandStretcher::Options options, + double timeRatio, + double pitchScale, + bool printDebug) { - int n = 40000; - int multiple = 8; - int nOut = n * multiple; + int n = (timeRatio < 1.0 ? 80000 : 40000); + int nOut = int(ceil(n * timeRatio)); float freq = 441.f; int rate = 44100; int bs = 512; @@ -238,12 +240,7 @@ BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) // latency compensation, and checks that the output is all in the // expected place - RubberBandStretcher stretcher - (rate, 1, - RubberBandStretcher::OptionEngineFiner | - RubberBandStretcher::OptionProcessRealTime | - RubberBandStretcher::OptionFormantPreserved, - multiple, 1.0); + RubberBandStretcher stretcher(rate, 1, options, timeRatio, pitchScale); stretcher.setMaxProcessSize(bs); @@ -264,10 +261,10 @@ BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) // Prime the start { float *source = out.data(); // just reuse out because it's silent - stretcher.process(&source, stretcher.getLatency(), false); + stretcher.process(&source, stretcher.getPreferredStartPad(), false); } - int toSkip = stretcher.getLatency(); + int toSkip = stretcher.getStartDelay(); int inOffset = 0, outOffset = 0; @@ -297,7 +294,12 @@ BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) float *source = in.data() + inOffset; stretcher.process(&source, toProcess, toProcess < required); inOffset += toProcess; - BOOST_TEST(stretcher.available() > 0); + if (options & RubberBandStretcher::OptionEngineFiner) { + //!!! Faster engine sometimes does return + // available == 0 here - I'm not sure why, need to + // look into this. The process still completes fine + BOOST_TEST(stretcher.available() > 0); + } continue; } else { // available > 0 @@ -317,10 +319,12 @@ BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) } } -// std::cout << "sample\tV" << std::endl; -// for (int i = 0; i < nOut; ++i) { -// std::cout << i << "\t" << out[i] << std::endl; -// } + if (printDebug) { + std::cout << "sample\tV" << std::endl; + for (int i = 0; i < nOut; ++i) { + std::cout << i << "\t" << out[i] << std::endl; + } + } // 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 @@ -341,15 +345,36 @@ BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) } } - int expectedCrossings = int(round((freq * double(i1 - i0)) / rate)); + int expectedCrossings = int(round((freq * pitchScale * + double(i1 - i0)) / rate)); - // In the last chunk we can miss one crossing - if (chunk == 19) { - BOOST_TEST(positiveCrossings <= expectedCrossings); - BOOST_TEST(positiveCrossings >= expectedCrossings - 1); +// std::cout << chunk << std::endl; + + // 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 & RubberBandStretcher::OptionEngineFiner) { + if (chunk == 0 || chunk == 19) { + slack = (timeRatio < 1.0 ? 10 : 1); + } } else { - BOOST_TEST(positiveCrossings == expectedCrossings); + if (chunk == 0) { + slack = (timeRatio < 1.0 ? 10 : 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 @@ -364,6 +389,86 @@ BOOST_AUTO_TEST_CASE(sinusoid_8x_realtime_finer) } } +BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_finer) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFiner | + RubberBandStretcher::OptionProcessRealTime, + 8.0, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_samepitch_realtime_faster) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFaster | + RubberBandStretcher::OptionProcessRealTime, + 8.0, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_finer) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFiner | + RubberBandStretcher::OptionProcessRealTime, + 0.5, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_samepitch_realtime_faster) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFaster | + RubberBandStretcher::OptionProcessRealTime, + 0.5, 1.0, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_finer) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFiner | + RubberBandStretcher::OptionProcessRealTime, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_higher_realtime_faster) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFaster | + RubberBandStretcher::OptionProcessRealTime, + 4.0, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_finer) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFiner | + RubberBandStretcher::OptionProcessRealTime, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_fast_higher_realtime_faster) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFaster | + RubberBandStretcher::OptionProcessRealTime, + 0.5, 1.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_finer) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFiner | + RubberBandStretcher::OptionProcessRealTime, + 8.0, 0.5, + false); +} + +BOOST_AUTO_TEST_CASE(sinusoid_slow_lower_realtime_faster) +{ + sinusoid_realtime(RubberBandStretcher::OptionEngineFaster | + RubberBandStretcher::OptionProcessRealTime, + 8.0, 0.5, + false); +} + BOOST_AUTO_TEST_CASE(impulses_2x_offline_faster) { int n = 10000; @@ -396,7 +501,7 @@ BOOST_AUTO_TEST_CASE(impulses_2x_offline_faster) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n * 2); - BOOST_TEST(stretcher.getLatency() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode size_t got = stretcher.retrieve(&outp, n * 2); BOOST_TEST(got == n * 2); @@ -465,7 +570,7 @@ BOOST_AUTO_TEST_CASE(impulses_2x_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n * 2); - BOOST_TEST(stretcher.getLatency() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode size_t got = stretcher.retrieve(&outp, n * 2); BOOST_TEST(got == n * 2); @@ -535,7 +640,7 @@ BOOST_AUTO_TEST_CASE(impulses_2x_5up_offline_finer) stretcher.process(&inp, n, true); BOOST_TEST(stretcher.available() == n * 2); - BOOST_TEST(stretcher.getLatency() == 0); // offline mode + BOOST_TEST(stretcher.getStartDelay() == 0); // offline mode size_t got = stretcher.retrieve(&outp, n * 2); BOOST_TEST(got == n * 2);