diff --git a/include/AudioResampler.h b/include/AudioResampler.h new file mode 100644 index 000000000..6dd6fcc60 --- /dev/null +++ b/include/AudioResampler.h @@ -0,0 +1,65 @@ +/* + * AudioResampler.h - wrapper around libsamplerate + * + * Copyright (c) 2023 saker + * + * 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 + +#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 diff --git a/include/Sample.h b/include/Sample.h index 754350368..3fd5bc38e 100644 --- a/include/Sample.h +++ b/include/Sample.h @@ -25,18 +25,24 @@ #ifndef LMMS_SAMPLE_H #define LMMS_SAMPLE_H +#include #include -#include +#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{64, 64, 64, 4, 4}; + enum class Loop { Off, @@ -44,25 +50,30 @@ public: PingPong }; - struct LMMS_EXPORT PlaybackState + class LMMS_EXPORT PlaybackState { - PlaybackState(int interpolationMode = SRC_LINEAR) - : resampleState(src_callback_new(&Sample::render, interpolationMode, DEFAULT_CHANNELS, &error, this)) + public: + PlaybackState(bool varyingPitch = false, int interpolationMode = SRC_LINEAR) + : m_resampler(interpolationMode, DEFAULT_CHANNELS) + , m_varyingPitch(varyingPitch) { - assert(resampleState && src_strerror(error)); } - ~PlaybackState() - { - src_delete(resampleState); - } + 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; } - const Sample* sample = nullptr; - Loop* loop = nullptr; - SRC_STATE* resampleState = nullptr; - int frameIndex = 0; - int error = 0; - bool backwards = false; + 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; }; Sample() = default; @@ -76,7 +87,7 @@ public: auto operator=(const Sample&) -> Sample&; auto operator=(Sample&&) -> Sample&; - auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, double frequency = DefaultBaseFreq, + auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency = DefaultBaseFreq, Loop loopMode = Loop::Off) const -> bool; auto sampleDuration() const -> std::chrono::milliseconds; @@ -106,14 +117,17 @@ public: void setReversed(bool reversed) { m_reversed.store(reversed, std::memory_order_relaxed); } private: - static auto render(void* callbackData, float** data) -> long; + void playRaw(SampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const; + void advance(PlaybackState* state, size_t advanceAmount, Loop loopMode) const; + +private: std::shared_ptr m_buffer = SampleBuffer::emptyBuffer(); std::atomic m_startFrame = 0; std::atomic m_endFrame = 0; std::atomic m_loopStartFrame = 0; std::atomic m_loopEndFrame = 0; std::atomic m_amplification = 1.0f; - std::atomic m_frequency = DefaultBaseFreq; + std::atomic m_frequency = DefaultBaseFreq; std::atomic m_reversed = false; }; } // namespace lmms diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 114634577..8ec6c5886 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 2e63b5178..4cc14ba9c 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -144,9 +144,9 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, srcmode = SRC_SINC_MEDIUM_QUALITY; break; } - _n->m_pluginData = new Sample::PlaybackState(srcmode); - static_cast(_n->m_pluginData)->frameIndex = m_nextPlayStartPoint; - static_cast(_n->m_pluginData)->backwards = m_nextPlayBackwards; + _n->m_pluginData = new Sample::PlaybackState(_n->hasDetuningInfo(), srcmode); + static_cast(_n->m_pluginData)->setFrameIndex(m_nextPlayStartPoint); + static_cast(_n->m_pluginData)->setBackwards(m_nextPlayBackwards); // debug code /* qDebug( "frames %d", m_sample->frames() ); @@ -162,7 +162,7 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, static_cast(m_loopModel.value()))) { applyRelease( _working_buffer, _n ); - emit isPlaying(static_cast(_n->m_pluginData)->frameIndex); + emit isPlaying(static_cast(_n->m_pluginData)->frameIndex()); } else { @@ -176,8 +176,8 @@ void AudioFileProcessor::playNote( NotePlayHandle * _n, } if( m_stutterModel.value() == true ) { - m_nextPlayStartPoint = static_cast(_n->m_pluginData)->frameIndex; - m_nextPlayBackwards = static_cast(_n->m_pluginData)->backwards; + m_nextPlayStartPoint = static_cast(_n->m_pluginData)->frameIndex(); + m_nextPlayBackwards = static_cast(_n->m_pluginData)->backwards(); } } diff --git a/plugins/GigPlayer/GigPlayer.cpp b/plugins/GigPlayer/GigPlayer.cpp index 061df7bd5..b72e30b33 100644 --- a/plugins/GigPlayer/GigPlayer.cpp +++ b/plugins/GigPlayer/GigPlayer.cpp @@ -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 + s_interpolationMargins[m_interpolation]; + samples = frames / freq_factor + Sample::s_interpolationMargins[m_interpolation]; } // Load this note's data diff --git a/plugins/GigPlayer/GigPlayer.h b/plugins/GigPlayer/GigPlayer.h index 117178e54..685c7f546 100644 --- a/plugins/GigPlayer/GigPlayer.h +++ b/plugins/GigPlayer/GigPlayer.h @@ -240,12 +240,6 @@ 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{64, 64, 64, 4, 4}; - GigInstrument( InstrumentTrack * _instrument_track ); ~GigInstrument() override; diff --git a/src/core/AudioResampler.cpp b/src/core/AudioResampler.cpp new file mode 100644 index 000000000..8fb7d95a2 --- /dev/null +++ b/src/core/AudioResampler.cpp @@ -0,0 +1,69 @@ +/* + * AudioResampler.cpp - wrapper for libsamplerate + * + * Copyright (c) 2023 saker + * + * 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 +#include +#include + +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 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9eeb33904..3608d2848 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -4,6 +4,7 @@ set(LMMS_SRCS core/AudioEngine.cpp core/AudioEngineProfiler.cpp core/AudioEngineWorkerThread.cpp + core/AudioResampler.cpp core/AutomatableModel.cpp core/AutomationClip.cpp core/AutomationNode.cpp diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp index 564e08201..db99620c9 100644 --- a/src/core/Sample.cpp +++ b/src/core/Sample.cpp @@ -26,8 +26,6 @@ #include -#include "MixHelpers.h" - namespace lmms { Sample::Sample(const QString& audioFile) @@ -118,28 +116,43 @@ auto Sample::operator=(Sample&& other) -> Sample& return *this; } -bool Sample::play(SampleFrame* dst, PlaybackState* state, size_t numFrames, double frequency, Loop loopMode) const +bool Sample::play(SampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency, Loop loopMode) const { assert(numFrames > 0); - assert(frequency > 0); - if (m_buffer->empty()) { return false; } + assert(desiredFrequency > 0); - const auto outputSampleRate = Engine::audioEngine()->outputSampleRate() * m_frequency / frequency; + 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->frameIndex = std::max(m_startFrame, state->frameIndex); - state->sample = this; - state->loop = &loopMode; + state->m_frameIndex = std::max(m_startFrame, state->m_frameIndex); - src_set_ratio(state->resampleState, resampleRatio); - if (src_callback_read(state->resampleState, resampleRatio, numFrames, &dst[0][0]) != 0) + auto playBuffer = std::vector(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(resampleResult.outputFramesGenerated); + if (outputFrames < numFrames) { std::fill_n(dst + outputFrames, numFrames - outputFrames, SampleFrame{}); } + + if (!typeInfo::isEqual(m_amplification, 1.0f)) { - MixHelpers::multiply(dst, m_amplification, numFrames); - return true; + for (auto i = std::size_t{0}; i < numFrames; ++i) + { + dst[i][0] *= m_amplification; + dst[i][1] *= m_amplification; + } } - return false; + return true; } auto Sample::sampleDuration() const -> std::chrono::milliseconds @@ -157,43 +170,82 @@ void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, setLoopEndFrame(loopEndFrame); } -long Sample::render(void* callbackData, float** data) +void Sample::playRaw(SampleFrame* dst, size_t numFrames, const PlaybackState* state, Loop loopMode) const { - const auto state = static_cast(callbackData); - const auto loop = *state->loop; - const auto sample = state->sample; - auto& index = state->frameIndex; - auto& backwards = state->backwards; + if (m_buffer->size() < 1) { return; } - switch (loop) + 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::Off: - if (index < 0 || index >= sample->m_endFrame) { return 0; } - break; case Loop::On: - if (index < sample->m_loopStartFrame && state->backwards) { index = sample->m_loopEndFrame - 1; } - else if (index >= sample->m_loopEndFrame) { index = sample->m_loopStartFrame; } + 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 (index < sample->m_loopStartFrame && state->backwards) + if (state->m_frameIndex < m_loopStartFrame && state->m_backwards) { - index = sample->m_loopStartFrame; - backwards = false; + state->m_frameIndex = m_loopStartFrame + distanceFromLoopStart % loopSize; + state->m_backwards = false; } - else if (index >= sample->m_loopEndFrame) + else if (state->m_frameIndex >= m_loopEndFrame) { - index = sample->m_loopEndFrame - 1; - backwards = true; + state->m_frameIndex = m_loopEndFrame - 1 - distanceFromLoopEnd % loopSize; + state->m_backwards = true; } break; default: break; } - - const auto srcIndex = sample->m_reversed ? sample->m_buffer->size() - index - 1 : index; - *data = const_cast(&sample->m_buffer->data()[srcIndex][0]); - backwards ? --index : ++index; - return 1; } } // namespace lmms