Fix audio resampling functionality (#7858)
Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 ),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
{
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() )
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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() );
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user