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

@@ -105,40 +105,6 @@ public:
AudioEngine* m_audioEngine;
};
struct qualitySettings
{
enum class Interpolation
{
Linear,
SincFastest,
SincMedium,
SincBest
} ;
Interpolation interpolation;
qualitySettings(Interpolation i) :
interpolation(i)
{
}
int libsrcInterpolation() const
{
switch( interpolation )
{
case Interpolation::Linear:
return SRC_ZERO_ORDER_HOLD;
case Interpolation::SincFastest:
return SRC_SINC_FASTEST;
case Interpolation::SincMedium:
return SRC_SINC_MEDIUM_QUALITY;
case Interpolation::SincBest:
return SRC_SINC_BEST_QUALITY;
}
return SRC_LINEAR;
}
} ;
void initDevices();
void clear();
void clearNewPlayHandles();
@@ -160,10 +126,7 @@ public:
//! Set new audio device. Old device will be deleted,
//! unless it's stored using storeAudioDevice
void setAudioDevice( AudioDevice * _dev,
const struct qualitySettings & _qs,
bool _needs_fifo,
bool startNow );
void setAudioDevice(AudioDevice* _dev, bool _needs_fifo, bool startNow);
void storeAudioDevice();
void restoreAudioDevice();
inline AudioDevice * audioDev()
@@ -230,12 +193,6 @@ public:
return m_profiler.detailLoad(type);
}
const qualitySettings & currentQualitySettings() const
{
return m_qualitySettings;
}
sample_rate_t baseSampleRate() const { return m_baseSampleRate; }
@@ -300,8 +257,6 @@ public:
return hasFifoWriter() ? m_fifo->read() : renderNextBuffer();
}
void changeQuality(const struct qualitySettings & qs);
//! Block until a change in model can be done (i.e. wait for audio thread)
void requestChangeInModel();
void doneChangeInModel();
@@ -390,8 +345,6 @@ private:
LocklessList<PlayHandle *> m_newPlayHandles;
ConstPlayHandleList m_playHandlesToRemove;
struct qualitySettings m_qualitySettings;
float m_masterGain;
// audio device stuff

View File

@@ -1,7 +1,7 @@
/*
* AudioResampler.h - wrapper around libsamplerate
* AudioResampler.h
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
* Copyright (c) 2025 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
@@ -25,41 +25,104 @@
#ifndef LMMS_AUDIO_RESAMPLER_H
#define LMMS_AUDIO_RESAMPLER_H
#include <samplerate.h>
#include <memory>
#include "AudioBufferView.h"
#include "lmms_export.h"
namespace lmms {
/**
* @class AudioResampler
* @brief A utility class for resampling interleaved audio buffers using various resampling algorithms.
*
* This class provides support for zero-order hold, linear, and several levels of sinc-based resampling.
*/
class LMMS_EXPORT AudioResampler
{
public:
struct ProcessResult
/**
* @enum Mode
* @brief Defines the resampling method to use.
*/
enum class Mode
{
int error;
long inputFramesUsed;
long outputFramesGenerated;
ZOH, //!< Zero Order Hold (nearest-neighbor) interpolation.
Linear, //!< Linear interpolation.
SincFastest, //!< Fastest sinc-based resampling.
SincMedium, //!< Medium quality sinc-based resampling.
SincBest //!< Highest quality sinc-based resampling.
};
AudioResampler(int interpolationMode, int channels);
AudioResampler(const AudioResampler&) = delete;
AudioResampler(AudioResampler&&) = delete;
~AudioResampler();
/**
* @struct Result
* @brief Result of a resampling operation.
*/
struct Result
{
f_cnt_t inputFramesUsed; //!< The number of input frames used during processing.
f_cnt_t outputFramesGenerated; //!< The number of output frames generated during processing.
};
AudioResampler& operator=(const AudioResampler&) = delete;
AudioResampler& operator=(AudioResampler&&) = delete;
/**
* @brief Constructs an `AudioResampler` instance.
* @param mode The resampling mode to use.
* @param channels Number of audio channels. Defaults to `2` (stereo).
*/
AudioResampler(Mode mode, ch_cnt_t channels = 2);
auto resample(const float* in, long inputFrames, float* out, long outputFrames, double ratio) -> ProcessResult;
auto interpolationMode() const -> int { return m_interpolationMode; }
auto channels() const -> int { return m_channels; }
void setRatio(double ratio);
/**
* @brief Process a block of interleaved audio input from `input` and resample it into `output`.
*
* @param input The interleaved audio input.
* @param output The interleaved audio output.
*
* @throws `std::invalid_argument` if a channel mismatch has been detected.
* @throws `std::runtime_error` if the resampling process has failed.
*
* @remark This utility class does not cache the input and output buffers, making it stateless. In other words,
* `input` is directly resampled into the `output`.
*
* @returns the result of the resampling process. See @ref Result for more details.
*/
[[nodiscard]] auto process(InterleavedBufferView<const float> input, InterleavedBufferView<float> output) -> Result;
/**
* @brief Resets the internal resampler state.
* Useful when working with unreleated pieces of audio.
*/
void reset();
/**
* @brief Sets the resampling ratio to `ratio`.
* @param ratio Output sample rate divided by input sample rate.
*/
void setRatio(double ratio) { m_ratio = ratio; }
/**
* @brief Sets the resampling ratio to `output / input`.
* @param input Input sample rate.
* @param output Output sample rate.
*/
void setRatio(sample_rate_t input, sample_rate_t output) { m_ratio = static_cast<double>(output) / input; }
//! @returns the resampling ratio.
auto ratio() const -> double { return m_ratio; }
//! @returns the number of channels expected by the resampler.
auto channels() const -> ch_cnt_t { return m_channels; }
//! @returns the interpolation mode used by this resampler.
auto mode() const -> Mode { return m_mode; }
private:
int m_interpolationMode = -1;
int m_channels = 0;
struct LMMS_EXPORT StateDeleter { void operator()(void* state); };
std::unique_ptr<void, StateDeleter> m_state;
Mode m_mode;
ch_cnt_t m_channels = 0;
double m_ratio = 1.0;
int m_error = 0;
SRC_STATE* m_state = nullptr;
};
} // namespace lmms
#endif // LMMS_AUDIO_RESAMPLER_H

View File

@@ -55,7 +55,6 @@ public:
Effect( const Plugin::Descriptor * _desc,
Model * _parent,
const Descriptor::SubPluginFeatures::Key * _key );
~Effect() override;
void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override;
void loadSettings( const QDomElement & _this ) override;
@@ -176,29 +175,6 @@ protected:
gui::PluginView* instantiateView( QWidget * ) override;
// some effects might not be capable of higher sample-rates so they can
// sample it down before processing and back after processing
inline void sampleDown( const SampleFrame* _src_buf,
SampleFrame* _dst_buf,
sample_rate_t _dst_sr )
{
resample( 0, _src_buf,
Engine::audioEngine()->outputSampleRate(),
_dst_buf, _dst_sr,
Engine::audioEngine()->framesPerPeriod() );
}
inline void sampleBack( const SampleFrame* _src_buf,
SampleFrame* _dst_buf,
sample_rate_t _src_sr )
{
resample( 1, _src_buf, _src_sr, _dst_buf,
Engine::audioEngine()->outputSampleRate(),
Engine::audioEngine()->framesPerPeriod() * _src_sr /
Engine::audioEngine()->outputSampleRate() );
}
void reinitSRC();
virtual void onEnabledChanged() {}
@@ -212,10 +188,6 @@ private:
EffectChain * m_parent;
void resample( int _i, const SampleFrame* _src_buf,
sample_rate_t _src_sr,
SampleFrame* _dst_buf, sample_rate_t _dst_sr,
const f_cnt_t _frames );
bool m_okay;
bool m_noRun;
@@ -230,10 +202,6 @@ private:
bool m_autoQuitEnabled = false;
SRC_DATA m_srcData[2];
SRC_STATE * m_srcState[2];
friend class gui::EffectView;
friend class EffectChain;

View File

@@ -59,11 +59,7 @@ public:
AudioFileDeviceInstantiaton m_getDevInst;
} ;
ProjectRenderer( const AudioEngine::qualitySettings & _qs,
const OutputSettings & _os,
ExportFileFormat _file_format,
const QString & _out_file );
ProjectRenderer(const OutputSettings& _os, ExportFileFormat _file_format, const QString& _out_file);
~ProjectRenderer() override = default;
bool isReady() const
@@ -93,7 +89,6 @@ private:
void run() override;
AudioFileDevice * m_fileDev;
AudioEngine::qualitySettings m_qualitySettings;
volatile int m_progress;
volatile bool m_abort;

View File

@@ -40,11 +40,7 @@ class RenderManager : public QObject
{
Q_OBJECT
public:
RenderManager(
const AudioEngine::qualitySettings & qualitySettings,
const OutputSettings & outputSettings,
ProjectRenderer::ExportFileFormat fmt,
QString outputPath);
RenderManager(const OutputSettings& outputSettings, ProjectRenderer::ExportFileFormat fmt, QString outputPath);
~RenderManager() override;
@@ -70,8 +66,6 @@ private:
void render( QString outputPath );
const AudioEngine::qualitySettings m_qualitySettings;
const AudioEngine::qualitySettings m_oldQualitySettings;
const OutputSettings m_outputSettings;
ProjectRenderer::ExportFileFormat m_format;
QString m_outputPath;

View File

@@ -1,7 +1,7 @@
/*
* Sample.h - State for container-class SampleBuffer
* Sample.h
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
* Copyright (c) 2025 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
@@ -36,12 +36,6 @@ namespace lmms {
class LMMS_EXPORT Sample
{
public:
// values for buffer margins, used for various libsamplerate interpolation modes
// the array positions correspond to the converter_type parameter values in libsamplerate
// if there appears problems with playback on some interpolation mode, then the value for that mode
// may need to be higher - conversely, to optimize, some may work with lower values
static constexpr auto s_interpolationMargins = std::array<int, 5>{64, 64, 64, 4, 4};
enum class Loop
{
Off,
@@ -52,42 +46,41 @@ public:
class LMMS_EXPORT PlaybackState
{
public:
PlaybackState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR)
: m_resampler(interpolationMode, DEFAULT_CHANNELS)
, m_varyingPitch(varyingPitch)
PlaybackState(AudioResampler::Mode interpolationMode = AudioResampler::Mode::Linear, int frameIndex = 0)
: m_resampler(interpolationMode)
, m_frameIndex(frameIndex)
{
}
auto resampler() -> AudioResampler& { return m_resampler; }
auto frameIndex() const -> int { return m_frameIndex; }
auto varyingPitch() const -> bool { return m_varyingPitch; }
auto backwards() const -> bool { return m_backwards; }
void setFrameIndex(int frameIndex) { m_frameIndex = frameIndex; }
void setVaryingPitch(bool varyingPitch) { m_varyingPitch = varyingPitch; }
void setFrameIndex(int index) { m_frameIndex = index; }
void setBackwards(bool backwards) { m_backwards = backwards; }
private:
AudioResampler m_resampler;
std::array<SampleFrame, DEFAULT_BUFFER_SIZE> m_buffer;
std::span<SampleFrame> m_bufferView;
int m_frameIndex = 0;
bool m_varyingPitch = false;
bool m_backwards = false;
friend class Sample;
};
Sample() = default;
Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->outputSampleRate());
Sample(const SampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->outputSampleRate());
Sample(const Sample& other);
Sample(Sample&& other);
Sample(Sample&& other) noexcept;
explicit Sample(const QString& audioFile);
explicit Sample(std::shared_ptr<const SampleBuffer> buffer);
auto operator=(const Sample&) -> Sample&;
auto operator=(Sample&&) -> Sample&;
auto operator=(Sample&&) noexcept -> Sample&;
auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency = DefaultBaseFreq,
Loop loopMode = Loop::Off) const -> bool;
auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, Loop loopMode = Loop::Off,
double ratio = 1.0) const -> bool;
auto sampleDuration() const -> std::chrono::milliseconds;
auto sampleFile() const -> const QString& { return m_buffer->audioFile(); }
@@ -116,10 +109,7 @@ public:
void setReversed(bool reversed) { m_reversed.store(reversed, std::memory_order_relaxed); }
private:
void playRaw(SampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const;
void advance(PlaybackState* state, size_t advanceAmount, Loop loopMode) const;
private:
f_cnt_t render(SampleFrame* dst, f_cnt_t size, PlaybackState* state, Loop loop) const;
std::shared_ptr<const SampleBuffer> m_buffer = SampleBuffer::emptyBuffer();
std::atomic<int> m_startFrame = 0;
std::atomic<int> m_endFrame = 0;