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 5e04eeb2c7df..92067d8628f9 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.1 @@ -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 ca4ecd104c43..c6221b1855e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3488,6 +3488,56 @@ if(KEYFINDER) target_compile_definitions(mixxx-lib PUBLIC __KEYFINDER__) endif() +# SignalSmith +option( + SIGNALSMITH + "Enable the SignalSmith Stretch engine for pitch-bending (experimental)" + OFF +) +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} + -$,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 . + 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) @@ -3903,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) @@ -5185,6 +5239,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..077133cb958a --- /dev/null +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.cpp @@ -0,0 +1,281 @@ +#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_frameFractionalLeftover(0), + m_expectedFrameLatency(0), + 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); + + // 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_stretch.inputLatency()) + + static_cast(std::round(m_effectiveRate * + static_cast(m_stretch.outputLatency()))); +} + +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; + m_frameFractionalLeftover = 0; +} + +SINT EngineBufferScaleSignalSmith::fetchAndDeinterleave(SINT sampleToRead, SINT 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(), + 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(), + 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()[frameIdx * chCount + channel]; + } + } + } break; + } + return frameRead; +} + +double EngineBufferScaleSignalSmith::scaleBuffer(CSAMPLE* pOutputBuffer, SINT iOutputBufferSize) { + ScopedTimer t(QStringLiteral("EngineBufferScaleSignalsmith::scaleBuffer")); + + // 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) { + // 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); + } + + const SINT frameRequired = static_cast(dFrameRequired); + VERIFY_OR_DEBUG_ASSERT(frameRequired <= MAX_BUFFER_LEN) { + return 0.0; + } + + m_frameFractionalLeftover = dFrameRequired - static_cast(frameRequired); + DEBUG_ASSERT(0 <= m_frameFractionalLeftover && m_frameFractionalLeftover < 1); + + 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[ch].data(frameRead), frameRequired - frameRead); + } + frameRead = frameRequired; + break; + } else if (frameRead <= 0) { + last_read_failed = true; + } + } + + DEBUG_ASSERT(frameRead == frameRequired); + + { + ScopedTimer t(QStringLiteral("Signalsmith::process")); + 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), + outputFrameSize); + 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), + outputFrameSize); + 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; + } + + // 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 * outputFrames; +} diff --git a/src/engine/bufferscalers/enginebufferscalesignalsmith.h b/src/engine/bufferscalers/enginebufferscalesignalsmith.h new file mode 100644 index 000000000000..13e9eeaba559 --- /dev/null +++ b/src/engine/bufferscalers/enginebufferscalesignalsmith.h @@ -0,0 +1,61 @@ +#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; + // 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; + Preset m_currentPreset; +}; diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index 9a0cb9ae0874..075b759b91ec 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 || is_scratching) { + if (speed != 0.0 || is_scratching +#ifdef __SIGNALSMITH__ + || m_pScale == m_pScaleSignalSmith +#endif + ) { // Do not switch scaler when we have no transport, except when we start scratching. 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..8e3224462f25 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("Signalsmith Stretch (Cheaper)"); + case KeylockEngine::SignalSmithDefault: + return tr("Signalsmith Stretch (Default)"); #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;