From 3b74f031d9ae31ea85e2d313e9c2b7e3d4469fc8 Mon Sep 17 00:00:00 2001 From: sakertooth Date: Sat, 23 Apr 2022 11:15:37 -0400 Subject: [PATCH] Implement sample caching (#6390) This implements a framework for the caching/sharing of samples. Reintegration still needs to occur, so not everything is complete. This is just to simplify the changes. SampleBufferV2 is where samples and any pertaining data to that sample is stored. From there, Sample's use that buffer but do not change it. They do that by storing a std::shared_ptr, which gives read-only access to the buffer. SampleBufferCache tracks the samples used in a project from a file or from Base64. If something needs a certain sample that is tracked by this cache, it will just create a std::shared_ptr from that tracked sample. The tracking is done with std::weak_ptr's within the SampleBufferCache. --- include/Engine.h | 9 +- include/Sample.h | 119 +++++++ include/SampleBufferCache.h | 44 +++ include/SampleBufferV2.h | 78 +++++ include/SampleClip.h | 55 ++-- include/SamplePlayHandle.h | 66 ++-- src/core/CMakeLists.txt | 3 + src/core/Engine.cpp | 6 + src/core/Sample.cpp | 544 ++++++++++++++++++++++++++++++++ src/core/SampleBufferCache.cpp | 47 +++ src/core/SampleBufferV2.cpp | 235 ++++++++++++++ src/core/SampleClip.cpp | 327 +++++++------------ src/core/SamplePlayHandle.cpp | 125 ++++---- src/core/SampleRecordHandle.cpp | 4 +- src/gui/FileBrowser.cpp | 2 +- src/gui/SampleClipView.cpp | 32 +- src/gui/SampleTrackView.cpp | 2 +- src/tracks/SampleTrack.cpp | 11 +- 18 files changed, 1322 insertions(+), 387 deletions(-) create mode 100644 include/Sample.h create mode 100644 include/SampleBufferCache.h create mode 100644 include/SampleBufferV2.h create mode 100644 src/core/Sample.cpp create mode 100644 src/core/SampleBufferCache.cpp create mode 100644 src/core/SampleBufferV2.cpp 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..40c8d34801c --- /dev/null +++ b/include/Sample.h @@ -0,0 +1,119 @@ +/* + * 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 + +#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 QString& strData, SampleBufferV2::StrDataType dataType); + Sample(const sampleFrame* data, const f_cnt_t numFrames); + explicit Sample(const SampleBufferV2* buffer); + explicit Sample(const f_cnt_t numFrames); + + Sample(const Sample& other); + Sample& operator=(const Sample& other); + + Sample(Sample&& other); + Sample& operator=(Sample&& other); + + bool play(sampleFrame* dst, const fpp_t numFrames, const float freq); + void visualize(QPainter& painter, const QRect& drawingRect, f_cnt_t fromFrame = 0, f_cnt_t toFrame = 0); + + QString sampleFile() const; + std::shared_ptr sampleBuffer() const; + sample_rate_t sampleRate() const; + float amplification() const; + float frequency() const; + bool reversed() const; + bool varyingPitch() const; + int interpolationMode() const; + f_cnt_t startFrame() const; + f_cnt_t endFrame() const; + f_cnt_t loopStartFrame() const; + f_cnt_t loopEndFrame() const; + f_cnt_t frameIndex() const; + f_cnt_t numFrames() const; + PlaybackType playback() const; + + void setSampleData(const QString& str, SampleBufferV2::StrDataType dataType); + void setSampleBuffer(const SampleBufferV2* buffer); + void setAmplification(float amplification); + void setFrequency(float frequency); + void setReversed(bool reversed); + void setVaryingPitch(bool varyingPitch); + void setInterpolationMode(int interpolationMode); + void setStartFrame(f_cnt_t start); + void setEndFrame(f_cnt_t end); + void setLoopStartFrame(f_cnt_t loopStart); + void setLoopEndFrame(f_cnt_t loopEnd); + void setFrameIndex(f_cnt_t frameIndex); + void setPlayback(PlaybackType playback); + + void loadAudioFile(const QString& audioFile); + void loadBase64(const QString& base64); + + void resetMarkers(); + int calculateTickLength() const; + QString openSample(); + +private: + std::shared_ptr m_sampleBuffer = nullptr; + 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; + f_cnt_t m_startFrame = 0; + f_cnt_t m_endFrame = 0; + f_cnt_t m_loopStartFrame = 0; + f_cnt_t m_loopEndFrame = 0; + f_cnt_t m_frameIndex = 0; + PlaybackType m_playback = PlaybackType::Regular; + SRC_STATE* 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..5f8acaad3e9 --- /dev/null +++ b/include/SampleBufferCache.h @@ -0,0 +1,44 @@ +/* + * 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 + +#include "SampleBufferV2.h" + +class SampleBufferCache +{ +public: + std::shared_ptr get(const QString& id); + std::shared_ptr add(const QString& id, const SampleBufferV2* buffer); + +private: + QHash> m_hash; +}; + +#endif diff --git a/include/SampleBufferV2.h b/include/SampleBufferV2.h new file mode 100644 index 00000000000..9f20cf29e3a --- /dev/null +++ b/include/SampleBufferV2.h @@ -0,0 +1,78 @@ +/* + * 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 "Engine.h" +#include "AudioEngine.h" +#include "lmms_basics.h" + +#include +#include +#include +#include + + +class SampleBufferV2 +{ +public: + enum class StrDataType + { + AudioFile, + Base64 + }; + + SampleBufferV2(const QString& strData, StrDataType dataType); + SampleBufferV2(const sampleFrame* data, const f_cnt_t numFrames); + explicit SampleBufferV2(const f_cnt_t numFrames); + + SampleBufferV2(const SampleBufferV2& other) = delete; + SampleBufferV2& operator=(const SampleBufferV2& other) = delete; + + SampleBufferV2(SampleBufferV2&& other); + SampleBufferV2& operator=(SampleBufferV2&& other); + + const std::vector& sampleData() const; + sample_rate_t originalSampleRate() const; + f_cnt_t numFrames() const; + + const QString& filePath() const; + bool hasFilePath() const; + + QString toBase64() const; + +private: + void loadFromAudioFile(const QString& audioFilePath); + void loadFromDrumSynthFile(const QString& drumSynthFilePath); + void loadFromBase64(const QString& str); + void resample(const sample_rate_t oldSampleRate, const sample_rate_t newSampleRate); + +private: + std::vector m_sampleData; + sample_rate_t m_originalSampleRate; + QString m_filePath; +}; + +#endif \ No newline at end of file diff --git a/include/SampleClip.h b/include/SampleClip.h index 7c4f9cf606a..aa4ee8a1cc2 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -21,72 +21,55 @@ * Boston, MA 02110-1301 USA. * */ - + #ifndef SAMPLE_CLIP_H #define SAMPLE_CLIP_H #include "Clip.h" - -class SampleBuffer; - +#include "Sample.h" class SampleClip : public Clip { Q_OBJECT - mapPropertyFromModel(bool,isRecord,setRecord,m_recordModel); -public: - SampleClip( Track * _track ); - SampleClip( const SampleClip& orig ); - virtual ~SampleClip(); + mapPropertyFromModel(bool, isRecord, setRecord, m_recordModel); - SampleClip& operator=( const SampleClip& that ) = delete; +public: + explicit SampleClip(Track* _track); + ~SampleClip() override; - void changeLength( const TimePos & _length ) override; - const QString & sampleFile() const; + void changeLength(const TimePos& _length) override; + QString sampleFile() const; - void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; - void loadSettings( const QDomElement & _this ) override; - inline QString nodeName() const override - { - return "sampleclip"; - } + void saveSettings(QDomDocument& _doc, QDomElement& _parent) override; + void loadSettings(const QDomElement& _this) override; - SampleBuffer* sampleBuffer() - { - return m_sampleBuffer; - } + void loadSample(const QString& strData, SampleBufferV2::StrDataType dataType); + QString nodeName() const override; + Sample& sample(); TimePos sampleLength() const; - void setSampleStartFrame( f_cnt_t startFrame ); - void setSamplePlayLength( f_cnt_t length ); - ClipView * createView( TrackView * _tv ) override; - - + ClipView* createView(TrackView* _tv) override; + bool isPlaying() const; void setIsPlaying(bool isPlaying); + + std::unique_ptr clone(); public slots: - void setSampleBuffer( SampleBuffer* sb ); - void setSampleFile( const QString & _sf ); void updateLength(); void toggleRecord(); void playbackPositionChanged(); void updateTrackClips(); - private: - SampleBuffer* m_sampleBuffer; + Sample m_sample; BoolModel m_recordModel; bool m_isPlaying; friend class SampleClipView; - signals: void sampleChanged(); void wasReversed(); -} ; - - - +}; #endif diff --git a/include/SamplePlayHandle.h b/include/SamplePlayHandle.h index 04360a26e83..fc915a1b7b4 100644 --- a/include/SamplePlayHandle.h +++ b/include/SamplePlayHandle.h @@ -26,72 +26,46 @@ #ifndef SAMPLE_PLAY_HANDLE_H #define SAMPLE_PLAY_HANDLE_H -#include "SampleBuffer.h" #include "AutomatableModel.h" #include "PlayHandle.h" +#include "Sample.h" +#include "SampleBuffer.h" class PatternTrack; class SampleClip; class Track; class AudioPort; - class SamplePlayHandle : public PlayHandle { public: - SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort = true ); - SamplePlayHandle( const QString& sampleFile ); - SamplePlayHandle( SampleClip* clip ); + SamplePlayHandle(Sample* sample, bool ownAudioPort = true); + explicit SamplePlayHandle(const QString& sampleFile); + explicit SamplePlayHandle(SampleClip* clip); virtual ~SamplePlayHandle(); - inline bool affinityMatters() const override - { - return true; - } - - - void play( sampleFrame * buffer ) override; + void play(sampleFrame* buffer) override; + + bool affinityMatters() const override; bool isFinished() const override; - - bool isFromTrack( const Track * _track ) const override; + bool isFromTrack(const Track* _track) const override; f_cnt_t totalFrames() const; - inline f_cnt_t framesDone() const - { - return( m_frame ); - } - void setDoneMayReturnTrue( bool _enable ) - { - m_doneMayReturnTrue = _enable; - } - - void setPatternTrack(PatternTrack* pt) - { - m_patternTrack = pt; - } - - void setVolumeModel( FloatModel * _model ) - { - m_volumeModel = _model; - } - + f_cnt_t framesDone() const; + void setDoneMayReturnTrue(bool _enable); + void setPatternTrack(PatternTrack* pt); + void setVolumeModel(FloatModel* _model); private: - SampleBuffer * m_sampleBuffer; - bool m_doneMayReturnTrue; - + Sample* m_sample; f_cnt_t m_frame; - SampleBuffer::handleState m_state; - - const bool m_ownAudioPort; - + bool m_doneMayReturnTrue; + bool m_ownAudioPort; + bool m_ownSample; FloatModel m_defaultVolumeModel; - FloatModel * m_volumeModel; - Track * m_track; - + FloatModel* m_volumeModel; + Track* m_track; PatternTrack* m_patternTrack; - -} ; - +}; #endif 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..82a5c6f77a9 --- /dev/null +++ b/src/core/Sample.cpp @@ -0,0 +1,544 @@ +/* + * 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" + +Sample::Sample(const QString& strData, SampleBufferV2::StrDataType dataType) +{ + setSampleData(strData, dataType); +} + +Sample::Sample(const sampleFrame* data, const f_cnt_t 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 f_cnt_t 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::operator=(const Sample& other) +{ + if (this == &other) { return *this; } + + 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; + + return *this; +} + +Sample::Sample(Sample&& other) + : m_sampleBuffer(std::move(other.m_sampleBuffer)) + , m_amplification(std::move(other.m_amplification)) + , m_frequency(std::move(other.m_frequency)) + , m_varyingPitch(std::move(other.m_varyingPitch)) + , m_interpolationMode(std::move(other.m_interpolationMode)) + , m_startFrame(std::move(other.m_startFrame)) + , m_endFrame(std::move(other.m_endFrame)) + , m_frameIndex(std::move(other.m_frameIndex)) +{ + other.m_sampleBuffer = nullptr; + other.m_amplification = 1.0; + other.m_frequency = 0; + other.m_reversed = false; + other.m_varyingPitch = false; + other.m_interpolationMode = SRC_LINEAR; + other.m_startFrame = 0; + other.m_endFrame = 0; + other.m_frameIndex = 0; +} + +Sample& Sample::operator=(Sample&& other) +{ + if (this == &other) { return *this; } + + m_sampleBuffer = std::move(other.m_sampleBuffer); + m_amplification = std::move(other.m_amplification); + m_frequency = std::move(other.m_frequency); + m_varyingPitch = std::move(other.m_varyingPitch); + m_interpolationMode = std::move(other.m_interpolationMode); + m_startFrame = std::move(other.m_startFrame); + m_endFrame = std::move(other.m_endFrame); + m_frameIndex = std::move(other.m_frameIndex); + + other.m_sampleBuffer = nullptr; + other.m_amplification = 0.0f; + other.m_frequency = 0; + other.m_reversed = false; + other.m_varyingPitch = false; + other.m_interpolationMode = SRC_LINEAR; + other.m_startFrame = 0; + other.m_endFrame = 0; + other.m_frameIndex = 0; + + return *this; +} + +bool Sample::play(sampleFrame* dst, const fpp_t 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 sampleDataBegin = sampleData.begin(); + auto sampleDataRbegin = sampleData.rbegin(); + + double freqFactor = static_cast(freq) / m_frequency; + if (static_cast(m_sampleBuffer->numFrames() / freqFactor) == 0) { return false; } + + if (freqFactor != 1.0 || m_varyingPitch) + { + if (!resampleState) + { + int error = 0; + resampleState = src_new(m_interpolationMode, DEFAULT_CHANNELS, &error); + + if (error) { return false; } + } + + std::array sampleMargin = {64, 64, 64, 4, 4}; + f_cnt_t fragmentSize = static_cast(framesToPlay * freqFactor) + sampleMargin[m_interpolationMode]; + + SRC_DATA srcData; + if (m_reversed) { srcData.data_in = (sampleDataRbegin + m_frameIndex)->data(); } + else + { + srcData.data_in = (sampleDataBegin + m_frameIndex)->data(); + } + + srcData.data_out = dst->data(); + srcData.input_frames = fragmentSize; + srcData.output_frames = framesToPlay; + srcData.src_ratio = 1.0 / freqFactor; + + int error = src_process(resampleState, &srcData); + if (error || srcData.output_frames_gen > framesToPlay) { return false; } + } + else + { + if (m_reversed) + { + std::advance(sampleDataRbegin, m_frameIndex); + std::copy_n(sampleDataRbegin, framesToPlay, dst); + } + else + { + std::advance(sampleDataBegin, m_frameIndex); + std::copy_n(sampleDataBegin, framesToPlay, 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) + { + setReversed(!m_reversed); + m_pingPongBackwards = true; + m_frameIndex = m_loopEndFrame; + } + else if (m_pingPongBackwards && m_frameIndex <= m_loopStartFrame) + { + setReversed(!m_reversed); + m_pingPongBackwards = false; + m_frameIndex = m_loopStartFrame; + } + else if (m_pingPongBackwards && m_frameIndex > m_loopStartFrame) + { + m_frameIndex -= framesToPlay; + } + 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 f_cnt_t fromFrame: First frame of the range + * @param f_cnt_t toFrame: Last frame of the range non-inclusive + */ +void Sample::visualize(QPainter& painter, const QRect& drawingRect, f_cnt_t fromFrame, f_cnt_t 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. + */ + + 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]); + } +} + +QString Sample::sampleFile() const +{ + return m_sampleBuffer->filePath(); +} + +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; +} + +f_cnt_t Sample::startFrame() const +{ + return m_startFrame; +} + +f_cnt_t Sample::endFrame() const +{ + return m_endFrame; +} + +f_cnt_t Sample::loopStartFrame() const +{ + return m_loopStartFrame; +} + +f_cnt_t Sample::loopEndFrame() const +{ + return m_loopEndFrame; +} + +f_cnt_t Sample::frameIndex() const +{ + return m_frameIndex; +} + +f_cnt_t Sample::numFrames() const +{ + return m_sampleBuffer ? m_sampleBuffer->numFrames() : 0; +} + +Sample::PlaybackType Sample::playback() const +{ + return m_playback; +} + +void Sample::setSampleData(const QString& strData, 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(float amplification) +{ + m_amplification = amplification; +} + +void Sample::setFrequency(float frequency) +{ + m_frequency = frequency; +} + +void Sample::setReversed(bool reversed) +{ + m_reversed = reversed; +} + +void Sample::setVaryingPitch(bool varyingPitch) +{ + m_varyingPitch = varyingPitch; +} + +void Sample::setInterpolationMode(int interpolationMode) +{ + m_interpolationMode = interpolationMode; +} + +void Sample::setStartFrame(f_cnt_t start) +{ + m_startFrame = start; +} + +void Sample::setEndFrame(f_cnt_t end) +{ + m_endFrame = end; +} + +void Sample::setLoopStartFrame(f_cnt_t loopStart) +{ + m_loopStartFrame = loopStart; +} + +void Sample::setLoopEndFrame(f_cnt_t loopEnd) +{ + m_loopEndFrame = loopEnd; +} + +void Sample::setFrameIndex(f_cnt_t frameIndex) +{ + m_frameIndex = frameIndex; +} + +void Sample::setPlayback(PlaybackType playback) +{ + m_playback = playback; +} + +void Sample::loadAudioFile(const QString& audioFile) +{ + setSampleData(audioFile, SampleBufferV2::StrDataType::AudioFile); +} + +void Sample::loadBase64(const QString& base64) +{ + setSampleData(base64, SampleBufferV2::StrDataType::Base64); +} + +void Sample::resetMarkers() +{ + m_startFrame = 0; + m_endFrame = m_sampleBuffer->numFrames(); + m_frameIndex = qBound(0, m_frameIndex, m_endFrame); +} + +int Sample::calculateTickLength() const +{ + return 1 / Engine::framesPerTick() * m_sampleBuffer->numFrames(); +} + +QString Sample::openSample() +{ + auto dialog = QFileDialog(nullptr, QObject::tr("Open audio file")); + + QString dir = ""; + if (m_sampleBuffer->hasFilePath()) + { + auto fileInfo = QFileInfo(m_sampleBuffer->filePath()); + if (fileInfo.isRelative()) + { + fileInfo.setFile(ConfigManager::inst()->userSamplesDir() + fileInfo.fileName()); + QString fileName = fileInfo.fileName(); + + if (!fileInfo.exists()) { fileInfo.setFile(ConfigManager::inst()->factorySamplesDir() + fileName); } + } + dir = fileInfo.absolutePath(); + } + else + { + dir = ConfigManager::inst()->userSamplesDir(); + } + + dialog.setDirectory(dir); + dialog.setFileMode(FileDialog::ExistingFiles); + + QStringList audioTypes = {QObject::tr("Audio Files (*.wav *.mp3 *.ogg *.ds *.flac *.voc *.aif *.aiff *.au *.raw)"), + QObject::tr("Wave Files (*.wav)"), QObject::tr("MP3 Files (*.mp3)"), QObject::tr("OGG Files (*.ogg)"), + QObject::tr("DrumSynth Files (*.ds)"), QObject::tr("FLAC Files (*.flac)"), QObject::tr("VOC Files (*.voc)"), + QObject::tr("AIFF Files (*.aif *.aiff)"), QObject::tr("AU Files (*.au)"), QObject::tr("RAW Files (*.raw)")}; + + dialog.setNameFilters(audioTypes); + if (m_sampleBuffer->hasFilePath()) { dialog.selectFile(QFileInfo(m_sampleBuffer->filePath()).fileName()); } + + QString chosenFile = ""; + if (dialog.exec() == QDialog::Accepted && !dialog.selectedFiles().isEmpty()) + { + chosenFile = PathUtil::toShortestRelative(dialog.selectedFiles()[0]); + } + + return chosenFile; +} \ No newline at end of file diff --git a/src/core/SampleBufferCache.cpp b/src/core/SampleBufferCache.cpp new file mode 100644 index 00000000000..9a8e6ca17b9 --- /dev/null +++ b/src/core/SampleBufferCache.cpp @@ -0,0 +1,47 @@ +/* + * SampleBufferCache.cpp - Used to cache sample buffers + * + * Copyright (C) 2022 JGHFunRun + * 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 QString& id) +{ + if (!m_hash.contains(id)) { return nullptr; } + return m_hash.value(id).lock(); +} + +std::shared_ptr SampleBufferCache::add(const QString& id, const SampleBufferV2* buffer) +{ + if (m_hash.contains(id)) { return m_hash.value(id).lock(); } + + auto sharedBuffer = std::shared_ptr(buffer, [=](auto ptr) { + delete ptr; + m_hash.remove(id); + }); + + m_hash.insert(id, std::weak_ptr(sharedBuffer)); + return sharedBuffer; +} \ No newline at end of file diff --git a/src/core/SampleBufferV2.cpp b/src/core/SampleBufferV2.cpp new file mode 100644 index 00000000000..3b513ee4a1b --- /dev/null +++ b/src/core/SampleBufferV2.cpp @@ -0,0 +1,235 @@ +/* + * 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 +#include +#include +#include + +#include "lmms_basics.h" + +SampleBufferV2::SampleBufferV2(const QString& strData, StrDataType dataType) +{ + if (strData.isEmpty()) { throw std::runtime_error("SampleBufferV2.cpp: strData is empty."); } + + switch (dataType) + { + case StrDataType::AudioFile: + if (!QFile::exists(strData)) + { + throw std::runtime_error("SampleBufferV2.cpp: non existing file " + strData.toStdString()); + } + + if (QFileInfo(strData).completeSuffix() == "ds") { loadFromDrumSynthFile(strData); } + else + { + loadFromAudioFile(strData); + } + + break; + case StrDataType::Base64: + loadFromBase64(strData); + break; + } +} + +SampleBufferV2::SampleBufferV2(const sampleFrame* data, const f_cnt_t numFrames) + : m_sampleData(data, data + numFrames) + , m_originalSampleRate(Engine::audioEngine()->processingSampleRate()) + , m_filePath("") +{ +} + +SampleBufferV2::SampleBufferV2(const f_cnt_t numFrames) + : m_sampleData(numFrames) + , m_originalSampleRate(Engine::audioEngine()->processingSampleRate()) + , m_filePath("") +{ +} + +SampleBufferV2::SampleBufferV2(SampleBufferV2&& other) + : m_sampleData(std::move(other.m_sampleData)) + , m_originalSampleRate(std::move(other.m_originalSampleRate)) + , m_filePath(std::move(other.m_filePath)) +{ + other.m_sampleData.clear(); + other.m_originalSampleRate = 0; + other.m_filePath = ""; +} + +SampleBufferV2& SampleBufferV2::operator=(SampleBufferV2&& other) +{ + if (this == &other) { return *this; } + + m_sampleData = std::move(other.m_sampleData); + m_originalSampleRate = std::move(other.m_originalSampleRate); + m_filePath = std::move(other.m_filePath); + other.m_sampleData.clear(); + other.m_originalSampleRate = 0; + other.m_filePath = ""; + + return *this; +} + +const std::vector& SampleBufferV2::sampleData() const +{ + return m_sampleData; +} + +const QString& SampleBufferV2::filePath() const +{ + return m_filePath; +} + +bool SampleBufferV2::hasFilePath() const +{ + return !m_filePath.isEmpty(); +} + +QString SampleBufferV2::toBase64() const +{ + const char* rawData = reinterpret_cast(m_sampleData.data()); + QByteArray data = QByteArray(rawData, m_sampleData.size() * sizeof(sampleFrame)); + return data.toBase64(); +} + +sample_rate_t SampleBufferV2::originalSampleRate() const +{ + return m_originalSampleRate; +} + +f_cnt_t SampleBufferV2::numFrames() const +{ + return m_sampleData.size(); +} + +void SampleBufferV2::resample(const sample_rate_t oldSampleRate, const sample_rate_t newSampleRate) +{ + const f_cnt_t 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 QString& audioFilePath) +{ + auto audioFile = QFile(audioFilePath); + if (!audioFile.open(QIODevice::ReadOnly)) + { + throw std::runtime_error("Could not open file " + audioFilePath.toStdString()); + } + + SF_INFO sfInfo; + sfInfo.format = 0; + + auto sndFileDeleter = [&](SNDFILE* ptr) { + sf_close(ptr); + audioFile.close(); + }; + + auto sndFile = std::unique_ptr( + sf_open_fd(audioFile.handle(), SFM_READ, &sfInfo, false), sndFileDeleter); + + if (!sndFile) { throw std::runtime_error(sf_strerror(sndFile.get())); } + + sf_count_t numSamples = sfInfo.frames * sfInfo.channels; + auto samples = std::vector(numSamples); + + sf_count_t samplesRead = sf_read_float(sndFile.get(), samples.data(), numSamples); + if (samplesRead != numSamples) { throw std::runtime_error(sf_strerror(sndFile.get())); } + + 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 QString& drumSynthFilePath) +{ + DrumSynth ds; + int16_t* samplesPtr = nullptr; + + f_cnt_t numSamples = ds.GetDSFileSamples( + drumSynthFilePath, samplesPtr, DEFAULT_CHANNELS, Engine::audioEngine()->processingSampleRate()); + + if (numSamples == 0 || samplesPtr == nullptr) + { + throw std::runtime_error("Could not read DrumSynth file " + drumSynthFilePath.toStdString()); + } + + m_sampleData.resize(numSamples / DEFAULT_CHANNELS); + m_filePath = drumSynthFilePath; + + for (f_cnt_t sampleIndex = 0; sampleIndex < numSamples; ++sampleIndex) + { + f_cnt_t frameIndex = sampleIndex / DEFAULT_CHANNELS; + m_sampleData[frameIndex][sampleIndex % DEFAULT_CHANNELS] + = samplesPtr[sampleIndex] * (1 / OUTPUT_SAMPLE_MULTIPLIER); + } + + delete samplesPtr; +} + +void SampleBufferV2::loadFromBase64(const QString& str) +{ + QByteArray base64Data = QByteArray::fromBase64(str.toUtf8()); + sampleFrame* dataAsSampleFrame = reinterpret_cast(base64Data.data()); + m_sampleData.assign(dataAsSampleFrame, dataAsSampleFrame + base64Data.size()); +} \ No newline at end of file diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 46bb6e6b70c..2aad95b1ca1 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -21,302 +21,193 @@ * Boston, MA 02110-1301 USA. * */ - + #include "SampleClip.h" #include +#include -#include "SampleBuffer.h" #include "SampleClipView.h" #include "SampleTrack.h" +#include "Song.h" #include "TimeLineWidget.h" -SampleClip::SampleClip( Track * _track ) : - Clip( _track ), - m_sampleBuffer( new SampleBuffer ), - m_isPlaying( false ) +SampleClip::SampleClip(Track* track) + : Clip(track) + , m_sample(1) + , m_recordModel() + , m_isPlaying(false) { - saveJournallingState( false ); - setSampleFile( "" ); + Song* song = Engine::getSong(); + MeterModel& timeSigModel = song->getTimeSigModel(); + + saveJournallingState(false); + changeLength(DefaultTicksPerBar * (timeSigModel.getNumerator() / timeSigModel.getDenominator())); restoreJournallingState(); - // we need to receive bpm-change-events, because then we have to - // change length of this Clip - connect( Engine::getSong(), SIGNAL( tempoChanged( bpm_t ) ), - this, SLOT( updateLength() ), Qt::DirectConnection ); - connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int,int ) ), - this, SLOT( updateLength() ) ); + connect(song, &Song::tempoChanged, this, &SampleClip::updateLength, Qt::DirectConnection); + connect(song, &Song::timeSignatureChanged, this, &SampleClip::updateLength, Qt::DirectConnection); - //care about positionmarker - TimeLineWidget * timeLine = Engine::getSong()->getPlayPos( Engine::getSong()->Mode_PlaySong ).m_timeLine; - if( timeLine ) - { - connect( timeLine, SIGNAL( positionMarkerMoved() ), this, SLOT( playbackPositionChanged() ) ); - } - //playbutton clicked or space key / on Export Song set isPlaying to false - connect( Engine::getSong(), SIGNAL( playbackStateChanged() ), - this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); - //care about loops - connect( Engine::getSong(), SIGNAL( updateSampleTracks() ), - this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); - //care about mute Clips - connect( this, SIGNAL( dataChanged() ), this, SLOT( playbackPositionChanged() ) ); - //care about mute track - connect( getTrack()->getMutedModel(), SIGNAL( dataChanged() ), - this, SLOT( playbackPositionChanged() ), Qt::DirectConnection ); - //care about Clip position - connect( this, SIGNAL( positionChanged() ), this, SLOT( updateTrackClips() ) ); - - switch( getTrack()->trackContainer()->type() ) + TimeLineWidget* timeLine = song->getPlayPos(song->Mode_PlaySong).m_timeLine; + if (timeLine != nullptr) { - case TrackContainer::PatternContainer: - setAutoResize( true ); - break; - - case TrackContainer::SongContainer: - // move down - default: - setAutoResize( false ); - break; + connect(timeLine, &TimeLineWidget::positionMarkerMoved, this, &SampleClip::playbackPositionChanged); } + + connect(song, &Song::playbackStateChanged, this, &SampleClip::playbackPositionChanged, Qt::DirectConnection); + connect(song, &Song::updateSampleTracks, this, &SampleClip::playbackPositionChanged, Qt::DirectConnection); + connect(song, &Song::dataChanged, this, &SampleClip::playbackPositionChanged); + connect(getTrack()->getMutedModel(), &BoolModel::dataChanged, this, &SampleClip::playbackPositionChanged, + Qt::DirectConnection); + connect(this, &SampleClip::positionChanged, this, &SampleClip::updateTrackClips); + + setAutoResize(false); updateTrackClips(); } -SampleClip::SampleClip(const SampleClip& orig) : - SampleClip(orig.getTrack()) +SampleClip::~SampleClip() { - // TODO: This creates a new SampleBuffer for the new Clip, eating up memory - // & eventually causing performance issues. Letting tracks share buffers - // when they're identical would fix this, but isn't possible right now. - *m_sampleBuffer = *orig.m_sampleBuffer; - m_isPlaying = orig.m_isPlaying; + updateTrackClips(); } +void SampleClip::changeLength(const TimePos& length) +{ + Clip::changeLength(qMax(1, static_cast(length))); +} +QString SampleClip::sampleFile() const +{ + return m_sample.sampleFile(); +} - -SampleClip::~SampleClip() +void SampleClip::saveSettings(QDomDocument&, QDomElement& current) { - SampleTrack * sampletrack = dynamic_cast( getTrack() ); - if ( sampletrack ) + if (current.parentNode().nodeName() == "clipboard") { current.setAttribute("pos", -1); } + else { - sampletrack->updateClips(); + current.setAttribute("pos", startPosition()); } - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); -} + current.setAttribute("len", length()); + current.setAttribute("muted", isMuted()); + current.setAttribute("src", sampleFile()); + current.setAttribute("off", startTimeOffset()); + if (sampleFile().isEmpty()) { current.setAttribute("data", m_sample.sampleBuffer()->toBase64()); } + current.setAttribute("original_sample_rate", m_sample.sampleBuffer()->originalSampleRate()); + if (usesCustomClipColor()) { current.setAttribute("color", color().name()); } -void SampleClip::changeLength( const TimePos & _length ) -{ - Clip::changeLength( qMax( static_cast( _length ), 1 ) ); + current.setAttribute("reversed", m_sample.reversed() ? "true" : "false"); } - - - -const QString & SampleClip::sampleFile() const +void SampleClip::loadSettings(const QDomElement& doc) { - return m_sampleBuffer->audioFile(); -} + int pos = doc.attribute("pos").toInt(); + if (pos >= 0) { movePosition(pos); } + QString src = doc.attribute("src"); + if (src.isEmpty() && doc.hasAttribute("data")) + { + m_sample.loadBase64(doc.attribute("data")); + } + else + { + if (!QFile::exists(src)) + { + Engine::getSong()->collectError(tr("The sample \"%1\" wasn't found or could not be loaded!").arg(src)); + return; + } + m_sample.loadAudioFile(src); + } -void SampleClip::setSampleBuffer( SampleBuffer* sb ) -{ - Engine::audioEngine()->requestChangeInModel(); - sharedObject::unref( m_sampleBuffer ); - Engine::audioEngine()->doneChangeInModel(); - m_sampleBuffer = sb; - updateLength(); + changeLength(doc.attribute("len").toInt()); + setMuted(doc.attribute("muted").toInt()); + setStartTimeOffset(doc.attribute("off").toInt()); - emit sampleChanged(); -} + bool hasColor = doc.hasAttribute("color"); + useCustomClipColor(hasColor); + if (hasColor) { setColor(doc.attribute("color")); } + if (doc.hasAttribute("reversed") && doc.attribute("reversed") == "true") + { + m_sample.setReversed(true); + emit wasReversed(); + } +} -void SampleClip::setSampleFile( const QString & _sf ) +void SampleClip::loadSample(const QString& strData, SampleBufferV2::StrDataType dataType) { - int length; - if ( _sf.isEmpty() ) - { //When creating an empty sample clip make it a bar long - float nom = Engine::getSong()->getTimeSigModel().getNumerator(); - float den = Engine::getSong()->getTimeSigModel().getDenominator(); - length = DefaultTicksPerBar * ( nom / den ); - } - else - { //Otherwise set it to the sample's length - m_sampleBuffer->setAudioFile( _sf ); - length = sampleLength(); - } - changeLength(length); + if (strData.isEmpty()) { return; } - setStartTimeOffset( 0 ); + m_sample.setSampleData(strData, dataType); + changeLength(m_sample.calculateTickLength()); + setStartTimeOffset(0); emit sampleChanged(); emit playbackPositionChanged(); } - - - -void SampleClip::toggleRecord() +QString SampleClip::nodeName() const { - m_recordModel.setValue( !m_recordModel.value() ); - emit dataChanged(); + return "sampleclip"; } - - - -void SampleClip::playbackPositionChanged() +Sample& SampleClip::sample() { - Engine::audioEngine()->removePlayHandlesOfTypes( getTrack(), PlayHandle::TypeSamplePlayHandle ); - SampleTrack * st = dynamic_cast( getTrack() ); - st->setPlayingClips( false ); + return m_sample; } - - - -void SampleClip::updateTrackClips() +TimePos SampleClip::sampleLength() const { - SampleTrack * sampletrack = dynamic_cast( getTrack() ); - if( sampletrack) - { - sampletrack->updateClips(); - } + return m_sample.calculateTickLength(); } - - +ClipView* SampleClip::createView(TrackView* tv) +{ + return new SampleClipView(this, tv); +} bool SampleClip::isPlaying() const { return m_isPlaying; } - - - void SampleClip::setIsPlaying(bool isPlaying) { m_isPlaying = isPlaying; } - - - void SampleClip::updateLength() { emit sampleChanged(); } - - - -TimePos SampleClip::sampleLength() const -{ - return (int)( m_sampleBuffer->frames() / Engine::framesPerTick() ); -} - - - - -void SampleClip::setSampleStartFrame(f_cnt_t startFrame) +void SampleClip::toggleRecord() { - m_sampleBuffer->setStartFrame( startFrame ); + m_recordModel.setValue(!m_recordModel.value()); + emit dataChanged(); } - - - -void SampleClip::setSamplePlayLength(f_cnt_t length) +void SampleClip::playbackPositionChanged() { - m_sampleBuffer->setEndFrame( length ); + Engine::audioEngine()->removePlayHandlesOfTypes(getTrack(), PlayHandle::TypeSamplePlayHandle); + SampleTrack* st = dynamic_cast(getTrack()); + st->setPlayingClips(false); } - - - -void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) -{ - if( _this.parentNode().nodeName() == "clipboard" ) - { - _this.setAttribute( "pos", -1 ); - } - else - { - _this.setAttribute( "pos", startPosition() ); - } - _this.setAttribute( "len", length() ); - _this.setAttribute( "muted", isMuted() ); - _this.setAttribute( "src", sampleFile() ); - _this.setAttribute( "off", startTimeOffset() ); - if( sampleFile() == "" ) - { - QString s; - _this.setAttribute( "data", m_sampleBuffer->toBase64( s ) ); - } - - _this.setAttribute( "sample_rate", m_sampleBuffer->sampleRate()); - if( usesCustomClipColor() ) - { - _this.setAttribute( "color", color().name() ); - } - if (m_sampleBuffer->reversed()) - { - _this.setAttribute("reversed", "true"); - } - // TODO: start- and end-frame -} - - - - -void SampleClip::loadSettings( const QDomElement & _this ) +void SampleClip::updateTrackClips() { - if( _this.attribute( "pos" ).toInt() >= 0 ) - { - movePosition( _this.attribute( "pos" ).toInt() ); - } - setSampleFile( _this.attribute( "src" ) ); - if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) - { - m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) ); - } - changeLength( _this.attribute( "len" ).toInt() ); - setMuted( _this.attribute( "muted" ).toInt() ); - setStartTimeOffset( _this.attribute( "off" ).toInt() ); - - if ( _this.hasAttribute( "sample_rate" ) ) { - m_sampleBuffer->setSampleRate( _this.attribute( "sample_rate" ).toInt() ); - } - - if( _this.hasAttribute( "color" ) ) - { - useCustomClipColor( true ); - setColor( _this.attribute( "color" ) ); - } - else - { - useCustomClipColor(false); - } - - if(_this.hasAttribute("reversed")) - { - m_sampleBuffer->setReversed(true); - emit wasReversed(); // tell SampleClipView to update the view - } + SampleTrack* sampleTrack = dynamic_cast(getTrack()); + if (sampleTrack != nullptr) { sampleTrack->updateClips(); } } - - - -ClipView * SampleClip::createView( TrackView * _tv ) +std::unique_ptr SampleClip::clone() { - return new SampleClipView( this, _tv ); -} + auto newClip = std::make_unique(getTrack()); + newClip->m_sample = m_sample; + newClip->m_isPlaying = m_isPlaying; + return newClip; +} \ No newline at end of file diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp index f82f5dbf462..204fd1c7ee8 100644 --- a/src/core/SamplePlayHandle.cpp +++ b/src/core/SamplePlayHandle.cpp @@ -23,130 +23,127 @@ */ #include "SamplePlayHandle.h" + +#include +#include + #include "AudioEngine.h" #include "AudioPort.h" #include "Engine.h" #include "Note.h" #include "PatternTrack.h" +#include "Sample.h" +#include "SampleBufferV2.h" #include "SampleClip.h" #include "SampleTrack.h" - - -SamplePlayHandle::SamplePlayHandle( SampleBuffer* sampleBuffer , bool ownAudioPort ) : - PlayHandle( TypeSamplePlayHandle ), - m_sampleBuffer( sharedObject::ref( sampleBuffer ) ), - m_doneMayReturnTrue( true ), - m_frame( 0 ), - m_ownAudioPort( ownAudioPort ), - m_defaultVolumeModel( DefaultVolume, MinVolume, MaxVolume, 1 ), - m_volumeModel( &m_defaultVolumeModel ), - m_track( nullptr ), - m_patternTrack( nullptr ) +SamplePlayHandle::SamplePlayHandle(Sample* sample, bool ownAudioPort) + : PlayHandle(TypeSamplePlayHandle) + , m_sample(sample) + , m_frame(0) + , m_doneMayReturnTrue(true) + , m_ownAudioPort(ownAudioPort) + , m_ownSample(false) + , m_defaultVolumeModel(DefaultVolume, MinVolume, MaxVolume, 1) + , m_volumeModel(&m_defaultVolumeModel) + , m_track(nullptr) + , m_patternTrack(nullptr) { - if (ownAudioPort) - { - setAudioPort( new AudioPort( "SamplePlayHandle", false ) ); - } + if (ownAudioPort) { setAudioPort(new AudioPort("SamplePlayHandle", false)); } } - - - -SamplePlayHandle::SamplePlayHandle( const QString& sampleFile ) : - SamplePlayHandle( new SampleBuffer( sampleFile ) , true) +SamplePlayHandle::SamplePlayHandle(const QString& sampleFile) + : SamplePlayHandle(new Sample(sampleFile, SampleBufferV2::StrDataType::AudioFile), true) { - sharedObject::unref( m_sampleBuffer ); + m_ownSample = true; } - - - -SamplePlayHandle::SamplePlayHandle( SampleClip* clip ) : - SamplePlayHandle( clip->sampleBuffer() , false) +SamplePlayHandle::SamplePlayHandle(SampleClip* clip) + : SamplePlayHandle(&clip->sample(), false) { m_track = clip->getTrack(); - setAudioPort( ( (SampleTrack *)clip->getTrack() )->audioPort() ); + setAudioPort(((SampleTrack*)clip->getTrack())->audioPort()); } - - - SamplePlayHandle::~SamplePlayHandle() { - sharedObject::unref( m_sampleBuffer ); - if( m_ownAudioPort ) - { - delete audioPort(); - } + if (m_ownAudioPort) { delete audioPort(); } + if (m_ownSample) { delete m_sample; } } - - - -void SamplePlayHandle::play( sampleFrame * buffer ) +void SamplePlayHandle::play(sampleFrame* buffer) { const fpp_t fpp = Engine::audioEngine()->framesPerPeriod(); - //play( 0, _try_parallelizing ); - if( framesDone() >= totalFrames() ) + // play( 0, _try_parallelizing ); + if (framesDone() >= totalFrames()) { - memset( buffer, 0, sizeof( sampleFrame ) * fpp ); + std::memset(buffer, 0, sizeof(sampleFrame) * fpp); return; } - sampleFrame * workingBuffer = buffer; + sampleFrame* workingBuffer = buffer; f_cnt_t frames = fpp; // apply offset for the first period - if( framesDone() == 0 ) + if (framesDone() == 0) { - memset( buffer, 0, sizeof( sampleFrame ) * offset() ); + std::memset(buffer, 0, sizeof(sampleFrame) * offset()); workingBuffer += offset(); frames -= offset(); } - if( !( m_track && m_track->isMuted() ) - && !(m_patternTrack && m_patternTrack->isMuted())) + if (!(m_track && m_track->isMuted()) && !(m_patternTrack && m_patternTrack->isMuted())) { -/* StereoVolumeVector v = - { { m_volumeModel->value() / DefaultVolume, - m_volumeModel->value() / DefaultVolume } };*/ + /* StereoVolumeVector v = + { { m_volumeModel->value() / DefaultVolume, + m_volumeModel->value() / DefaultVolume } };*/ // SamplePlayHandle always plays the sample at its original pitch; // it is used only for previews, SampleTracks and the metronome. - if (!m_sampleBuffer->play(workingBuffer, &m_state, frames, DefaultBaseFreq)) + if (!m_sample->play(workingBuffer, frames, DefaultBaseFreq)) { - memset(workingBuffer, 0, frames * sizeof(sampleFrame)); + std::memset(workingBuffer, 0, frames * sizeof(sampleFrame)); } } m_frame += frames; } - - +bool SamplePlayHandle::affinityMatters() const +{ + return true; +} bool SamplePlayHandle::isFinished() const { return framesDone() >= totalFrames() && m_doneMayReturnTrue == true; } - - - -bool SamplePlayHandle::isFromTrack( const Track * _track ) const +bool SamplePlayHandle::isFromTrack(const Track* _track) const { return m_track == _track || m_patternTrack == _track; } - - - f_cnt_t SamplePlayHandle::totalFrames() const { - return ( m_sampleBuffer->endFrame() - m_sampleBuffer->startFrame() ) * - ( Engine::audioEngine()->processingSampleRate() / m_sampleBuffer->sampleRate() ); + return m_sample->sampleBuffer()->numFrames(); } +f_cnt_t SamplePlayHandle::framesDone() const +{ + return m_frame; +} +void SamplePlayHandle::setDoneMayReturnTrue(bool _enable) +{ + m_doneMayReturnTrue = _enable; +} +void SamplePlayHandle::setPatternTrack(PatternTrack* pt) +{ + m_patternTrack = pt; +} +void SamplePlayHandle::setVolumeModel(FloatModel* _model) +{ + m_volumeModel = _model; +} \ No newline at end of file diff --git a/src/core/SampleRecordHandle.cpp b/src/core/SampleRecordHandle.cpp index cc0ce163517..27708fd7ccf 100644 --- a/src/core/SampleRecordHandle.cpp +++ b/src/core/SampleRecordHandle.cpp @@ -51,7 +51,9 @@ SampleRecordHandle::~SampleRecordHandle() { SampleBuffer* sb; createSampleBuffer( &sb ); - m_clip->setSampleBuffer( sb ); + + //TODO: samplecaching, SampleRecordHandle::~SampleRecordHandle + //m_clip->setSampleBuffer( sb ); } while( !m_buffers.empty() ) diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 383431ca137..fd0c526267b 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -861,7 +861,7 @@ bool FileBrowserTreeWidget::openInNewSampleTrack(FileItem* item) // Add the sample clip to the track Engine::audioEngine()->requestChangeInModel(); SampleClip* clip = static_cast(sampleTrack->createClip(0)); - clip->setSampleFile(item->fullName()); + clip->loadSample(item->fullName(), SampleBufferV2::StrDataType::AudioFile); Engine::audioEngine()->doneChangeInModel(); return true; } diff --git a/src/gui/SampleClipView.cpp b/src/gui/SampleClipView.cpp index 80b0e6ef2f3..56bce0f5b6c 100644 --- a/src/gui/SampleClipView.cpp +++ b/src/gui/SampleClipView.cpp @@ -28,6 +28,7 @@ #include #include +#include "SampleBufferV2.h" #include "embed.h" #include "PathUtil.h" #include "SampleBuffer.h" @@ -35,6 +36,8 @@ #include "Song.h" #include "StringPairDrag.h" +#include "Sample.h" + SampleClipView::SampleClipView( SampleClip * _clip, TrackView * _tv ) : ClipView( _clip, _tv ), m_clip( _clip ), @@ -56,9 +59,9 @@ void SampleClipView::updateSample() update(); // set tooltip to filename so that user can see what sample this // sample-clip contains - setToolTip(m_clip->m_sampleBuffer->audioFile() != "" ? - PathUtil::toAbsolute(m_clip->m_sampleBuffer->audioFile()) : - tr( "Double-click to open sample" ) ); + setToolTip(( m_clip->m_sample.sampleFile() != "" ) ? + PathUtil::toAbsolute(m_clip->m_sample.sampleFile()) : + tr( "Double-click to open sample" )); } @@ -103,13 +106,12 @@ void SampleClipView::dropEvent( QDropEvent * _de ) { if( StringPairDrag::decodeKey( _de ) == "samplefile" ) { - m_clip->setSampleFile( StringPairDrag::decodeValue( _de ) ); + m_clip->loadSample( StringPairDrag::decodeValue( _de ), SampleBufferV2::StrDataType::AudioFile ); _de->accept(); } else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) { - m_clip->m_sampleBuffer->loadFromBase64( - StringPairDrag::decodeValue( _de ) ); + m_clip->loadSample(StringPairDrag::decodeValue( _de ), SampleBufferV2::StrDataType::Base64); m_clip->updateLength(); update(); _de->accept(); @@ -167,17 +169,17 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { - QString af = m_clip->m_sampleBuffer->openAudioFile(); + QString af = m_clip->m_sample.openSample(); if ( af.isEmpty() ) {} //Don't do anything if no file is loaded - else if ( af == m_clip->m_sampleBuffer->audioFile() ) + else if ( af == m_clip->m_sample.sampleFile() ) { //Instead of reloading the existing file, just reset the size - int length = (int) ( m_clip->m_sampleBuffer->frames() / Engine::framesPerTick() ); + int length = (int) ( m_clip->m_sample.numFrames() / Engine::framesPerTick() ); m_clip->changeLength(length); } else { //Otherwise load the new file as ususal - m_clip->setSampleFile( af ); + m_clip->loadSample( af, SampleBufferV2::StrDataType::AudioFile ); Engine::getSong()->setModified(); } } @@ -259,9 +261,10 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) float offset = m_clip->startTimeOffset() / ticksPerBar * pixelsPerBar(); QRect r = QRect( offset, spacing, qMax( static_cast( m_clip->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing ); - m_clip->m_sampleBuffer->visualize( p, r, pe->rect() ); + + m_clip->m_sample.visualize( p, r ); - QString name = PathUtil::cleanName(m_clip->m_sampleBuffer->audioFile()); + QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); paintTextLabel(name, p); // disable antialiasing for borders, since its not needed @@ -314,7 +317,8 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) void SampleClipView::reverseSample() { - m_clip->sampleBuffer()->setReversed(!m_clip->sampleBuffer()->reversed()); + auto& sample = m_clip->sample(); + sample.setReversed(!sample.reversed()); Engine::getSong()->setModified(); update(); } @@ -338,7 +342,7 @@ bool SampleClipView::splitClip( const TimePos pos ) m_clip->getTrack()->addJournalCheckPoint(); m_clip->getTrack()->saveJournallingState( false ); - SampleClip * rightClip = new SampleClip ( *m_clip ); + std::unique_ptr rightClip = m_clip->clone(); m_clip->changeLength( splitPos - m_initialClipPos ); diff --git a/src/gui/SampleTrackView.cpp b/src/gui/SampleTrackView.cpp index a73724911b1..bbfcccc6153 100644 --- a/src/gui/SampleTrackView.cpp +++ b/src/gui/SampleTrackView.cpp @@ -203,7 +203,7 @@ void SampleTrackView::dropEvent(QDropEvent *de) ).quantize(1.0); SampleClip * sClip = static_cast(getTrack()->createClip(clipPos)); - if (sClip) { sClip->setSampleFile(value); } + if (sClip) { sClip->loadSample(value, SampleBufferV2::StrDataType::AudioFile); } } } diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index b1fd4c406d1..85e26a68e94 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -89,6 +89,7 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, if (trackContainer() == Engine::patternStore()) { pattern_track = PatternTrack::findPatternTrack(_clip_num); + dynamic_cast(getClip(_clip_num))->sample().setFrameIndex(0); setPlaying(true); } } @@ -104,18 +105,20 @@ bool SampleTrack::play( const TimePos & _start, const fpp_t _frames, { if( sClip->isPlaying() == false && _start >= (sClip->startPosition() + sClip->startTimeOffset()) ) { - auto bufferFramesPerTick = Engine::framesPerTick (sClip->sampleBuffer ()->sampleRate ()); + auto bufferFramesPerTick = Engine::framesPerTick (); f_cnt_t sampleStart = bufferFramesPerTick * ( _start - sClip->startPosition() - sClip->startTimeOffset() ); f_cnt_t clipFrameLength = bufferFramesPerTick * ( sClip->endPosition() - sClip->startPosition() - sClip->startTimeOffset() ); - f_cnt_t sampleBufferLength = sClip->sampleBuffer()->frames(); + f_cnt_t sampleBufferLength = sClip->sample().numFrames(); //if the Clip smaller than the sample length we play only until Clip end //else we play the sample to the end but nothing more f_cnt_t samplePlayLength = clipFrameLength > sampleBufferLength ? sampleBufferLength : clipFrameLength; //we only play within the sampleBuffer limits if( sampleStart < sampleBufferLength ) { - sClip->setSampleStartFrame( sampleStart ); - sClip->setSamplePlayLength( samplePlayLength ); + auto& sample = sClip->sample(); + sample.setStartFrame( sampleStart ); + sample.setFrameIndex( sampleStart ); + sample.setEndFrame( samplePlayLength ); clips.push_back( sClip ); sClip->setIsPlaying( true ); nowPlaying = true;