diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index eb3d229a3c0..a63a8208f45 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -292,7 +292,7 @@ protected slots: QColor m_detuningNoteColor; QColor m_ghostSampleColor; - SampleThumbnail m_sampleThumbnail; + std::shared_ptr m_sampleThumbnail; friend class AutomationEditorWindow; diff --git a/include/EnvelopeAndLfoParameters.h b/include/EnvelopeAndLfoParameters.h index 50bfdf78709..7911e1f7df4 100644 --- a/include/EnvelopeAndLfoParameters.h +++ b/include/EnvelopeAndLfoParameters.h @@ -201,7 +201,7 @@ public slots: sample_t * m_lfoShapeData; sample_t m_random; bool m_bad_lfoShapeData; - std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userWave = std::make_shared(); constexpr static auto NumLfoShapes = static_cast(LfoShape::Count); diff --git a/include/LfoController.h b/include/LfoController.h index 01b4b1862f7..e919dcff043 100644 --- a/include/LfoController.h +++ b/include/LfoController.h @@ -87,7 +87,7 @@ public slots: private: float m_heldSample; - std::shared_ptr m_userDefSampleBuffer = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userDefSampleBuffer = std::make_shared(); protected slots: void updatePhase(); diff --git a/include/Oscillator.h b/include/Oscillator.h index 648fe3bb897..aa7e3c78b8d 100644 --- a/include/Oscillator.h +++ b/include/Oscillator.h @@ -168,8 +168,8 @@ class LMMS_EXPORT Oscillator static sample_t userWaveSample(const SampleBuffer* buffer, const float sample) { - if (buffer == nullptr || buffer->size() == 0) { return 0; } - const auto frames = buffer->size(); + if (buffer == nullptr || buffer->data().size() == 0) { return 0; } + const auto frames = buffer->data().size(); const auto frame = absFraction(sample) * frames; const auto f1 = static_cast(frame); @@ -251,7 +251,7 @@ class LMMS_EXPORT Oscillator Oscillator * m_subOsc; float m_phaseOffset; float m_phase; - std::shared_ptr m_userWave = SampleBuffer::emptyBuffer(); + std::shared_ptr m_userWave = std::make_shared(); std::shared_ptr m_userAntiAliasWaveTable; bool m_useWaveTable; // There are many update*() variants; the modulator flag is stored as a member variable to avoid diff --git a/include/PathUtil.h b/include/PathUtil.h index 9b410d014a0..43c06074e0a 100644 --- a/include/PathUtil.h +++ b/include/PathUtil.h @@ -28,6 +28,7 @@ #include "lmms_export.h" #include +#include namespace lmms::PathUtil { @@ -68,6 +69,12 @@ namespace lmms::PathUtil //! Defaults to an absolute path if all bases fail. QString LMMS_EXPORT toShortestRelative(const QString & input, bool allowLocal = false); + //! Converts a QString path to a STL filesystem path. + std::filesystem::path LMMS_EXPORT pathFromQString(const QString& path); + + //! Converts an STL filesystem path to a QString path. + QString LMMS_EXPORT qStringFromPath(const std::filesystem::path& path); + } // namespace lmms::PathUtil #endif // LMMS_PATHUTIL_H diff --git a/include/ResourceCache.h b/include/ResourceCache.h new file mode 100644 index 00000000000..65f29c31810 --- /dev/null +++ b/include/ResourceCache.h @@ -0,0 +1,113 @@ +/* + * ResourceCache.h + * + * Copyright (c) 2025 Sotonye Atemie + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_RESOURCE_CACHE_H +#define LMMS_RESOURCE_CACHE_H + +#include +#include +#include +#include +#include +#include + +namespace lmms { +class ResourceCache +{ +public: + static constexpr auto CacheSize = 32; + + class Resource + { + int m_age = 0; + friend class ResourceCache; + }; + + template >, + typename... Args> + static auto fetch(Args&&... args) -> std::shared_ptr + { + static auto hasher = QCryptographicHash{QCryptographicHash::Algorithm::Md5}; + + hasher.reset(); + hasher.addData(typeid(T).name()); + hash(hasher, std::forward(args)...); + + const auto digest = hasher.result().toStdString(); + auto& resource = s_resources[std::move(digest)]; + + if (resource == nullptr) + { + if (s_resources.size() == CacheSize) + { + const auto it = std::min_element(s_resources.begin(), s_resources.end(), + [](const auto& first, const auto& second) { return first.second->m_age < first.second->m_age; }); + s_resources.erase(it); + } + + resource = std::make_shared(std::forward(args)...); + return std::static_pointer_cast(resource); + } + + ++resource->m_age; + return std::static_pointer_cast(resource); + } + + static auto instance() -> ResourceCache& + { + static auto s_inst = ResourceCache{}; + return s_inst; + } + +private: + static void hash(QCryptographicHash& hasher, const std::filesystem::path& path) + { + static constexpr auto blockSize = 8192; + static auto block = std::array{}; + auto fstream = std::fstream{path, std::ios::in | std::ios::binary}; + + do + { + fstream.read(block.data(), blockSize); + hasher.addData(block.data(), fstream.gcount()); + } while (fstream.gcount() > 0); + } + + static void hash(QCryptographicHash& hasher, const std::string& base64Audio, int sampleRate) + { + hasher.addData(QByteArray::fromStdString(base64Audio)); + hasher.addData(reinterpret_cast(&sampleRate), sizeof(sampleRate)); + } + + static void hash(QCryptographicHash& hasher, const std::string& str) + { + hasher.addData(QByteArray::fromStdString(str)); + } + + ResourceCache() { s_resources.reserve(CacheSize); } + inline static std::unordered_map> s_resources; +}; +} // namespace lmms + +#endif // LMMS_RESOURCE_CACHE_H diff --git a/include/Sample.h b/include/Sample.h index 3fd5bc38ef1..ada10da976a 100644 --- a/include/Sample.h +++ b/include/Sample.h @@ -28,8 +28,11 @@ #include #include +#include "AudioEngine.h" #include "AudioResampler.h" +#include "Engine.h" #include "Note.h" +#include "PathUtil.h" #include "SampleBuffer.h" #include "lmms_export.h" @@ -90,14 +93,11 @@ class LMMS_EXPORT Sample auto play(SampleFrame* dst, PlaybackState* state, size_t numFrames, float desiredFrequency = DefaultBaseFreq, Loop loopMode = Loop::Off) const -> bool; - auto sampleDuration() const -> std::chrono::milliseconds; - auto sampleFile() const -> const QString& { return m_buffer->audioFile(); } - auto sampleRate() const -> int { return m_buffer->sampleRate(); } + auto sampleFile() const -> QString { return PathUtil::qStringFromPath(m_buffer->path()); } auto sampleSize() const -> size_t { return m_buffer->size(); } + auto sampleRate() const -> int { return m_buffer->sampleRate(); } + auto sampleDuration() const -> std::chrono::milliseconds; - auto toBase64() const -> QString { return m_buffer->toBase64(); } - - auto data() const -> const SampleFrame* { return m_buffer->data(); } auto buffer() const -> std::shared_ptr { return m_buffer; } auto startFrame() const -> int { return m_startFrame.load(std::memory_order_relaxed); } auto endFrame() const -> int { return m_endFrame.load(std::memory_order_relaxed); } @@ -121,7 +121,7 @@ class LMMS_EXPORT Sample void advance(PlaybackState* state, size_t advanceAmount, Loop loopMode) const; private: - std::shared_ptr m_buffer = SampleBuffer::emptyBuffer(); + std::shared_ptr m_buffer = std::make_shared(); std::atomic m_startFrame = 0; std::atomic m_endFrame = 0; std::atomic m_loopStartFrame = 0; diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 8ec6c58863c..17781f5331e 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -25,73 +25,48 @@ #ifndef LMMS_SAMPLE_BUFFER_H #define LMMS_SAMPLE_BUFFER_H -#include -#include -#include -#include -#include #include #include "AudioEngine.h" #include "Engine.h" -#include "lmms_basics.h" +#include "ResourceCache.h" +#include "SampleFrame.h" #include "lmms_export.h" namespace lmms { -class LMMS_EXPORT SampleBuffer + +class LMMS_EXPORT SampleBuffer : public ResourceCache::Resource { public: - using value_type = SampleFrame; - using reference = SampleFrame&; - using const_reference = const SampleFrame&; - using iterator = std::vector::iterator; - using const_iterator = std::vector::const_iterator; - using difference_type = std::vector::difference_type; - using size_type = std::vector::size_type; - using reverse_iterator = std::vector::reverse_iterator; - using const_reverse_iterator = std::vector::const_reverse_iterator; - SampleBuffer() = default; - explicit SampleBuffer(const QString& audioFile); - SampleBuffer(const QString& base64, int sampleRate); - SampleBuffer(std::vector data, int sampleRate); - SampleBuffer( - const SampleFrame* data, size_t numFrames, int sampleRate = Engine::audioEngine()->outputSampleRate()); - - friend void swap(SampleBuffer& first, SampleBuffer& second) noexcept; - auto toBase64() const -> QString; - - auto audioFile() const -> const QString& { return m_audioFile; } - auto sampleRate() const -> sample_rate_t { return m_sampleRate; } - - auto begin() -> iterator { return m_data.begin(); } - auto end() -> iterator { return m_data.end(); } + explicit SampleBuffer(const std::filesystem::path& path); + SampleBuffer(const std::string& base64, int sampleRate = Engine::audioEngine()->outputSampleRate()); + SampleBuffer(const SampleFrame* buffer, size_t size, int sampleRate); + SampleBuffer(std::vector buffer, int sampleRate); - auto begin() const -> const_iterator { return m_data.begin(); } - auto end() const -> const_iterator { return m_data.end(); } + std::string toBase64() const; - auto cbegin() const -> const_iterator { return m_data.cbegin(); } - auto cend() const -> const_iterator { return m_data.cend(); } + int sampleRate() const { return m_sampleRate; } + void setSampleRate(int sampleRate) { m_sampleRate = sampleRate; } - auto rbegin() -> reverse_iterator { return m_data.rbegin(); } - auto rend() -> reverse_iterator { return m_data.rend(); } + auto path() const -> const std::filesystem::path& { return m_path; } - auto rbegin() const -> const_reverse_iterator { return m_data.rbegin(); } - auto rend() const -> const_reverse_iterator { return m_data.rend(); } + auto data() const -> const std::vector& { return m_data; } + auto data() -> std::vector& { return m_data; } - auto crbegin() const -> const_reverse_iterator { return m_data.crbegin(); } - auto crend() const -> const_reverse_iterator { return m_data.crend(); } + auto size() const { return m_data.size(); } + auto empty() const { return m_data.empty(); } - auto data() const -> const SampleFrame* { return m_data.data(); } - auto size() const -> size_type { return m_data.size(); } - auto empty() const -> bool { return m_data.empty(); } + auto begin() const { return m_data.begin(); } + auto begin() { return m_data.begin(); } - static auto emptyBuffer() -> std::shared_ptr; + auto end() const { return m_data.end(); } + auto end() { return m_data.end(); } private: std::vector m_data; - QString m_audioFile; - sample_rate_t m_sampleRate = Engine::audioEngine()->outputSampleRate(); + std::filesystem::path m_path; + int m_sampleRate = 0; }; } // namespace lmms diff --git a/include/SampleClip.h b/include/SampleClip.h index 3beca338bcd..e9b3e0c2cfc 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -56,7 +56,7 @@ class SampleClip : public Clip void changeLength( const TimePos & _length ) override; void changeLengthToSampleLength(); - const QString& sampleFile() const; + QString sampleFile() const; bool hasSampleFileLoaded(const QString & filename) const; void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index 10ce5b2f300..5d8ac4f7a47 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -65,7 +65,7 @@ public slots: private: SampleClip * m_clip; - SampleThumbnail m_sampleThumbnail; + std::shared_ptr m_sampleThumbnail; QPixmap m_paintPixmap; bool splitClip( const TimePos pos ) override; } ; diff --git a/include/SampleLoader.h b/include/SampleFilePicker.h similarity index 64% rename from include/SampleLoader.h rename to include/SampleFilePicker.h index fd8f1135725..ae40b6385ca 100644 --- a/include/SampleLoader.h +++ b/include/SampleFilePicker.h @@ -1,7 +1,7 @@ /* - * SampleLoader.h - Load audio and waveform files + * SampleFilePicker.h * - * Copyright (c) 2023 saker + * Copyright (c) 2024 saker * * This file is part of LMMS - https://lmms.io * @@ -22,27 +22,19 @@ * */ -#ifndef LMMS_GUI_SAMPLE_LOADER_H -#define LMMS_GUI_SAMPLE_LOADER_H +#ifndef LMMS_GUI_SAMPLE_FILE_PICKER_H +#define LMMS_GUI_SAMPLE_FILE_PICKER_H #include -#include - -#include "SampleBuffer.h" #include "lmms_export.h" namespace lmms::gui { -class LMMS_EXPORT SampleLoader +class LMMS_EXPORT SampleFilePicker { public: static QString openAudioFile(const QString& previousFile = ""); static QString openWaveformFile(const QString& previousFile = ""); - static std::shared_ptr createBufferFromFile(const QString& filePath); - static std::shared_ptr createBufferFromBase64( - const QString& base64, int sampleRate = Engine::audioEngine()->outputSampleRate()); -private: - static void displayError(const QString& message); }; } // namespace lmms::gui -#endif // LMMS_GUI_SAMPLE_LOADER_H +#endif // LMMS_GUI_SAMPLE_FILE_PICKER_H diff --git a/include/SampleThumbnail.h b/include/SampleThumbnail.h index b8e1e85af46..1cbd7d41031 100644 --- a/include/SampleThumbnail.h +++ b/include/SampleThumbnail.h @@ -31,7 +31,9 @@ #include #include -#include "Sample.h" +#include "ResourceCache.h" +#include "SampleBuffer.h" +#include "SampleFrame.h" #include "lmms_export.h" namespace lmms { @@ -47,7 +49,7 @@ namespace lmms { the visualization however (i.e., we are not reading from original sample data when drawing), this provides a significant performance boost that wouldn't be possible otherwise. */ -class LMMS_EXPORT SampleThumbnail +class LMMS_EXPORT SampleThumbnail : public ResourceCache::Resource { public: struct VisualizeParameters @@ -69,7 +71,9 @@ class LMMS_EXPORT SampleThumbnail }; SampleThumbnail() = default; - SampleThumbnail(const Sample& sample); + SampleThumbnail(std::shared_ptr buffer); + SampleThumbnail(const std::filesystem::path& path); + SampleThumbnail(const std::string& base64); void visualize(VisualizeParameters parameters, QPainter& painter) const; private: @@ -116,26 +120,7 @@ class LMMS_EXPORT SampleThumbnail double m_samplesPerPeak = 0.0; }; - struct SampleThumbnailEntry - { - QString filePath; - QDateTime lastModified; - - friend bool operator==(const SampleThumbnailEntry& first, const SampleThumbnailEntry& second) - { - return first.filePath == second.filePath && first.lastModified == second.lastModified; - } - }; - - struct Hash - { - std::size_t operator()(const SampleThumbnailEntry& entry) const noexcept { return qHash(entry.filePath); } - }; - - using ThumbnailCache = std::vector; - std::shared_ptr m_thumbnailCache = std::make_shared(); - - inline static std::unordered_map, Hash> s_sampleThumbnailCacheMap; + std::vector m_thumbnails; }; } // namespace lmms diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 4cc14ba9cdb..d887eae73cb 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -27,7 +27,6 @@ #include "InstrumentTrack.h" #include "PathUtil.h" -#include "SampleLoader.h" #include "Song.h" #include "lmms_basics.h" @@ -195,9 +194,9 @@ void AudioFileProcessor::deleteNotePluginData( NotePlayHandle * _n ) void AudioFileProcessor::saveSettings(QDomDocument& doc, QDomElement& elem) { elem.setAttribute("src", m_sample.sampleFile()); - if (m_sample.sampleFile().isEmpty()) + if (!m_sample.sampleFile().isEmpty()) { - elem.setAttribute("sampledata", m_sample.toBase64()); + elem.setAttribute("sampledata", QString::fromStdString(m_sample.buffer()->toBase64())); } m_reverseModel.saveSettings(doc, elem, "reversed"); m_loopModel.saveSettings(doc, elem, "looped"); @@ -224,7 +223,8 @@ void AudioFileProcessor::loadSettings(const QDomElement& elem) } else if (auto sampleData = elem.attribute("sampledata"); !sampleData.isEmpty()) { - m_sample = Sample(gui::SampleLoader::createBufferFromBase64(sampleData)); + const auto base64 = ResourceCache::fetch(sampleData.toStdString()); + m_sample = Sample{std::move(base64)}; } m_loopModel.loadSettings(elem, "looped"); @@ -307,17 +307,16 @@ gui::PluginView* AudioFileProcessor::instantiateView( QWidget * _parent ) void AudioFileProcessor::setAudioFile(const QString& _audio_file, bool _rename) { // is current channel-name equal to previous-filename?? - if( _rename && - ( instrumentTrack()->name() == - QFileInfo(m_sample.sampleFile()).fileName() || - m_sample.sampleFile().isEmpty())) + if (_rename && (instrumentTrack()->name() == m_sample.sampleFile() || m_sample.sampleFile().isEmpty())) { // then set it to new one instrumentTrack()->setName( PathUtil::cleanName( _audio_file ) ); } // else we don't touch the track-name, because the user named it self - m_sample = Sample(gui::SampleLoader::createBufferFromFile(_audio_file)); + const auto buffer = ResourceCache::fetch(PathUtil::pathFromQString(_audio_file)); + m_sample = Sample{std::move(buffer)}; + loopPointChanged(); emit sampleUpdated(); } diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp index 298e79c5ed1..8c81c6f897a 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp @@ -33,7 +33,7 @@ #include "DataFile.h" #include "FontHelper.h" #include "PixmapButton.h" -#include "SampleLoader.h" +#include "SampleFilePicker.h" #include "Song.h" #include "StringPairDrag.h" #include "Track.h" @@ -257,7 +257,7 @@ void AudioFileProcessorView::sampleUpdated() void AudioFileProcessorView::openAudioFile() { - QString af = SampleLoader::openAudioFile(); + QString af = SampleFilePicker::openAudioFile(); if (af.isEmpty()) { return; } castModel()->setAudioFile(af); diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index f120fbf255e..25246176051 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -82,7 +82,7 @@ AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget* parent, int w, i m_reversed(false), m_framesPlayed(0), m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()), - m_sampleThumbnail(*buf) + m_sampleThumbnail(ResourceCache::fetch(buf->buffer()->path())) { setFixedSize(w, h); setMouseTracking(true); @@ -340,7 +340,7 @@ void AudioFileProcessorWaveView::updateGraph() QPainter p(&m_graph); p.setPen(QColor(255, 255, 255)); - m_sampleThumbnail = SampleThumbnail{*m_sample}; + m_sampleThumbnail = ResourceCache::fetch(m_sample->buffer()->path()); const auto param = SampleThumbnail::VisualizeParameters{ .sampleRect = m_graph.rect(), @@ -350,7 +350,7 @@ void AudioFileProcessorWaveView::updateGraph() .reversed = m_sample->reversed(), }; - m_sampleThumbnail.visualize(param, p); + m_sampleThumbnail->visualize(param, p); } void AudioFileProcessorWaveView::zoom(const bool out) diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h index 6440570e660..9e6a898c90b 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h @@ -145,7 +145,7 @@ public slots: bool m_reversed; f_cnt_t m_framesPlayed; bool m_animation; - SampleThumbnail m_sampleThumbnail; + std::shared_ptr m_sampleThumbnail = std::make_shared(); friend class AudioFileProcessorView; diff --git a/plugins/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index ef533cd1a4a..27913608f24 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -31,7 +31,6 @@ #include "Engine.h" #include "InstrumentTrack.h" #include "PathUtil.h" -#include "SampleLoader.h" #include "Song.h" #include "embed.h" #include "interpolation.h" @@ -120,7 +119,7 @@ void SlicerT::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) SRC_STATE* resampleState = playbackState->resamplingState(); SRC_DATA resampleData; - resampleData.data_in = (m_originalSample.data() + noteFrame)->data(); + resampleData.data_in = (&m_originalSample.buffer()->data()[0] + noteFrame)->data(); resampleData.data_out = (workingBuffer + offset)->data(); resampleData.input_frames = noteLeft * m_originalSample.sampleSize(); resampleData.output_frames = frames; @@ -173,7 +172,7 @@ void SlicerT::findSlices() std::vector singleChannel(m_originalSample.sampleSize(), 0); for (auto i = std::size_t{0}; i < m_originalSample.sampleSize(); i++) { - singleChannel[i] = (m_originalSample.data()[i][0] + m_originalSample.data()[i][1]) / 2; + singleChannel[i] = (m_originalSample.buffer()->data()[i][0] + m_originalSample.buffer()->data()[i][1]) / 2; maxMag = std::max(maxMag, singleChannel[i]); } @@ -241,7 +240,7 @@ void SlicerT::findSlices() } float beatsPerMin = m_originalBPM.value() / 60.0f; - float samplesPerBeat = m_originalSample.sampleRate() / beatsPerMin * 4.0f; + float samplesPerBeat = m_originalSample.sampleSize() / beatsPerMin * 4.0f; int noteSnap = m_sliceSnap.value(); int sliceLock = samplesPerBeat / std::exp2(noteSnap + 1); if (noteSnap == 0) { sliceLock = 1; } @@ -271,7 +270,7 @@ void SlicerT::findBPM() if (m_originalSample.sampleSize() <= 1) { return; } float sampleRate = m_originalSample.sampleRate(); - float totalFrames = m_originalSample.sampleSize(); + float totalFrames = m_originalSample.sampleRate(); float sampleLength = totalFrames / sampleRate; float bpmEstimate = 240.0f / sampleLength; @@ -320,7 +319,8 @@ std::vector SlicerT::getMidi() void SlicerT::updateFile(QString file) { - if (auto buffer = gui::SampleLoader::createBufferFromFile(file)) { m_originalSample = Sample(std::move(buffer)); } + const auto buffer = ResourceCache::fetch(PathUtil::pathFromQString(file)); + m_originalSample = Sample{std::move(buffer)}; findBPM(); findSlices(); @@ -339,7 +339,7 @@ void SlicerT::saveSettings(QDomDocument& document, QDomElement& element) element.setAttribute("src", m_originalSample.sampleFile()); if (m_originalSample.sampleFile().isEmpty()) { - element.setAttribute("sampledata", m_originalSample.toBase64()); + element.setAttribute("sampledata", QString::fromStdString(m_originalSample.buffer()->toBase64())); } element.setAttribute("totalSlices", static_cast(m_slicePoints.size())); @@ -360,7 +360,7 @@ void SlicerT::loadSettings(const QDomElement& element) { if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { - auto buffer = gui::SampleLoader::createBufferFromFile(srcFile); + auto buffer = ResourceCache::fetch(PathUtil::pathFromQString(srcFile)); m_originalSample = Sample(std::move(buffer)); } else @@ -371,8 +371,8 @@ void SlicerT::loadSettings(const QDomElement& element) } else if (auto sampleData = element.attribute("sampledata"); !sampleData.isEmpty()) { - auto buffer = gui::SampleLoader::createBufferFromBase64(sampleData); - m_originalSample = Sample(std::move(buffer)); + auto buffer = ResourceCache::fetch(sampleData.toStdString()); + m_originalSample = Sample{std::move(buffer)}; } if (!element.attribute("totalSlices").isEmpty()) diff --git a/plugins/SlicerT/SlicerTView.cpp b/plugins/SlicerT/SlicerTView.cpp index 7af2db1430e..153b29175d1 100644 --- a/plugins/SlicerT/SlicerTView.cpp +++ b/plugins/SlicerT/SlicerTView.cpp @@ -31,9 +31,7 @@ #include "Clipboard.h" #include "DataFile.h" #include "InstrumentTrack.h" -#include "InstrumentView.h" -#include "PixmapButton.h" -#include "SampleLoader.h" +#include "SampleFilePicker.h" #include "SlicerT.h" #include "StringPairDrag.h" #include "Track.h" @@ -136,7 +134,7 @@ void SlicerTView::exportMidi() void SlicerTView::openFiles() { - const auto audioFile = SampleLoader::openAudioFile(); + const auto audioFile = SampleFilePicker::openAudioFile(); if (audioFile.isEmpty()) { return; } m_slicerTParent->updateFile(audioFile); } diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 37f55572f4c..a5f5a9cbc17 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -116,7 +116,7 @@ void SlicerTWaveform::drawSeekerWaveform() const auto& sample = m_slicerTParent->m_originalSample; - m_sampleThumbnail = SampleThumbnail{sample}; + m_sampleThumbnail = ResourceCache::fetch(sample.buffer()->path()); const auto param = SampleThumbnail::VisualizeParameters{ .sampleRect = m_seekerWaveform.rect(), @@ -126,7 +126,7 @@ void SlicerTWaveform::drawSeekerWaveform() .reversed = sample.reversed() }; - m_sampleThumbnail.visualize(param, brush); + m_sampleThumbnail->visualize(param, brush); // increase brightness in inner color @@ -184,7 +184,7 @@ void SlicerTWaveform::drawEditorWaveform() const auto& sample = m_slicerTParent->m_originalSample; - m_sampleThumbnail = SampleThumbnail{sample}; + m_sampleThumbnail = ResourceCache::fetch(sample.buffer()->path()); const auto param = SampleThumbnail::VisualizeParameters{ .sampleRect = QRect(0, zoomOffset, m_editorWidth, static_cast(m_zoomLevel * m_editorHeight)), @@ -194,7 +194,7 @@ void SlicerTWaveform::drawEditorWaveform() .reversed = sample.reversed(), }; - m_sampleThumbnail.visualize(param, brush); + m_sampleThumbnail->visualize(param, brush); // increase brightness in inner color QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index 029b6932027..b1ea5f913ff 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -111,7 +111,7 @@ public slots: QPixmap m_sliceEditor; QPixmap m_emptySampleIcon; - SampleThumbnail m_sampleThumbnail; + std::shared_ptr m_sampleThumbnail = std::make_shared(); SlicerT* m_slicerTParent; diff --git a/plugins/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index 61a6c491959..51894dd8209 100644 --- a/plugins/TripleOscillator/TripleOscillator.cpp +++ b/plugins/TripleOscillator/TripleOscillator.cpp @@ -29,7 +29,6 @@ #include "TripleOscillator.h" #include "AudioEngine.h" #include "AutomatableButton.h" -#include "debug.h" #include "Engine.h" #include "InstrumentTrack.h" #include "Knob.h" @@ -38,7 +37,7 @@ #include "PathUtil.h" #include "PixmapButton.h" #include "SampleBuffer.h" -#include "SampleLoader.h" +#include "SampleFilePicker.h" #include "Song.h" #include "embed.h" #include "plugin_export.h" @@ -137,13 +136,13 @@ OscillatorObject::OscillatorObject( Model * _parent, int _idx ) : void OscillatorObject::oscUserDefWaveDblClick() { - auto af = gui::SampleLoader::openWaveformFile(); + auto af = gui::SampleFilePicker::openWaveformFile(); if( af != "" ) { - m_sampleBuffer = gui::SampleLoader::createBufferFromFile(af); + m_sampleBuffer = ResourceCache::fetch(PathUtil::pathFromQString(af)); m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_sampleBuffer.get()); // TODO: - //m_usrWaveBtn->setToolTip(m_sampleBuffer->audioFile()); + //m_usrWaveBtn->setToolTip(PathUtil::qStringFromPath(m_sampleBuffer->path())); } } @@ -252,8 +251,7 @@ void TripleOscillator::saveSettings( QDomDocument & _doc, QDomElement & _this ) "modalgo" + QString::number( i+1 ) ); m_osc[i]->m_useWaveTableModel.saveSettings( _doc, _this, "useWaveTable" + QString::number (i+1 ) ); - _this.setAttribute( "userwavefile" + is, - m_osc[i]->m_sampleBuffer->audioFile() ); + _this.setAttribute("userwavefile" + is, PathUtil::qStringFromPath(m_osc[i]->m_sampleBuffer->path())); } } @@ -285,7 +283,8 @@ void TripleOscillator::loadSettings( const QDomElement & _this ) { if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) { - m_osc[i]->m_sampleBuffer = gui::SampleLoader::createBufferFromFile(userWaveFile); + const auto path = PathUtil::pathFromQString(userWaveFile); + m_osc[i]->m_sampleBuffer = ResourceCache::fetch(path); m_osc[i]->m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_osc[i]->m_sampleBuffer.get()); } else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } diff --git a/plugins/TripleOscillator/TripleOscillator.h b/plugins/TripleOscillator/TripleOscillator.h index fd6fc85ee3f..98c8ad24111 100644 --- a/plugins/TripleOscillator/TripleOscillator.h +++ b/plugins/TripleOscillator/TripleOscillator.h @@ -71,7 +71,7 @@ class OscillatorObject : public Model IntModel m_waveShapeModel; IntModel m_modulationAlgoModel; BoolModel m_useWaveTableModel; - std::shared_ptr m_sampleBuffer = SampleBuffer::emptyBuffer(); + std::shared_ptr m_sampleBuffer = std::make_shared(); std::shared_ptr m_userAntiAliasWaveTable; float m_volumeLeft; diff --git a/src/core/EnvelopeAndLfoParameters.cpp b/src/core/EnvelopeAndLfoParameters.cpp index a3c3bcf9184..bcde36916b6 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -31,7 +31,6 @@ #include "Engine.h" #include "Oscillator.h" #include "PathUtil.h" -#include "SampleLoader.h" #include "Song.h" namespace lmms @@ -353,7 +352,7 @@ void EnvelopeAndLfoParameters::saveSettings( QDomDocument & _doc, m_lfoAmountModel.saveSettings( _doc, _parent, "lamt" ); m_x100Model.saveSettings( _doc, _parent, "x100" ); m_controlEnvAmountModel.saveSettings( _doc, _parent, "ctlenvamt" ); - _parent.setAttribute("userwavefile", m_userWave->audioFile()); + _parent.setAttribute("userwavefile", PathUtil::qStringFromPath(m_userWave->path())); } @@ -389,7 +388,8 @@ void EnvelopeAndLfoParameters::loadSettings( const QDomElement & _this ) { if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) { - m_userWave = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + const auto path = PathUtil::pathFromQString(_this.attribute("userwavefile")); + m_userWave = ResourceCache::fetch(path); } else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } } diff --git a/src/core/LfoController.cpp b/src/core/LfoController.cpp index 96ea71f7b50..754630b001f 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -30,7 +30,6 @@ #include "AudioEngine.h" #include "PathUtil.h" -#include "SampleLoader.h" #include "Song.h" namespace lmms @@ -223,7 +222,7 @@ void LfoController::saveSettings( QDomDocument & _doc, QDomElement & _this ) m_phaseModel.saveSettings( _doc, _this, "phase" ); m_waveModel.saveSettings( _doc, _this, "wave" ); m_multiplierModel.saveSettings( _doc, _this, "multiplier" ); - _this.setAttribute("userwavefile", m_userDefSampleBuffer->audioFile()); + _this.setAttribute("userwavefile", PathUtil::qStringFromPath(m_userDefSampleBuffer->path())); } @@ -243,7 +242,8 @@ void LfoController::loadSettings( const QDomElement & _this ) { if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) { - m_userDefSampleBuffer = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + const auto path = PathUtil::pathFromQString(_this.attribute(userWaveFile)); + m_userDefSampleBuffer = ResourceCache::fetch(path); } else { Engine::getSong()->collectError(QString("%1: %2").arg(tr("Sample not found"), userWaveFile)); } } diff --git a/src/core/PathUtil.cpp b/src/core/PathUtil.cpp index 03ec465a929..d564a167a93 100644 --- a/src/core/PathUtil.cpp +++ b/src/core/PathUtil.cpp @@ -188,4 +188,22 @@ namespace lmms::PathUtil return basePrefix(shortestBase) + relativeOrAbsolute(absolutePath, shortestBase); } + std::filesystem::path pathFromQString(const QString& path) + { +#ifdef _WIN32 + return std::filesystem::path{path.toStdWString()}; +#else + return std::filesystem::path{path.toStdString()}; +#endif + } + + QString qStringFromPath(const std::filesystem::path& path) + { +#ifdef _WIN32 + return QString::fromStdWString(path.generic_wstring()); +#else + return QString::fromStdString(path.native()); +#endif + } + } // namespace lmms::PathUtil diff --git a/src/core/Sample.cpp b/src/core/Sample.cpp index 3a1dbfcb256..e4efda3eb84 100644 --- a/src/core/Sample.cpp +++ b/src/core/Sample.cpp @@ -23,6 +23,7 @@ */ #include "Sample.h" +#include "PathUtil.h" #include "lmms_math.h" @@ -31,7 +32,7 @@ namespace lmms { Sample::Sample(const QString& audioFile) - : m_buffer(std::make_shared(audioFile)) + : m_buffer(std::make_shared(PathUtil::pathFromQString(audioFile))) , m_startFrame(0) , m_endFrame(m_buffer->size()) , m_loopStartFrame(0) @@ -40,7 +41,7 @@ Sample::Sample(const QString& audioFile) } Sample::Sample(const QByteArray& base64, int sampleRate) - : m_buffer(std::make_shared(base64, sampleRate)) + : m_buffer(std::make_shared(base64.toStdString(), sampleRate)) , m_startFrame(0) , m_endFrame(m_buffer->size()) , m_loopStartFrame(0) diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp index fda3f2f663e..7e258084e28 100644 --- a/src/core/SampleBuffer.cpp +++ b/src/core/SampleBuffer.cpp @@ -23,11 +23,16 @@ */ #include "SampleBuffer.h" + +#include +#include +#include #include +#include "GuiApplication.h" +#include "MainWindow.h" #include "PathUtil.h" #include "SampleDecoder.h" -#include "lmms_basics.h" namespace lmms { @@ -37,29 +42,32 @@ SampleBuffer::SampleBuffer(const SampleFrame* data, size_t numFrames, int sample { } -SampleBuffer::SampleBuffer(const QString& audioFile) +SampleBuffer::SampleBuffer(const std::filesystem::path& path) + : m_path(path) { - if (audioFile.isEmpty()) { throw std::runtime_error{"Failure loading audio file: Audio file path is empty."}; } - const auto absolutePath = PathUtil::toAbsolute(audioFile); + const auto absolutePath = PathUtil::toAbsolute(PathUtil::qStringFromPath(path)); if (auto decodedResult = SampleDecoder::decode(absolutePath)) { auto& [data, sampleRate] = *decodedResult; m_data = std::move(data); m_sampleRate = sampleRate; - m_audioFile = PathUtil::toShortestRelative(audioFile); return; } - throw std::runtime_error{ - "Failed to decode audio file: Either the audio codec is unsupported, or the file is corrupted."}; + // TODO: Move GUI error handling outside the core + if (gui::getGUI()) + { + QMessageBox::critical(gui::getGUI()->mainWindow(), QObject::tr("Failed to load audio file"), + QObject::tr("The audio codec is possibly unsupported, the audio file corrupted, or the path is invalid.")); + } } -SampleBuffer::SampleBuffer(const QString& base64, int sampleRate) +SampleBuffer::SampleBuffer(const std::string& base64, int sampleRate) : m_sampleRate(sampleRate) { // TODO: Replace with non-Qt equivalent - const auto bytes = QByteArray::fromBase64(base64.toUtf8()); + const auto bytes = QByteArray::fromBase64(QString::fromStdString(base64).toUtf8()); m_data.resize(bytes.size() / sizeof(SampleFrame)); std::memcpy(reinterpret_cast(m_data.data()), bytes, m_data.size() * sizeof(SampleFrame)); } @@ -70,27 +78,13 @@ SampleBuffer::SampleBuffer(std::vector data, int sampleRate) { } -void swap(SampleBuffer& first, SampleBuffer& second) noexcept -{ - using std::swap; - swap(first.m_data, second.m_data); - swap(first.m_audioFile, second.m_audioFile); - swap(first.m_sampleRate, second.m_sampleRate); -} - -QString SampleBuffer::toBase64() const +std::string SampleBuffer::toBase64() const { // TODO: Replace with non-Qt equivalent const auto data = reinterpret_cast(m_data.data()); const auto size = static_cast(m_data.size() * sizeof(SampleFrame)); const auto byteArray = QByteArray{data, size}; - return byteArray.toBase64(); -} - -auto SampleBuffer::emptyBuffer() -> std::shared_ptr -{ - static auto s_buffer = std::make_shared(); - return s_buffer; + return byteArray.toBase64().toStdString(); } } // namespace lmms diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 5ef001e20d1..d580c261bc3 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -30,7 +30,6 @@ #include "PathUtil.h" #include "SampleBuffer.h" #include "SampleClipView.h" -#include "SampleLoader.h" #include "SampleTrack.h" #include "TimeLineWidget.h" @@ -120,7 +119,7 @@ void SampleClip::changeLengthToSampleLength() -const QString& SampleClip::sampleFile() const +QString SampleClip::sampleFile() const { return m_sample.sampleFile(); } @@ -150,7 +149,8 @@ void SampleClip::setSampleFile(const QString& sf) if (!sf.isEmpty()) { //Otherwise set it to the sample's length - m_sample = Sample(gui::SampleLoader::createBufferFromFile(sf)); + const auto buffer = ResourceCache::fetch(PathUtil::pathFromQString(sf)); + m_sample = Sample{std::move(buffer)}; length = sampleLength(); } @@ -270,7 +270,7 @@ void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) if( sampleFile() == "" ) { QString s; - _this.setAttribute("data", m_sample.toBase64()); + _this.setAttribute("data", QString::fromStdString(m_sample.buffer()->toBase64())); } _this.setAttribute( "sample_rate", m_sample.sampleRate()); @@ -309,7 +309,7 @@ void SampleClip::loadSettings( const QDomElement & _this ) auto sampleRate = _this.hasAttribute("sample_rate") ? _this.attribute("sample_rate").toInt() : Engine::audioEngine()->outputSampleRate(); - auto buffer = gui::SampleLoader::createBufferFromBase64(_this.attribute("data"), sampleRate); + auto buffer = ResourceCache::fetch(_this.attribute("data").toStdString(), sampleRate); m_sample = Sample(std::move(buffer)); } changeLength( _this.attribute( "len" ).toInt() ); diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp index f2ddc2a4a04..4e0be89fed6 100644 --- a/src/core/SamplePlayHandle.cpp +++ b/src/core/SamplePlayHandle.cpp @@ -145,7 +145,7 @@ bool SamplePlayHandle::isFromTrack( const Track * _track ) const f_cnt_t SamplePlayHandle::totalFrames() const { return (m_sample->endFrame() - m_sample->startFrame()) * - (static_cast(Engine::audioEngine()->outputSampleRate()) / m_sample->sampleRate()); + (static_cast(Engine::audioEngine()->outputSampleRate()) / m_sample->buffer()->sampleRate()); } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 51f4638326a..b5eeebb2cc8 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -33,7 +33,7 @@ SET(LMMS_SRCS gui/PluginBrowser.cpp gui/ProjectNotes.cpp gui/RowTableView.cpp - gui/SampleLoader.cpp + gui/SampleFilePicker.cpp gui/SampleTrackWindow.cpp gui/SampleThumbnail.cpp gui/SendButtonIndicator.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 8985a047588..02b29ba1c95 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -54,12 +54,12 @@ #include "InstrumentTrackWindow.h" #include "KeyboardShortcuts.h" #include "MainWindow.h" +#include "PathUtil.h" #include "PatternStore.h" #include "PluginFactory.h" #include "PresetPreviewPlayHandle.h" #include "Sample.h" #include "SampleClip.h" -#include "SampleLoader.h" #include "SamplePlayHandle.h" #include "SampleTrack.h" #include "Song.h" @@ -744,12 +744,13 @@ void FileBrowserTreeWidget::previewFileItem(FileItem* file) embed::getIconPixmap("sample_file", 24, 24), 0); // TODO: this can be removed once we do this outside the event thread qApp->processEvents(QEventLoop::ExcludeUserInputEvents); - if (auto buffer = SampleLoader::createBufferFromFile(fileName)) - { - auto s = new SamplePlayHandle(new lmms::Sample{std::move(buffer)}); - s->setDoneMayReturnTrue(false); - newPPH = s; - } + + const auto path = PathUtil::pathFromQString(fileName); + const auto buffer = ResourceCache::fetch(path); + + auto s = new SamplePlayHandle(new lmms::Sample{std::move(buffer)}); + s->setDoneMayReturnTrue(false); + newPPH = s; delete tf; } else if ( diff --git a/src/gui/LfoControllerDialog.cpp b/src/gui/LfoControllerDialog.cpp index 559ac13360c..8d1c9f45360 100644 --- a/src/gui/LfoControllerDialog.cpp +++ b/src/gui/LfoControllerDialog.cpp @@ -24,6 +24,7 @@ */ +#include "PathUtil.h" #include "embed.h" @@ -31,7 +32,7 @@ #include "Knob.h" #include "TempoSyncKnob.h" #include "PixmapButton.h" -#include "SampleLoader.h" +#include "SampleFilePicker.h" namespace lmms::gui { @@ -211,14 +212,15 @@ LfoControllerDialog::~LfoControllerDialog() void LfoControllerDialog::askUserDefWave() { - const auto fileName = SampleLoader::openWaveformFile(); + const auto fileName = SampleFilePicker::openWaveformFile(); + const auto path = PathUtil::pathFromQString(fileName); if (fileName.isEmpty()) { return; } auto lfoModel = dynamic_cast(model()); auto& buffer = lfoModel->m_userDefSampleBuffer; - buffer = SampleLoader::createBufferFromFile(fileName); + buffer = ResourceCache::fetch(path); - m_userWaveBtn->setToolTip(buffer->audioFile()); + m_userWaveBtn->setToolTip(PathUtil::qStringFromPath(buffer->path())); } diff --git a/src/gui/SampleLoader.cpp b/src/gui/SampleFilePicker.cpp similarity index 63% rename from src/gui/SampleLoader.cpp rename to src/gui/SampleFilePicker.cpp index d72b0ba5cc5..0e3f8709ba3 100644 --- a/src/gui/SampleLoader.cpp +++ b/src/gui/SampleFilePicker.cpp @@ -1,7 +1,7 @@ /* - * SampleLoader.cpp - Static functions that open audio files + * SampleFilePicker.cpp * - * Copyright (c) 2023 saker + * Copyright (c) 2024 saker * * This file is part of LMMS - https://lmms.io * @@ -22,24 +22,18 @@ * */ -#include "SampleLoader.h" - -#include -#include -#include +#include "SampleFilePicker.h" #include "ConfigManager.h" #include "FileDialog.h" -#include "GuiApplication.h" #include "PathUtil.h" #include "SampleDecoder.h" -#include "Song.h" namespace lmms::gui { -QString SampleLoader::openAudioFile(const QString& previousFile) +QString SampleFilePicker::openAudioFile(const QString& previousFile) { auto openFileDialog = FileDialog(nullptr, QObject::tr("Open audio file")); - auto dir = !previousFile.isEmpty() ? QFileInfo(PathUtil::toAbsolute(previousFile)).absolutePath() : ConfigManager::inst()->userSamplesDir(); + auto dir = !previousFile.isEmpty() ? PathUtil::toAbsolute(previousFile) : ConfigManager::inst()->userSamplesDir(); // change dir to position of previously opened file openFileDialog.setDirectory(dir); @@ -82,45 +76,10 @@ QString SampleLoader::openAudioFile(const QString& previousFile) return ""; } -QString SampleLoader::openWaveformFile(const QString& previousFile) +QString SampleFilePicker::openWaveformFile(const QString& previousFile) { return openAudioFile( previousFile.isEmpty() ? ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac" : previousFile); } -std::shared_ptr SampleLoader::createBufferFromFile(const QString& filePath) -{ - if (filePath.isEmpty()) { return SampleBuffer::emptyBuffer(); } - - try - { - return std::make_shared(filePath); - } - catch (const std::runtime_error& error) - { - if (getGUI()) { displayError(QString::fromStdString(error.what())); } - return SampleBuffer::emptyBuffer(); - } -} - -std::shared_ptr SampleLoader::createBufferFromBase64(const QString& base64, int sampleRate) -{ - if (base64.isEmpty()) { return SampleBuffer::emptyBuffer(); } - - try - { - return std::make_shared(base64, sampleRate); - } - catch (const std::runtime_error& error) - { - if (getGUI()) { displayError(QString::fromStdString(error.what())); } - return SampleBuffer::emptyBuffer(); - } -} - -void SampleLoader::displayError(const QString& message) -{ - QMessageBox::critical(nullptr, QObject::tr("Error loading sample"), message); -} - } // namespace lmms::gui diff --git a/src/gui/SampleThumbnail.cpp b/src/gui/SampleThumbnail.cpp index c31c0d93e9c..dbc23c86ddb 100644 --- a/src/gui/SampleThumbnail.cpp +++ b/src/gui/SampleThumbnail.cpp @@ -28,9 +28,12 @@ #include #include +#include "AudioEngine.h" +#include "Engine.h" +#include "SampleBuffer.h" + namespace { - constexpr auto MaxSampleThumbnailCacheSize = 32; - constexpr auto AggregationPerZoomStep = 10; +constexpr auto AggregationPerZoomStep = 10; } namespace lmms { @@ -69,41 +72,30 @@ SampleThumbnail::Thumbnail SampleThumbnail::Thumbnail::zoomOut(float factor) con return Thumbnail{std::move(peaks), m_samplesPerPeak * factor}; } -SampleThumbnail::SampleThumbnail(const Sample& sample) +SampleThumbnail::SampleThumbnail(std::shared_ptr buffer) { - auto entry = SampleThumbnailEntry{sample.sampleFile(), QFileInfo{sample.sampleFile()}.lastModified()}; - if (!entry.filePath.isEmpty()) - { - const auto it = s_sampleThumbnailCacheMap.find(entry); - if (it != s_sampleThumbnailCacheMap.end()) - { - m_thumbnailCache = it->second; - return; - } - - if (s_sampleThumbnailCacheMap.size() == MaxSampleThumbnailCacheSize) - { - const auto leastUsed = std::min_element(s_sampleThumbnailCacheMap.begin(), s_sampleThumbnailCacheMap.end(), - [](const auto& a, const auto& b) { return a.second.use_count() < b.second.use_count(); }); - s_sampleThumbnailCacheMap.erase(leastUsed->first); - } - - s_sampleThumbnailCacheMap[std::move(entry)] = m_thumbnailCache; - } + if (buffer == nullptr || buffer->size() == 0) { return; } - if (!sample.buffer()) { throw std::runtime_error{"Cannot create a sample thumbnail with no buffer"}; } - if (sample.sampleSize() == 0) { return; } + const auto fullResolutionWidth = buffer->size() * DEFAULT_CHANNELS; + m_thumbnails.emplace_back(&buffer->data()[0][0], fullResolutionWidth, fullResolutionWidth); - const auto fullResolutionWidth = sample.sampleSize() * DEFAULT_CHANNELS; - m_thumbnailCache->emplace_back(&sample.buffer()->data()->left(), fullResolutionWidth, fullResolutionWidth); - - while (m_thumbnailCache->back().width() >= AggregationPerZoomStep) + while (m_thumbnails.back().width() >= AggregationPerZoomStep) { - auto zoomedOutThumbnail = m_thumbnailCache->back().zoomOut(AggregationPerZoomStep); - m_thumbnailCache->emplace_back(std::move(zoomedOutThumbnail)); + auto zoomedOutThumbnail = m_thumbnails.back().zoomOut(AggregationPerZoomStep); + m_thumbnails.emplace_back(std::move(zoomedOutThumbnail)); } } +SampleThumbnail::SampleThumbnail(const std::filesystem::path& path) + : SampleThumbnail(ResourceCache::fetch(path)) +{ +} + +SampleThumbnail::SampleThumbnail(const std::string& base64) + : SampleThumbnail(ResourceCache::fetch(base64)) +{ +} + void SampleThumbnail::visualize(VisualizeParameters parameters, QPainter& painter) const { const auto& sampleRect = parameters.sampleRect; @@ -117,10 +109,10 @@ void SampleThumbnail::visualize(VisualizeParameters parameters, QPainter& painte if (sampleRange <= 0 || sampleRange > 1) { return; } const auto targetThumbnailWidth = static_cast(static_cast(sampleRect.width()) / sampleRange); - const auto finerThumbnail = std::find_if(m_thumbnailCache->rbegin(), m_thumbnailCache->rend(), + const auto finerThumbnail = std::find_if(m_thumbnails.rbegin(), m_thumbnails.rend(), [&](const auto& thumbnail) { return thumbnail.width() >= targetThumbnailWidth; }); - if (finerThumbnail == m_thumbnailCache->rend()) + if (finerThumbnail == m_thumbnails.rend()) { qDebug() << "Could not find closest finer thumbnail for a target width of" << targetThumbnailWidth; return; diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index d5cfb211ee8..7c8f2bb4d43 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -23,7 +23,6 @@ */ #include "SampleClipView.h" - #include #include #include @@ -33,7 +32,7 @@ #include "embed.h" #include "PathUtil.h" #include "SampleClip.h" -#include "SampleLoader.h" +#include "SampleFilePicker.h" #include "SampleThumbnail.h" #include "Song.h" #include "StringPairDrag.h" @@ -62,15 +61,13 @@ void SampleClipView::updateSample() { update(); - m_sampleThumbnail = SampleThumbnail{m_clip->m_sample}; + m_sampleThumbnail = ResourceCache::fetch(m_clip->m_sample.buffer()->path()); // set tooltip to filename so that user can see what sample this // sample-clip contains - setToolTip( - !m_clip->m_sample.sampleFile().isEmpty() - ? PathUtil::toAbsolute(m_clip->m_sample.sampleFile()) - : tr("Double-click to open sample") - ); + + const auto sampleFile = m_clip->m_sample.sampleFile(); + setToolTip(!sampleFile.isEmpty() ? PathUtil::toAbsolute(sampleFile) : tr("Double-click to open sample")); } @@ -126,7 +123,10 @@ void SampleClipView::dropEvent( QDropEvent * _de ) } else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) { - m_clip->setSampleBuffer(SampleLoader::createBufferFromBase64(StringPairDrag::decodeValue(_de))); + const auto de = StringPairDrag::decodeValue(_de); + const auto path = PathUtil::pathFromQString(de); + const auto buffer = ResourceCache::fetch(path); + m_clip->setSampleBuffer(std::move(buffer)); m_clip->updateLength(); update(); _de->accept(); @@ -183,7 +183,7 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { - const QString selectedAudioFile = SampleLoader::openAudioFile(); + const QString selectedAudioFile = SampleFilePicker::openAudioFile(); if (selectedAudioFile.isEmpty()) { return; } @@ -193,11 +193,8 @@ void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) } else { - auto sampleBuffer = SampleLoader::createBufferFromFile(selectedAudioFile); - if (sampleBuffer != SampleBuffer::emptyBuffer()) - { - m_clip->setSampleBuffer(sampleBuffer); - } + auto sampleBuffer = ResourceCache::fetch(PathUtil::pathFromQString(selectedAudioFile)); + m_clip->setSampleBuffer(sampleBuffer); } } @@ -284,7 +281,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) .reversed = sample.reversed() }; - m_sampleThumbnail.visualize(param, p); + m_sampleThumbnail->visualize(param, p); } QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index e3867e7ab6f..afca2988568 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -1021,7 +1021,7 @@ void AutomationEditor::setGhostSample(SampleClip* newGhostSample) // Expects a pointer to a Sample buffer or nullptr. m_ghostSample = newGhostSample; m_renderSample = true; - m_sampleThumbnail = SampleThumbnail{newGhostSample->sample()}; + m_sampleThumbnail = ResourceCache::fetch(newGhostSample->sample().buffer()->path()); } void AutomationEditor::paintEvent(QPaintEvent * pe ) @@ -1220,12 +1220,12 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) const auto param = SampleThumbnail::VisualizeParameters{ .sampleRect = QRect(startPos, yOffset, sampleWidth, sampleHeight), .amplification = sample.amplification(), - .sampleStart = static_cast(sample.startFrame()) / sample.sampleSize(), - .sampleEnd = static_cast(sample.endFrame()) / sample.sampleSize(), + .sampleStart = static_cast(sample.startFrame()) / sample.sampleRate(), + .sampleEnd = static_cast(sample.endFrame()) / sample.sampleRate(), .reversed = sample.reversed() }; - m_sampleThumbnail.visualize(param, p); + m_sampleThumbnail->visualize(param, p); } // draw ghost notes diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index 95926450680..05ee13f5a6b 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -32,11 +32,12 @@ #include "EnvelopeGraph.h" #include "LfoGraph.h" #include "EnvelopeAndLfoParameters.h" -#include "SampleLoader.h" #include "Knob.h" #include "LedCheckBox.h" #include "DataFile.h" +#include "PathUtil.h" #include "PixmapButton.h" +#include "SampleFrame.h" #include "StringPairDrag.h" #include "TempoSyncKnob.h" #include "TextFloat.h" @@ -241,7 +242,8 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) QString value = StringPairDrag::decodeValue( _de ); if( type == "samplefile" ) { - m_params->m_userWave = SampleLoader::createBufferFromFile(value); + const auto path = PathUtil::pathFromQString(value); + m_params->m_userWave = ResourceCache::fetch(path); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -253,7 +255,9 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) auto file = dataFile.content(). firstChildElement().firstChildElement(). firstChildElement().attribute("src"); - m_params->m_userWave = SampleLoader::createBufferFromFile(file); + + const auto path = PathUtil::pathFromQString(file); + m_params->m_userWave = ResourceCache::fetch(path); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -269,7 +273,7 @@ void EnvelopeAndLfoView::lfoUserWaveChanged() if( static_cast(m_params->m_lfoWaveModel.value()) == EnvelopeAndLfoParameters::LfoShape::UserDefinedWave ) { - if (m_params->m_userWave->size() <= 1) + if (m_params->m_userWave->data().size() <= 1) { TextFloat::displayMessage( tr( "Hint" ), tr( "Drag and drop a sample into this window." ), diff --git a/src/gui/widgets/Graph.cpp b/src/gui/widgets/Graph.cpp index 4e06e2f1a4b..6dc231b620f 100644 --- a/src/gui/widgets/Graph.cpp +++ b/src/gui/widgets/Graph.cpp @@ -26,9 +26,9 @@ #include #include "Graph.h" -#include "SampleLoader.h" +#include "PathUtil.h" +#include "SampleFilePicker.h" #include "StringPairDrag.h" -#include "SampleBuffer.h" #include "Oscillator.h" namespace lmms @@ -589,13 +589,14 @@ void graphModel::setWaveToNoise() QString graphModel::setWaveToUser() { - QString fileName = gui::SampleLoader::openWaveformFile(); - if( fileName.isEmpty() == false ) + QString fileName = gui::SampleFilePicker::openWaveformFile(); + if (!fileName.isEmpty()) { - auto sampleBuffer = gui::SampleLoader::createBufferFromFile(fileName); + const auto path = PathUtil::pathFromQString(fileName); + auto buffer = ResourceCache::fetch(path); for( int i = 0; i < length(); i++ ) { - m_samples[i] = Oscillator::userWaveSample(sampleBuffer.get(), i / static_cast(length())); + m_samples[i] = Oscillator::userWaveSample(buffer.get(), i / static_cast(length())); } }