Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cache for samples #7497

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/PathUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "lmms_export.h"

#include <QDir>
#include <filesystem>

namespace lmms::PathUtil
{
Expand Down Expand Up @@ -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
125 changes: 125 additions & 0 deletions include/SampleCache.h
Original file line number Diff line number Diff line change
@@ -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 <QString>
#include <filesystem>
#include <memory>
#include <unordered_map>

#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<SampleBuffer>;

/**
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<SampleBuffer>;

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<std::string>()(entry.base64);
}
};

template <typename T, typename ...Args>
static auto get(const T& entry, std::unordered_map<T, std::weak_ptr<SampleBuffer>, Hash>& map, Args... args)
{
const auto it = map.find(entry);

if (it == map.end())
{
const auto buffer = std::make_shared<SampleBuffer>(std::forward<Args>(args)...);
map.insert(std::make_pair(entry, buffer));
return buffer;
}

const auto entryLock = it->second.lock();
if (!entryLock)
{
const auto buffer = std::make_shared<SampleBuffer>(std::forward<Args>(args)...);
map[entry] = buffer;
return buffer;
}

return entryLock;
}

inline static std::unordered_map<AudioFileEntry, std::weak_ptr<SampleBuffer>, Hash> s_audioFileMap;
inline static std::unordered_map<Base64Entry, std::weak_ptr<SampleBuffer>, Hash> s_base64Map;
};
} // namespace lmms

#endif // LMMS_SAMPLE_CACHE_H
40 changes: 40 additions & 0 deletions include/SampleFilePicker.h
Original file line number Diff line number Diff line change
@@ -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 <QString>
#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
22 changes: 9 additions & 13 deletions include/SampleLoader.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* SampleLoader.h - Load audio and waveform files
* SampleLoader.h
*
* Copyright (c) 2023 saker <[email protected]>
* Copyright (c) 2024 saker
*
* This file is part of LMMS - https://lmms.io
*
Expand All @@ -22,27 +22,23 @@
*
*/

#ifndef LMMS_GUI_SAMPLE_LOADER_H
#define LMMS_GUI_SAMPLE_LOADER_H
#ifndef LMMS_SAMPLE_LOADER_H
#define LMMS_SAMPLE_LOADER_H

#include <QString>
#include <memory>

#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<const SampleBuffer> createBufferFromFile(const QString& filePath);
static std::shared_ptr<const SampleBuffer> createBufferFromBase64(
static std::shared_ptr<const SampleBuffer> loadBufferFromFile(const QString& filePath);
static std::shared_ptr<const SampleBuffer> 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
4 changes: 2 additions & 2 deletions plugins/AudioFileProcessor/AudioFileProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
}
Expand Down
4 changes: 2 additions & 2 deletions plugins/AudioFileProcessor/AudioFileProcessorView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -257,7 +257,7 @@ void AudioFileProcessorView::sampleUpdated()

void AudioFileProcessorView::openAudioFile()
{
QString af = SampleLoader::openAudioFile();
QString af = SampleFilePicker::openAudioFile();
if (af.isEmpty()) { return; }

castModel<AudioFileProcessor>()->setAudioFile(af);
Expand Down
6 changes: 3 additions & 3 deletions plugins/SlicerT/SlicerT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ std::vector<Note> 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();
Expand Down Expand Up @@ -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
Expand All @@ -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));
}

Expand Down
6 changes: 2 additions & 4 deletions plugins/SlicerT/SlicerTView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
}
Expand Down
8 changes: 4 additions & 4 deletions plugins/TripleOscillator/TripleOscillator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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)); }
Expand Down
1 change: 1 addition & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/core/EnvelopeAndLfoParameters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)); }
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/LfoController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)); }
}
Expand Down
Loading
Loading