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