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;