Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
61 changes: 61 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not work with debian. We either need to contribute a debian folder for signal smith or copy signalsmith to our lib folder.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the issue with debian? I guess the copy option could be viable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do not allow to download third party software during the build process.

@acolombier acolombier Jan 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is going to be a pain for #15888. Though TagLib merged to contribution this morning and I assume will likely release this in the next version, Debian will likely not upgrade to it till the next major release (same goes for Ubuntu).

Not a problem for SignalSmith tho, so let's discuss that there. Is there any objection with using submodules? The SignalSmith stretcher internally relies on submodules too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submodule do not really fit to the modular concept of vcpkg, Debian and RPM.
Having them in our lib folder is IMHO an unnecessary hassle. We don't have submodules yet. I prefer a literal copy in that case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try to contact the author to see if he has any interest.

@daschuer daschuer Jan 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, but if we have the opportunity to centralise one in a single place, I guess this is better.

It depends. Currently while it is under active development a lib folder copy is better.
When I look after years to Rubberband and Soundtouch the VCPKG and Linux Distro maintained version is "better". Original we had Soundtouch in our lib folder and have removed it as soon a suitable version was found in all distros.

The Linux maintainer anyway try to rip off everything from our libs folder. So we need to keep an eye on the distro maintained version anyway.
See also the mess with "libshout"

In case of taglib we can also decide if we want to ship it via lib folder or PPA.
The same is an issue with Portaudio which has now Pulse support, but is not yet released to use it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do not allow to download third party software during the build process.

same for fedora/rpms afaik.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that Geraint (the library author) came back to me and mentioned they haven't had interest to maintain a Debian package at present, but were happy for us to vendor the code and add a copy to lib. They also offered to double license the library in case MIT wasn't ideal, but I believe we should be fine with the current license.
I will capture that as an issue. It should be pretty straight forward to setup if we want to make SignalSmith stretch part of the stable release, but this solution allows us to easily update during the trial.

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=<INSTALL_DIR>
-DCMAKE_PREFIX_PATH=${PIPE_DELIMITED_CMAKE_PREFIX_PATH}
-$<IF:$<BOOL:${CMAKE_TOOLCHAIN_FILE}>,D,U>CMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
-$<IF:$<BOOL:${CMAKE_OSX_DEPLOYMENT_TARGET}>,D,U>CMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
-$<IF:$<BOOL:${CMAKE_OSX_ARCHITECTURES}>,D,U>CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
-$<IF:$<BOOL:${CMAKE_ANDROID_NDK}>,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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
281 changes: 281 additions & 0 deletions src/engine/bufferscalers/enginebufferscalesignalsmith.cpp
Original file line number Diff line number Diff line change
@@ -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<float>(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
Comment thread
daschuer marked this conversation as resolved.
// 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<SINT>(m_stretch.inputLatency()) +
static_cast<SINT>(std::round(m_effectiveRate *
static_cast<double>(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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my expectation we alwasy need this m_currentFrameOffset == m_expectedFrameLatency.

@acolombier acolombier Jan 27, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, but due to limit listed in the comment, we may not be able to correct this in a single process round (e.g because we have now a too large offset compare to the new latency, but we can only correct by iOuputBufferSize).
This means there is no expectation that these value are equal at the end, there is expectation that they will be closer tho, so I can put an assert which capture this (like if we start with values that differ, we expect them to have move closer by the end of the process)

Comment on lines +154 to +155

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this become:

Suggested change
if (m_currentFrameOffset == 0 &&
m_currentFrameOffset < m_expectedFrameLatency
if (m_currentFrameOffset == 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The m_expectedFrameLatency is always > 0 and therefore is the second condition redundant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but m_currentFrameOffset is not always zero, as you have assumed

// 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))));
Comment on lines +164 to +165

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::min(m_expectedFrameLatency - m_currentFrameOffset,
SINT(MAX_BUFFER_LEN))));
std::min(m_expectedFrameLatency, SINT(MAX_BUFFER_LEN))));

@acolombier acolombier Feb 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the current code is correct - m_currentFrameOffset contains the current offset, but we need to see how much more (or less) frames we need to adjust the offset.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is always zero.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not. It is zero only on first process, then it contains the input latency of the previous process.

FYI, I have tested all your suggestion and they don't work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not. It is zero only on first process, then it contains the input latency of the previous process.

FYI, I have tested all your suggestion and they don't work.

m_stretch.outputSeek(m_bufferPtrs.data(), frameRead);
Comment thread
acolombier marked this conversation as resolved.
m_currentFrameOffset += frameRead;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
m_currentFrameOffset += frameRead;
m_currentFrameOffset = frameRead;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code is correct. frameRead contains the relative offset, but m_currentFrameOffset is absolute

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_currentFrameOffset is 0 here. Therefore the + operation is redundant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, see other thread

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have probably missed that we are her in the if (m_currentFrameOffset == 0 && block.

}

const SINT outputFrames = getOutputSignal().samples2frames(iOutputBufferSize);
auto dFrameRequired =
(m_dBaseRate * m_dTempoRatio * static_cast<double>(outputFrames)) +
m_frameFractionalLeftover;
Comment thread
daschuer marked this conversation as resolved.

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<double>(m_expectedFrameLatency - m_currentFrameOffset));
dFrameRequired += frameOffset;
m_currentFrameOffset += static_cast<SINT>(frameOffset);
}

const SINT frameRequired = static_cast<SINT>(dFrameRequired);
VERIFY_OR_DEBUG_ASSERT(frameRequired <= MAX_BUFFER_LEN) {
return 0.0;
}

m_frameFractionalLeftover = dFrameRequired - static_cast<double>(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;
}
Loading