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/SampleCache.h b/include/SampleCache.h new file mode 100644 index 00000000000..9777ff759c1 --- /dev/null +++ b/include/SampleCache.h @@ -0,0 +1,125 @@ +/* + * SampleCache.h + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SAMPLE_CACHE_H +#define LMMS_SAMPLE_CACHE_H + +#include +#include +#include +#include + +#include "SampleBuffer.h" + +namespace lmms { +class SampleCache +{ +public: + /** + Fetches a sample from the cache through a path to an audio file, + and returns the stored buffer. + + If `path` exists in the cache, its last write time is checked with what is currently in the cache. If + there is a mismatch, the sample is reloaded from disk, its entry in the cache is updated, and the sample is + returned. + + If `path` does not exist in the cache, the sample is loaded from disk and + then returned. + */ + static auto fetch(const QString& path) -> std::shared_ptr; + + /** + Fetches a sample from the cache through a Base64 string and a sample rate + and returns the stored buffer. + + If an entry for a `base64` string with a certain `sampleRate` exists in the cache, the stored sample is + returned. Otherwise, if it does not exist in the cache, the sample is loaded and then returned. + */ + static auto fetch(const QString& base64, int sampleRate) -> std::shared_ptr; + +private: + struct AudioFileEntry + { + friend bool operator==(const AudioFileEntry& first, const AudioFileEntry& second) noexcept + { + return first.path == second.path && first.lastWriteTime == second.lastWriteTime; + } + + std::filesystem::path path; + std::filesystem::file_time_type lastWriteTime; + }; + + struct Base64Entry + { + friend bool operator==(const Base64Entry& first, const Base64Entry& second) noexcept + { + return first.base64 == second.base64 && first.sampleRate == second.sampleRate; + } + + std::string base64; + int sampleRate; + }; + + struct Hash + { + std::size_t operator()(const AudioFileEntry& entry) const noexcept + { + return std::filesystem::hash_value(entry.path); + } + + std::size_t operator()(const Base64Entry& entry) const noexcept + { + return std::hash()(entry.base64); + } + }; + + template + static auto get(const T& entry, std::unordered_map, Hash>& map, Args... args) + { + const auto it = map.find(entry); + + if (it == map.end()) + { + const auto buffer = std::make_shared(std::forward(args)...); + map.insert(std::make_pair(entry, buffer)); + return buffer; + } + + const auto entryLock = it->second.lock(); + if (!entryLock) + { + const auto buffer = std::make_shared(std::forward(args)...); + map[entry] = buffer; + return buffer; + } + + return entryLock; + } + + inline static std::unordered_map, Hash> s_audioFileMap; + inline static std::unordered_map, Hash> s_base64Map; +}; +} // namespace lmms + +#endif // LMMS_SAMPLE_CACHE_H diff --git a/include/SampleFilePicker.h b/include/SampleFilePicker.h new file mode 100644 index 00000000000..ae40b6385ca --- /dev/null +++ b/include/SampleFilePicker.h @@ -0,0 +1,40 @@ +/* + * SampleFilePicker.h + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SAMPLE_FILE_PICKER_H +#define LMMS_GUI_SAMPLE_FILE_PICKER_H + +#include +#include "lmms_export.h" + +namespace lmms::gui { +class LMMS_EXPORT SampleFilePicker +{ +public: + static QString openAudioFile(const QString& previousFile = ""); + static QString openWaveformFile(const QString& previousFile = ""); +}; +} // namespace lmms::gui + +#endif // LMMS_GUI_SAMPLE_FILE_PICKER_H diff --git a/include/SampleLoader.h b/include/SampleLoader.h index fd8f1135725..d4dd52b2ea5 100644 --- a/include/SampleLoader.h +++ b/include/SampleLoader.h @@ -1,7 +1,7 @@ /* - * SampleLoader.h - Load audio and waveform files + * SampleLoader.h * - * Copyright (c) 2023 saker + * Copyright (c) 2024 saker * * This file is part of LMMS - https://lmms.io * @@ -22,8 +22,8 @@ * */ -#ifndef LMMS_GUI_SAMPLE_LOADER_H -#define LMMS_GUI_SAMPLE_LOADER_H +#ifndef LMMS_SAMPLE_LOADER_H +#define LMMS_SAMPLE_LOADER_H #include #include @@ -31,18 +31,14 @@ #include "SampleBuffer.h" #include "lmms_export.h" -namespace lmms::gui { +namespace lmms { class LMMS_EXPORT SampleLoader { 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( + static std::shared_ptr loadBufferFromFile(const QString& filePath); + static std::shared_ptr loadBufferFromBase64( const QString& base64, int sampleRate = Engine::audioEngine()->outputSampleRate()); -private: - static void displayError(const QString& message); }; -} // namespace lmms::gui +} // namespace lmms -#endif // LMMS_GUI_SAMPLE_LOADER_H +#endif // LMMS_SAMPLE_LOADER_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 4cc14ba9cdb..82f188fd198 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -224,7 +224,7 @@ void AudioFileProcessor::loadSettings(const QDomElement& elem) } else if (auto sampleData = elem.attribute("sampledata"); !sampleData.isEmpty()) { - m_sample = Sample(gui::SampleLoader::createBufferFromBase64(sampleData)); + m_sample = Sample(SampleLoader::loadBufferFromBase64(sampleData)); } m_loopModel.loadSettings(elem, "looped"); @@ -317,7 +317,7 @@ void AudioFileProcessor::setAudioFile(const QString& _audio_file, bool _rename) } // else we don't touch the track-name, because the user named it self - m_sample = Sample(gui::SampleLoader::createBufferFromFile(_audio_file)); + m_sample = Sample(SampleLoader::loadBufferFromFile(_audio_file)); 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/SlicerT/SlicerT.cpp b/plugins/SlicerT/SlicerT.cpp index 3b060258401..d41b4ee7d55 100644 --- a/plugins/SlicerT/SlicerT.cpp +++ b/plugins/SlicerT/SlicerT.cpp @@ -320,7 +320,7 @@ std::vector SlicerT::getMidi() void SlicerT::updateFile(QString file) { - if (auto buffer = gui::SampleLoader::createBufferFromFile(file)) { m_originalSample = Sample(std::move(buffer)); } + if (auto buffer = SampleLoader::loadBufferFromFile(file)) { m_originalSample = Sample(std::move(buffer)); } findBPM(); findSlices(); @@ -360,7 +360,7 @@ void SlicerT::loadSettings(const QDomElement& element) { if (QFileInfo(PathUtil::toAbsolute(srcFile)).exists()) { - auto buffer = gui::SampleLoader::createBufferFromFile(srcFile); + auto buffer = SampleLoader::loadBufferFromFile(srcFile); m_originalSample = Sample(std::move(buffer)); } else @@ -371,7 +371,7 @@ void SlicerT::loadSettings(const QDomElement& element) } else if (auto sampleData = element.attribute("sampledata"); !sampleData.isEmpty()) { - auto buffer = gui::SampleLoader::createBufferFromBase64(sampleData); + auto buffer = SampleLoader::loadBufferFromBase64(sampleData); m_originalSample = Sample(std::move(buffer)); } 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/TripleOscillator/TripleOscillator.cpp b/plugins/TripleOscillator/TripleOscillator.cpp index f04cee81835..9c7f9602269 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" @@ -39,6 +38,7 @@ #include "PixmapButton.h" #include "SampleBuffer.h" #include "SampleLoader.h" +#include "SampleFilePicker.h" #include "Song.h" #include "embed.h" #include "plugin_export.h" @@ -137,10 +137,10 @@ 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 = SampleLoader::loadBufferFromFile(af); m_userAntiAliasWaveTable = Oscillator::generateAntiAliasUserWaveTable(m_sampleBuffer.get()); // TODO: //m_usrWaveBtn->setToolTip(m_sampleBuffer->audioFile()); @@ -287,7 +287,7 @@ void TripleOscillator::loadSettings( const QDomElement & _this ) { if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) { - m_osc[i]->m_sampleBuffer = gui::SampleLoader::createBufferFromFile(userWaveFile); + m_osc[i]->m_sampleBuffer = SampleLoader::loadBufferFromFile(userWaveFile); 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/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1e2c4f3cfdb..c388dbae2a3 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -67,6 +67,7 @@ set(LMMS_SRCS core/RenderManager.cpp core/RingBuffer.cpp core/Sample.cpp + core/SampleCache.cpp core/SampleBuffer.cpp core/SampleClip.cpp core/SampleDecoder.cpp diff --git a/src/core/EnvelopeAndLfoParameters.cpp b/src/core/EnvelopeAndLfoParameters.cpp index a3c3bcf9184..8c7ad659894 100644 --- a/src/core/EnvelopeAndLfoParameters.cpp +++ b/src/core/EnvelopeAndLfoParameters.cpp @@ -389,7 +389,7 @@ void EnvelopeAndLfoParameters::loadSettings( const QDomElement & _this ) { if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) { - m_userWave = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + m_userWave = SampleLoader::loadBufferFromFile(_this.attribute("userwavefile")); } 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..a65930557d9 100644 --- a/src/core/LfoController.cpp +++ b/src/core/LfoController.cpp @@ -243,7 +243,7 @@ void LfoController::loadSettings( const QDomElement & _this ) { if (QFileInfo(PathUtil::toAbsolute(userWaveFile)).exists()) { - m_userDefSampleBuffer = gui::SampleLoader::createBufferFromFile(_this.attribute("userwavefile")); + m_userDefSampleBuffer = SampleLoader::loadBufferFromFile(_this.attribute("userwavefile")); } 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/SampleCache.cpp b/src/core/SampleCache.cpp new file mode 100644 index 00000000000..9c4750ebf7f --- /dev/null +++ b/src/core/SampleCache.cpp @@ -0,0 +1,53 @@ +/* + * SampleCache.cpp + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SampleCache.h" + +#include + +#include "PathUtil.h" +#include "SampleBuffer.h" + +namespace lmms { +auto SampleCache::fetch(const QString& path) -> std::shared_ptr +{ + // To ensure that only the main thread is requesting samples from the cache + // Can be removed if proven that other threads are requesting samples besides the main thread + assert(QThread::currentThread() == QCoreApplication::instance()->thread()); + + const auto fsPath = PathUtil::pathFromQString(path); + const auto entry = AudioFileEntry{fsPath, std::filesystem::last_write_time(fsPath)}; + return get(entry, s_audioFileMap, path); +} + +auto SampleCache::fetch(const QString& base64, int sampleRate) -> std::shared_ptr +{ + // To ensure that only the main thread is requesting samples from the cache + // Can be removed if proven that other threads are requesting samples besides the main thread + assert(QThread::currentThread() == QCoreApplication::instance()->thread()); + + const auto entry = Base64Entry{base64.toStdString(), sampleRate}; + return get(entry, s_base64Map, base64, sampleRate); +} +} // namespace lmms diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 5ef001e20d1..6fe3f22586b 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -150,7 +150,7 @@ void SampleClip::setSampleFile(const QString& sf) if (!sf.isEmpty()) { //Otherwise set it to the sample's length - m_sample = Sample(gui::SampleLoader::createBufferFromFile(sf)); + m_sample = Sample(SampleLoader::loadBufferFromFile(sf)); length = sampleLength(); } @@ -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 = SampleLoader::loadBufferFromBase64(_this.attribute("data"), sampleRate); m_sample = Sample(std::move(buffer)); } changeLength( _this.attribute( "len" ).toInt() ); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index fe4a2c462b2..07570ea0bce 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -33,6 +33,7 @@ SET(LMMS_SRCS gui/PluginBrowser.cpp gui/ProjectNotes.cpp gui/RowTableView.cpp + gui/SampleFilePicker.cpp gui/SampleLoader.cpp gui/SampleTrackWindow.cpp gui/SampleWaveform.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index e5168fac8bb..c414d538071 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -743,7 +743,7 @@ 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)) + if (auto buffer = SampleLoader::loadBufferFromFile(fileName)) { auto s = new SamplePlayHandle(new lmms::Sample{std::move(buffer)}); s->setDoneMayReturnTrue(false); diff --git a/src/gui/LfoControllerDialog.cpp b/src/gui/LfoControllerDialog.cpp index 559ac13360c..7e88b5c9ce9 100644 --- a/src/gui/LfoControllerDialog.cpp +++ b/src/gui/LfoControllerDialog.cpp @@ -32,6 +32,7 @@ #include "TempoSyncKnob.h" #include "PixmapButton.h" #include "SampleLoader.h" +#include "SampleFilePicker.h" namespace lmms::gui { @@ -211,12 +212,12 @@ LfoControllerDialog::~LfoControllerDialog() void LfoControllerDialog::askUserDefWave() { - const auto fileName = SampleLoader::openWaveformFile(); + const auto fileName = SampleFilePicker::openWaveformFile(); if (fileName.isEmpty()) { return; } auto lfoModel = dynamic_cast(model()); auto& buffer = lfoModel->m_userDefSampleBuffer; - buffer = SampleLoader::createBufferFromFile(fileName); + buffer = SampleLoader::loadBufferFromFile(fileName); m_userWaveBtn->setToolTip(buffer->audioFile()); } diff --git a/src/gui/SampleFilePicker.cpp b/src/gui/SampleFilePicker.cpp new file mode 100644 index 00000000000..e18992dc80d --- /dev/null +++ b/src/gui/SampleFilePicker.cpp @@ -0,0 +1,85 @@ +/* + * SampleFilePicker.cpp + * + * Copyright (c) 2024 saker + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SampleFilePicker.h" + +#include "ConfigManager.h" +#include "FileDialog.h" +#include "PathUtil.h" +#include "SampleDecoder.h" + +namespace lmms::gui { +QString SampleFilePicker::openAudioFile(const QString& previousFile) +{ + auto openFileDialog = FileDialog(nullptr, QObject::tr("Open audio file")); + auto dir = !previousFile.isEmpty() ? PathUtil::toAbsolute(previousFile) : ConfigManager::inst()->userSamplesDir(); + + // change dir to position of previously opened file + openFileDialog.setDirectory(dir); + openFileDialog.setFileMode(FileDialog::ExistingFiles); + + // set filters + auto fileTypes = QStringList{}; + auto allFileTypes = QStringList{}; + auto nameFilters = QStringList{}; + const auto& supportedAudioTypes = SampleDecoder::supportedAudioTypes(); + + for (const auto& audioType : supportedAudioTypes) + { + const auto name = QString::fromStdString(audioType.name); + const auto extension = QString::fromStdString(audioType.extension); + const auto displayExtension = QString{"*.%1"}.arg(extension); + fileTypes.append(QString{"%1 (%2)"}.arg(FileDialog::tr("%1 files").arg(name), displayExtension)); + allFileTypes.append(displayExtension); + } + + nameFilters.append(QString{"%1 (%2)"}.arg(FileDialog::tr("All audio files"), allFileTypes.join(" "))); + nameFilters.append(fileTypes); + nameFilters.append(QString("%1 (*)").arg(FileDialog::tr("Other files"))); + + openFileDialog.setNameFilters(nameFilters); + + if (!previousFile.isEmpty()) + { + // select previously opened file + openFileDialog.selectFile(QFileInfo{previousFile}.fileName()); + } + + if (openFileDialog.exec() == QDialog::Accepted) + { + if (openFileDialog.selectedFiles().isEmpty()) { return ""; } + + return PathUtil::toShortestRelative(openFileDialog.selectedFiles()[0]); + } + + return ""; +} + +QString SampleFilePicker::openWaveformFile(const QString& previousFile) +{ + return openAudioFile( + previousFile.isEmpty() ? ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac" : previousFile); +} + +} // namespace lmms::gui \ No newline at end of file diff --git a/src/gui/SampleLoader.cpp b/src/gui/SampleLoader.cpp index f2340852d77..25d28a715a0 100644 --- a/src/gui/SampleLoader.cpp +++ b/src/gui/SampleLoader.cpp @@ -28,99 +28,47 @@ #include #include -#include "ConfigManager.h" -#include "FileDialog.h" #include "GuiApplication.h" -#include "PathUtil.h" -#include "SampleDecoder.h" -#include "Song.h" +#include "SampleCache.h" -namespace lmms::gui { -QString SampleLoader::openAudioFile(const QString& previousFile) -{ - auto openFileDialog = FileDialog(nullptr, QObject::tr("Open audio file")); - auto dir = !previousFile.isEmpty() ? PathUtil::toAbsolute(previousFile) : ConfigManager::inst()->userSamplesDir(); - - // change dir to position of previously opened file - openFileDialog.setDirectory(dir); - openFileDialog.setFileMode(FileDialog::ExistingFiles); - - // set filters - auto fileTypes = QStringList{}; - auto allFileTypes = QStringList{}; - auto nameFilters = QStringList{}; - const auto& supportedAudioTypes = SampleDecoder::supportedAudioTypes(); - - for (const auto& audioType : supportedAudioTypes) - { - const auto name = QString::fromStdString(audioType.name); - const auto extension = QString::fromStdString(audioType.extension); - const auto displayExtension = QString{"*.%1"}.arg(extension); - fileTypes.append(QString{"%1 (%2)"}.arg(FileDialog::tr("%1 files").arg(name), displayExtension)); - allFileTypes.append(displayExtension); - } +namespace lmms { - nameFilters.append(QString{"%1 (%2)"}.arg(FileDialog::tr("All audio files"), allFileTypes.join(" "))); - nameFilters.append(fileTypes); - nameFilters.append(QString("%1 (*)").arg(FileDialog::tr("Other files"))); - - openFileDialog.setNameFilters(nameFilters); - - if (!previousFile.isEmpty()) - { - // select previously opened file - openFileDialog.selectFile(QFileInfo{previousFile}.fileName()); - } - - if (openFileDialog.exec() == QDialog::Accepted) - { - if (openFileDialog.selectedFiles().isEmpty()) { return ""; } - - return PathUtil::toShortestRelative(openFileDialog.selectedFiles()[0]); - } - - return ""; -} - -QString SampleLoader::openWaveformFile(const QString& previousFile) +namespace { +void displayError(const QString& message) { - return openAudioFile( - previousFile.isEmpty() ? ConfigManager::inst()->factorySamplesDir() + "waveforms/10saw.flac" : previousFile); + QMessageBox::critical(nullptr, QObject::tr("Error loading sample"), message); } +} // namespace -std::shared_ptr SampleLoader::createBufferFromFile(const QString& filePath) +std::shared_ptr SampleLoader::loadBufferFromFile(const QString& filePath) { if (filePath.isEmpty()) { return SampleBuffer::emptyBuffer(); } try { - return std::make_shared(filePath); + return SampleCache::fetch(filePath); } catch (const std::runtime_error& error) { - if (getGUI()) { displayError(QString::fromStdString(error.what())); } + // TODO: Remove calls to `gui::getGUI` from core and use something to signal back to the GUI instead + if (gui::getGUI()) { displayError(QString::fromStdString(error.what())); } return SampleBuffer::emptyBuffer(); } } -std::shared_ptr SampleLoader::createBufferFromBase64(const QString& base64, int sampleRate) +std::shared_ptr SampleLoader::loadBufferFromBase64(const QString& base64, int sampleRate) { if (base64.isEmpty()) { return SampleBuffer::emptyBuffer(); } try { - return std::make_shared(base64, sampleRate); + return SampleCache::fetch(base64, sampleRate); } catch (const std::runtime_error& error) { - if (getGUI()) { displayError(QString::fromStdString(error.what())); } + // TODO: Remove calls to `gui::getGUI` from core and use something to signal back to the GUI instead + if (gui::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 +} // namespace lmms diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index a7251be8de6..02ea034483a 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -23,7 +23,6 @@ */ #include "SampleClipView.h" - #include #include #include @@ -33,6 +32,7 @@ #include "embed.h" #include "PathUtil.h" #include "SampleClip.h" +#include "SampleFilePicker.h" #include "SampleLoader.h" #include "SampleWaveform.h" #include "Song.h" @@ -123,7 +123,7 @@ void SampleClipView::dropEvent( QDropEvent * _de ) } else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) { - m_clip->setSampleBuffer(SampleLoader::createBufferFromBase64(StringPairDrag::decodeValue(_de))); + m_clip->setSampleBuffer(SampleLoader::loadBufferFromBase64(StringPairDrag::decodeValue(_de))); m_clip->updateLength(); update(); _de->accept(); @@ -180,7 +180,7 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { - const QString selectedAudioFile = SampleLoader::openAudioFile(); + const QString selectedAudioFile = SampleFilePicker::openAudioFile(); if (selectedAudioFile.isEmpty()) { return; } @@ -190,7 +190,7 @@ void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) } else { - auto sampleBuffer = SampleLoader::createBufferFromFile(selectedAudioFile); + auto sampleBuffer = SampleLoader::loadBufferFromFile(selectedAudioFile); if (sampleBuffer != SampleBuffer::emptyBuffer()) { m_clip->setSampleBuffer(sampleBuffer); diff --git a/src/gui/instrument/EnvelopeAndLfoView.cpp b/src/gui/instrument/EnvelopeAndLfoView.cpp index 95926450680..fa79df4decd 100644 --- a/src/gui/instrument/EnvelopeAndLfoView.cpp +++ b/src/gui/instrument/EnvelopeAndLfoView.cpp @@ -241,7 +241,7 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) QString value = StringPairDrag::decodeValue( _de ); if( type == "samplefile" ) { - m_params->m_userWave = SampleLoader::createBufferFromFile(value); + m_params->m_userWave = SampleLoader::loadBufferFromFile(value); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); @@ -253,7 +253,7 @@ void EnvelopeAndLfoView::dropEvent( QDropEvent * _de ) auto file = dataFile.content(). firstChildElement().firstChildElement(). firstChildElement().attribute("src"); - m_params->m_userWave = SampleLoader::createBufferFromFile(file); + m_params->m_userWave = SampleLoader::loadBufferFromFile(file); m_userLfoBtn->model()->setValue( true ); m_params->m_lfoWaveModel.setValue(static_cast(EnvelopeAndLfoParameters::LfoShape::UserDefinedWave)); _de->accept(); diff --git a/src/gui/widgets/Graph.cpp b/src/gui/widgets/Graph.cpp index 0781d4f1113..87ce7ef7827 100644 --- a/src/gui/widgets/Graph.cpp +++ b/src/gui/widgets/Graph.cpp @@ -26,9 +26,9 @@ #include #include "Graph.h" +#include "SampleFilePicker.h" #include "SampleLoader.h" #include "StringPairDrag.h" -#include "SampleBuffer.h" #include "Oscillator.h" namespace lmms @@ -589,10 +589,10 @@ void graphModel::setWaveToNoise() QString graphModel::setWaveToUser() { - QString fileName = gui::SampleLoader::openWaveformFile(); + QString fileName = gui::SampleFilePicker::openWaveformFile(); if( fileName.isEmpty() == false ) { - auto sampleBuffer = gui::SampleLoader::createBufferFromFile(fileName); + auto sampleBuffer = SampleLoader::loadBufferFromFile(fileName); for( int i = 0; i < length(); i++ ) { m_samples[i] = Oscillator::userWaveSample(sampleBuffer.get(), i / static_cast(length()));