From 224c6f364d282f797277a16db8ac7c25dd2a5d58 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Thu, 22 Jan 2026 23:12:28 +0000 Subject: [PATCH 1/8] feat: add SignalSmith keylock engine --- CMakeLists.txt | 52 ++++ .../enginebufferscalesignalsmith.cpp | 242 ++++++++++++++++++ .../enginebufferscalesignalsmith.h | 54 ++++ src/engine/enginebuffer.cpp | 84 +++++- src/engine/enginebuffer.h | 27 +- 5 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 src/engine/bufferscalers/enginebufferscalesignalsmith.cpp create mode 100644 src/engine/bufferscalers/enginebufferscalesignalsmith.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ca4ecd104c43..1a0a1add9e38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3488,6 +3488,51 @@ if(KEYFINDER) target_compile_definitions(mixxx-lib PUBLIC __KEYFINDER__) endif() +# SignalSmith +option(SIGNALSMITH "Enable the SignalSmith engine for pitch-bending" ON) +if(SIGNALSMITH) + set( + SIGNALSMITH_INSTALL_DIR + "${CMAKE_CURRENT_BINARY_DIR}/lib/signalsmith-install" + ) + ExternalProject_Add( + signalsmith-stretch + GIT_REPOSITORY + "https://github.com/Signalsmith-Audio/signalsmith-stretch.git" + GIT_TAG "57b93f4e9206a089a45387eaa39bdc9f310d3308" + PREFIX "signalsmith-stretch" + INSTALL_DIR "${SIGNALSMITH_INSTALL_DIR}" + LIST_SEPARATOR "|" + CMAKE_ARGS + -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_PREFIX:PATH= + -DCMAKE_PREFIX_PATH=${PIPE_DELIMITED_CMAKE_PREFIX_PATH} + -$,D,U>CMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE} + -$,D,U>CMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} + -$,D,U>CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + -DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR} + -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME} + BUILD_COMMAND ${CMAKE_COMMAND} --build . + INSTALL_COMMAND true + EXCLUDE_FROM_ALL TRUE + ) + + add_dependencies(mixxx-lib signalsmith-stretch) + target_include_directories( + mixxx-lib + SYSTEM + PUBLIC + "${CMAKE_CURRENT_BINARY_DIR}/signalsmith-stretch/src/signalsmith-stretch-build/_deps/signalsmith-linear-src/include/" + "${CMAKE_CURRENT_BINARY_DIR}/signalsmith-stretch/src/signalsmith-stretch/" + ) + target_sources( + mixxx-lib + PRIVATE src/engine/bufferscalers/enginebufferscalesignalsmith.cpp + ) + target_compile_definitions(mixxx-lib PUBLIC __SIGNALSMITH__) +endif() + # FLAC find_package(FLAC REQUIRED) target_link_libraries(mixxx-lib PRIVATE FLAC::FLAC) @@ -5185,6 +5230,13 @@ if(QML) target_compile_definitions(mixxx-qml-lib PRIVATE rendergraph=rendergraph_sg) target_compile_definitions(mixxx-qml-lib PRIVATE __SCENEGRAPH__) target_compile_definitions(mixxx-qml-lib PRIVATE allshader=allshader_sg) + target_include_directories( + mixxx-qml-lib + SYSTEM + PUBLIC + "${CMAKE_CURRENT_BINARY_DIR}/signalsmith-stretch/src/signalsmith-stretch-build/_deps/signalsmith-linear-src/include/" + "${CMAKE_CURRENT_BINARY_DIR}/signalsmith-stretch/src/signalsmith-stretch/" + ) endif() # WavPack audio file support diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp new file mode 100644 index 000000000000..cada6d90d336 --- /dev/null +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp @@ -0,0 +1,242 @@ +#include "engine/bufferscalers/enginebufferscalesignalsmith.h" + +#include "engine/engine.h" +#include "engine/readaheadmanager.h" +#include "moc_enginebufferscalesignalsmith.cpp" +#include "util/assert.h" +#include "util/defs.h" +#include "util/sample.h" +#include "util/timer.h" + +EngineBufferScaleSignalSmith::EngineBufferScaleSignalSmith(ReadAheadManager* pReadAheadManager) + : m_pReadAheadManager(pReadAheadManager), + m_buffers(), + m_bufferPtrs(), + m_interleavedBuffer(mixxx::kMaxSupportedStems * MAX_BUFFER_LEN), + m_currentFrameOffset(0), + m_currentPreset(Preset::Default) { + onSignalChanged(); +} + +void EngineBufferScaleSignalSmith::setScaleParameters( + double base_rate, double* pTempoRatio, double* pPitchRatio) { + m_dBaseRate = base_rate; + m_bBackwards = *pTempoRatio < 0; + m_dTempoRatio = std::fabs(*pTempoRatio); + m_dPitchRatio = *pPitchRatio; + m_effectiveRate = m_dBaseRate * m_dTempoRatio; + + m_stretch.setTransposeFactor(static_cast(m_dBaseRate * m_dPitchRatio)); + m_stretch.setFormantFactor(1.0); +} + +void EngineBufferScaleSignalSmith::onSignalChanged() { + if (!getOutputSignal().isValid()) { + return; + } + + uint8_t channelCount = getOutputSignal().getChannelCount(); + if (m_buffers.size() != channelCount) { + m_buffers.resize(channelCount); + } + + if (m_bufferPtrs.size() != channelCount) { + m_bufferPtrs.resize(channelCount); + } + + for (int chIdx = 0; chIdx < channelCount; chIdx++) { + if (m_buffers[chIdx].size() == MAX_BUFFER_LEN) { + continue; + } + m_buffers[chIdx] = mixxx::SampleBuffer(MAX_BUFFER_LEN); + m_bufferPtrs[chIdx] = m_buffers[chIdx].data(); + } + + // Configure stretcher with preset settings + switch (m_currentPreset) { + case Preset::Cheaper: + m_stretch.presetCheaper(channelCount, getOutputSignal().getSampleRate()); + break; + default: + qWarning() << "Unsupported presset" << m_currentPreset << " so defaulting to default."; + [[fallthrough]]; + case Preset::Default: + m_stretch.presetDefault(channelCount, getOutputSignal().getSampleRate()); + break; + } + clear(); +} + +void EngineBufferScaleSignalSmith::clear() { + m_stretch.reset(); + m_currentFrameOffset = 0; +} + +SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT frameOffset) { + auto sampleOffset = getOutputSignal().frames2samples(frameOffset); + auto frameRead = getOutputSignal().samples2frames( + m_pReadAheadManager->getNextSamples( + // The value doesn't matter here. All that matters is we + // are going forward or backward. + (m_bBackwards ? -1 : 1) * m_dBaseRate * m_dTempoRatio, + m_interleavedBuffer.data(), + sampleToRead, + getOutputSignal().getChannelCount())); + + switch (getOutputSignal().getChannelCount()) { + case mixxx::audio::ChannelCount::stereo(): + SampleUtil::deinterleaveBuffer( + m_buffers[0].data(frameOffset), + m_buffers[1].data(frameOffset), + m_interleavedBuffer.data(sampleOffset), + frameRead); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::deinterleaveBuffer( + m_buffers[0].data(frameOffset), + m_buffers[1].data(frameOffset), + m_buffers[2].data(frameOffset), + m_buffers[3].data(frameOffset), + m_buffers[4].data(frameOffset), + m_buffers[5].data(frameOffset), + m_buffers[6].data(frameOffset), + m_buffers[7].data(frameOffset), + m_interleavedBuffer.data(sampleOffset), + frameRead); + break; + default: { + int chCount = getOutputSignal().getChannelCount(); + // The sampler are ordered as following in pBuffer + // 1234..X1234...X... + // And need to be reordered as following + // m_buffers#1 = 11.. + // m_buffers#2 = 22.. + // m_buffers#3 = 33.. + // m_buffers#4 = 44..fff + // m_buffers#X = XX.. + // + // Because of the unanticipated number of buffer and channel, we cannot + // use any SampleUtil in this case + for (SINT frameIdx = 0; frameIdx < frameRead; ++frameIdx) { + for (int channel = 0; channel < chCount; channel++) { + m_buffers[channel].data(frameOffset)[frameIdx] = + m_interleavedBuffer.data(sampleOffset)[frameIdx * chCount + channel]; + } + } + } break; + } + return frameRead; +} + +double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iOutputBufferSize) { + ScopedTimer t(QStringLiteral("EngineBufferScaleSignalsmith::scaleBuffer")); + + auto frameLatency = static_cast(m_stretch.inputLatency() + m_stretch.outputLatency()); + while (m_currentFrameOffset < frameLatency) { + ScopedTimer t(QStringLiteral("EngineBufferScaleSignalsmith::scaleBuffer::latencyAdjust")); + SINT currentFrameLatency = std::min(static_cast(MAX_BUFFER_LEN), frameLatency); + const SINT frameRead = fetchAndDeinterleave( + getOutputSignal().frames2samples(currentFrameLatency)); + m_stretch.seek(m_bufferPtrs.data(), frameRead, m_dBaseRate * m_dTempoRatio); + m_currentFrameOffset += currentFrameLatency; + qDebug() + << "EngineBufferScaleSignalSmith::scaleBuffer adjust latency to" + << m_currentFrameOffset; + } + + DEBUG_ASSERT(m_currentFrameOffset == frameLatency); + + SINT requestFrames = getOutputSignal().samples2frames(iOutputBufferSize); + + const SINT frameRequired = static_cast(std::round( + m_dBaseRate * m_dTempoRatio * static_cast(requestFrames))); + bool last_read_failed = false; + SINT frameRead = 0; + while (frameRead < frameRequired) { + auto currentFrameRead = fetchAndDeinterleave(getOutputSignal().frames2samples( + frameRequired - frameRead), + frameRead); + frameRead += currentFrameRead; + + if (last_read_failed && currentFrameRead <= 0) { + // flush and break out after + // the next retrieval. If we are at EOF this serves to get + // the last samples out of the scaler. + for (int ch = 0; ch < getOutputSignal().getChannelCount(); ch++) { + SampleUtil::clear(m_buffers[0].data(frameRead), frameRequired - frameRead); + } + frameRead = frameRequired; + break; + } else if (frameRead <= 0) { + last_read_failed = true; + } + } + + DEBUG_ASSERT(frameRead == frameRequired); + + auto output_frame = getOutputSignal().samples2frames(iOutputBufferSize); + float* outputBufferPtr[8] = { + m_interleavedBuffer.data(), + m_interleavedBuffer.data(iOutputBufferSize), + m_interleavedBuffer.data(2 * iOutputBufferSize), + m_interleavedBuffer.data(3 * iOutputBufferSize), + m_interleavedBuffer.data(4 * iOutputBufferSize), + m_interleavedBuffer.data(5 * iOutputBufferSize), + m_interleavedBuffer.data(6 * iOutputBufferSize), + m_interleavedBuffer.data(7 * iOutputBufferSize), + }; + { + ScopedTimer t(QStringLiteral("Signalsmith::process")); + m_stretch.process(m_bufferPtrs.data(), frameRead, outputBufferPtr, output_frame); + } + + switch (getOutputSignal().getChannelCount()) { + case mixxx::audio::ChannelCount::stereo(): + SampleUtil::interleaveBuffer(pOutputBuffer, + m_interleavedBuffer.data(), + m_interleavedBuffer.data(iOutputBufferSize), + output_frame); + break; + case mixxx::audio::ChannelCount::stem(): + SampleUtil::interleaveBuffer(pOutputBuffer, + m_interleavedBuffer.data(), + m_interleavedBuffer.data(iOutputBufferSize), + m_interleavedBuffer.data(2 * iOutputBufferSize), + m_interleavedBuffer.data(3 * iOutputBufferSize), + m_interleavedBuffer.data(4 * iOutputBufferSize), + m_interleavedBuffer.data(5 * iOutputBufferSize), + m_interleavedBuffer.data(6 * iOutputBufferSize), + m_interleavedBuffer.data(7 * iOutputBufferSize), + output_frame); + break; + default: { + int chCount = getOutputSignal().getChannelCount(); + // The buffers samples are ordered as following + // m_buffers#1 = 11.. + // m_buffers#2 = 22.. + // m_buffers#3 = 33.. + // m_buffers#4 = 44.. + // m_buffers#X = XX.. + // And need to be reordered as following in pBuffer + // 1234..X1234...X... + // + // Because of the unanticipated number of buffer and channel, we cannot + // use any SampleUtil in this case + for (SINT frameIdx = 0; + frameIdx < getOutputSignal().samples2frames(iOutputBufferSize); + ++frameIdx) { + for (int channel = 0; channel < chCount; channel++) { + pOutputBuffer[frameIdx * chCount + channel] = + m_buffers[channel].data()[frameIdx]; + } + } + } break; + } + + DEBUG_ASSERT(std::round(m_effectiveRate * requestFrames) == frameRead); + + // 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. + return m_effectiveRate * requestFrames; +} diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.h b/src/engine/bufferscalers/enginebufferscalesignalsmith.h new file mode 100644 index 000000000000..bf7826039258 --- /dev/null +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include "engine/bufferscalers/enginebufferscale.h" +#include "signalsmith-stretch.h" +#include "util/samplebuffer.h" + +class ReadAheadManager; + +class EngineBufferScaleSignalSmith final : public EngineBufferScale { + Q_OBJECT + public: + enum class Preset { + Default, + Cheaper + }; + Q_ENUM(Preset); + + explicit EngineBufferScaleSignalSmith(ReadAheadManager* pReadAheadManager); + ~EngineBufferScaleSignalSmith() override = default; + + void setScaleParameters(double base_rate, double* pTempoRatio, double* pPitchRatio) override; + void setPreset(Preset preset) { + m_currentPreset = preset; + } + void clear() override; + double scaleBuffer(CSAMPLE* pOutputBuffer, SINT iOutputBufferSize) override; + + private: + void onSignalChanged() override; + SINT fetchAndDeinterleave(SINT frames, SINT offset = 0); + + ReadAheadManager* m_pReadAheadManager; + signalsmith::stretch::SignalsmithStretch m_stretch; + + /// The audio buffers samples used to send audio to Rubber Band and to + /// receive processed audio from Rubber Band. This is needed because Mixxx + /// uses interleaved buffers in most other places. + std::vector m_buffers; + /// These point to the buffers in `m_buffers`. They can be defined here + /// since this object cannot be moved or copied. + std::vector m_bufferPtrs; + + /// Contains interleaved samples read from `m_pReadAheadManager`. These need + /// to be deinterleaved before they can be passed to Rubber Band. + mixxx::SampleBuffer m_interleavedBuffer; + + mixxx::SampleBuffer m_buffer; + SINT m_currentFrameOffset; + // Holds the playback direction + bool m_bBackwards; + Preset m_currentPreset; +}; diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index cb4f2ae4a1c1..faa9d06fd844 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -7,6 +7,9 @@ #include "control/controlproxy.h" #include "control/controlpushbutton.h" #include "engine/bufferscalers/enginebufferscalelinear.h" +#ifdef __SIGNALSMITH__ +#include "engine/bufferscalers/enginebufferscalesignalsmith.h" +#endif #include "engine/bufferscalers/enginebufferscalest.h" #include "engine/cachingreader/cachingreader.h" #include "engine/channels/enginechannel.h" @@ -18,6 +21,7 @@ #include "engine/controls/loopingcontrol.h" #include "engine/controls/quantizecontrol.h" #include "engine/controls/ratecontrol.h" +#include "engine/engine.h" #include "engine/enginemixer.h" #include "engine/readaheadmanager.h" #include "engine/sync/enginesync.h" @@ -53,6 +57,28 @@ constexpr int kPlaypositionUpdateRate = 15; // updates per second const QString kAppGroup = QStringLiteral("[App]"); +#ifdef __SIGNALSMITH__ +EngineBufferScaleSignalSmith::Preset keylockSignalSmithEngineToPreset( + EngineBuffer::KeylockEngine engine) { + switch (engine) { + case EngineBuffer::KeylockEngine::SignalSmithCheaper: + return EngineBufferScaleSignalSmith::Preset::Cheaper; + case EngineBuffer::KeylockEngine::SoundTouch: + case EngineBuffer::KeylockEngine::RubberBandFaster: + case EngineBuffer::KeylockEngine::RubberBandFiner: + DEBUG_ASSERT(!"SignalSmith helper called with another type of engine!"); + + [[fallthrough]]; + default: + qWarning() << engine << "has no matching SignalSmith preset so fall back to default."; + [[fallthrough]]; + case EngineBuffer::KeylockEngine::SignalSmithDefault: + break; + } + return EngineBufferScaleSignalSmith::Preset::Default; +} +#endif + } // anonymous namespace EngineBuffer::EngineBuffer(const QString& group, @@ -279,6 +305,9 @@ EngineBuffer::EngineBuffer(const QString& group, m_pScaleST = new EngineBufferScaleST(m_pReadAheadManager); #ifdef __RUBBERBAND__ m_pScaleRB = new EngineBufferScaleRubberBand(m_pReadAheadManager); +#endif +#ifdef __SIGNALSMITH__ + m_pScaleSignalSmith = new EngineBufferScaleSignalSmith(m_pReadAheadManager); #endif slotKeylockEngineChanged(m_pKeylockEngine->get()); m_pScaleVinyl = m_pScaleLinear; @@ -879,6 +908,13 @@ void EngineBuffer::slotKeylockEngineChanged(double dIndex) { true); // in case of Rubberband V2 it falls back to RUBBERBAND_FASTER m_pScaleKeylock = m_pScaleRB; break; +#endif +#ifdef __SIGNALSMITH__ + case KeylockEngine::SignalSmithDefault: + case KeylockEngine::SignalSmithCheaper: + m_pScaleSignalSmith->setPreset(keylockSignalSmithEngineToPreset(engine)); + m_pScaleKeylock = m_pScaleSignalSmith; + break; #endif default: slotKeylockEngineChanged(static_cast(defaultKeylockEngine())); @@ -946,24 +982,45 @@ void EngineBuffer::processTrackLocked( bool useIndependentPitchAndTempoScaling = false; - // TODO(owen): Maybe change this so that rubberband doesn't disable - // keylock on scratch. (just check m_pScaleKeylock == m_pScaleST) - if (is_scratching || fabs(speed) > 1.9) { + if (is_scratching || +#ifdef __SIGNALSMITH__ + (fabs(speed) > 1.9 && m_pScale != m_pScaleSignalSmith) || fabs(speed) > 4 +#else + fabs(speed) > 1.9 +#endif + ) { // Scratching and high speeds with always disables keylock // because Soundtouch sounds terrible in these conditions. Rubberband // sounds better, but still has some problems (it may reallocate in // a party-crashing manner at extremely slow speeds). + // However, SignalSmith sounds fairly good up to about 4x, before you + // start hearing artifacts. Memory impact is limited as it will only + // required to use an input buffer 4 times bigger than the output, and + // input buffer is always allocated with MAX_BUFFER_LEN due to latency + // adjustments needs, which allows plenty of + // room. For safety, the following assert ensures this would get caught + // in case the constant was getting updated, we ensure that this remains + // the case for a 8192 channel buffer size, which is currently the max possible at + // 96kHz@85.3 ms + static_assert(MAX_BUFFER_LEN > mixxx::kMaxSupportedStems * 8192 * 4); // High seek speeds also disables keylock. Our pitch slider could go // to 90%, so that's the cutoff point. // Force pitchRatio to the linear pitch set by speed pitchRatio = speed; // This is for the natural speed pitch found on turn tables - } else if (fabs(speed) < 0.1) { + } else if (fabs(speed) < 0.1 +#ifdef __SIGNALSMITH__ + && m_pScale != m_pScaleSignalSmith + +#endif + ) { // We have pre-allocated big buffers in Rubberband and Soundtouch for // a minimum speed of 0.1. Slower speeds will re-allocate much bigger - // buffers which may cause underruns. - // Disable keylock under these conditions. + // However, SignalSmith has no impact on buffer size, since the driving + // factor is the input buffer (lower rate means lower input buffer, + // whilst always keeping a steady output buffer) buffers which may cause + // underruns. Disable keylock under these conditions. // Force pitchRatio to the linear pitch set by speed pitchRatio = speed; @@ -995,7 +1052,11 @@ void EngineBuffer::processTrackLocked( } } - if (speed != 0.0) { + if (speed != 0.0 +#ifdef __SIGNALSMITH__ + || m_pScale == m_pScaleSignalSmith +#endif + ) { // Do not switch scaler when we have no transport enableIndependentPitchTempoScaling(useIndependentPitchAndTempoScaling, bufferSize); @@ -1107,7 +1168,11 @@ void EngineBuffer::processTrackLocked( if (atEnd && !backwards) { // do not play past end bCurBufferPaused = true; - } else if (rate == 0 && !is_scratching) { + } else if (rate == 0 && !is_scratching +#ifdef __SIGNALSMITH__ + && m_pScale != m_pScaleSignalSmith +#endif + ) { // do not process samples if have no transport // the linear scaler supports ramping down to 0 // this is used for pause by scratching only @@ -1232,6 +1297,9 @@ void EngineBuffer::process(CSAMPLE* pOutput, const std::size_t bufferSize) { #ifdef __RUBBERBAND__ m_pScaleRB->setSignal(m_sampleRate, m_channelCount); #endif +#ifdef __SIGNALSMITH__ + m_pScaleSignalSmith->setSignal(m_sampleRate, m_channelCount); +#endif bool hasStableTrack = m_pTrackLoaded->toBool() && m_iTrackLoading.loadAcquire() == 0; if (hasStableTrack && m_pause.tryLock()) { diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index 65bd095bc134..c47b5b2b2543 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -22,6 +22,9 @@ #ifdef __RUBBERBAND__ #include "engine/bufferscalers/enginebufferscalerubberband.h" #endif +#ifdef __SIGNALSMITH__ +#include "engine/bufferscalers/enginebufferscalesignalsmith.h" +#endif //for the writer #ifdef __SCALER_DEBUG__ @@ -88,6 +91,10 @@ class EngineBuffer : public EngineObject { #ifdef __RUBBERBAND__ RubberBandFaster = 1, RubberBandFiner = 2, +#endif +#ifdef __SIGNALSMITH__ + SignalSmithDefault = 3, + SignalSmithCheaper = 4, #endif }; Q_ENUM(KeylockEngine); @@ -97,7 +104,11 @@ class EngineBuffer : public EngineObject { KeylockEngine::SoundTouch, #ifdef __RUBBERBAND__ KeylockEngine::RubberBandFaster, - KeylockEngine::RubberBandFiner + KeylockEngine::RubberBandFiner, +#endif +#ifdef __SIGNALSMITH__ + KeylockEngine::SignalSmithDefault, + KeylockEngine::SignalSmithCheaper, #endif }; @@ -184,6 +195,12 @@ class EngineBuffer : public EngineObject { return tr("Rubberband R3 (near-hi-fi quality)"); } [[fallthrough]]; +#endif +#ifdef __SIGNALSMITH__ + case KeylockEngine::SignalSmithCheaper: + return tr("Signal Smith (better and faster)"); + case KeylockEngine::SignalSmithDefault: + return tr("Signal Smith (harder, better, faster, stronger)"); #endif default: #ifdef __RUBBERBAND__ @@ -203,6 +220,11 @@ class EngineBuffer : public EngineObject { return true; case KeylockEngine::RubberBandFiner: return EngineBufferScaleRubberBand::isEngineFinerAvailable(); +#endif +#ifdef __SIGNALSMITH__ + case KeylockEngine::SignalSmithDefault: + case KeylockEngine::SignalSmithCheaper: + return true; #endif default: return false; @@ -465,6 +487,9 @@ class EngineBuffer : public EngineObject { #ifdef __RUBBERBAND__ EngineBufferScaleRubberBand* m_pScaleRB; #endif +#ifdef __SIGNALSMITH__ + EngineBufferScaleSignalSmith* m_pScaleSignalSmith; +#endif // Indicates whether the scaler has changed since the last process() bool m_bScalerChanged; From b39a8e7155710b86928fe91cd28707869d40036d Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 25 Jan 2026 18:15:56 +0000 Subject: [PATCH 2/8] chore: bump up the minimum CMake version to 3.24 --- .github/workflows/benchmark.yml | 4 ++-- .github/workflows/build.yml | 4 ++-- CMakeLists.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 2b22b1a815e8..e12a013b62ca 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -126,7 +126,7 @@ jobs: with: # This should always match the minimum required version in # our CMakeLists.txt - cmake-version: "3.21.x" + cmake-version: "3.24.x" - name: "[Windows] Set up cmake" uses: jwlawson/actions-setup-cmake@v2.0 @@ -137,7 +137,7 @@ jobs: # This is a workaround for a SSL false positive in cmake 3.26.4 # When downloading the manual. 3.21 is required for installing the # ANGLE Dlls via IMPORTED_RUNTIME_ARTIFACTS - cmake-version: "3.21.x" + cmake-version: "3.24.x" - name: "[Windows] Set up MSVC Developer Command Prompt" if: runner.os == 'Windows' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbb93b23a85f..baac8f2fa242 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,7 +236,7 @@ jobs: with: # This should always match the minimum required version in # our CMakeLists.txt - cmake-version: "3.21.x" + cmake-version: "3.24.x" - name: "[Windows] Set up cmake" uses: jwlawson/actions-setup-cmake@v2.0 @@ -247,7 +247,7 @@ jobs: # This is a workaround for a SSL false positive in cmake 3.26.4 # When downloading the manual. 3.21 is required for installing the # ANGLE Dlls via IMPORTED_RUNTIME_ARTIFACTS - cmake-version: "3.21.x" + cmake-version: "3.24.x" - name: "[Windows] Set up MSVC Developer Command Prompt" if: runner.os == 'Windows' diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a0a1add9e38..46bd1e0745f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.24) # lint_cmake: -readability/wonkycase message(STATUS "CMAKE_VERSION: ${CMAKE_VERSION}") From b3cfe16b87e058656ab205938a496f16431a4a2b Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Tue, 27 Jan 2026 02:03:24 +0000 Subject: [PATCH 3/8] fixup! feat: add SignalSmith keylock engine --- .../enginebufferscalesignalsmith.cpp | 113 +++++++++++++----- .../enginebufferscalesignalsmith.h | 7 ++ src/engine/enginebuffer.h | 4 +- 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp index cada6d90d336..d110e44f417f 100644 --- a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp @@ -13,6 +13,8 @@ EngineBufferScaleSignalSmith::EngineBufferScaleSignalSmith(ReadAheadManager* pRe m_buffers(), m_bufferPtrs(), m_interleavedBuffer(mixxx::kMaxSupportedStems * MAX_BUFFER_LEN), + m_frameFractionalLeftover(0), + m_expectedFrameLatency(0), m_currentFrameOffset(0), m_currentPreset(Preset::Default) { onSignalChanged(); @@ -28,6 +30,17 @@ void EngineBufferScaleSignalSmith::setScaleParameters( m_stretch.setTransposeFactor(static_cast(m_dBaseRate * m_dPitchRatio)); m_stretch.setFormantFactor(1.0); + + // The following value is calculated from the block and interval samples + // size, which are set in the above preset. It remains constant during the + // stretcher process. + // As documented in + // https://signalsmith-audio.co.uk/code/stretch/#how-to-use-latency-starting-and-ending, + // stretch factor should be used when computing total latency + m_expectedFrameLatency = + static_cast(m_effectiveRate * + static_cast(m_stretch.inputLatency())) + + static_cast(m_stretch.outputLatency()); } void EngineBufferScaleSignalSmith::onSignalChanged() { @@ -70,6 +83,7 @@ void EngineBufferScaleSignalSmith::onSignalChanged() { void EngineBufferScaleSignalSmith::clear() { m_stretch.reset(); m_currentFrameOffset = 0; + m_frameFractionalLeftover = 0; } SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT frameOffset) { @@ -131,25 +145,62 @@ SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iOutputBufferSize) { ScopedTimer t(QStringLiteral("EngineBufferScaleSignalsmith::scaleBuffer")); - auto frameLatency = static_cast(m_stretch.inputLatency() + m_stretch.outputLatency()); - while (m_currentFrameOffset < frameLatency) { - ScopedTimer t(QStringLiteral("EngineBufferScaleSignalsmith::scaleBuffer::latencyAdjust")); - SINT currentFrameLatency = std::min(static_cast(MAX_BUFFER_LEN), frameLatency); - const SINT frameRead = fetchAndDeinterleave( - getOutputSignal().frames2samples(currentFrameLatency)); - m_stretch.seek(m_bufferPtrs.data(), frameRead, m_dBaseRate * m_dTempoRatio); - m_currentFrameOffset += currentFrameLatency; - qDebug() - << "EngineBufferScaleSignalSmith::scaleBuffer adjust latency to" - << m_currentFrameOffset; + // Unlike RubberBand, SignalSmith Stretch always output as much audio as it + // was given. However, it does introduce latency (documented at + // https://signalsmith-audio.co.uk/code/stretch/#how-to-use-latency) which + // initially lead to a silence. To compensate that, we need to use the + // `.outputSeek` method, which allows to pre-roll samples a realign the actual + // output to real time. + // However, this method will reset the buffer so it can only be used right after a reset + if (m_currentFrameOffset == 0 && + m_currentFrameOffset < m_expectedFrameLatency + // If the track has a zero rate, we skip correction as this is + // usually a sign that the track is not playing. This will likely + // create undesired silence (as opposite to a "zero BPM" play affect + // if a track start playing with a zero BPM, but this is an + // acceptable trade off for now) + && m_dTempoRatio > 0) { + const SINT frameRead = + fetchAndDeinterleave(getOutputSignal().frames2samples( + std::min(m_expectedFrameLatency - m_currentFrameOffset, + SINT(MAX_BUFFER_LEN)))); + m_stretch.outputSeek(m_bufferPtrs.data(), frameRead); + m_currentFrameOffset += frameRead; + } + + const SINT outputFrames = getOutputSignal().samples2frames(iOutputBufferSize); + auto dFrameRequired = + (m_dBaseRate * m_dTempoRatio * static_cast(outputFrames)) + + m_frameFractionalLeftover; + + if (m_currentFrameOffset != m_expectedFrameLatency && dFrameRequired > 0) { + // In case the latency is not matching anymore, we apply a correction + // factor up to 16th of the output buffer. While this might sound much. + // The trade favours catching up as quick as possible to synchronisation + // without being hearable, as such a high value should lead to sync + // after a small amount of buffer process, thus not hearable. + // + // TLDR; best to have delay in BPM adjustment (a quick 100% to 1%, will in practice + // make a few stops along the way), rather than having the actual track + // position desynchronised. We could review this decision in the future + // depending of the user feedback. + auto maxCorrection = std::min(dFrameRequired, static_cast(iOutputBufferSize / 16)); + auto frameOffset = std::min(maxCorrection, + std::max(-maxCorrection, + static_cast(m_expectedFrameLatency) - + static_cast(m_currentFrameOffset))); + dFrameRequired += frameOffset; + m_currentFrameOffset += static_cast(frameOffset); } - DEBUG_ASSERT(m_currentFrameOffset == frameLatency); + const SINT frameRequired = static_cast(dFrameRequired); + VERIFY_OR_DEBUG_ASSERT(frameRequired <= MAX_BUFFER_LEN) { + return 0.0; + } - SINT requestFrames = getOutputSignal().samples2frames(iOutputBufferSize); + m_frameFractionalLeftover = dFrameRequired - static_cast(frameRequired); + DEBUG_ASSERT(0 <= m_frameFractionalLeftover && m_frameFractionalLeftover < 1); - const SINT frameRequired = static_cast(std::round( - m_dBaseRate * m_dTempoRatio * static_cast(requestFrames))); bool last_read_failed = false; SINT frameRead = 0; while (frameRead < frameRequired) { @@ -174,28 +225,28 @@ double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iO DEBUG_ASSERT(frameRead == frameRequired); - auto output_frame = getOutputSignal().samples2frames(iOutputBufferSize); - float* outputBufferPtr[8] = { - m_interleavedBuffer.data(), - m_interleavedBuffer.data(iOutputBufferSize), - m_interleavedBuffer.data(2 * iOutputBufferSize), - m_interleavedBuffer.data(3 * iOutputBufferSize), - m_interleavedBuffer.data(4 * iOutputBufferSize), - m_interleavedBuffer.data(5 * iOutputBufferSize), - m_interleavedBuffer.data(6 * iOutputBufferSize), - m_interleavedBuffer.data(7 * iOutputBufferSize), - }; { ScopedTimer t(QStringLiteral("Signalsmith::process")); - m_stretch.process(m_bufferPtrs.data(), frameRead, outputBufferPtr, output_frame); + float* outputBufferPtr[8] = { + m_interleavedBuffer.data(), + m_interleavedBuffer.data(iOutputBufferSize), + m_interleavedBuffer.data(2 * iOutputBufferSize), + m_interleavedBuffer.data(3 * iOutputBufferSize), + m_interleavedBuffer.data(4 * iOutputBufferSize), + m_interleavedBuffer.data(5 * iOutputBufferSize), + m_interleavedBuffer.data(6 * iOutputBufferSize), + m_interleavedBuffer.data(7 * iOutputBufferSize), + }; + m_stretch.process(m_bufferPtrs.data(), frameRead, outputBufferPtr, outputFrames); } + auto outputFrameSize = getOutputSignal().samples2frames(iOutputBufferSize); switch (getOutputSignal().getChannelCount()) { case mixxx::audio::ChannelCount::stereo(): SampleUtil::interleaveBuffer(pOutputBuffer, m_interleavedBuffer.data(), m_interleavedBuffer.data(iOutputBufferSize), - output_frame); + outputFrameSize); break; case mixxx::audio::ChannelCount::stem(): SampleUtil::interleaveBuffer(pOutputBuffer, @@ -207,7 +258,7 @@ double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iO m_interleavedBuffer.data(5 * iOutputBufferSize), m_interleavedBuffer.data(6 * iOutputBufferSize), m_interleavedBuffer.data(7 * iOutputBufferSize), - output_frame); + outputFrameSize); break; default: { int chCount = getOutputSignal().getChannelCount(); @@ -233,10 +284,8 @@ double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iO } break; } - DEBUG_ASSERT(std::round(m_effectiveRate * requestFrames) == frameRead); - // 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. - return m_effectiveRate * requestFrames; + return m_effectiveRate * outputFrames; } diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.h b/src/engine/bufferscalers/enginebufferscalesignalsmith.h index bf7826039258..13e9eeaba559 100644 --- a/src/engine/bufferscalers/enginebufferscalesignalsmith.h +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.h @@ -47,6 +47,13 @@ class EngineBufferScaleSignalSmith final : public EngineBufferScale { mixxx::SampleBuffer m_interleavedBuffer; mixxx::SampleBuffer m_buffer; + // This stores the fractional part of the sample count that should have been + // inputted to perfectly match the requested the rates. However, since there + // is no such a thing as fractional sample, we keep memory of it till it + // constitute and entire frame and add it up to stay as much in sync as + // possible. + double m_frameFractionalLeftover; + SINT m_expectedFrameLatency; SINT m_currentFrameOffset; // Holds the playback direction bool m_bBackwards; diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index c47b5b2b2543..a0fabda93820 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -198,9 +198,9 @@ class EngineBuffer : public EngineObject { #endif #ifdef __SIGNALSMITH__ case KeylockEngine::SignalSmithCheaper: - return tr("Signal Smith (better and faster)"); + return tr("Signal Smith Stretch (Cheaper)"); case KeylockEngine::SignalSmithDefault: - return tr("Signal Smith (harder, better, faster, stronger)"); + return tr("Signal Smith Stretch (Default)"); #endif default: #ifdef __RUBBERBAND__ From dabeac98118fce4cc9e5a65d51b731796c1df654 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Tue, 27 Jan 2026 02:03:33 +0000 Subject: [PATCH 4/8] fixup! chore: bump up the minimum CMake version to 3.24 --- CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 46bd1e0745f5..43ae2d843b00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.24) +cmake_minimum_required(VERSION 3.21) # lint_cmake: -readability/wonkycase message(STATUS "CMAKE_VERSION: ${CMAKE_VERSION}") @@ -3489,7 +3489,11 @@ if(KEYFINDER) endif() # SignalSmith -option(SIGNALSMITH "Enable the SignalSmith engine for pitch-bending" ON) +option( + SIGNALSMITH + "Enable the SignalSmith Stretch engine for pitch-bending (experimental)" + OFF +) if(SIGNALSMITH) set( SIGNALSMITH_INSTALL_DIR From 2e00ee3cf0c5fea9e4a94646e1cb7875cb35dd15 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Tue, 27 Jan 2026 22:51:49 +0000 Subject: [PATCH 5/8] fixup! feat: add SignalSmith keylock engine --- src/engine/bufferscalers/enginebufferscalesignalsmith.cpp | 6 +++--- src/engine/enginebuffer.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp index d110e44f417f..93a38abacb07 100644 --- a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp @@ -38,8 +38,8 @@ void EngineBufferScaleSignalSmith::setScaleParameters( // https://signalsmith-audio.co.uk/code/stretch/#how-to-use-latency-starting-and-ending, // stretch factor should be used when computing total latency m_expectedFrameLatency = - static_cast(m_effectiveRate * - static_cast(m_stretch.inputLatency())) + + static_cast(std::round(m_effectiveRate * + static_cast(m_stretch.inputLatency()))) + static_cast(m_stretch.outputLatency()); } @@ -214,7 +214,7 @@ double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iO // the next retrieval. If we are at EOF this serves to get // the last samples out of the scaler. for (int ch = 0; ch < getOutputSignal().getChannelCount(); ch++) { - SampleUtil::clear(m_buffers[0].data(frameRead), frameRequired - frameRead); + SampleUtil::clear(m_buffers[ch].data(frameRead), frameRequired - frameRead); } frameRead = frameRequired; break; diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index a0fabda93820..8e3224462f25 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -198,9 +198,9 @@ class EngineBuffer : public EngineObject { #endif #ifdef __SIGNALSMITH__ case KeylockEngine::SignalSmithCheaper: - return tr("Signal Smith Stretch (Cheaper)"); + return tr("Signalsmith Stretch (Cheaper)"); case KeylockEngine::SignalSmithDefault: - return tr("Signal Smith Stretch (Default)"); + return tr("Signalsmith Stretch (Default)"); #endif default: #ifdef __RUBBERBAND__ From fba81546de307c118e164ab7be05108b62e5c05c Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Thu, 22 Jan 2026 23:12:28 +0000 Subject: [PATCH 6/8] fixup! feat: add SignalSmith keylock engine --- CMakeLists.txt | 5 +++++ res/qml/Settings/SoundHardware.qml | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43ae2d843b00..c6221b1855e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3515,6 +3515,7 @@ if(SIGNALSMITH) -$,D,U>CMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE} -$,D,U>CMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} -$,D,U>CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + -$,D,U>CMAKE_ANDROID_NDK=${CMAKE_ANDROID_NDK} -DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR} -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME} BUILD_COMMAND ${CMAKE_COMMAND} --build . @@ -3952,6 +3953,10 @@ if(QML) DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/qml" DESTINATION "${MIXXX_INSTALL_DATADIR}" ) + + if(SIGNALSMITH) + target_compile_definitions(mixxx-qml-lib PUBLIC __SIGNALSMITH__) + endif() endif() option(DEBUG_ASSERTIONS_FATAL "Fail if debug become true assertions" OFF) diff --git a/res/qml/Settings/SoundHardware.qml b/res/qml/Settings/SoundHardware.qml index 758ea7718c82..6cc4088f4245 100644 --- a/res/qml/Settings/SoundHardware.qml +++ b/res/qml/Settings/SoundHardware.qml @@ -321,6 +321,14 @@ Category { options.push(qsTr("Rubberband R3")); tooltips.push(qsTr("Near-hi-fi quality")); break; + case 3: + options.push(qsTr("SiS (Default)")); + tooltips.push(qsTr("Near-hi-fi quality")); + break; + case 4: + options.push(qsTr("SiS (Cheaper)")); + tooltips.push(qsTr("Near-hi-fi quality")); + break; } } keylock.options = options; From 2244ee570fc6430c8b423caa69f804e467ef932b Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sat, 14 Feb 2026 23:35:28 +0000 Subject: [PATCH 7/8] fixup! feat: add SignalSmith keylock engine --- .../enginebufferscalesignalsmith.cpp | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp index 93a38abacb07..2845c6f6521f 100644 --- a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp @@ -174,21 +174,12 @@ double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iO m_frameFractionalLeftover; if (m_currentFrameOffset != m_expectedFrameLatency && dFrameRequired > 0) { - // In case the latency is not matching anymore, we apply a correction - // factor up to 16th of the output buffer. While this might sound much. - // The trade favours catching up as quick as possible to synchronisation - // without being hearable, as such a high value should lead to sync - // after a small amount of buffer process, thus not hearable. - // - // TLDR; best to have delay in BPM adjustment (a quick 100% to 1%, will in practice - // make a few stops along the way), rather than having the actual track - // position desynchronised. We could review this decision in the future - // depending of the user feedback. - auto maxCorrection = std::min(dFrameRequired, static_cast(iOutputBufferSize / 16)); - auto frameOffset = std::min(maxCorrection, - std::max(-maxCorrection, - static_cast(m_expectedFrameLatency) - - static_cast(m_currentFrameOffset))); + // This happens when the rate changes because the rate scales the input + // latency. We need more or less latency frames to keep the output steady. + // The rate changed is immediately applied to the audio without any glitch. + // Pitch changes do not affect latency. + double frameOffset = std::max(-dFrameRequired, + static_cast(m_expectedFrameLatency - m_currentFrameOffset)); dFrameRequired += frameOffset; m_currentFrameOffset += static_cast(frameOffset); } From 600e262d8450feedbf70585e1b3a37a82d1ebfd3 Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sun, 14 Jun 2026 18:30:36 +0100 Subject: [PATCH 8/8] fixup! feat: add SignalSmith keylock engine --- .../bufferscalers/enginebufferscalesignalsmith.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp index 2845c6f6521f..077133cb958a 100644 --- a/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp @@ -38,9 +38,9 @@ void EngineBufferScaleSignalSmith::setScaleParameters( // https://signalsmith-audio.co.uk/code/stretch/#how-to-use-latency-starting-and-ending, // stretch factor should be used when computing total latency m_expectedFrameLatency = + static_cast(m_stretch.inputLatency()) + static_cast(std::round(m_effectiveRate * - static_cast(m_stretch.inputLatency()))) + - static_cast(m_stretch.outputLatency()); + static_cast(m_stretch.outputLatency()))); } void EngineBufferScaleSignalSmith::onSignalChanged() { @@ -87,7 +87,6 @@ void EngineBufferScaleSignalSmith::clear() { } SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT frameOffset) { - auto sampleOffset = getOutputSignal().frames2samples(frameOffset); auto frameRead = getOutputSignal().samples2frames( m_pReadAheadManager->getNextSamples( // The value doesn't matter here. All that matters is we @@ -102,7 +101,7 @@ SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT SampleUtil::deinterleaveBuffer( m_buffers[0].data(frameOffset), m_buffers[1].data(frameOffset), - m_interleavedBuffer.data(sampleOffset), + m_interleavedBuffer.data(), frameRead); break; case mixxx::audio::ChannelCount::stem(): @@ -115,7 +114,7 @@ SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT m_buffers[5].data(frameOffset), m_buffers[6].data(frameOffset), m_buffers[7].data(frameOffset), - m_interleavedBuffer.data(sampleOffset), + m_interleavedBuffer.data(), frameRead); break; default: { @@ -134,7 +133,7 @@ SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT for (SINT frameIdx = 0; frameIdx < frameRead; ++frameIdx) { for (int channel = 0; channel < chCount; channel++) { m_buffers[channel].data(frameOffset)[frameIdx] = - m_interleavedBuffer.data(sampleOffset)[frameIdx * chCount + channel]; + m_interleavedBuffer.data()[frameIdx * chCount + channel]; } } } break;