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;

View File

@@ -131,20 +131,21 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n,
m_nextPlayBackwards = false;
}
// set interpolation mode for libsamplerate
int srcmode = SRC_LINEAR;
auto interpolationMode = AudioResampler::Mode::Linear;
switch( m_interpolationModel.value() )
{
case 0:
srcmode = SRC_ZERO_ORDER_HOLD;
interpolationMode = AudioResampler::Mode::ZOH;
break;
case 1:
srcmode = SRC_LINEAR;
interpolationMode = AudioResampler::Mode::Linear;
break;
case 2:
srcmode = SRC_SINC_MEDIUM_QUALITY;
interpolationMode = AudioResampler::Mode::SincMedium;
break;
}
_n->m_pluginData = new Sample::PlaybackState(_n->hasDetuningInfo(), srcmode);
_n->m_pluginData = new Sample::PlaybackState(interpolationMode);
static_cast<Sample::PlaybackState*>(_n->m_pluginData)->setFrameIndex(m_nextPlayStartPoint);
static_cast<Sample::PlaybackState*>(_n->m_pluginData)->setBackwards(m_nextPlayBackwards);
@@ -158,8 +159,8 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n,
{
if (m_sample.play(_working_buffer + offset,
static_cast<Sample::PlaybackState*>(_n->m_pluginData),
frames, _n->frequency(),
static_cast<Sample::Loop>(m_loopModel.value())))
frames, static_cast<Sample::Loop>(m_loopModel.value()),
DefaultBaseFreq / _n->frequency()))
{
applyRelease( _working_buffer, _n );
emit isPlaying(static_cast<Sample::PlaybackState*>(_n->m_pluginData)->frameIndex());

View File

@@ -44,6 +44,7 @@
#include "InstrumentTrack.h"
#include "InstrumentPlayHandle.h"
#include "Knob.h"
#include "MixHelpers.h"
#include "NotePlayHandle.h"
#include "PathUtil.h"
#include "Sample.h"
@@ -77,20 +78,16 @@ Plugin::Descriptor PLUGIN_EXPORT gigplayer_plugin_descriptor =
}
GigInstrument::GigInstrument( InstrumentTrack * _instrument_track ) :
Instrument(_instrument_track, &gigplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsNotBendable),
m_instance( nullptr ),
m_instrument( nullptr ),
m_filename( "" ),
m_bankNum( 0, 0, 999, this, tr( "Bank" ) ),
m_patchNum( 0, 0, 127, this, tr( "Patch" ) ),
m_gain( 1.0f, 0.0f, 5.0f, 0.01f, this, tr( "Gain" ) ),
m_interpolation( SRC_LINEAR ),
m_RandomSeed( 0 ),
m_currentKeyDimension( 0 )
GigInstrument::GigInstrument(InstrumentTrack* _instrument_track)
: Instrument(_instrument_track, &gigplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsNotBendable)
, m_instance(nullptr)
, m_instrument(nullptr)
, m_filename("")
, m_bankNum(0, 0, 999, this, tr("Bank"))
, m_patchNum(0, 0, 127, this, tr("Patch"))
, m_gain(1.0f, 0.0f, 5.0f, 0.01f, this, tr("Gain"))
, m_RandomSeed(0)
, m_currentKeyDimension(0)
{
auto iph = new InstrumentPlayHandle(this, _instrument_track);
Engine::audioEngine()->addPlayHandle( iph );
@@ -373,8 +370,7 @@ void GigInstrument::play( SampleFrame* _working_buffer )
}
// Delete ended samples
for( QList<GigSample>::iterator sample = it->samples.begin();
sample != it->samples.end(); ++sample )
for (auto sample = it->samples.begin(); sample != it->samples.end(); ++sample)
{
// Delete if the ADSR for a sample is complete for normal
// notes, or if a release sample, then if we've reached
@@ -417,71 +413,67 @@ void GigInstrument::play( SampleFrame* _working_buffer )
{
if (sample.sample == nullptr || sample.region == nullptr) { continue; }
// Will change if resampling
bool resample = false;
f_cnt_t samples = frames; // How many to grab
f_cnt_t used = frames; // How many we used
float freq_factor = 1.0; // How to resample
float freq_factor = 1.0; // How much to resample
// Resample to be the correct pitch when the sample provided isn't
// solely for this one note (e.g. one or two samples per octave) or
// we are processing at a different sample rate
if (sample.region->PitchTrack == true || rate != sample.sample->SamplesPerSecond)
{
resample = true;
// Factor just for resampling
freq_factor = 1.0 * rate / sample.sample->SamplesPerSecond;
// Factor for pitch shifting as well as resampling
if (sample.region->PitchTrack == true) { freq_factor *= sample.freqFactor; }
// We need a bit of margin so we don't get glitching
samples = frames / freq_factor + Sample::s_interpolationMargins[m_interpolation];
}
// Load this note's data
SampleFrame sampleData[samples];
loadSample(sample, sampleData, samples);
// Apply ADSR using a copy so if we don't use these samples when
// resampling, the ADSR doesn't get messed up
ADSR copy = sample.adsr;
for( f_cnt_t i = 0; i < samples; ++i )
{
float amplitude = copy.value();
sampleData[i][0] *= amplitude;
sampleData[i][1] *= amplitude;
}
sample.m_resampler.setRatio(freq_factor);
// Output the data resampling if needed
if( resample == true )
// 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.
auto framesMixed = f_cnt_t{0};
while (framesMixed < frames)
{
SampleFrame convertBuf[frames];
// Only output if resampling is successful (note that "used" is output)
if (sample.convertSampleRate(*sampleData, *convertBuf, samples, frames, freq_factor, used))
if (sample.m_sourceBufferView.empty())
{
for( f_cnt_t i = 0; i < frames; ++i )
loadSample(sample, sample.m_sourceBuffer.data(), sample.m_sourceBuffer.size());
for (auto& frame : sample.m_sourceBuffer)
{
_working_buffer[i][0] += convertBuf[i][0];
_working_buffer[i][1] += convertBuf[i][1];
frame *= copy.value();
}
}
}
else
{
for( f_cnt_t i = 0; i < frames; ++i )
{
_working_buffer[i][0] += sampleData[i][0];
_working_buffer[i][1] += sampleData[i][1];
}
}
// Update note position with how many samples we actually used
sample.pos += used;
sample.adsr.inc(used);
sample.pos += sample.m_sourceBuffer.size();
sample.adsr.inc(sample.m_sourceBuffer.size());
sample.m_sourceBufferView = sample.m_sourceBuffer;
}
if (sample.m_mixBufferView.empty()) { sample.m_mixBufferView = sample.m_mixBuffer; }
const auto [inputFramesUsed, outputFramesGenerated] = sample.m_resampler.process(
{&sample.m_sourceBufferView.data()[0][0], 2, sample.m_sourceBufferView.size()},
{&sample.m_mixBufferView.data()[0][0], 2, sample.m_mixBufferView.size()});
if (inputFramesUsed == 0 && outputFramesGenerated == 0)
{
std::fill_n(&_working_buffer[framesMixed], frames - framesMixed, SampleFrame{});
break;
}
const auto framesToMix = std::min(outputFramesGenerated, frames - framesMixed);
for (auto i = f_cnt_t{0}; i < framesToMix; ++i)
{
_working_buffer[framesMixed + i] += sample.m_mixBufferView[i];
}
sample.m_sourceBufferView = sample.m_sourceBufferView.subspan(inputFramesUsed);
sample.m_mixBufferView = sample.m_mixBufferView.subspan(framesToMix);
framesMixed += framesToMix;
}
}
}
@@ -760,8 +752,7 @@ void GigInstrument::addSamples( GigNote & gignote, bool wantReleaseSample )
attenuation *= pDimRegion->SampleAttenuation;
}
gignote.samples.push_back( GigSample( pSample, pDimRegion,
attenuation, m_interpolation, gignote.frequency ) );
gignote.samples.emplace_back(pSample, pDimRegion, attenuation, AudioResampler::Mode::Linear, gignote.frequency);
}
}
@@ -1083,19 +1074,18 @@ void GigInstrumentView::showPatchDialog()
// Store information related to playing a sample from the GIG file
GigSample::GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion,
float attenuation, int interpolation, float desiredFreq )
: sample( pSample ), region( pDimRegion ), attenuation( attenuation ),
pos( 0 ), interpolation( interpolation ), srcState( nullptr ),
sampleFreq( 0 ), freqFactor( 1 )
GigSample::GigSample(gig::Sample* pSample, gig::DimensionRegion* pDimRegion, float attenuation,
AudioResampler::Mode interpolation, float desiredFreq)
: sample(pSample)
, region(pDimRegion)
, attenuation(attenuation)
, pos(0)
, m_resampler(interpolation)
, sampleFreq(0)
, freqFactor(1)
{
if( sample != nullptr && region != nullptr )
{
// Note: we don't create the libsamplerate object here since we always
// also call the copy constructor when appending to the end of the
// QList. We'll create it only in the copy constructor so we only have
// to create it once.
// Calculate note pitch and frequency factor only if we're actually
// going to be changing the pitch of the notes
if( region->PitchTrack == true )
@@ -1112,27 +1102,16 @@ GigSample::GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion,
}
}
GigSample::~GigSample()
GigSample::GigSample(const GigSample& g)
: sample(g.sample)
, region(g.region)
, attenuation(g.attenuation)
, adsr(g.adsr)
, pos(g.pos)
, m_resampler(AudioResampler::Mode::Linear, DEFAULT_CHANNELS)
, sampleFreq(g.sampleFreq)
, freqFactor(g.freqFactor)
{
if( srcState != nullptr )
{
src_delete( srcState );
}
}
GigSample::GigSample( const GigSample& g )
: sample( g.sample ), region( g.region ), attenuation( g.attenuation ),
adsr( g.adsr ), pos( g.pos ), interpolation( g.interpolation ),
srcState( nullptr ), sampleFreq( g.sampleFreq ), freqFactor( g.freqFactor )
{
// On the copy, we want to create the object
updateSampleRate();
}
@@ -1145,88 +1124,11 @@ GigSample& GigSample::operator=( const GigSample& g )
attenuation = g.attenuation;
adsr = g.adsr;
pos = g.pos;
interpolation = g.interpolation;
srcState = nullptr;
sampleFreq = g.sampleFreq;
freqFactor = g.freqFactor;
if( g.srcState != nullptr )
{
updateSampleRate();
}
return *this;
}
void GigSample::updateSampleRate()
{
if( srcState != nullptr )
{
src_delete( srcState );
}
int error = 0;
srcState = src_new( interpolation, DEFAULT_CHANNELS, &error );
if( srcState == nullptr || error != 0 )
{
qCritical( "error while creating libsamplerate data structure in GigSample" );
}
}
bool GigSample::convertSampleRate( SampleFrame & oldBuf, SampleFrame & newBuf,
f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used )
{
if( srcState == nullptr )
{
return false;
}
SRC_DATA src_data;
src_data.data_in = &oldBuf[0];
src_data.data_out = &newBuf[0];
src_data.input_frames = oldSize;
src_data.output_frames = newSize;
src_data.src_ratio = freq_factor;
src_data.end_of_input = 0;
// We don't need to lock this assuming that we're only outputting the
// samples in one thread
int error = src_process( srcState, &src_data );
used = src_data.input_frames_used;
if( error != 0 )
{
qCritical( "GigInstrument: error while resampling: %s", src_strerror( error ) );
return false;
}
if( oldSize != 0 && src_data.output_frames_gen == 0 )
{
qCritical( "GigInstrument: could not resample, no frames generated" );
return false;
}
if (src_data.output_frames_gen > 0 && static_cast<f_cnt_t>(src_data.output_frames_gen) < newSize)
{
qCritical() << "GigInstrument: not enough frames, wanted"
<< newSize << "generated" << src_data.output_frames_gen;
return false;
}
return true;
}
ADSR::ADSR()
: preattack( 0 ), attack( 0 ), decay1( 0 ), decay2( 0 ), infiniteSustain( false ),
sustain( 0 ), release( 0 ),

View File

@@ -32,12 +32,14 @@
#include <QMutexLocker>
#include <samplerate.h>
#include "AudioEngine.h"
#include "AudioResampler.h"
#include "Instrument.h"
#include "PixmapButton.h"
#include "InstrumentView.h"
#include "Knob.h"
#include "LcdSpinBox.h"
#include "LedCheckBox.h"
#include "SampleFrame.h"
#include "gig.h"
@@ -147,19 +149,14 @@ public:
class GigSample
{
public:
GigSample( gig::Sample * pSample, gig::DimensionRegion * pDimRegion,
float attenuation, int interpolation, float desiredFreq );
~GigSample();
GigSample(gig::Sample* pSample, gig::DimensionRegion* pDimRegion, float attenuation,
AudioResampler::Mode interpolation, float desiredFreq);
~GigSample() = default;
// Needed when initially creating in QList
GigSample( const GigSample& g );
GigSample& operator=( const GigSample& g );
// Needed since libsamplerate stores data internally between calls
void updateSampleRate();
bool convertSampleRate( SampleFrame & oldBuf, SampleFrame & newBuf,
f_cnt_t oldSize, f_cnt_t newSize, float freq_factor, f_cnt_t& used );
gig::Sample * sample;
gig::DimensionRegion * region;
float attenuation;
@@ -174,8 +171,11 @@ public:
bool pitchtrack;
// Used to convert sample rates
int interpolation;
SRC_STATE * srcState;
AudioResampler m_resampler;
std::array<SampleFrame, DEFAULT_BUFFER_SIZE> m_sourceBuffer;
std::array<SampleFrame, DEFAULT_BUFFER_SIZE> m_mixBuffer;
std::span<SampleFrame> m_sourceBufferView;
std::span<SampleFrame> m_mixBufferView;
// Used changing the pitch of the note if desired
float sampleFreq;
@@ -213,7 +213,7 @@ public:
bool isRelease; // Whether this is a release sample, changes when we delete it
GigState state;
float frequency;
QList<GigSample> samples;
std::vector<GigSample> samples;
// Used to determine which note should be released on key up
//
@@ -290,9 +290,6 @@ private:
QMutex m_synthMutex;
QMutex m_notesMutex;
// Used for resampling
int m_interpolation;
// List of all the currently playing notes
QList<GigNote> m_notes;

View File

@@ -66,13 +66,10 @@ Plugin::Descriptor PLUGIN_EXPORT ladspaeffect_plugin_descriptor =
}
LadspaEffect::LadspaEffect( Model * _parent,
const Descriptor::SubPluginFeatures::Key * _key ) :
Effect( &ladspaeffect_plugin_descriptor, _parent, _key ),
m_controls( nullptr ),
m_maxSampleRate( 0 ),
m_key( LadspaSubPluginFeatures::subPluginKeyToLadspaKey( _key ) )
LadspaEffect::LadspaEffect(Model* _parent, const Descriptor::SubPluginFeatures::Key* _key)
: Effect(&ladspaeffect_plugin_descriptor, _parent, _key)
, m_controls(nullptr)
, m_key(LadspaSubPluginFeatures::subPluginKeyToLadspaKey(_key))
{
Ladspa2LMMS * manager = Engine::getLADSPAManager();
if( manager->getDescription( m_key ) == nullptr )
@@ -137,19 +134,6 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
return ProcessStatus::Sleep;
}
auto outFrames = frames;
SampleFrame* outBuf = nullptr;
QVarLengthArray<SampleFrame> sBuf(frames);
if( m_maxSampleRate < Engine::audioEngine()->outputSampleRate() )
{
outBuf = buf;
buf = sBuf.data();
sampleDown(outBuf, buf, m_maxSampleRate);
outFrames = frames * m_maxSampleRate /
Engine::audioEngine()->outputSampleRate();
}
// Copy the LMMS audio buffer to the LADSPA input buffer and initialize
// the control ports.
ch_cnt_t channel = 0;
@@ -161,7 +145,7 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
switch( pp->rate )
{
case BufferRate::ChannelIn:
for (fpp_t frame = 0; frame < outFrames; ++frame)
for (fpp_t frame = 0; frame < frames; ++frame)
{
pp->buffer[frame] = buf[frame][channel];
}
@@ -172,7 +156,7 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
ValueBuffer * vb = pp->control->valueBuffer();
if( vb )
{
memcpy(pp->buffer, vb->values(), outFrames * sizeof(float));
memcpy(pp->buffer, vb->values(), frames * sizeof(float));
}
else
{
@@ -181,7 +165,7 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
// This only supports control rate ports, so the audio rates are
// treated as though they were control rate by setting the
// port buffer to all the same value.
for (fpp_t frame = 0; frame < outFrames; ++frame)
for (fpp_t frame = 0; frame < frames; ++frame)
{
pp->buffer[frame] = pp->value;
}
@@ -212,7 +196,7 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
// Process the buffers.
for( ch_cnt_t proc = 0; proc < processorCount(); ++proc )
{
(m_descriptor->run)(m_handles[proc], outFrames);
(m_descriptor->run)(m_handles[proc], frames);
}
// Copy the LADSPA output buffers to the LMMS buffer.
@@ -231,7 +215,7 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
case BufferRate::ControlRateInput:
break;
case BufferRate::ChannelOut:
for (fpp_t frame = 0; frame < outFrames; ++frame)
for (fpp_t frame = 0; frame < frames; ++frame)
{
buf[frame][channel] = d * buf[frame][channel] + w * pp->buffer[frame];
}
@@ -246,11 +230,6 @@ Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t fr
}
}
if (outBuf != nullptr)
{
sampleBack(buf, outBuf, m_maxSampleRate);
}
m_pluginMutex.unlock();
return ProcessStatus::ContinueIfNotQuiet;
@@ -273,8 +252,6 @@ void LadspaEffect::setControl( int _control, LADSPA_Data _value )
void LadspaEffect::pluginInstantiation()
{
m_maxSampleRate = maxSamplerate( displayName() );
Ladspa2LMMS * manager = Engine::getLADSPAManager();
// Calculate how many processing units are needed.
@@ -406,7 +383,7 @@ void LadspaEffect::pluginInstantiation()
if( manager->areHintsSampleRateDependent(
m_key, port ) )
{
p->max *= m_maxSampleRate;
p->max *= Engine::audioEngine()->outputSampleRate();
}
p->min = manager->getLowerBound( m_key, port );
@@ -418,7 +395,7 @@ void LadspaEffect::pluginInstantiation()
if( manager->areHintsSampleRateDependent(
m_key, port ) )
{
p->min *= m_maxSampleRate;
p->min *= Engine::audioEngine()->outputSampleRate();
}
p->def = manager->getDefaultSetting( m_key, port );
@@ -435,7 +412,7 @@ void LadspaEffect::pluginInstantiation()
}
else if( manager->areHintsSampleRateDependent( m_key, port ) )
{
p->def *= m_maxSampleRate;
p->def *= Engine::audioEngine()->outputSampleRate();
}
@@ -480,8 +457,7 @@ void LadspaEffect::pluginInstantiation()
}
for( ch_cnt_t proc = 0; proc < processorCount(); proc++ )
{
LADSPA_Handle effect = manager->instantiate( m_key,
m_maxSampleRate );
LADSPA_Handle effect = manager->instantiate(m_key, Engine::audioEngine()->outputSampleRate());
if( effect == nullptr )
{
QMessageBox::warning( 0, "Effect",
@@ -554,32 +530,6 @@ void LadspaEffect::pluginDestruction()
m_portControls.clear();
}
static QMap<QString, sample_rate_t> __buggy_plugins;
sample_rate_t LadspaEffect::maxSamplerate( const QString & _name )
{
if( __buggy_plugins.isEmpty() )
{
__buggy_plugins["C* AmpVTS"] = 88200;
__buggy_plugins["Chorus2"] = 44100;
__buggy_plugins["Notch Filter"] = 96000;
__buggy_plugins["TAP Reflector"] = 192000;
}
if( __buggy_plugins.contains( _name ) )
{
return( __buggy_plugins[_name] );
}
return( Engine::audioEngine()->outputSampleRate() );
}
extern "C"
{

View File

@@ -76,11 +76,9 @@ private:
static sample_rate_t maxSamplerate( const QString & _name );
QMutex m_pluginMutex;
LadspaControls * m_controls;
sample_rate_t m_maxSampleRate;
ladspa_key_t m_key;
int m_portCount;
bool m_inPlaceBroken;

View File

@@ -154,7 +154,7 @@ void PatmanInstrument::playNote( NotePlayHandle * _n,
hdata->sample->frequency();
if (hdata->sample->play(_working_buffer + offset, hdata->state, frames,
play_freq, m_loopedModel.value() ? Sample::Loop::On : Sample::Loop::Off))
m_loopedModel.value() ? Sample::Loop::On : Sample::Loop::Off, DefaultBaseFreq / play_freq))
{
applyRelease( _working_buffer, _n );
}
@@ -407,7 +407,7 @@ void PatmanInstrument::selectSample( NotePlayHandle * _n )
auto hdata = new handle_data;
hdata->tuned = m_tunedModel.value();
hdata->sample = sample ? sample : std::make_shared<Sample>();
hdata->state = new Sample::PlaybackState(_n->hasDetuningInfo());
hdata->state = new Sample::PlaybackState(AudioResampler::Mode::Linear);
_n->m_pluginData = hdata;
}

View File

@@ -123,7 +123,7 @@ struct Sf2PluginData
Sf2Instrument::Sf2Instrument( InstrumentTrack * _instrument_track ) :
Instrument(_instrument_track, &sf2player_plugin_descriptor, nullptr, Flag::IsSingleStreamed),
m_srcState( nullptr ),
m_resampler(AudioResampler::Mode::Linear),
m_synth(nullptr),
m_font( nullptr ),
m_fontId( 0 ),
@@ -235,11 +235,6 @@ Sf2Instrument::~Sf2Instrument()
freeFont();
delete_fluid_synth( m_synth );
delete_fluid_settings( m_settings );
if( m_srcState != nullptr )
{
src_delete( m_srcState );
}
}
@@ -590,7 +585,9 @@ void Sf2Instrument::reloadSynth()
// Set & get, returns the true sample rate
fluid_settings_setnum( m_settings, (char *) "synth.sample-rate", Engine::audioEngine()->outputSampleRate() );
fluid_settings_getnum( m_settings, (char *) "synth.sample-rate", &tempRate );
m_internalSampleRate = static_cast<int>( tempRate );
m_resampler.setRatio(m_internalSampleRate, Engine::audioEngine()->outputSampleRate());
if( m_font )
{
@@ -620,31 +617,19 @@ void Sf2Instrument::reloadSynth()
}
m_synthMutex.lock();
if( Engine::audioEngine()->currentQualitySettings().interpolation >=
AudioEngine::qualitySettings::Interpolation::SincFastest )
if (m_internalSampleRate != Engine::audioEngine()->outputSampleRate())
{
fluid_synth_set_interp_method( m_synth, -1, FLUID_INTERP_7THORDER );
}
else
{
fluid_synth_set_interp_method( m_synth, -1, FLUID_INTERP_DEFAULT );
// LMMS supports a sample rate of 192 kHZ, while FluidSynth only supports up to 96 kHZ.
// Because of this, the instrument is resampled using libsamplerate when necessary.
// This uses linear interpolation, so the instrument's interpolation is set to FLUID_INTERP_LINEAR
// to match. A better option might be to make the interpolation option modifiable by the user, as well as only
// supporting only up to 96 kHZ (though that may be a problem if theres a strong need for 192 kHZ).
fluid_synth_set_interp_method(m_synth, -1, FLUID_INTERP_LINEAR);
}
m_synthMutex.unlock();
if( m_internalSampleRate < Engine::audioEngine()->outputSampleRate() )
{
m_synthMutex.lock();
if( m_srcState != nullptr )
{
src_delete( m_srcState );
}
int error;
m_srcState = src_new( Engine::audioEngine()->currentQualitySettings().libsrcInterpolation(), DEFAULT_CHANNELS, &error );
if( m_srcState == nullptr || error )
{
qCritical("error while creating libsamplerate data structure in Sf2Instrument::reloadSynth()");
}
m_synthMutex.unlock();
}
updateReverb();
updateChorus();
updateReverbOn();
@@ -884,44 +869,38 @@ void Sf2Instrument::play( SampleFrame* _working_buffer )
void Sf2Instrument::renderFrames( f_cnt_t frames, SampleFrame* buf )
{
m_synthMutex.lock();
fluid_synth_get_gain(m_synth); // This flushes voice updates as a side effect
if( m_internalSampleRate < Engine::audioEngine()->outputSampleRate() &&
m_srcState != nullptr )
{
const fpp_t f = frames * m_internalSampleRate / Engine::audioEngine()->outputSampleRate();
#ifdef __GNUC__
SampleFrame tmp[f];
#else
SampleFrame* tmp = new SampleFrame[f];
#endif
fluid_synth_write_float( m_synth, f, tmp, 0, 2, tmp, 1, 2 );
const auto guard = std::lock_guard{m_synthMutex};
SRC_DATA src_data;
src_data.data_in = (float *)tmp;
src_data.data_out = (float *)buf;
src_data.input_frames = f;
src_data.output_frames = frames;
src_data.src_ratio = (double) frames / f;
src_data.end_of_input = 0;
int error = src_process( m_srcState, &src_data );
#ifndef __GNUC__
delete[] tmp;
#endif
if( error )
{
qCritical( "Sf2Instrument: error while resampling: %s", src_strerror( error ) );
}
if (static_cast<f_cnt_t>(src_data.output_frames_gen) < frames)
{
qCritical("Sf2Instrument: not enough frames: %ld / %zu", src_data.output_frames_gen, frames);
}
fluid_synth_get_gain(m_synth); // This flushes voice updates as a side effect
if (m_internalSampleRate == Engine::audioEngine()->outputSampleRate()) {
fluid_synth_write_float(m_synth, frames, buf, 0, 2, buf, 1, 2);
return;
}
else
// 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 (frames > 0)
{
fluid_synth_write_float( m_synth, frames, buf, 0, 2, buf, 1, 2 );
if (m_bufferView.empty())
{
fluid_synth_write_float(m_synth, m_buffer.size(), m_buffer.data(), 0, 2, m_buffer.data(), 1, 2);
m_bufferView = m_buffer;
}
const auto [inputFramesUsed, outputFramesGenerated]
= m_resampler.process({&m_bufferView.data()[0][0], 2, m_bufferView.size()}, {&buf[0][0], 2, frames});
if (inputFramesUsed == 0 && outputFramesGenerated == 0)
{
std::fill_n(buf, frames, SampleFrame{});
break;
}
m_bufferView = m_bufferView.subspan(inputFramesUsed);
buf += outputFramesGenerated;
frames -= outputFramesGenerated;
}
m_synthMutex.unlock();
}

View File

@@ -32,9 +32,12 @@
#include <QMutex>
#include <samplerate.h>
#include "AudioEngine.h"
#include "AudioResampler.h"
#include "Instrument.h"
#include "InstrumentView.h"
#include "LcdSpinBox.h"
#include "SampleFrame.h"
class QLabel;
@@ -103,7 +106,9 @@ public slots:
void updateTuning();
private:
SRC_STATE * m_srcState;
AudioResampler m_resampler;
std::array<SampleFrame, DEFAULT_BUFFER_SIZE> m_buffer;
std::span<SampleFrame> m_bufferView;
fluid_settings_t* m_settings;
fluid_synth_t* m_synth;

View File

@@ -89,7 +89,6 @@ void SlicerT::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer)
float speedRatio = static_cast<float>(m_originalBPM.value()) / bpm;
if (!m_enableSync.value()) { speedRatio = 1; }
speedRatio *= pitchRatio;
speedRatio *= Engine::audioEngine()->outputSampleRate() / static_cast<float>(m_originalSample.sampleRate());
float sliceStart, sliceEnd;
if (noteIndex == 0) // full sample at base note
@@ -109,35 +108,21 @@ void SlicerT::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer)
return;
}
if (!handle->m_pluginData) { handle->m_pluginData = new PlaybackState(sliceStart); }
auto playbackState = static_cast<PlaybackState*>(handle->m_pluginData);
const auto startFrame = static_cast<int>(sliceStart * m_originalSample.sampleSize());
if (!handle->m_pluginData) { handle->m_pluginData = new Sample::PlaybackState(AudioResampler::Mode::Linear, startFrame); }
float noteDone = playbackState->noteDone();
float noteLeft = sliceEnd - noteDone;
auto playbackState = static_cast<Sample::PlaybackState*>(handle->m_pluginData);
const auto endFrame = sliceEnd * m_originalSample.sampleSize();
const auto framesLeft = endFrame - playbackState->frameIndex();
if (noteLeft > 0)
if (framesLeft > 0
&& m_originalSample.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off, speedRatio))
{
int noteFrame = noteDone * m_originalSample.sampleSize();
SRC_STATE* resampleState = playbackState->resamplingState();
SRC_DATA resampleData;
resampleData.data_in = (m_originalSample.data() + noteFrame)->data();
resampleData.data_out = (workingBuffer + offset)->data();
resampleData.input_frames = noteLeft * m_originalSample.sampleSize();
resampleData.output_frames = frames;
resampleData.src_ratio = speedRatio;
src_process(resampleState, &resampleData);
float nextNoteDone = noteDone + frames * (1.0f / speedRatio) / m_originalSample.sampleSize();
playbackState->setNoteDone(nextNoteDone);
// exponential fade out, applyRelease() not used since it extends the note length
int fadeOutFrames = m_fadeOutFrames.value() / 1000.0f * Engine::audioEngine()->outputSampleRate();
int noteFramesLeft = noteLeft * m_originalSample.sampleSize() * speedRatio;
for (auto i = std::size_t{0}; i < frames; i++)
{
float fadeValue = static_cast<float>(noteFramesLeft - static_cast<int>(i)) / fadeOutFrames;
float fadeValue = static_cast<float>(framesLeft * speedRatio - static_cast<int>(i)) / fadeOutFrames;
fadeValue = std::clamp(fadeValue, 0.0f, 1.0f);
fadeValue = cosinusInterpolate(0, 1, fadeValue);
@@ -145,14 +130,15 @@ void SlicerT::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer)
workingBuffer[i + offset][1] *= fadeValue;
}
emit isPlaying(noteDone, sliceStart, sliceEnd);
const auto currentNote = static_cast<float>(playbackState->frameIndex()) / m_originalSample.sampleSize();
emit isPlaying(currentNote, sliceStart, sliceEnd);
}
else { emit isPlaying(-1, 0, 0); }
}
void SlicerT::deleteNotePluginData(NotePlayHandle* handle)
{
delete static_cast<PlaybackState*>(handle->m_pluginData);
delete static_cast<Sample::PlaybackState*>(handle->m_pluginData);
emit isPlaying(-1, 0, 0);
}

View File

@@ -25,43 +25,15 @@
#ifndef LMMS_SLICERT_H
#define LMMS_SLICERT_H
#include <stdexcept>
#include "AutomatableModel.h"
#include "ComboBoxModel.h"
#include "Instrument.h"
#include "Note.h"
#include "Sample.h"
#include "SlicerTView.h"
namespace lmms {
class InstrumentTrack;
namespace gui {
class SlicerTView;
class SlicerTWaveform;
}
class PlaybackState
{
public:
explicit PlaybackState(float startFrame)
: m_currentNoteDone(startFrame)
, m_resamplingState(src_new(SRC_LINEAR, DEFAULT_CHANNELS, nullptr))
{
if (!m_resamplingState) { throw std::runtime_error{"Failed to create sample rate converter object"}; }
}
~PlaybackState() noexcept { src_delete(m_resamplingState); }
float noteDone() const { return m_currentNoteDone; }
void setNoteDone(float newNoteDone) { m_currentNoteDone = newNoteDone; }
SRC_STATE* resamplingState() const { return m_resamplingState; }
private:
float m_currentNoteDone;
SRC_STATE* m_resamplingState;
};
class SlicerT : public Instrument
{
Q_OBJECT

View File

@@ -26,6 +26,7 @@
#ifndef WATSYN_H
#define WATSYN_H
#include "AudioResampler.h"
#include "Instrument.h"
#include "InstrumentView.h"
#include "Graph.h"
@@ -187,28 +188,23 @@ private:
}
// memcpy utilizing libsamplerate (src) for sinc interpolation
inline void srccpy( float * _dst, float * _src )
inline void srccpy(float* _dst, float* _src)
{
int err;
const int margin = 64;
// copy to temp array
float tmps [ GRAPHLEN + margin ]; // temp array in stack
float * tmp = &tmps[0];
auto srcIndex = f_cnt_t{0};
auto dstIndex = f_cnt_t{0};
memcpy( tmp, _src, sizeof( float ) * GRAPHLEN );
memcpy( tmp + GRAPHLEN, _src, sizeof( float ) * margin );
SRC_STATE * src_state = src_new( SRC_SINC_FASTEST, 1, &err );
SRC_DATA src_data;
src_data.data_in = tmp;
src_data.input_frames = GRAPHLEN + margin;
src_data.data_out = _dst;
src_data.output_frames = WAVELEN;
src_data.src_ratio = static_cast<double>( WAVERATIO );
src_data.end_of_input = 0;
err = src_process( src_state, &src_data );
if( err ) { qDebug( "Watsyn SRC error: %s", src_strerror( err ) ); }
src_delete( src_state );
m_resampler.reset();
m_resampler.setRatio(WAVERATIO);
while (dstIndex < WAVELEN)
{
const auto input = InterleavedBufferView<const float, 1>{_src + srcIndex, GRAPHLEN - srcIndex};
const auto output = InterleavedBufferView<float, 1>{_dst + dstIndex, WAVELEN - dstIndex};
const auto result = m_resampler.process(input, output);
srcIndex = (srcIndex + result.inputFramesUsed) % GRAPHLEN;
dstIndex += result.outputFramesGenerated;
}
}
// memcpy utilizing cubic interpolation
@@ -242,6 +238,7 @@ private:
}
}*/
AudioResampler m_resampler = AudioResampler{AudioResampler::Mode::SincFastest, 1};
FloatModel a1_vol;
FloatModel a2_vol;

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>