Switch to libsamplerate's callback API in Sample (#7361)

This commit is contained in:
saker
2024-07-24 18:50:47 -04:00
committed by GitHub
parent 851c884f58
commit 2f5f12aaae
9 changed files with 69 additions and 265 deletions

View File

@@ -1,65 +0,0 @@
/*
* AudioResampler.h - wrapper around libsamplerate
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_AUDIO_RESAMPLER_H
#define LMMS_AUDIO_RESAMPLER_H
#include <samplerate.h>
#include "lmms_export.h"
namespace lmms {
class LMMS_EXPORT AudioResampler
{
public:
struct ProcessResult
{
int error;
long inputFramesUsed;
long outputFramesGenerated;
};
AudioResampler(int interpolationMode, int channels);
AudioResampler(const AudioResampler&) = delete;
AudioResampler(AudioResampler&&) = delete;
~AudioResampler();
AudioResampler& operator=(const AudioResampler&) = delete;
AudioResampler& operator=(AudioResampler&&) = delete;
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);
private:
int m_interpolationMode = -1;
int m_channels = 0;
int m_error = 0;
SRC_STATE* m_state = nullptr;
};
} // namespace lmms
#endif // LMMS_AUDIO_RESAMPLER_H

View File

@@ -25,24 +25,18 @@
#ifndef LMMS_SAMPLE_H
#define LMMS_SAMPLE_H
#include <cmath>
#include <memory>
#include <samplerate.h>
#include "AudioResampler.h"
#include "Note.h"
#include "SampleBuffer.h"
#include "lmms_basics.h"
#include "lmms_export.h"
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,
@@ -50,30 +44,25 @@ public:
PingPong
};
class LMMS_EXPORT PlaybackState
struct LMMS_EXPORT PlaybackState
{
public:
PlaybackState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR)
: m_resampler(interpolationMode, DEFAULT_CHANNELS)
, m_varyingPitch(varyingPitch)
PlaybackState(int interpolationMode = SRC_LINEAR)
: resampleState(src_callback_new(&Sample::render, interpolationMode, DEFAULT_CHANNELS, &error, this))
{
assert(resampleState && src_strerror(error));
}
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; }
~PlaybackState()
{
src_delete(resampleState);
}
void setFrameIndex(int frameIndex) { m_frameIndex = frameIndex; }
void setVaryingPitch(bool varyingPitch) { m_varyingPitch = varyingPitch; }
void setBackwards(bool backwards) { m_backwards = backwards; }
private:
AudioResampler m_resampler;
int m_frameIndex = 0;
bool m_varyingPitch = false;
bool m_backwards = false;
friend class Sample;
const Sample* sample = nullptr;
Loop* loop = nullptr;
SRC_STATE* resampleState = nullptr;
int frameIndex = 0;
int error = 0;
bool backwards = false;
};
Sample() = default;
@@ -87,7 +76,7 @@ public:
auto operator=(const Sample&) -> Sample&;
auto operator=(Sample&&) -> Sample&;
auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency = DefaultBaseFreq,
auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, double frequency = DefaultBaseFreq,
Loop loopMode = Loop::Off) const -> bool;
auto sampleDuration() const -> std::chrono::milliseconds;
@@ -117,17 +106,14 @@ 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:
static auto render(void* callbackData, float** data) -> long;
std::shared_ptr<const SampleBuffer> m_buffer = SampleBuffer::emptyBuffer();
std::atomic<int> m_startFrame = 0;
std::atomic<int> m_endFrame = 0;
std::atomic<int> m_loopStartFrame = 0;
std::atomic<int> m_loopEndFrame = 0;
std::atomic<float> m_amplification = 1.0f;
std::atomic<float> m_frequency = DefaultBaseFreq;
std::atomic<double> m_frequency = DefaultBaseFreq;
std::atomic<bool> m_reversed = false;
};
} // namespace lmms

View File

@@ -28,7 +28,6 @@
#include <QByteArray>
#include <QString>
#include <memory>
#include <optional>
#include <samplerate.h>
#include <vector>

View File

@@ -144,9 +144,9 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n,
srcmode = SRC_SINC_MEDIUM_QUALITY;
break;
}
_n->m_pluginData = new Sample::PlaybackState(_n->hasDetuningInfo(), srcmode);
static_cast<Sample::PlaybackState*>(_n->m_pluginData)->setFrameIndex(m_nextPlayStartPoint);
static_cast<Sample::PlaybackState*>(_n->m_pluginData)->setBackwards(m_nextPlayBackwards);
_n->m_pluginData = new Sample::PlaybackState(srcmode);
static_cast<Sample::PlaybackState*>(_n->m_pluginData)->frameIndex = m_nextPlayStartPoint;
static_cast<Sample::PlaybackState*>(_n->m_pluginData)->backwards = m_nextPlayBackwards;
// debug code
/* qDebug( "frames %d", m_sample->frames() );
@@ -162,7 +162,7 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n,
static_cast<Sample::Loop>(m_loopModel.value())))
{
applyRelease( _working_buffer, _n );
emit isPlaying(static_cast<Sample::PlaybackState*>(_n->m_pluginData)->frameIndex());
emit isPlaying(static_cast<Sample::PlaybackState*>(_n->m_pluginData)->frameIndex);
}
else
{
@@ -176,8 +176,8 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n,
}
if( m_stutterModel.value() == true )
{
m_nextPlayStartPoint = static_cast<Sample::PlaybackState*>(_n->m_pluginData)->frameIndex();
m_nextPlayBackwards = static_cast<Sample::PlaybackState*>(_n->m_pluginData)->backwards();
m_nextPlayStartPoint = static_cast<Sample::PlaybackState*>(_n->m_pluginData)->frameIndex;
m_nextPlayBackwards = static_cast<Sample::PlaybackState*>(_n->m_pluginData)->backwards;
}
}

View File

@@ -437,7 +437,7 @@ void GigInstrument::play( SampleFrame* _working_buffer )
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];
samples = frames / freq_factor + s_interpolationMargins[m_interpolation];
}
// Load this note's data

View File

@@ -240,6 +240,12 @@ class GigInstrument : public Instrument
mapPropertyFromModel( int, getPatch, setPatch, m_patchNum );
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};
GigInstrument( InstrumentTrack * _instrument_track );
~GigInstrument() override;

View File

@@ -1,69 +0,0 @@
/*
* AudioResampler.cpp - wrapper for libsamplerate
*
* Copyright (c) 2023 saker <sakertooth@gmail.com>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "AudioResampler.h"
#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))
{
if (!m_state)
{
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};
}
}
AudioResampler::~AudioResampler()
{
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);
}
} // namespace lmms

View File

@@ -4,7 +4,6 @@ set(LMMS_SRCS
core/AudioEngine.cpp
core/AudioEngineProfiler.cpp
core/AudioEngineWorkerThread.cpp
core/AudioResampler.cpp
core/AutomatableModel.cpp
core/AutomationClip.cpp
core/AutomationNode.cpp

View File

@@ -26,6 +26,8 @@
#include <cassert>
#include "MixHelpers.h"
namespace lmms {
Sample::Sample(const QString& audioFile)
@@ -116,43 +118,28 @@ 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, double frequency, Loop loopMode) const
{
assert(numFrames > 0);
assert(desiredFrequency > 0);
assert(frequency > 0);
if (m_buffer->empty()) { return false; }
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 outputSampleRate = Engine::audioEngine()->outputSampleRate() * m_frequency / frequency;
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);
state->frameIndex = std::max<int>(m_startFrame, state->frameIndex);
state->sample = this;
state->loop = &loopMode;
auto playBuffer = std::vector<SampleFrame>(numFrames / resampleRatio + marginSize);
playRaw(playBuffer.data(), playBuffer.size(), state, loopMode);
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 (!typeInfo<float>::isEqual(m_amplification, 1.0f))
src_set_ratio(state->resampleState, resampleRatio);
if (src_callback_read(state->resampleState, resampleRatio, numFrames, &dst[0][0]) != 0)
{
for (auto i = std::size_t{0}; i < numFrames; ++i)
{
dst[i][0] *= m_amplification;
dst[i][1] *= m_amplification;
}
MixHelpers::multiply(dst, m_amplification, numFrames);
return true;
}
return true;
return false;
}
auto Sample::sampleDuration() const -> std::chrono::milliseconds
@@ -170,82 +157,43 @@ 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
long Sample::render(void* callbackData, float** data)
{
if (m_buffer->size() < 1) { return; }
const auto state = static_cast<PlaybackState*>(callbackData);
const auto loop = *state->loop;
const auto sample = state->sample;
auto& index = state->frameIndex;
auto& backwards = state->backwards;
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)
switch (loop)
{
case Loop::Off:
if (index < 0 || index >= sample->m_endFrame) { return 0; }
break;
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;
}
if (index < sample->m_loopStartFrame && state->backwards) { index = sample->m_loopEndFrame - 1; }
else if (index >= sample->m_loopEndFrame) { index = sample->m_loopStartFrame; }
break;
case Loop::PingPong:
if (state->m_frameIndex < m_loopStartFrame && state->m_backwards)
if (index < sample->m_loopStartFrame && state->backwards)
{
state->m_frameIndex = m_loopStartFrame + distanceFromLoopStart % loopSize;
state->m_backwards = false;
index = sample->m_loopStartFrame;
backwards = false;
}
else if (state->m_frameIndex >= m_loopEndFrame)
else if (index >= sample->m_loopEndFrame)
{
state->m_frameIndex = m_loopEndFrame - 1 - distanceFromLoopEnd % loopSize;
state->m_backwards = true;
index = sample->m_loopEndFrame - 1;
backwards = true;
}
break;
default:
break;
}
const auto srcIndex = sample->m_reversed ? sample->m_buffer->size() - index - 1 : index;
*data = const_cast<float*>(&sample->m_buffer->data()[srcIndex][0]);
backwards ? --index : ++index;
return 1;
}
} // namespace lmms