diff --git a/include/Sample.h b/include/Sample.h new file mode 100644 index 000000000..ec55bec6e --- /dev/null +++ b/include/Sample.h @@ -0,0 +1,152 @@ +/* + * Sample.h - State for container-class SampleBuffer2 + * + * 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_SAMPLE_H +#define LMMS_SAMPLE_H + +#include +#include + +#include "Note.h" +#include "SampleBuffer2.h" +#include "lmms_export.h" + +#ifdef __MINGW32__ +#include +#include +#else +#include +#include +#endif + +class QPainter; +class QRect; + +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, + On, + PingPong + }; + + class LMMS_EXPORT PlaybackState + { + public: + PlaybackState(bool varyingPitch = false, int mode = SRC_LINEAR); + ~PlaybackState() noexcept; + + auto frameIndex() const -> f_cnt_t; + auto varyingPitch() const -> bool; + auto isBackwards() const -> bool; + auto interpolationMode() const -> int; + + auto setFrameIndex(f_cnt_t index) -> void; + auto setVaryingPitch(bool varyingPitch) -> void; + auto setBackwards(bool backwards) -> void; + + private: + f_cnt_t m_frameIndex = 0; + bool m_varyingPitch = false; + bool m_backwards = false; + SRC_STATE* m_resampleState = nullptr; + int m_interpolationMode = SRC_LINEAR; + friend class Sample; + }; + + Sample() = default; + Sample(const QString& audioFile); + Sample(const QByteArray& base64, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(const sampleFrame* data, int numFrames, int sampleRate = Engine::audioEngine()->processingSampleRate()); + Sample(std::shared_ptr buffer); + Sample(const Sample& other); + Sample(Sample&& other) noexcept; + + Sample& operator=(Sample other) noexcept; + friend auto swap(Sample& first, Sample& second) -> void; + + auto play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency = DefaultBaseFreq, + Loop loopMode = Loop::Off) const -> bool; + auto visualize(QPainter& p, const QRect& dr, int fromFrame = 0, int toFrame = 0) const -> void; + auto sampleDuration() const -> int; + auto playbackSize() const -> int; + + auto buffer() const -> std::shared_ptr; + auto startFrame() const -> int; + auto endFrame() const -> int; + auto loopStartFrame() const -> int; + auto loopEndFrame() const -> int; + auto amplification() const -> float; + auto frequency() const -> float; + auto reversed() const -> bool; + + auto setStartFrame(int startFrame) -> void; + auto setEndFrame(int endFrame) -> void; + auto setLoopStartFrame(int loopStartFrame) -> void; + auto setLoopEndFrame(int loopEndFrame) -> void; + auto setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, int loopEndFrame) -> void; + auto setAmplification(float amplification) -> void; + auto setFrequency(float frequency) -> void; + auto setReversed(bool reversed) -> void; + +private: + auto playSampleRange(PlaybackState* state, sampleFrame* dst, int numFrames, float resampleRatio = 1.0f) const + -> bool; + auto playSampleRangeLoop(PlaybackState* state, sampleFrame* dst, int numFrames, float resampleRatio = 1.0f) const + -> bool; + auto playSampleRangePingPong( + PlaybackState* state, sampleFrame* dst, int numFrames, float resampleRatio = 1.0f) const -> bool; + + auto copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const -> void; + auto copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const -> void; + + auto getPingPongIndex(int index, int startFrame, int endFrame) const -> int; + auto getLoopedIndex(int index, int startFrame, int endFrame) const -> int; + + auto resampleSampleRange(SRC_STATE* state, sampleFrame* src, sampleFrame* dst, int numInputFrames, + int numOutputFrames, double ratio) const -> SRC_DATA; + auto amplifySampleRange(sampleFrame* src, int numFrames) const -> void; + +private: + std::shared_ptr m_buffer = std::make_shared(); + int m_startFrame = 0; + int m_endFrame = 0; + int m_loopStartFrame = 0; + int m_loopEndFrame = 0; + float m_amplification = 1.0f; + float m_frequency = DefaultBaseFreq; + bool m_reversed = false; + mutable std::shared_mutex m_mutex; +}; +} // namespace lmms +#endif \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bbc5e8597..a66c134f8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -65,6 +65,7 @@ set(LMMS_SRCS core/RemotePlugin.cpp core/RenderManager.cpp core/RingBuffer.cpp + core/Sample.cpp core/SampleBuffer.cpp core/SampleBuffer2.cpp core/SampleClip.cpp diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp new file mode 100644 index 000000000..3f61cc24c --- /dev/null +++ b/src/core/Sample.cpp @@ -0,0 +1,542 @@ +/* + * Sample.cpp - State for container-class SampleBuffer2 + * + * 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 "Sample.h" + +#include +#include + +namespace lmms { + +Sample::Sample(const QString& audioFile) + : m_buffer(std::make_shared(audioFile)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const QByteArray& base64, int sampleRate) + : m_buffer(std::make_shared(base64, sampleRate)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const sampleFrame* data, int numFrames, int sampleRate) + : m_buffer(std::make_shared(data, numFrames, sampleRate)) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(std::shared_ptr buffer) + : m_buffer(buffer) + , m_startFrame(0) + , m_endFrame(m_buffer->size()) + , m_loopStartFrame(0) + , m_loopEndFrame(m_buffer->size()) +{ +} + +Sample::Sample(const Sample& other) +{ + auto lock = std::shared_lock{other.m_mutex}; + m_buffer = other.m_buffer; + m_startFrame = other.m_startFrame; + m_endFrame = other.m_endFrame; + m_loopStartFrame = other.m_loopStartFrame; + m_loopEndFrame = other.m_loopEndFrame; + m_amplification = other.m_amplification; + m_frequency = other.m_frequency; + m_reversed = other.m_reversed; +} + +Sample::Sample(Sample&& other) noexcept +{ + auto lock = std::unique_lock{other.m_mutex}; + m_buffer = std::move(other.m_buffer); + m_startFrame = std::exchange(other.m_startFrame, 0); + m_endFrame = std::exchange(other.m_endFrame, 0); + m_loopStartFrame = std::exchange(other.m_loopStartFrame, 0); + m_loopEndFrame = std::exchange(other.m_loopEndFrame, 0); + m_amplification = std::exchange(other.m_amplification, 0); + m_frequency = std::exchange(other.m_frequency, DefaultBaseFreq); + m_reversed = std::exchange(other.m_reversed, false); +} + +Sample& Sample::operator=(Sample other) noexcept +{ + swap(*this, other); + return *this; +} + +auto swap(Sample& first, Sample& second) -> void +{ + auto lock = std::scoped_lock{first.m_mutex, second.m_mutex}; + using std::swap; + swap(first.m_buffer, second.m_buffer); + swap(first.m_startFrame, second.m_startFrame); + swap(first.m_endFrame, second.m_endFrame); + swap(first.m_loopStartFrame, second.m_loopStartFrame); + swap(first.m_loopEndFrame, second.m_loopEndFrame); + swap(first.m_amplification, second.m_amplification); + swap(first.m_frequency, second.m_frequency); + swap(first.m_reversed, second.m_reversed); +} + +bool Sample::play(sampleFrame* dst, PlaybackState* state, int numFrames, float desiredFrequency, Loop loopMode) const +{ + if (m_buffer->sampleRate() <= 0) { return false; } + + const auto lock = std::shared_lock{m_mutex}; + const auto resampleRatio + = m_frequency / desiredFrequency * Engine::audioEngine()->processingSampleRate() / m_buffer->sampleRate(); + auto playedSuccessfully = false; + + switch (loopMode) + { + case Loop::Off: + playedSuccessfully = playSampleRange(state, dst, numFrames, resampleRatio); + break; + case Loop::On: + playedSuccessfully = playSampleRangeLoop(state, dst, numFrames, resampleRatio); + break; + case Loop::PingPong: + playedSuccessfully = playSampleRangePingPong(state, dst, numFrames, resampleRatio); + break; + default: + return false; + } + + if (src_error(state->m_resampleState) != 0 || !playedSuccessfully) { return false; } + amplifySampleRange(dst, numFrames); + return true; +} + +void Sample::visualize(QPainter& p, const QRect& dr, int fromFrame, int toFrame) const +{ + const auto lock = std::shared_lock{m_mutex}; + const auto numFrames = static_cast(m_buffer->size()); + if (numFrames == 0) { return; } + + const bool focusOnRange = toFrame <= numFrames && 0 <= fromFrame && fromFrame < toFrame; + const int w = dr.width(); + const int h = dr.height(); + + const int yb = h / 2 + dr.y(); + const float ySpace = h * 0.5f; + const int nbFrames = focusOnRange ? toFrame - fromFrame : numFrames; + + const double fpp = std::max(1., static_cast(nbFrames) / w); + // There are 2 possibilities: Either nbFrames is bigger than + // the width, so we will have width points, or nbFrames is + // smaller than the width (fpp = 1) and we will have nbFrames + // points + const int totalPoints = nbFrames > w ? w : nbFrames; + std::vector fEdgeMax(totalPoints); + std::vector fEdgeMin(totalPoints); + std::vector fRmsMax(totalPoints); + std::vector fRmsMin(totalPoints); + int curPixel = 0; + const int xb = dr.x(); + const int first = focusOnRange ? fromFrame : 0; + const int last = focusOnRange ? toFrame - 1 : numFrames - 1; + // When the number of frames isn't perfectly divisible by the + // width, the remaining frames don't fit the last pixel and are + // past the visible area. lastVisibleFrame is the index number of + // the last visible frame. + const int visibleFrames = (fpp * w); + const int lastVisibleFrame = focusOnRange ? fromFrame + visibleFrames - 1 : visibleFrames - 1; + + for (double frame = first; frame <= last && frame <= lastVisibleFrame; frame += fpp) + { + float maxData = -1; + float minData = 1; + + auto rmsData = std::array{}; + + // Find maximum and minimum samples within range + for (int i = 0; i < fpp && frame + i <= last; ++i) + { + for (int j = 0; j < 2; ++j) + { + auto curData = m_buffer->data()[static_cast(frame) + i][j]; + + if (curData > maxData) { maxData = curData; } + if (curData < minData) { minData = curData; } + + rmsData[j] += curData * curData; + } + } + + const float trueRmsData = (rmsData[0] + rmsData[1]) / 2 / fpp; + const float sqrtRmsData = std::sqrt(trueRmsData); + const float maxRmsData = std::clamp(sqrtRmsData, minData, maxData); + const float minRmsData = std::clamp(-sqrtRmsData, minData, maxData); + + // If nbFrames >= w, we can use curPixel to calculate X + // but if nbFrames < w, we need to calculate it proportionally + // to the total number of points + auto x = nbFrames >= w ? xb + curPixel : xb + ((static_cast(curPixel) / nbFrames) * w); + + if (m_reversed) { x = w - 1 - x; } + + // Partial Y calculation + auto py = ySpace * m_amplification; + fEdgeMax[curPixel] = QPointF(x, (yb - (maxData * py))); + fEdgeMin[curPixel] = QPointF(x, (yb - (minData * py))); + fRmsMax[curPixel] = QPointF(x, (yb - (maxRmsData * py))); + fRmsMin[curPixel] = QPointF(x, (yb - (minRmsData * py))); + ++curPixel; + } + + for (int i = 0; i < totalPoints; ++i) + { + p.drawLine(fEdgeMax[i], fEdgeMin[i]); + } + + p.setPen(p.pen().color().lighter(123)); + + for (int i = 0; i < totalPoints; ++i) + { + p.drawLine(fRmsMax[i], fRmsMin[i]); + } +} + +auto Sample::sampleDuration() const -> int +{ + const auto lock = std::shared_lock{m_mutex}; + return m_buffer->sampleRate() > 0 ? static_cast(m_endFrame - m_startFrame) / m_buffer->sampleRate() * 1000 + : 0; +} + +auto Sample::playbackSize() const -> int +{ + const auto lock = std::shared_lock{m_mutex}; + return m_buffer->sampleRate() > 0 + ? m_buffer->size() * Engine::audioEngine()->processingSampleRate() / m_buffer->sampleRate() + : 0; +} + +auto Sample::buffer() const -> std::shared_ptr +{ + const auto lock = std::shared_lock{m_mutex}; + return m_buffer; +} + +auto Sample::startFrame() const -> int +{ + const auto lock = std::shared_lock{m_mutex}; + return m_startFrame; +} + +auto Sample::endFrame() const -> int +{ + const auto lock = std::shared_lock{m_mutex}; + return m_endFrame; +} + +auto Sample::loopStartFrame() const -> int +{ + const auto lock = std::shared_lock{m_mutex}; + return m_loopStartFrame; +} + +auto Sample::loopEndFrame() const -> int +{ + const auto lock = std::shared_lock{m_mutex}; + return m_loopEndFrame; +} + +auto Sample::amplification() const -> float +{ + const auto lock = std::shared_lock{m_mutex}; + return m_amplification; +} + +auto Sample::frequency() const -> float +{ + const auto lock = std::shared_lock{m_mutex}; + return m_frequency; +} + +auto Sample::reversed() const -> bool +{ + const auto lock = std::shared_lock{m_mutex}; + return m_reversed; +} + +auto Sample::setStartFrame(int startFrame) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_startFrame = startFrame; +} + +auto Sample::setEndFrame(int endFrame) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_endFrame = endFrame; +} + +auto Sample::setLoopStartFrame(int loopStartFrame) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_loopStartFrame = loopStartFrame; +} + +auto Sample::setLoopEndFrame(int loopEndFrame) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_loopEndFrame = loopEndFrame; +} + +void Sample::setAllPointFrames(int startFrame, int endFrame, int loopStartFrame, int loopEndFrame) +{ + const auto lock = std::unique_lock{m_mutex}; + m_startFrame = startFrame; + m_endFrame = endFrame; + m_loopStartFrame = loopStartFrame; + m_loopEndFrame = loopEndFrame; +} + +auto Sample::setAmplification(float amplification) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_amplification = amplification; +} + +auto Sample::setFrequency(float frequency) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_frequency = frequency; +} + +auto Sample::setReversed(bool reversed) -> void +{ + const auto lock = std::unique_lock{m_mutex}; + m_reversed = reversed; +} + +auto Sample::playSampleRange(PlaybackState* state, sampleFrame* dst, int numFrames, float resampleRatio) const -> bool +{ + if (state->m_frameIndex >= m_endFrame || numFrames <= 0) { return false; } + state->m_frameIndex = std::max(m_startFrame, state->m_frameIndex); + + const auto numFramesToCopy = std::min( + numFrames / resampleRatio + (resampleRatio != 1.0f ? s_interpolationMargins[state->m_interpolationMode] : 0), + m_endFrame - state->m_frameIndex); + + auto buffer = std::vector(numFramesToCopy); + copyBufferForward(buffer.data(), state->m_frameIndex, numFramesToCopy); + + auto resample + = resampleSampleRange(state->m_resampleState, buffer.data(), dst, numFramesToCopy, numFrames, resampleRatio); + state->m_frameIndex += resample.input_frames_used; + return true; +} + +auto Sample::playSampleRangeLoop(PlaybackState* state, sampleFrame* dst, int numFrames, float resampleRatio) const -> bool +{ + if (numFrames <= 0) { return false; } + if (state->m_frameIndex >= m_loopEndFrame) { state->m_frameIndex = m_loopStartFrame; } + auto playFrame = std::max(m_startFrame, state->m_frameIndex); + + const auto totalFramesToCopy = static_cast( + numFrames / resampleRatio + (resampleRatio != 1.0f ? s_interpolationMargins[state->m_interpolationMode] : 0)); + auto buffer = std::vector(totalFramesToCopy); + + auto numFramesCopied = 0; + while (numFramesCopied != totalFramesToCopy) + { + auto numFramesToCopy = std::min(totalFramesToCopy - numFramesCopied, m_loopEndFrame - playFrame); + copyBufferForward(buffer.data() + numFramesCopied, playFrame, numFramesToCopy); + + playFrame += numFramesToCopy; + numFramesCopied += numFramesToCopy; + + if (playFrame >= m_loopEndFrame) { playFrame = m_loopStartFrame; } + } + + const auto resample + = resampleSampleRange(state->m_resampleState, buffer.data(), dst, totalFramesToCopy, numFrames, resampleRatio); + state->m_frameIndex + = getLoopedIndex(state->m_frameIndex + resample.input_frames_used, m_loopStartFrame, m_loopEndFrame); + return true; +} + +auto Sample::playSampleRangePingPong(PlaybackState* state, sampleFrame* dst, int numFrames, float resampleRatio) const -> bool +{ + if (numFrames <= 0) { return false; } + if (state->m_frameIndex >= m_loopEndFrame) + { + state->m_frameIndex = m_loopEndFrame - 1; + state->m_backwards = true; + } + + auto playFrame = std::min(m_endFrame, state->m_frameIndex); + + const auto totalFramesToCopy = static_cast( + numFrames / resampleRatio + (resampleRatio != 1.0f ? s_interpolationMargins[state->m_interpolationMode] : 0)); + auto buffer = std::vector(totalFramesToCopy); + + auto numFramesCopied = 0; + while (numFramesCopied != totalFramesToCopy) + { + auto numFramesToCopy = 0; + if (!state->m_backwards) + { + numFramesToCopy = std::min(totalFramesToCopy - numFramesCopied, m_loopEndFrame - playFrame); + copyBufferForward(buffer.data() + numFramesCopied, playFrame, numFramesToCopy); + playFrame += numFramesToCopy; + } + else + { + numFramesToCopy = std::min(totalFramesToCopy - numFramesCopied, playFrame - m_loopStartFrame); + copyBufferBackward(buffer.data() + numFramesCopied, playFrame, numFramesToCopy); + playFrame -= numFramesToCopy; + } + + numFramesCopied += numFramesToCopy; + if (playFrame >= m_loopEndFrame && !state->m_backwards) + { + playFrame = m_loopEndFrame - 1; + state->m_backwards = true; + } + else if (playFrame <= m_loopStartFrame && state->m_backwards) + { + playFrame = m_loopStartFrame; + state->m_backwards = false; + } + } + + const auto resample + = resampleSampleRange(state->m_resampleState, buffer.data(), dst, totalFramesToCopy, numFrames, resampleRatio); + state->m_frameIndex += (state->m_backwards ? -1 : 1) * resample.input_frames_used; + state->m_frameIndex = getPingPongIndex(state->m_frameIndex, m_loopStartFrame, m_loopEndFrame); + return true; +} + +auto Sample::copyBufferForward(sampleFrame* dst, int initialPosition, int advanceAmount) const -> void +{ + m_reversed ? std::copy_n(m_buffer->rbegin() + initialPosition, advanceAmount, dst) + : std::copy_n(m_buffer->begin() + initialPosition, advanceAmount, dst); +} + +auto Sample::copyBufferBackward(sampleFrame* dst, int initialPosition, int advanceAmount) const -> void +{ + m_reversed ? std::reverse_copy(m_buffer->rbegin() + initialPosition - advanceAmount, m_buffer->rbegin() + initialPosition, dst) + : std::reverse_copy(m_buffer->begin() + initialPosition - advanceAmount, m_buffer->begin() + initialPosition, dst); +} + +auto Sample::getLoopedIndex(int index, int startFrame, int endFrame) const -> int +{ + return index < endFrame ? index : startFrame + (index - startFrame) % (endFrame - startFrame); +} + +auto Sample::getPingPongIndex(int index, int startFrame, int endFrame) const -> int +{ + if (index < endFrame) { return index; } + const auto loopPos = getLoopedIndex(index, startFrame, endFrame * 2); + return loopPos > endFrame ? endFrame * 2 - loopPos : loopPos; +} + +auto Sample::resampleSampleRange(SRC_STATE* state, sampleFrame* src, sampleFrame* dst, int numInputFrames, + int numOutputFrames, double ratio) const -> SRC_DATA +{ + auto data = SRC_DATA{}; + data.data_in = &src[0][0]; + data.data_out = &dst[0][0]; + data.input_frames = numInputFrames; + data.output_frames = numOutputFrames; + data.src_ratio = ratio; + data.end_of_input = 0; + src_process(state, &data); + return data; +} + +auto Sample::amplifySampleRange(sampleFrame* src, int numFrames) const -> void +{ + const auto lock = std::shared_lock{m_mutex}; + for (int i = 0; i < numFrames; ++i) + { + src[i][0] *= m_amplification; + src[i][1] *= m_amplification; + } +} + +Sample::PlaybackState::PlaybackState(bool varyingPitch, int mode) + : m_varyingPitch(varyingPitch) + , m_interpolationMode(mode) +{ + int error = 0; + m_resampleState = src_new(m_interpolationMode, DEFAULT_CHANNELS, &error); + if (error != 0) { throw std::runtime_error{"Error creating resample state: " + std::string{src_strerror(error)}}; } +} + +Sample::PlaybackState::~PlaybackState() noexcept +{ + src_delete(m_resampleState); +} + +auto Sample::PlaybackState::frameIndex() const -> f_cnt_t +{ + return m_frameIndex; +} + +auto Sample::PlaybackState::varyingPitch() const -> bool +{ + return m_varyingPitch; +} + +auto Sample::PlaybackState::isBackwards() const -> bool +{ + return m_backwards; +} + +auto Sample::PlaybackState::interpolationMode() const -> int +{ + return m_interpolationMode; +} + +auto Sample::PlaybackState::setFrameIndex(f_cnt_t index) -> void +{ + m_frameIndex = index; +} + +auto Sample::PlaybackState::setVaryingPitch(bool varyingPitch) -> void +{ + m_varyingPitch = varyingPitch; +} + +auto Sample::PlaybackState::setBackwards(bool backwards) -> void +{ + m_backwards = backwards; +} +} // namespace lmms \ No newline at end of file