Fix audio resampling functionality (#7858)

Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
This commit is contained in:
Sotonye Atemie
2025-10-22 08:34:07 -04:00
committed by GitHub
parent 38ceac80dd
commit 44a68b8b01
27 changed files with 432 additions and 901 deletions

View File

@@ -81,7 +81,6 @@ AudioEngine::AudioEngine( bool renderOnly ) :
m_workers(),
m_numWorkers( QThread::idealThreadCount()-1 ),
m_newPlayHandles( PlayHandle::MaxNumber ),
m_qualitySettings(qualitySettings::Interpolation::Linear),
m_masterGain( 1.0f ),
m_audioDev( nullptr ),
m_oldAudioDev( nullptr ),
@@ -464,25 +463,6 @@ void AudioEngine::clearInternal()
}
}
void AudioEngine::changeQuality(const struct qualitySettings & qs)
{
// don't delete the audio-device
stopProcessing();
m_qualitySettings = qs;
emit sampleRateChanged();
emit qualitySettingsChanged();
startProcessing();
}
void AudioEngine::doSetAudioDevice( AudioDevice * _dev )
{
// TODO: Use shared_ptr here in the future.
@@ -503,18 +483,10 @@ void AudioEngine::doSetAudioDevice( AudioDevice * _dev )
}
}
void AudioEngine::setAudioDevice(AudioDevice * _dev,
const struct qualitySettings & _qs,
bool _needs_fifo,
bool startNow)
void AudioEngine::setAudioDevice(AudioDevice* _dev, bool _needs_fifo, bool startNow)
{
stopProcessing();
m_qualitySettings = _qs;
doSetAudioDevice( _dev );
emit qualitySettingsChanged();

View File

@@ -1,7 +1,7 @@
/*
* AudioResampler.cpp - wrapper for libsamplerate
* AudioResampler.cpp
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
* Copyright (c) 2025 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
@@ -26,44 +26,77 @@
#include <samplerate.h>
#include <stdexcept>
#include <string>
namespace lmms {
AudioResampler::AudioResampler(int interpolationMode, int channels)
: m_interpolationMode(interpolationMode)
, m_channels(channels)
, m_state(src_new(interpolationMode, channels, &m_error))
namespace {
constexpr auto converterType(AudioResampler::Mode mode) -> int
{
if (!m_state)
switch (mode)
{
const auto errorMessage = std::string{src_strerror(m_error)};
const auto fullMessage = std::string{"Failed to create an AudioResampler: "} + errorMessage;
throw std::runtime_error{fullMessage};
case AudioResampler::Mode::ZOH:
return SRC_ZERO_ORDER_HOLD;
case AudioResampler::Mode::Linear:
return SRC_LINEAR;
case AudioResampler::Mode::SincFastest:
return SRC_SINC_FASTEST;
case AudioResampler::Mode::SincMedium:
return SRC_SINC_MEDIUM_QUALITY;
case AudioResampler::Mode::SincBest:
return SRC_SINC_BEST_QUALITY;
default:
throw std::invalid_argument{"Invalid interpolation mode"};
}
}
} // namespace
AudioResampler::AudioResampler(Mode mode, ch_cnt_t channels)
: m_state{src_new(converterType(mode), channels, &m_error)}
, m_mode{mode}
, m_channels{channels}
{
if (channels <= 0) { throw std::logic_error{"Invalid channel count"}; }
if (!m_state) { throw std::runtime_error{src_strerror(m_error)}; }
}
auto AudioResampler::process(InterleavedBufferView<const float> input, InterleavedBufferView<float> output) -> Result
{
if (input.channels() != m_channels || output.channels() != m_channels)
{
throw std::invalid_argument{"Invalid channel count"};
}
auto data = SRC_DATA{};
data.data_in = input.data();
data.input_frames = input.frames();
data.data_out = output.data();
data.output_frames = output.frames();
data.src_ratio = m_ratio;
data.end_of_input = 0;
if ((m_error = src_process(static_cast<SRC_STATE*>(m_state.get()), &data)))
{
throw std::runtime_error{src_strerror(m_error)};
}
return {static_cast<f_cnt_t>(data.input_frames_used), static_cast<f_cnt_t>(data.output_frames_gen)};
}
void AudioResampler::reset()
{
if ((m_error = src_reset(static_cast<SRC_STATE*>(m_state.get()))))
{
throw std::runtime_error{src_strerror(m_error)};
}
}
AudioResampler::~AudioResampler()
void AudioResampler::StateDeleter::operator()(void* state)
{
src_delete(m_state);
}
auto AudioResampler::resample(const float* in, long inputFrames, float* out, long outputFrames, double ratio)
-> ProcessResult
{
auto data = SRC_DATA{};
data.data_in = in;
data.input_frames = inputFrames;
data.data_out = out;
data.output_frames = outputFrames;
data.src_ratio = ratio;
data.end_of_input = 0;
return {src_process(m_state, &data), data.input_frames_used, data.output_frames_gen};
}
void AudioResampler::setRatio(double ratio)
{
src_set_ratio(m_state, ratio);
src_delete(static_cast<SRC_STATE*>(state));
}
} // namespace lmms

View File

@@ -52,31 +52,11 @@ Effect::Effect( const Plugin::Descriptor * _desc,
{
m_wetDryModel.setCenterValue(0);
m_srcState[0] = m_srcState[1] = nullptr;
reinitSRC();
// Call the virtual method onEnabledChanged so that effects can react to changes,
// e.g. by resetting state.
connect(&m_enabledModel, &BoolModel::dataChanged, [this] { onEnabledChanged(); });
}
Effect::~Effect()
{
for (const auto& state : m_srcState)
{
if (state != nullptr)
{
src_delete(state);
}
}
}
void Effect::saveSettings( QDomDocument & _doc, QDomElement & _this )
{
m_enabledModel.saveSettings( _doc, _this, "on" );
@@ -222,50 +202,4 @@ gui::PluginView * Effect::instantiateView( QWidget * _parent )
return new gui::EffectView( this, _parent );
}
void Effect::reinitSRC()
{
for (auto& state : m_srcState)
{
if (state != nullptr)
{
src_delete(state);
}
int error;
const int currentInterpolation = Engine::audioEngine()->currentQualitySettings().libsrcInterpolation();
if((state = src_new(currentInterpolation, DEFAULT_CHANNELS, &error)) == nullptr)
{
qFatal( "Error: src_new() failed in effect.cpp!\n" );
}
}
}
void Effect::resample( int _i, const SampleFrame* _src_buf,
sample_rate_t _src_sr,
SampleFrame* _dst_buf, sample_rate_t _dst_sr,
f_cnt_t _frames )
{
if( m_srcState[_i] == nullptr )
{
return;
}
m_srcData[_i].input_frames = _frames;
m_srcData[_i].output_frames = Engine::audioEngine()->framesPerPeriod();
m_srcData[_i].data_in = const_cast<float*>(_src_buf[0].data());
m_srcData[_i].data_out = _dst_buf[0].data ();
m_srcData[_i].src_ratio = (double) _dst_sr / _src_sr;
m_srcData[_i].end_of_input = 0;
if (int error = src_process(m_srcState[_i], &m_srcData[_i]))
{
qFatal( "Effect::resample(): error while resampling: %s\n",
src_strerror( error ) );
}
}
} // namespace lmms

View File

@@ -75,18 +75,12 @@ const std::array<ProjectRenderer::FileEncodeDevice, 5> ProjectRenderer::fileEnco
} ;
ProjectRenderer::ProjectRenderer( const AudioEngine::qualitySettings & qualitySettings,
const OutputSettings & outputSettings,
ExportFileFormat exportFileFormat,
const QString & outputFilename ) :
QThread( Engine::audioEngine() ),
m_fileDev( nullptr ),
m_qualitySettings( qualitySettings ),
m_progress( 0 ),
m_abort( false )
ProjectRenderer::ProjectRenderer(
const OutputSettings& outputSettings, ExportFileFormat exportFileFormat, const QString& outputFilename)
: QThread(Engine::audioEngine())
, m_fileDev(nullptr)
, m_progress(0)
, m_abort(false)
{
AudioFileDeviceInstantiaton audioEncoderFactory = fileEncodeDevices[static_cast<std::size_t>(exportFileFormat)].m_getDevInst;
@@ -145,7 +139,7 @@ void ProjectRenderer::startProcessing()
{
// Have to do audio engine stuff with GUI-thread affinity in order to
// make slots connected to sampleRateChanged()-signals being called immediately.
Engine::audioEngine()->setAudioDevice( m_fileDev, m_qualitySettings, false, false );
Engine::audioEngine()->setAudioDevice(m_fileDev, false, false);
start(
#ifndef LMMS_BUILD_WIN32

View File

@@ -34,17 +34,11 @@
namespace lmms
{
RenderManager::RenderManager(
const AudioEngine::qualitySettings & qualitySettings,
const OutputSettings & outputSettings,
ProjectRenderer::ExportFileFormat fmt,
QString outputPath) :
m_qualitySettings(qualitySettings),
m_oldQualitySettings( Engine::audioEngine()->currentQualitySettings() ),
m_outputSettings(outputSettings),
m_format(fmt),
m_outputPath(outputPath)
const OutputSettings& outputSettings, ProjectRenderer::ExportFileFormat fmt, QString outputPath)
: m_outputSettings(outputSettings)
, m_format(fmt)
, m_outputPath(outputPath)
{
Engine::audioEngine()->storeAudioDevice();
}
@@ -52,7 +46,6 @@ RenderManager::RenderManager(
RenderManager::~RenderManager()
{
Engine::audioEngine()->restoreAudioDevice(); // Also deletes audio dev.
Engine::audioEngine()->changeQuality( m_oldQualitySettings );
}
void RenderManager::abortProcessing()
@@ -141,11 +134,7 @@ void RenderManager::renderProject()
void RenderManager::render(QString outputPath)
{
m_activeRenderer = std::make_unique<ProjectRenderer>(
m_qualitySettings,
m_outputSettings,
m_format,
outputPath);
m_activeRenderer = std::make_unique<ProjectRenderer>(m_outputSettings, m_format, outputPath);
if( m_activeRenderer->isReady() )
{

View File

@@ -1,7 +1,7 @@
/*
* Sample.cpp - State for container-class SampleBuffer
* Sample.cpp
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
* Copyright (c) 2025 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
@@ -24,10 +24,6 @@
#include "Sample.h"
#include "lmms_math.h"
#include <cassert>
namespace lmms {
Sample::Sample(const QString& audioFile)
@@ -78,7 +74,7 @@ Sample::Sample(const Sample& other)
{
}
Sample::Sample(Sample&& other)
Sample::Sample(Sample&& other) noexcept
: m_buffer(std::move(other.m_buffer))
, m_startFrame(other.startFrame())
, m_endFrame(other.endFrame())
@@ -104,7 +100,7 @@ auto Sample::operator=(const Sample& other) -> Sample&
return *this;
}
auto Sample::operator=(Sample&& other) -> Sample&
auto Sample::operator=(Sample&& other) noexcept -> Sample&
{
m_buffer = std::move(other.m_buffer);
m_startFrame = other.startFrame();
@@ -118,43 +114,81 @@ auto Sample::operator=(Sample&& other) -> Sample&
return *this;
}
bool Sample::play(SampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency, Loop loopMode) const
bool Sample::play(SampleFrame* dst, PlaybackState* state, size_t numFrames, Loop loop, double ratio) const
{
assert(numFrames > 0);
assert(desiredFrequency > 0);
const auto pastBounds = state->m_frameIndex >= m_endFrame || (state->m_frameIndex < 0 && state->m_backwards);
if (loopMode == Loop::Off && pastBounds) { return false; }
const auto outputSampleRate = Engine::audioEngine()->outputSampleRate() * m_frequency / desiredFrequency;
const auto inputSampleRate = m_buffer->sampleRate();
const auto resampleRatio = outputSampleRate / inputSampleRate;
const auto marginSize = s_interpolationMargins[state->resampler().interpolationMode()];
state->m_frameIndex = std::max<int>(m_startFrame, state->m_frameIndex);
auto playBuffer = std::vector<SampleFrame>(numFrames / resampleRatio + marginSize);
playRaw(playBuffer.data(), playBuffer.size(), state, loopMode);
const auto sampleRateRatio = static_cast<double>(Engine::audioEngine()->outputSampleRate()) / m_buffer->sampleRate();
const auto freqRatio = frequency() / DefaultBaseFreq;
state->m_resampler.setRatio(sampleRateRatio * freqRatio * ratio);
state->resampler().setRatio(resampleRatio);
const auto resampleResult
= state->resampler().resample(&playBuffer[0][0], playBuffer.size(), &dst[0][0], numFrames, resampleRatio);
advance(state, resampleResult.inputFramesUsed, loopMode);
const auto outputFrames = static_cast<f_cnt_t>(resampleResult.outputFramesGenerated);
if (outputFrames < numFrames) { std::fill_n(dst + outputFrames, numFrames - outputFrames, SampleFrame{}); }
if (!approximatelyEqual(m_amplification, 1.0f))
// TODO: These kind of playback pipelines/graphs are repeated within other parts of the codebase that work with
// audio samples. We should find a way to unify this but the right abstraction is not so clear yet.
while (numFrames > 0)
{
for (auto i = std::size_t{0}; i < numFrames; ++i)
if (state->m_bufferView.empty())
{
dst[i][0] *= m_amplification;
dst[i][1] *= m_amplification;
const auto rendered = render(state->m_buffer.data(), state->m_buffer.size(), state, loop);
state->m_bufferView = {state->m_buffer.data(), rendered};
}
const auto [inputFramesUsed, outputFramesGenerated] = state->m_resampler.process(
{&state->m_bufferView.data()[0][0], 2, state->m_bufferView.size()}, {&dst[0][0], 2, numFrames});
if (inputFramesUsed == 0 && outputFramesGenerated == 0)
{
std::fill_n(dst, numFrames, SampleFrame{});
break;
}
state->m_bufferView = state->m_bufferView.subspan(inputFramesUsed);
dst += outputFramesGenerated;
numFrames -= outputFramesGenerated;
}
return true;
return numFrames < Engine::audioEngine()->framesPerPeriod();
}
f_cnt_t Sample::render(SampleFrame* dst, f_cnt_t size, PlaybackState* state, Loop loop) const
{
for (f_cnt_t frame = 0; frame < size; ++frame)
{
switch (loop)
{
case Loop::Off:
if (state->m_frameIndex < 0 || state->m_frameIndex >= m_endFrame) { return frame; }
break;
case Loop::On:
if (state->m_frameIndex < m_loopStartFrame && state->m_backwards)
{
state->m_frameIndex = m_loopEndFrame - 1;
}
else if (state->m_frameIndex >= m_loopEndFrame) { state->m_frameIndex = m_loopStartFrame; }
break;
case Loop::PingPong:
if (state->m_frameIndex < m_loopStartFrame && state->m_backwards)
{
state->m_frameIndex = m_loopStartFrame;
state->m_backwards = false;
}
else if (state->m_frameIndex >= m_loopEndFrame)
{
state->m_frameIndex = m_loopEndFrame - 1;
state->m_backwards = true;
}
break;
default:
break;
}
const auto value
= m_buffer->data()[m_reversed ? m_buffer->size() - state->m_frameIndex - 1 : state->m_frameIndex]
* m_amplification;
dst[frame] = value;
state->m_backwards ? --state->m_frameIndex : ++state->m_frameIndex;
}
return size;
}
auto Sample::sampleDuration() const -> std::chrono::milliseconds
@@ -172,82 +206,4 @@ void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame,
setLoopEndFrame(loopEndFrame);
}
void Sample::playRaw(SampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const
{
if (m_buffer->size() < 1) { return; }
auto index = state->m_frameIndex;
auto backwards = state->m_backwards;
for (size_t i = 0; i < numFrames; ++i)
{
switch (loopMode)
{
case Loop::Off:
if (index < 0 || index >= m_endFrame) { return; }
break;
case Loop::On:
if (index < m_loopStartFrame && backwards) { index = m_loopEndFrame - 1; }
else if (index >= m_loopEndFrame) { index = m_loopStartFrame; }
break;
case Loop::PingPong:
if (index < m_loopStartFrame && backwards)
{
index = m_loopStartFrame;
backwards = false;
}
else if (index >= m_loopEndFrame)
{
index = m_loopEndFrame - 1;
backwards = true;
}
break;
default:
break;
}
dst[i] = m_buffer->data()[m_reversed ? m_buffer->size() - index - 1 : index];
backwards ? --index : ++index;
}
}
void Sample::advance(PlaybackState* state, size_t advanceAmount, Loop loopMode) const
{
state->m_frameIndex += (state->m_backwards ? -1 : 1) * advanceAmount;
if (loopMode == Loop::Off) { return; }
const auto distanceFromLoopStart = std::abs(state->m_frameIndex - m_loopStartFrame);
const auto distanceFromLoopEnd = std::abs(state->m_frameIndex - m_loopEndFrame);
const auto loopSize = m_loopEndFrame - m_loopStartFrame;
if (loopSize == 0) { return; }
switch (loopMode)
{
case Loop::On:
if (state->m_frameIndex < m_loopStartFrame && state->m_backwards)
{
state->m_frameIndex = m_loopEndFrame - 1 - distanceFromLoopStart % loopSize;
}
else if (state->m_frameIndex >= m_loopEndFrame)
{
state->m_frameIndex = m_loopStartFrame + distanceFromLoopEnd % loopSize;
}
break;
case Loop::PingPong:
if (state->m_frameIndex < m_loopStartFrame && state->m_backwards)
{
state->m_frameIndex = m_loopStartFrame + distanceFromLoopStart % loopSize;
state->m_backwards = false;
}
else if (state->m_frameIndex >= m_loopEndFrame)
{
state->m_frameIndex = m_loopEndFrame - 1 - distanceFromLoopEnd % loopSize;
state->m_backwards = true;
}
break;
default:
break;
}
}
} // namespace lmms

View File

@@ -114,7 +114,7 @@ void SamplePlayHandle::play( SampleFrame* buffer )
m_volumeModel->value() / DefaultVolume } };*/
// SamplePlayHandle always plays the sample at its original pitch;
// it is used only for previews, SampleTracks and the metronome.
if (!m_sample->play(workingBuffer, &m_state, frames, DefaultBaseFreq))
if (!m_sample->play(workingBuffer, &m_state, frames))
{
zeroSampleFrames(workingBuffer, frames);
}

View File

@@ -190,12 +190,6 @@ void printHelp()
" Default: 160.\n"
" -f, --format <format> Specify format of render-output where\n"
" Format is either 'wav', 'flac', 'ogg' or 'mp3'.\n"
" -i, --interpolation <method> Specify interpolation method\n"
" Possible values:\n"
" - linear\n"
" - sincfastest (default)\n"
" - sincmedium\n"
" - sincbest\n"
" -l, --loop Render as a loop\n"
" -m, --mode Stereo mode used for MP3 export\n"
" Possible values: s, j, m\n"
@@ -375,7 +369,6 @@ int main( int argc, char * * argv )
new QCoreApplication( argc, argv ) :
new gui::MainApplication(argc, argv);
AudioEngine::qualitySettings qs(AudioEngine::qualitySettings::Interpolation::Linear);
OutputSettings os(44100, 160, OutputSettings::BitDepth::Depth16Bit, OutputSettings::StereoMode::JointStereo);
ProjectRenderer::ExportFileFormat eff = ProjectRenderer::ExportFileFormat::Wave;
@@ -615,39 +608,6 @@ int main( int argc, char * * argv )
{
os.setBitDepth(OutputSettings::BitDepth::Depth32Bit);
}
else if( arg == "--interpolation" || arg == "-i" )
{
++i;
if( i == argc )
{
return usageError( "No interpolation method specified" );
}
const QString ip = QString( argv[i] );
if( ip == "linear" )
{
qs.interpolation = AudioEngine::qualitySettings::Interpolation::Linear;
}
else if( ip == "sincfastest" )
{
qs.interpolation = AudioEngine::qualitySettings::Interpolation::SincFastest;
}
else if( ip == "sincmedium" )
{
qs.interpolation = AudioEngine::qualitySettings::Interpolation::SincMedium;
}
else if( ip == "sincbest" )
{
qs.interpolation = AudioEngine::qualitySettings::Interpolation::SincBest;
}
else
{
return usageError( QString( "Invalid interpolation method %1" ).arg( argv[i] ) );
}
}
else if( arg == "--import" )
{
++i;
@@ -776,7 +736,7 @@ int main( int argc, char * * argv )
}
// create renderer
auto r = new RenderManager(qs, os, eff, renderOut);
auto r = new RenderManager(os, eff, renderOut);
QCoreApplication::instance()->connect( r,
SIGNAL(finished()), SLOT(quit()));

View File

@@ -162,8 +162,6 @@ OutputSettings::StereoMode mapToStereoMode(int index)
void ExportProjectDialog::startExport()
{
auto qs = AudioEngine::qualitySettings(
static_cast<AudioEngine::qualitySettings::Interpolation>(interpolationCB->currentIndex()));
const auto bitrates = std::array{64, 128, 160, 192, 256, 320};
OutputSettings os = OutputSettings(samplerateCB->currentData().toInt(), bitrates[bitrateCB->currentIndex()],
@@ -183,7 +181,8 @@ void ExportProjectDialog::startExport()
{
output_name+=m_fileExtension;
}
m_renderManager.reset(new RenderManager( qs, os, m_ft, output_name ));
m_renderManager.reset(new RenderManager(os, m_ft, output_name));
Engine::getSong()->setExportLoop( exportLoopCB->isChecked() );
Engine::getSong()->setRenderBetweenMarkers( renderMarkersCB->isChecked() );

View File

@@ -331,62 +331,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="qualityGroupBox">
<property name="title">
<string>Quality settings</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Interpolation:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="interpolationCB">
<property name="currentIndex">
<number>2</number>
</property>
<item>
<property name="text">
<string>Zero order hold</string>
</property>
</item>
<item>
<property name="text">
<string>Sinc worst (fastest)</string>
</property>
</item>
<item>
<property name="text">
<string>Sinc medium (recommended)</string>
</property>
</item>
<item>
<property name="text">
<string>Sinc best (slowest)</string>
</property>
</item>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>