diff --git a/include/Engine.h b/include/Engine.h index 531e2422037..77ed32d243a 100644 --- a/include/Engine.h +++ b/include/Engine.h @@ -29,7 +29,6 @@ #include #include - #include "lmmsconfig.h" #include "lmms_export.h" #include "lmms_basics.h" @@ -40,7 +39,7 @@ class PatternStore; class ProjectJournal; class Song; class Ladspa2LMMS; - +class SampleBufferCache; // Note: This class is called 'LmmsCore' instead of 'Engine' because of naming // conflicts caused by ZynAddSubFX. See https://github.com/LMMS/lmms/issues/2269 @@ -87,6 +86,11 @@ class LMMS_EXPORT LmmsCore : public QObject return s_projectJournal; } + static SampleBufferCache* sampleBufferCache() + { + return s_sampleBufferCache; + } + static bool ignorePluginBlacklist(); #ifdef LMMS_HAVE_LV2 @@ -143,6 +147,7 @@ class LMMS_EXPORT LmmsCore : public QObject static AudioEngine *s_audioEngine; static Mixer * s_mixer; static Song * s_song; + static SampleBufferCache * s_sampleBufferCache; static PatternStore * s_patternStore; static ProjectJournal * s_projectJournal; diff --git a/include/Sample.h b/include/Sample.h new file mode 100644 index 00000000000..e8cddcf36cb --- /dev/null +++ b/include/Sample.h @@ -0,0 +1,115 @@ +/* + * Sample.h - a SampleBuffer with its own characteristics + * + * Copyright (c) 2022 sakertooth + * + * 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 SAMPLE_H +#define SAMPLE_H + +#include +#include +#include +#include +#include + +#include "Note.h" +#include "SampleBufferCache.h" +#include "SampleBufferV2.h" +#include "lmms_basics.h" + +class Sample +{ +public: + enum class PlaybackType + { + Regular, + LoopPoints, + PingPong + }; + + Sample() = default; + Sample(const std::string& strData, const SampleBufferV2::StrDataType dataType); + Sample(const sampleFrame* data, const int numFrames); + explicit Sample(const SampleBufferV2* buffer); + explicit Sample(const int numFrames); + Sample(const Sample& other); + Sample(Sample&& other); + + Sample& operator=(Sample other); + friend void swap(Sample& first, Sample& second); + + bool play(sampleFrame* dst, const int numFrames, const float freq); + void visualize(QPainter& painter, const QRect& drawingRect, const int fromFrame = 0, const int toFrame = 0); + + std::string sampleFile() const; + std::shared_ptr sampleBuffer() const; + int sampleRate() const; + float amplification() const; + float frequency() const; + bool reversed() const; + bool varyingPitch() const; + int interpolationMode() const; + int startFrame() const; + int endFrame() const; + int loopStartFrame() const; + int loopEndFrame() const; + int frameIndex() const; + int numFrames() const; + PlaybackType playback() const; + + void setSampleData(const std::string& str, const SampleBufferV2::StrDataType dataType); + void setSampleBuffer(const SampleBufferV2* buffer); + void setAmplification(const float amplification); + void setFrequency(const float frequency); + void setReversed(const bool reversed); + void setVaryingPitch(const bool varyingPitch); + void setInterpolationMode(const int interpolationMode); + void setStartFrame(const int start); + void setEndFrame(const int end); + void setLoopStartFrame(const int loopStart); + void setLoopEndFrame(const int loopEnd); + void setFrameIndex(const int frameIndex); + void setPlayback(const PlaybackType playback); + + void loadAudioFile(const std::string& audioFile); + void loadBase64(const std::string& base64); + void resetMarkers(); + int calculateTickLength() const; + +private: + std::shared_ptr m_sampleBuffer; + float m_amplification = 1.0f; + float m_frequency = DefaultBaseFreq; + bool m_reversed = false; + bool m_varyingPitch = false; + bool m_pingPongBackwards = false; + int m_interpolationMode = SRC_LINEAR; + int m_startFrame = 0; + int m_endFrame = 0; + int m_loopStartFrame = 0; + int m_loopEndFrame = 0; + int m_frameIndex = 0; + PlaybackType m_playback = PlaybackType::Regular; + SRC_STATE* m_resampleState = nullptr; +}; + +#endif \ No newline at end of file diff --git a/include/SampleBufferCache.h b/include/SampleBufferCache.h new file mode 100644 index 00000000000..1f62e53ab49 --- /dev/null +++ b/include/SampleBufferCache.h @@ -0,0 +1,43 @@ +/* + * SampleBufferCache.h - Used to cache sample buffers + * + * Copyright (c) 2022 sakertooth + * + * 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 SAMPLE_BUFFER_CACHE_H +#define SAMPLE_BUFFER_CACHE_H + +#include +#include + +#include "SampleBufferV2.h" + +class SampleBufferCache +{ +public: + std::shared_ptr get(const std::string& id); + std::shared_ptr add(const std::string& id, const SampleBufferV2* buffer); + bool contains(const std::string& id); +private: + std::unordered_map> m_map; +}; + +#endif diff --git a/include/SampleBufferV2.h b/include/SampleBufferV2.h new file mode 100644 index 00000000000..56bf72b53a8 --- /dev/null +++ b/include/SampleBufferV2.h @@ -0,0 +1,74 @@ +/* + * SampleBufferV2.h - container class for immutable sample data + * + * Copyright (c) 2022 sakertooth + * + * 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 SAMPLE_BUFFER_V2_H +#define SAMPLE_BUFFER_V2_H + +#include +#include +#include +#include + +#include "AudioEngine.h" +#include "Engine.h" +#include "lmms_basics.h" + +class SampleBufferV2 +{ +public: + enum class StrDataType + { + AudioFile, + Base64 + }; + + SampleBufferV2(const std::string& strData, const StrDataType dataType); + SampleBufferV2(const sampleFrame* data, const int numFrames); + SampleBufferV2(const SampleBufferV2& other) = delete; + SampleBufferV2(SampleBufferV2&& other); + explicit SampleBufferV2(const int numFrames); + + SampleBufferV2& operator=(SampleBufferV2& other) = delete; + SampleBufferV2& operator=(SampleBufferV2&& other); + + const std::vector& sampleData() const; + const std::optional& filePath() const; + int originalSampleRate() const; + + std::string toBase64() const; + int numFrames() const; + +private: + void loadFromAudioFile(const std::filesystem::path& audioFilePath); + void loadFromDrumSynthFile(const std::filesystem::path& drumSynthFilePath); + void loadFromBase64(const std::string& base64); + void resample(const int oldSampleRate, const int newSampleRate); + +private: + std::vector m_sampleData; + std::optional m_filePath; + int m_originalSampleRate = 0; +}; + +#endif \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8809ed78f3..55356b8d819 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -65,7 +65,10 @@ set(LMMS_SRCS core/RemotePlugin.cpp core/RenderManager.cpp core/RingBuffer.cpp + core/Sample.cpp core/SampleBuffer.cpp + core/SampleBufferCache.cpp + core/SampleBufferV2.cpp core/SampleClip.cpp core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp index a465901883e..a037afda9bf 100644 --- a/src/core/Engine.cpp +++ b/src/core/Engine.cpp @@ -33,14 +33,17 @@ #include "Plugin.h" #include "PresetPreviewPlayHandle.h" #include "ProjectJournal.h" +#include "SampleBufferCache.h" #include "Song.h" #include "BandLimitedWave.h" #include "Oscillator.h" float LmmsCore::s_framesPerTick; + AudioEngine* LmmsCore::s_audioEngine = nullptr; Mixer * LmmsCore::s_mixer = nullptr; PatternStore * LmmsCore::s_patternStore = nullptr; +SampleBufferCache * LmmsCore::s_sampleBufferCache = nullptr; Song * LmmsCore::s_song = nullptr; ProjectJournal * LmmsCore::s_projectJournal = nullptr; #ifdef LMMS_HAVE_LV2 @@ -65,6 +68,7 @@ void LmmsCore::init( bool renderOnly ) emit engine->initProgress(tr("Initializing data structures")); s_projectJournal = new ProjectJournal; s_audioEngine = new AudioEngine( renderOnly ); + s_sampleBufferCache = new SampleBufferCache; s_song = new Song; s_mixer = new Mixer; s_patternStore = new PatternStore; @@ -113,6 +117,8 @@ void LmmsCore::destroy() deleteHelper( &s_song ); + deleteHelper( &s_sampleBufferCache ); + delete ConfigManager::inst(); // The oscillator FFT plans remain throughout the application lifecycle diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp new file mode 100644 index 00000000000..17bf1f11b82 --- /dev/null +++ b/src/core/Sample.cpp @@ -0,0 +1,476 @@ +/* + * Sample.cpp - a SampleBuffer with its own characteristics + * + * Copyright (c) 2022 sakertooth + * + * 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 +#include +#include +#include +#include + +#include "ConfigManager.h" +#include "FileDialog.h" +#include "PathUtil.h" +#include "SampleBufferV2.h" + +Sample::Sample(const std::string& strData, SampleBufferV2::StrDataType dataType) +{ + setSampleData(strData, dataType); +} + +Sample::Sample(const sampleFrame* data, const int numFrames) + : m_sampleBuffer(std::make_shared(data, numFrames)) + , m_endFrame(m_sampleBuffer->numFrames()) +{ +} + +Sample::Sample(const SampleBufferV2* buffer) + : m_sampleBuffer(std::shared_ptr(buffer)) + , m_endFrame(m_sampleBuffer->numFrames()) +{ +} + +Sample::Sample(const int numFrames) + : m_sampleBuffer(std::make_shared(numFrames)) + , m_endFrame(numFrames) +{ +} + +Sample::Sample(const Sample& other) + : m_sampleBuffer(other.m_sampleBuffer) + , m_amplification(other.m_amplification) + , m_frequency(other.m_frequency) + , m_reversed(other.m_reversed) + , m_varyingPitch(other.m_varyingPitch) + , m_interpolationMode(other.m_interpolationMode) + , m_startFrame(other.m_startFrame) + , m_endFrame(other.m_endFrame) + , m_frameIndex(other.m_frameIndex) +{ +} + +Sample::Sample(Sample&& other) + : m_sampleBuffer(std::exchange(other.m_sampleBuffer, nullptr)) + , m_amplification(std::exchange(other.m_amplification, 1.0f)) + , m_frequency(std::exchange(other.m_frequency, 0.0f)) + , m_varyingPitch(std::exchange(other.m_varyingPitch, false)) + , m_interpolationMode(std::exchange(other.m_interpolationMode, SRC_LINEAR)) + , m_startFrame(std::exchange(other.m_startFrame, 0)) + , m_endFrame(std::exchange(other.m_endFrame, 0)) + , m_frameIndex(std::exchange(other.m_frameIndex, 0)) +{ +} + +Sample& Sample::operator=(Sample other) +{ + std::swap(*this, other); + return *this; +} + +void swap(Sample& first, Sample& second) +{ + first.m_sampleBuffer.swap(second.m_sampleBuffer); + std::swap(first.m_amplification, second.m_amplification); + std::swap(first.m_frequency, second.m_frequency); + std::swap(first.m_reversed, second.m_reversed); + std::swap(first.m_varyingPitch, second.m_varyingPitch); + std::swap(first.m_pingPongBackwards, second.m_pingPongBackwards); + std::swap(first.m_interpolationMode, second.m_interpolationMode); + std::swap(first.m_startFrame, second.m_startFrame); + std::swap(first.m_endFrame, second.m_endFrame); + std::swap(first.m_loopStartFrame, second.m_loopStartFrame); + std::swap(first.m_loopEndFrame, second.m_loopEndFrame); + std::swap(first.m_frameIndex, second.m_frameIndex); + std::swap(first.m_playback, second.m_playback); + std::swap(first.m_resampleState, second.m_resampleState); +} + +bool Sample::play(sampleFrame* dst, const int framesToPlay, const float freq) +{ + if (framesToPlay <= 0 || (m_frameIndex < 0 || m_frameIndex > m_endFrame)) { return false; } + + if ((m_playback == PlaybackType::LoopPoints || m_playback == PlaybackType::PingPong) + && (m_frameIndex < m_loopStartFrame || m_frameIndex > m_loopEndFrame)) + { + m_frameIndex = m_loopStartFrame; + } + + auto& sampleData = m_sampleBuffer->sampleData(); + auto sampleDataIt = m_reversed ? sampleData.end() : sampleData.begin(); + + sampleDataIt += (m_reversed ? -m_frameIndex : m_frameIndex); + auto advanceBy = m_reversed ? -framesToPlay : framesToPlay; + + double freqFactor = static_cast(freq) / m_frequency; + const int totalFramesForCurrentPitch = static_cast((m_endFrame - m_startFrame) / freqFactor); + + if (totalFramesForCurrentPitch == 0) { return false; } + if (freqFactor != 1.0 || m_varyingPitch) + { + if (!m_resampleState) + { + int error = 0; + m_resampleState = src_new(m_interpolationMode, DEFAULT_CHANNELS, &error); + + if (error) { return false; } + } + + std::array sampleMargin = {64, 64, 64, 4, 4}; + int fragmentSize = static_cast(framesToPlay * freqFactor) + sampleMargin[m_interpolationMode]; + + SRC_DATA srcData; + srcData.data_in = (sampleDataIt + advanceBy)->data(); + srcData.data_out = dst->data(); + srcData.input_frames = fragmentSize; + srcData.output_frames = framesToPlay; + srcData.src_ratio = 1.0 / freqFactor; + + int error = src_process(m_resampleState, &srcData); + if (error || srcData.output_frames_gen > framesToPlay) { return false; } + + if (m_reversed) { std::reverse(dst, dst + framesToPlay); } + } + else + { + if (m_reversed) { std::reverse_copy(sampleDataIt - framesToPlay, sampleDataIt, dst); } + else + { + std::copy(sampleDataIt, sampleDataIt + advanceBy, dst); + } + } + + for (int i = 0; i < framesToPlay; ++i) + { + dst[i][0] *= m_amplification; + dst[i][1] *= m_amplification; + } + + switch (m_playback) + { + case PlaybackType::Regular: + m_frameIndex += framesToPlay; + break; + case PlaybackType::LoopPoints: + m_frameIndex += framesToPlay; + if (m_frameIndex >= m_loopEndFrame) { m_frameIndex = m_loopStartFrame; } + break; + case PlaybackType::PingPong: + if (!m_pingPongBackwards && m_frameIndex < m_loopEndFrame) { m_frameIndex += framesToPlay; } + else if (!m_pingPongBackwards && m_frameIndex >= m_loopEndFrame) + { + setReversed(!m_reversed); + m_pingPongBackwards = true; + m_frameIndex = m_loopEndFrame; + } + else if (m_pingPongBackwards && m_frameIndex > m_loopStartFrame) + { + m_frameIndex -= framesToPlay; + } + else if (m_pingPongBackwards && m_frameIndex <= m_loopStartFrame) + { + setReversed(!m_reversed); + m_pingPongBackwards = false; + m_frameIndex = m_loopStartFrame; + } + break; + } + + return true; +} + +/* @brief Draws a sample on the QRect given in the range [fromFrame, toFrame) + * @param QPainter p: Painter object for the painting operations + * @param QRect dr: QRect where the buffer will be drawn in + * @param QRect clip: QRect used for clipping + * @param int fromFrame: First frame of the range + * @param int toFrame: Last frame of the range non-inclusive + */ +void Sample::visualize(QPainter& painter, const QRect& drawingRect, int fromFrame, int toFrame) +{ + /*TODO: + This function needs to be optimized. + - We do not have to recalculate peaks and rms every time we want to visualize the sample. + - You can store peaks and rms in 2 std::vector instead of 4 std::vector's + - Allocating large std::vectors on a hot path like Sample::visualize is not good. + - You can potentially reduce the number of frames you draw per pixel by choosing a certain frame per pixel + ratio beforehand. + + This function also needs to be moved out of Sample in favor of no Qt in the core. + */ + + if (m_sampleBuffer->numFrames() == 0) { return; } + + const bool focusOnRange = toFrame <= m_sampleBuffer->numFrames() && 0 <= fromFrame && fromFrame < toFrame; + // TODO: If the clip QRect is not being used we should remove it + // p.setClipRect(clip); + const int w = drawingRect.width(); + const int h = drawingRect.height(); + + const int yb = h / 2 + drawingRect.y(); + const float ySpace = h * 0.5f; + const int nbFrames = focusOnRange ? toFrame - fromFrame : m_sampleBuffer->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 = drawingRect.x(); + const int first = focusOnRange ? fromFrame : 0; + const int last = focusOnRange ? toFrame - 1 : m_sampleBuffer->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; + + float rmsData[2] = {0, 0}; + + // 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_sampleBuffer->sampleData()[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 = sqrt(trueRmsData); + const float maxRmsData = qBound(minData, sqrtRmsData, maxData); + const float minRmsData = qBound(minData, -sqrtRmsData, 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); + // 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) + { + painter.drawLine(fEdgeMax[i], fEdgeMin[i]); + } + + painter.setPen(painter.pen().color().lighter(123)); + + for (int i = 0; i < totalPoints; ++i) + { + painter.drawLine(fRmsMax[i], fRmsMin[i]); + } +} + +std::string Sample::sampleFile() const +{ + auto& path = m_sampleBuffer->filePath(); + return path.has_value() ? path->string() : ""; +} + +std::shared_ptr Sample::sampleBuffer() const +{ + return m_sampleBuffer; +} + +float Sample::amplification() const +{ + return m_amplification; +} + +float Sample::frequency() const +{ + return m_frequency; +} + +bool Sample::reversed() const +{ + return m_reversed; +} + +bool Sample::varyingPitch() const +{ + return m_varyingPitch; +} + +int Sample::interpolationMode() const +{ + return m_interpolationMode; +} + +int Sample::startFrame() const +{ + return m_startFrame; +} + +int Sample::endFrame() const +{ + return m_endFrame; +} + +int Sample::loopStartFrame() const +{ + return m_loopStartFrame; +} + +int Sample::loopEndFrame() const +{ + return m_loopEndFrame; +} + +int Sample::frameIndex() const +{ + return m_frameIndex; +} + +int Sample::numFrames() const +{ + return m_sampleBuffer ? m_sampleBuffer->numFrames() : 0; +} + +Sample::PlaybackType Sample::playback() const +{ + return m_playback; +} + +void Sample::setSampleData(const std::string& strData, const SampleBufferV2::StrDataType dataType) +{ + auto cachedSampleBuffer = Engine::sampleBufferCache()->get(strData); + + if (cachedSampleBuffer) { m_sampleBuffer = cachedSampleBuffer; } + else + { + m_sampleBuffer = Engine::sampleBufferCache()->add(strData, new SampleBufferV2(strData, dataType)); + } + + resetMarkers(); +} + +void Sample::setSampleBuffer(const SampleBufferV2* buffer) +{ + m_sampleBuffer.reset(buffer); + resetMarkers(); +} + +void Sample::setAmplification(const float amplification) +{ + m_amplification = amplification; +} + +void Sample::setFrequency(const float frequency) +{ + m_frequency = frequency; +} + +void Sample::setReversed(const bool reversed) +{ + m_reversed = reversed; +} + +void Sample::setVaryingPitch(const bool varyingPitch) +{ + m_varyingPitch = varyingPitch; +} + +void Sample::setInterpolationMode(const int interpolationMode) +{ + m_interpolationMode = interpolationMode; +} + +void Sample::setStartFrame(const int start) +{ + m_startFrame = start; +} + +void Sample::setEndFrame(const int end) +{ + m_endFrame = end; +} + +void Sample::setLoopStartFrame(const int loopStart) +{ + m_loopStartFrame = loopStart; +} + +void Sample::setLoopEndFrame(const int loopEnd) +{ + m_loopEndFrame = loopEnd; +} + +void Sample::setFrameIndex(const int frameIndex) +{ + m_frameIndex = frameIndex; +} + +void Sample::setPlayback(const PlaybackType playback) +{ + m_playback = playback; +} + +void Sample::loadAudioFile(const std::string& audioFile) +{ + setSampleData(audioFile, SampleBufferV2::StrDataType::AudioFile); +} + +void Sample::loadBase64(const std::string& base64) +{ + setSampleData(base64, SampleBufferV2::StrDataType::Base64); +} + +void Sample::resetMarkers() +{ + m_startFrame = 0; + m_endFrame = m_sampleBuffer->numFrames(); + m_loopStartFrame = std::clamp(0, m_loopStartFrame, m_endFrame); + m_loopEndFrame = std::clamp(0, m_loopEndFrame, m_endFrame); + m_frameIndex = std::clamp(0, m_frameIndex, m_endFrame); +} + +int Sample::calculateTickLength() const +{ + return 1 / Engine::framesPerTick() * m_sampleBuffer->numFrames(); +} diff --git a/src/core/SampleBufferCache.cpp b/src/core/SampleBufferCache.cpp new file mode 100644 index 00000000000..271e26f072b --- /dev/null +++ b/src/core/SampleBufferCache.cpp @@ -0,0 +1,50 @@ +/* + * SampleBufferCache.cpp - Used to cache sample buffers + * + * Copyright (c) 2022 sakertooth + * + * 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 "SampleBufferCache.h" + +#include "SampleBufferV2.h" + +std::shared_ptr SampleBufferCache::get(const std::string& id) +{ + return m_map.at(id).lock(); +} + +std::shared_ptr SampleBufferCache::add(const std::string& id, const SampleBufferV2* buffer) +{ + if (contains(id)) { return nullptr; } + + auto sharedBuffer = std::shared_ptr(buffer, [=](const SampleBufferV2* ptr) { + delete ptr; + m_map.erase(id); + }); + + m_map.emplace(id, std::weak_ptr(sharedBuffer)); + return sharedBuffer; +} + +bool SampleBufferCache::contains(const std::string& id) +{ + return m_map.find(id) != m_map.end(); +} \ No newline at end of file diff --git a/src/core/SampleBufferV2.cpp b/src/core/SampleBufferV2.cpp new file mode 100644 index 00000000000..0f5cfe9f6fb --- /dev/null +++ b/src/core/SampleBufferV2.cpp @@ -0,0 +1,215 @@ +/* + * SampleBufferV2.cpp - container class for immutable sample data + * + * Copyright (c) 2022 sakertooth + * + * 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 "SampleBufferV2.h" + +#include +#include +#include + +SampleBufferV2::SampleBufferV2(const std::string& strData, const StrDataType dataType) +{ + if (strData.empty()) { throw std::runtime_error("SampleBufferV2.cpp: strData is empty."); } + + if (dataType == StrDataType::AudioFile) + { + auto audioFilePath = std::filesystem::path(strData); + if (!std::filesystem::exists(audioFilePath)) + { + throw std::runtime_error("SampleBufferV2.cpp: non existing file " + strData); + } + + if (audioFilePath.extension() == ".ds") { loadFromDrumSynthFile(audioFilePath); } + else + { + loadFromAudioFile(audioFilePath); + } + } + else if (dataType == StrDataType::Base64) + { + loadFromBase64(strData); + } +} + +SampleBufferV2::SampleBufferV2(const sampleFrame* data, const int numFrames) + : m_sampleData(data, data + numFrames) + , m_filePath("") + , m_originalSampleRate(Engine::audioEngine()->processingSampleRate()) +{ +} + +SampleBufferV2::SampleBufferV2(const int numFrames) + : m_sampleData(numFrames) + , m_filePath("") + , m_originalSampleRate(Engine::audioEngine()->processingSampleRate()) +{ +} + +SampleBufferV2::SampleBufferV2(SampleBufferV2&& other) + : m_sampleData(std::move(other.m_sampleData)) + , m_filePath(std::exchange(other.m_filePath, std::nullopt)) + , m_originalSampleRate(std::exchange(other.m_originalSampleRate, 0)) +{ + other.m_sampleData.clear(); +} + +SampleBufferV2& SampleBufferV2::operator=(SampleBufferV2&& other) +{ + if (this == &other) { return *this; } + + m_sampleData = std::move(other.m_sampleData); + m_filePath = std::exchange(other.m_filePath, std::nullopt); + m_originalSampleRate = std::exchange(other.m_originalSampleRate, 0); + other.m_sampleData.clear(); + + return *this; +} + +const std::vector& SampleBufferV2::sampleData() const +{ + return m_sampleData; +} + +const std::optional& SampleBufferV2::filePath() const +{ + return m_filePath; +} + +int SampleBufferV2::originalSampleRate() const +{ + return m_originalSampleRate; +} + +std::string SampleBufferV2::toBase64() const +{ + const char* rawData = reinterpret_cast(m_sampleData.data()); + QByteArray data = QByteArray(rawData, m_sampleData.size() * sizeof(sampleFrame)); + return data.toBase64().constData(); +} + +int SampleBufferV2::numFrames() const +{ + return m_sampleData.size(); +} + +void SampleBufferV2::resample(const int oldSampleRate, const int newSampleRate) +{ + int dstFrames = static_cast(static_cast(numFrames()) / oldSampleRate * newSampleRate); + auto resampleBuf = std::vector(dstFrames); + + int error; + SRC_STATE* state; + if ((state = src_new(SRC_LINEAR, DEFAULT_CHANNELS, &error)) != nullptr) + { + SRC_DATA srcData; + srcData.data_in = m_sampleData.data()->data(); + srcData.input_frames = numFrames(); + srcData.data_out = resampleBuf.data()->data(); + srcData.output_frames = dstFrames; + srcData.src_ratio = static_cast(newSampleRate) / oldSampleRate; + srcData.end_of_input = 1; + + error = src_process(state, &srcData); + src_delete(state); + } + + if (error != 0) + { + throw std::runtime_error(std::string("An error occurred when resampling: ") + src_strerror(error) + '\n'); + } + + m_sampleData = std::move(resampleBuf); +} + +void SampleBufferV2::loadFromAudioFile(const std::filesystem::path& audioFilePath) +{ + SF_INFO sfInfo; + sfInfo.format = 0; + + auto sndFileDeleter = [](SNDFILE* ptr) { sf_close(ptr); }; + auto sndFile = std::unique_ptr( + sf_open(audioFilePath.c_str(), SFM_READ, &sfInfo), sndFileDeleter); + + if (!sndFile) { throw std::runtime_error("Failed to open audio file: " + std::string{sf_strerror(sndFile.get())}); } + + auto numSamples = sfInfo.frames * sfInfo.channels; + auto samples = std::vector(numSamples); + auto samplesRead = sf_read_float(sndFile.get(), samples.data(), numSamples); + + if (samplesRead != numSamples) + { + throw std::runtime_error("Failed to read audio samples: samplesRead != numSamples"); + } + + m_sampleData = std::vector(sfInfo.frames); + m_originalSampleRate = sfInfo.samplerate; + m_filePath = audioFilePath; + + for (sf_count_t frameIndex = 0; frameIndex < sfInfo.frames; ++frameIndex) + { + m_sampleData[frameIndex][0] = samples[frameIndex * sfInfo.channels]; + m_sampleData[frameIndex][1] = samples[frameIndex * sfInfo.channels + (sfInfo.channels > 1 ? 1 : 0)]; + } + + auto audioEngineSampleRate = Engine::audioEngine()->processingSampleRate(); + if (sfInfo.samplerate != static_cast(audioEngineSampleRate)) + { + resample(sfInfo.samplerate, audioEngineSampleRate); + } +} + +void SampleBufferV2::loadFromDrumSynthFile(const std::filesystem::path& drumSynthFilePath) +{ + auto dsFilePathStr = drumSynthFilePath.string(); + auto ds = DrumSynth(); + auto samples = std::make_unique(); + auto samplesRawPtr = samples.get(); + + // TODO: Remove QString::fromStdString when Qt is removed from DrumSynth + int numSamples = ds.GetDSFileSamples(QString::fromStdString(dsFilePathStr), samplesRawPtr, + DEFAULT_CHANNELS, Engine::audioEngine()->processingSampleRate()); + + if (numSamples == 0 || !samples) + { + throw std::runtime_error("Could not read DrumSynth file " + dsFilePathStr); + } + + m_sampleData.resize(numSamples / DEFAULT_CHANNELS); + m_filePath = drumSynthFilePath; + + for (int sampleIndex = 0; sampleIndex < numSamples; ++sampleIndex) + { + int frameIndex = sampleIndex / DEFAULT_CHANNELS; + m_sampleData[frameIndex][sampleIndex % DEFAULT_CHANNELS] + = samplesRawPtr[sampleIndex] * (1 / OUTPUT_SAMPLE_MULTIPLIER); + } +} + +void SampleBufferV2::loadFromBase64(const std::string& base64) +{ + // TODO: Base64 decoding without the use of Qt + QByteArray base64Data = QByteArray::fromBase64(QString::fromStdString(base64).toUtf8()); + sampleFrame* dataAsSampleFrame = reinterpret_cast(base64Data.data()); + m_sampleData.assign(dataAsSampleFrame, dataAsSampleFrame + base64Data.size()); +} \ No newline at end of file