Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soundtouch offset compensation updated #11154

Merged
merged 8 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/engine/bufferscalers/enginebufferscale.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ EngineBufferScale::EngineBufferScale()
m_dBaseRate(1.0),
m_bSpeedAffectsPitch(false),
m_dTempoRatio(1.0),
m_dPitchRatio(1.0) {
m_dPitchRatio(1.0),
m_effectiveRate(1.0) {
DEBUG_ASSERT(!m_outputSignal.isValid());
}

Expand Down
2 changes: 2 additions & 0 deletions src/engine/bufferscalers/enginebufferscale.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,6 @@ class EngineBufferScale : public QObject {
bool m_bSpeedAffectsPitch;
double m_dTempoRatio;
double m_dPitchRatio;
// Due to the scaler latency, tempo and pitch changes are not immediately effective.
double m_effectiveRate;
};
28 changes: 11 additions & 17 deletions src/engine/bufferscalers/enginebufferscalerubberband.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@ double EngineBufferScaleRubberBand::scaleBuffer(
return 0.0;
}

SINT total_received_frames = 0;

double readFramesProcessed = 0;
SINT remaining_frames = getOutputSignal().samples2frames(iOutputBufferSize);
CSAMPLE* read = pOutputBuffer;
bool last_read_failed = false;
Expand All @@ -178,7 +177,7 @@ double EngineBufferScaleRubberBand::scaleBuffer(
SINT received_frames = retrieveAndDeinterleave(
read, remaining_frames);
remaining_frames -= received_frames;
total_received_frames += received_frames;
readFramesProcessed += m_effectiveRate * received_frames;
read += getOutputSignal().frames2samples(received_frames);

size_t iLenFramesRequired = m_pRubberBand->getSamplesRequired();
Expand All @@ -196,12 +195,14 @@ double EngineBufferScaleRubberBand::scaleBuffer(
//qDebug() << "iLenFramesRequired" << iLenFramesRequired;

if (remaining_frames > 0 && iLenFramesRequired > 0) {
// The requested setting becomes effective after all previous frames have been processed
m_effectiveRate = m_dBaseRate * m_dTempoRatio;
SINT iAvailSamples = m_pReadAheadManager->getNextSamples(
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
m_buffer_back,
getOutputSignal().frames2samples(iLenFramesRequired));
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
m_buffer_back,
getOutputSignal().frames2samples(iLenFramesRequired));
SINT iAvailFrames = getOutputSignal().samples2frames(iAvailSamples);

if (iAvailFrames > 0) {
Expand Down Expand Up @@ -233,15 +234,8 @@ double EngineBufferScaleRubberBand::scaleBuffer(
counter.increment();
}

// framesRead is interpreted as the total number of virtual sample frames
// readFramesProcessed is interpreted as the total number of frames
// consumed to produce the scaled buffer. Due to this, we do not take into
// account directionality or starting point.
// NOTE(rryan): Why no m_dPitchAdjust here? Pitch does not change the time
// ratio. m_dSpeedAdjust is the ratio of unstretched time to stretched
// time. So, if we used total_received_frames in stretched time, then
// multiplying that by the ratio of unstretched time to stretched time
// will get us the unstretched sample frames read.
double framesRead = m_dBaseRate * m_dTempoRatio * total_received_frames;

return framesRead;
return readFramesProcessed;
}
81 changes: 41 additions & 40 deletions src/engine/bufferscalers/enginebufferscalest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,33 @@ using namespace soundtouch;

namespace {

// Due to filtering and oversampling, SoundTouch is some samples behind.
// The value below was experimental identified using a saw signal and SoundTouch 1.8
// at a speed of 1.0
// 0.918 (upscaling 44.1 kHz to 48 kHz) will produce an additional offset of 3 Frames
// 0.459 (upscaling 44.1 kHz to 96 kHz) will produce an additional offset of 18 Frames
// (Rubberband does not suffer this issue)
const SINT kSeekOffsetFrames = 519;
// Due to filtering and oversampling, SoundTouch is some samples behind (offset).
// The value below was experimental identified using a saw signal and SoundTouch 2.1.1
constexpr SINT kSeekOffsetFramesV20101 = 429;

// From V 2.3.0 Soundtouch has no initial offset at unity, but it is too late with
// lowered pitch and too early with raised pitch up to ~+-2000 frames (~+-50 ms).
// This can be seen in a recording of a saw signal with changed pitch.
// The saws tooth are shifted from their input position depending on the pitch.
// TODO() Compensate that. This is probably cause by the delayed adoption of pitch changes due
// to the SoundTouch chunk size.

constexpr SINT kBackBufferSize = 1024;

} // namespace

EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager *pReadAheadManager)
: m_pReadAheadManager(pReadAheadManager),
m_pSoundTouch(std::make_unique<soundtouch::SoundTouch>()),
m_bBackwards(false) {
EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager* pReadAheadManager)
: m_pReadAheadManager(pReadAheadManager),
m_pSoundTouch(std::make_unique<soundtouch::SoundTouch>()),
m_bufferBack(kBackBufferSize),
m_bBackwards(false) {
m_pSoundTouch->setChannels(getOutputSignal().getChannelCount());
m_pSoundTouch->setRate(m_dBaseRate);
m_pSoundTouch->setPitch(1.0);
m_pSoundTouch->setSetting(SETTING_USE_QUICKSEEK, 1);
// Initialize the internal buffers to prevent re-allocations
// in the real-time thread.
onSampleRateChanged();

}

EngineBufferScaleST::~EngineBufferScaleST() {
Expand Down Expand Up @@ -90,31 +95,31 @@ void EngineBufferScaleST::setScaleParameters(double base_rate,
}

void EngineBufferScaleST::onSampleRateChanged() {
buffer_back.clear();
m_bufferBack.clear();
if (!getOutputSignal().isValid()) {
return;
}
m_pSoundTouch->setSampleRate(getOutputSignal().getSampleRate());
const auto bufferSize = getOutputSignal().frames2samples(kSeekOffsetFrames);
if (bufferSize > buffer_back.size()) {
// grow buffer
buffer_back = mixxx::SampleBuffer(bufferSize);
}
Swiftb0y marked this conversation as resolved.
Show resolved Hide resolved

// Setting the tempo to a very low value will force SoundTouch
// to preallocate buffers large enough to (almost certainly)
// avoid memory reallocations during playback.
m_pSoundTouch->setTempo(0.1);
m_pSoundTouch->putSamples(buffer_back.data(), kSeekOffsetFrames);
m_pSoundTouch->clear();
m_pSoundTouch->setTempo(m_dTempoRatio);
clear();
}

void EngineBufferScaleST::clear() {
m_pSoundTouch->clear();

// compensate seek offset for a rate of 1.0
SampleUtil::clear(buffer_back.data(), buffer_back.size());
m_pSoundTouch->putSamples(buffer_back.data(), kSeekOffsetFrames);
if (SoundTouch::getVersionId() < 20302) {
DEBUG_ASSERT(SoundTouch::getVersionId() >= 20101);
// from SoundTouch 2.3.0 the initial offset is corrected internally
m_effectiveRate = m_dBaseRate * m_dTempoRatio;
SampleUtil::clear(m_bufferBack.data(), m_bufferBack.size());
m_pSoundTouch->putSamples(m_bufferBack.data(), kSeekOffsetFramesV20101);
}
}

double EngineBufferScaleST::scaleBuffer(
Expand All @@ -127,8 +132,7 @@ double EngineBufferScaleST::scaleBuffer(
return 0.0;
}

SINT total_received_frames = 0;

double readFramesProcessed = 0;
SINT remaining_frames = getOutputSignal().samples2frames(iOutputBufferSize);
CSAMPLE* read = pOutputBuffer;
bool last_read_failed = false;
Expand All @@ -137,21 +141,23 @@ double EngineBufferScaleST::scaleBuffer(
read, remaining_frames);
DEBUG_ASSERT(remaining_frames >= received_frames);
remaining_frames -= received_frames;
total_received_frames += received_frames;
readFramesProcessed += m_effectiveRate * received_frames;
read += getOutputSignal().frames2samples(received_frames);

if (remaining_frames > 0) {
// The requested setting becomes effective after all previous frames have been processed
m_effectiveRate = m_dBaseRate * m_dTempoRatio;
SINT iAvailSamples = m_pReadAheadManager->getNextSamples(
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
buffer_back.data(),
buffer_back.size());
// The value doesn't matter here. All that matters is we
// are going forward or backward.
(m_bBackwards ? -1.0 : 1.0) * m_effectiveRate,
m_bufferBack.data(),
m_bufferBack.size());
SINT iAvailFrames = getOutputSignal().samples2frames(iAvailSamples);

if (iAvailFrames > 0) {
last_read_failed = false;
m_pSoundTouch->putSamples(buffer_back.data(), iAvailFrames);
m_pSoundTouch->putSamples(m_bufferBack.data(), iAvailFrames);
} else {
// We may get 0 samples once if we just hit a loop trigger, e.g.
// when reloop_toggle jumps back to loop_in, or when moving a
Expand All @@ -163,21 +169,16 @@ double EngineBufferScaleST::scaleBuffer(
// a temporary buffer in the heap which maybe locking
qDebug() << "ReadAheadManager::getNextSamples() returned "
"zero samples repeatedly. Padding with silence.";
SampleUtil::clear(buffer_back.data(), buffer_back.size());
m_pSoundTouch->putSamples(buffer_back.data(), buffer_back.size());
SampleUtil::clear(m_bufferBack.data(), m_bufferBack.size());
m_pSoundTouch->putSamples(m_bufferBack.data(), m_bufferBack.size());
}
last_read_failed = true;
}
}
}

// framesRead is interpreted as the total number of virtual sample frames
// readFramesProcessed is interpreted as the total number of frames
// consumed to produce the scaled buffer. Due to this, we do not take into
// account directionality or starting point.
// NOTE(rryan): Why no m_dPitchAdjust here? SoundTouch implements pitch
// shifting as a tempo shift of (1/m_dPitchAdjust) and a rate shift of
// (*m_dPitchAdjust) so these two cancel out.
double framesRead = m_dBaseRate * m_dTempoRatio * total_received_frames;

return framesRead;
return readFramesProcessed;
}
2 changes: 1 addition & 1 deletion src/engine/bufferscalers/enginebufferscalest.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class EngineBufferScaleST : public EngineBufferScale {
std::unique_ptr<soundtouch::SoundTouch> m_pSoundTouch;

// Temporary buffer for reading from the RAMAN.
mixxx::SampleBuffer buffer_back;
mixxx::SampleBuffer m_bufferBack;

// Holds the playback direction.
bool m_bBackwards;
Expand Down