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

@@ -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;