Merge pull request #425 from paulfd/simpler-file-loading

Changes to the background loading
This commit is contained in:
Paul Ferrand 2020-10-07 13:57:03 +02:00 committed by GitHub
commit 2fc6aab964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 456 additions and 260 deletions

100
src/external/threadpool/ThreadPool.h vendored Normal file
View file

@ -0,0 +1,100 @@
// SPDX-License-Identifier: zlib
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
class ThreadPool {
public:
ThreadPool(size_t);
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
~ThreadPool();
private:
// need to keep track of threads so we can join them
std::vector< std::thread > workers;
// the task queue
std::queue< std::function<void()> > tasks;
// synchronization
std::mutex queue_mutex;
std::condition_variable condition;
volatile bool stop;
};
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
: stop(false)
{
for(size_t i = 0;i<threads;++i)
workers.emplace_back(
[this]
{
for(;;)
{
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition.wait(lock,
[this]{ return this->stop || !this->tasks.empty(); });
if(this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
}
);
}
// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared< std::packaged_task<return_type()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
// don't allow enqueueing after stopping the pool
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker: workers)
worker.join();
}
#endif

View file

@ -39,6 +39,7 @@ namespace config {
constexpr bool loggingEnabled { false }; constexpr bool loggingEnabled { false };
constexpr size_t numChannels { 2 }; constexpr size_t numChannels { 2 };
constexpr int numBackgroundThreads { 4 }; constexpr int numBackgroundThreads { 4 };
constexpr unsigned fileClearingPeriod { 5 }; // in seconds
constexpr int numVoices { 64 }; constexpr int numVoices { 64 };
constexpr unsigned maxVoices { 256 }; constexpr unsigned maxVoices { 256 };
constexpr unsigned smoothingSteps { 512 }; constexpr unsigned smoothingSteps { 512 };

View file

@ -45,6 +45,9 @@
#else #else
#include <pthread.h> #include <pthread.h>
#endif #endif
#include "threadpool/ThreadPool.h"
using namespace std::placeholders;
static ThreadPool threadPool { std::thread::hardware_concurrency() > 2 ? std::thread::hardware_concurrency() - 2 : 1 };
void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32_t numFrames) void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32_t numFrames)
{ {
@ -67,18 +70,18 @@ void readBaseFile(sfz::AudioReader& reader, sfz::FileAudioBuffer& output, uint32
} }
} }
std::unique_ptr<sfz::FileAudioBuffer> readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor) sfz::FileAudioBuffer readFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampling factor)
{ {
auto baseBuffer = absl::make_unique<sfz::FileAudioBuffer>(); sfz::FileAudioBuffer baseBuffer;
readBaseFile(reader, *baseBuffer, numFrames); readBaseFile(reader, baseBuffer, numFrames);
if (factor == sfz::Oversampling::x1) if (factor == sfz::Oversampling::x1)
return baseBuffer; return baseBuffer;
auto outputBuffer = absl::make_unique<sfz::FileAudioBuffer>(reader.channels(), numFrames * static_cast<int>(factor)); sfz::FileAudioBuffer outputBuffer { reader.channels(), numFrames * static_cast<int>(factor) };
outputBuffer->clear(); outputBuffer.clear();
sfz::Oversampler oversampler { factor }; sfz::Oversampler oversampler { factor };
oversampler.stream(*baseBuffer, *outputBuffer); oversampler.stream(baseBuffer, outputBuffer);
return outputBuffer; return outputBuffer;
} }
@ -93,37 +96,27 @@ void streamFromFile(sfz::AudioReader& reader, uint32_t numFrames, sfz::Oversampl
} }
sfz::FilePool::FilePool(sfz::Logger& logger) sfz::FilePool::FilePool(sfz::Logger& logger)
: logger(logger) : logger(logger)
{ {
FilePromise promise; loadingJobs.reserve(config::maxVoices);
if (!promise.dataStatus.is_lock_free()) lastUsedFiles.reserve(config::maxVoices);
DBG("atomic<DataStatus> is not lock-free; could cause issues with locking"); garbageToCollect.reserve(config::maxVoices);
for (int i = 0; i < config::numBackgroundThreads; ++i)
threadPool.emplace_back( &FilePool::loadingThread, this );
threadPool.emplace_back( &FilePool::clearingThread, this );
for (int i = 0; i < config::maxFilePromises; ++i)
emptyPromises.push_back(std::make_shared<FilePromise>());
} }
sfz::FilePool::~FilePool() sfz::FilePool::~FilePool()
{ {
quitThread = true;
std::error_code ec; std::error_code ec;
for (unsigned i = 0; i < threadPool.size(); ++i) { garbageFlag = false;
ec = std::error_code(); semGarbageBarrier.post(ec);
workerBarrier.post(ec); garbageThread.join();
}
ec = std::error_code(); dispatchFlag = false;
semClearingRequest.post(ec); dispatchBarrier.post(ec);
dispatchThread.join();
for (auto& thread: threadPool) for (auto& job : loadingJobs)
thread.join(); job.wait();
} }
bool sfz::FilePool::checkSample(std::string& filename) const noexcept bool sfz::FilePool::checkSample(std::string& filename) const noexcept
@ -142,7 +135,7 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept
static const fs::path dot { "." }; static const fs::path dot { "." };
static const fs::path dotdot { ".." }; static const fs::path dotdot { ".." };
for (const fs::path &part : oldPath.relative_path()) { for (const fs::path& part : oldPath.relative_path()) {
if (part == dot || part == dotdot) { if (part == dot || part == dotdot) {
path /= part; path /= part;
continue; continue;
@ -153,7 +146,7 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept
continue; continue;
} }
auto it = path.empty() ? fs::directory_iterator{ dot, ec } : fs::directory_iterator{ path, ec }; auto it = path.empty() ? fs::directory_iterator { dot, ec } : fs::directory_iterator { path, ec };
if (ec) { if (ec) {
DBG("Error creating a directory iterator for " << filename << " (Error code: " << ec.message() << ")"); DBG("Error creating a directory iterator for " << filename << " (Error code: " << ec.message() << ")");
return false; return false;
@ -169,10 +162,10 @@ bool sfz::FilePool::checkSample(std::string& filename) const noexcept
#endif #endif
}; };
while (it != fs::directory_iterator{} && !searchPredicate(*it)) while (it != fs::directory_iterator {} && !searchPredicate(*it))
it.increment(ec); it.increment(ec);
if (it == fs::directory_iterator{}) { if (it == fs::directory_iterator {}) {
DBG("File not found, could not resolve " << filename); DBG("File not found, could not resolve " << filename);
return false; return false;
} }
@ -275,22 +268,30 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce
const auto existingFile = preloadedFiles.find(fileId); const auto existingFile = preloadedFiles.find(fileId);
if (existingFile != preloadedFiles.end()) { if (existingFile != preloadedFiles.end()) {
if (framesToLoad > existingFile->second.preloadedData->getNumFrames()) { if (framesToLoad > existingFile->second.preloadedData.getNumFrames()) {
preloadedFiles[fileId].information.maxOffset = maxOffset; preloadedFiles[fileId].information.maxOffset = maxOffset;
preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad, oversamplingFactor); preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad, oversamplingFactor);
} }
} else { } else {
fileInformation->sampleRate = static_cast<float>(oversamplingFactor) * static_cast<float>(reader->sampleRate()); const auto factor = static_cast<double>(oversamplingFactor);
FileDataHandle handle { fileInformation->sampleRate = factor * static_cast<double>(reader->sampleRate());
fileInformation->end = static_cast<uint32_t>(factor * fileInformation->end);
fileInformation->loopBegin = static_cast<uint32_t>(factor * fileInformation->loopBegin);
fileInformation->loopEnd = static_cast<uint32_t>(factor * fileInformation->loopEnd);
auto insertedPair = preloadedFiles.insert_or_assign(fileId, {
readFromFile(*reader, framesToLoad, oversamplingFactor), readFromFile(*reader, framesToLoad, oversamplingFactor),
*fileInformation *fileInformation
}; });
preloadedFiles.insert_or_assign(fileId, handle);
if (!insertedPair.second)
return false;
insertedPair.first->second.status = FileData::Status::Preloaded;
} }
return true; return true;
} }
absl::optional<sfz::FileDataHandle> sfz::FilePool::loadFile(const FileId& fileId) noexcept sfz::FileDataHolder sfz::FilePool::loadFile(const FileId& fileId) noexcept
{ {
auto fileInformation = getFileInformation(fileId); auto fileInformation = getFileInformation(fileId);
if (!fileInformation) if (!fileInformation)
@ -299,54 +300,44 @@ absl::optional<sfz::FileDataHandle> sfz::FilePool::loadFile(const FileId& fileId
const fs::path file { rootDirectory / fileId.filename() }; const fs::path file { rootDirectory / fileId.filename() };
AudioReaderPtr reader = createAudioReader(file, fileId.isReverse()); AudioReaderPtr reader = createAudioReader(file, fileId.isReverse());
// FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same
const auto frames = static_cast<uint32_t>(reader->frames()); const auto frames = static_cast<uint32_t>(reader->frames());
const auto existingFile = loadedFiles.find(fileId); const auto existingFile = loadedFiles.find(fileId);
if (existingFile != loadedFiles.end()) { if (existingFile != loadedFiles.end()) {
return existingFile->second; return { &existingFile->second };
} else { } else {
fileInformation->sampleRate = static_cast<float>(oversamplingFactor) * static_cast<float>(reader->sampleRate()); const auto factor = static_cast<double>(oversamplingFactor);
FileDataHandle handle { fileInformation->sampleRate = factor * static_cast<double>(reader->sampleRate());
fileInformation->end = static_cast<uint32_t>(factor * fileInformation->end);
fileInformation->loopBegin = static_cast<uint32_t>(factor * fileInformation->loopBegin);
fileInformation->loopEnd = static_cast<uint32_t>(factor * fileInformation->loopEnd);
auto insertedPair = preloadedFiles.insert_or_assign(fileId, {
readFromFile(*reader, frames, oversamplingFactor), readFromFile(*reader, frames, oversamplingFactor),
*fileInformation *fileInformation
}; });
loadedFiles.insert_or_assign(fileId, handle); insertedPair.first->second.status = FileData::Status::Preloaded;
return handle; ASSERT(insertedPair.second);
return { &insertedPair.first->second };
} }
} }
sfz::FilePromisePtr sfz::FilePool::getFilePromise(const FileId& fileId) noexcept sfz::FileDataHolder sfz::FilePool::getFilePromise(const FileId& fileId) noexcept
{ {
if (emptyPromises.empty()) {
DBG("[sfizz] No empty promises left to honor the one for " << fileId);
return {};
}
const auto preloaded = preloadedFiles.find(fileId); const auto preloaded = preloadedFiles.find(fileId);
if (preloaded == preloadedFiles.end()) { if (preloaded == preloadedFiles.end()) {
DBG("[sfizz] File not found in the preloaded files: " << fileId); DBG("[sfizz] File not found in the preloaded files: " << fileId);
return {}; return {};
} }
QueuedFileData queuedData { fileId, &preloaded->second, std::chrono::high_resolution_clock::now() };
auto promise = emptyPromises.back(); if (!filesToLoad.try_push(queuedData)) {
promise->fileId = preloaded->first; DBG("[sfizz] Could not enqueue the file to load for " << fileId << " (queue capacity " << filesToLoad.capacity() << ")");
promise->preloadedData = preloaded->second.preloadedData;
promise->sampleRate = static_cast<float>(preloaded->second.information.sampleRate);
promise->oversamplingFactor = oversamplingFactor;
promise->creationTime = std::chrono::high_resolution_clock::now();
if (!promiseQueue.try_push(promise)) {
DBG("[sfizz] Could not enqueue the promise for " << fileId << " (queue capacity " << promiseQueue.capacity() << ")");
return {}; return {};
} }
std::error_code ec; std::error_code ec;
workerBarrier.post(ec); dispatchBarrier.post(ec);
ASSERT(!ec); ASSERT(!ec);
emptyPromises.pop_back(); return { &preloaded->second };
return promise;
} }
void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept
@ -364,112 +355,60 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept
} }
} }
void sfz::FilePool::tryToClearPromises() void sfz::FilePool::loadingJob(QueuedFileData data) noexcept
{ {
const std::lock_guard<SpinMutex> promiseLock { promiseGuard }; raiseCurrentThreadPriority();
for (auto& promise: promisesToClear) { const auto loadStartTime = std::chrono::high_resolution_clock::now();
if (promise->dataStatus != FilePromise::DataStatus::Wait) const auto waitDuration = loadStartTime - data.queuedTime;
promise->reset(); const fs::path file { rootDirectory / data.id.filename() };
std::error_code readError;
AudioReaderPtr reader = createAudioReader(file, data.id.isReverse(), &readError);
if (readError) {
DBG("[sfizz] libsndfile errored for " << data.id << " with message " << readError.message());
return;
} }
}
void sfz::FilePool::clearingThread() FileData::Status currentStatus = data.data->status.load();
{
raiseCurrentThreadPriority();
RTSemaphore& request = semClearingRequest; unsigned spinCounter { 0 };
do { while (currentStatus == FileData::Status::Invalid) {
request.wait(); // Spin until the state changes
if (quitThread) if (spinCounter > 1024) {
DBG("[sfizz] " << data.id << " is stuck on Invalid? Leaving the load");
return; return;
tryToClearPromises();
} while (1);
}
void sfz::FilePool::loadingThread() noexcept
{
raiseCurrentThreadPriority();
FilePromisePtr promise;
do {
workerBarrier.wait();
if (emptyQueue) {
while (promiseQueue.try_pop(promise)) {
// We're just dequeuing
}
emptyQueue = false;
semEmptyQueueFinished.post();
continue;
} }
if (quitThread) std::this_thread::sleep_for(std::chrono::microseconds(100));
return; currentStatus = data.data->status.load();
spinCounter += 1;
}
if (!promiseQueue.try_pop(promise)) { // Already loading or loaded
continue; if (currentStatus != FileData::Status::Preloaded)
} return;
threadsLoading++; // Someone else got the token
const auto loadStartTime = std::chrono::high_resolution_clock::now(); if (!data.data->status.compare_exchange_strong(currentStatus, FileData::Status::Streaming))
const auto waitDuration = loadStartTime - promise->creationTime; return;
const fs::path file { rootDirectory / promise->fileId.filename() }; const auto frames = static_cast<uint32_t>(reader->frames());
std::error_code readError; streamFromFile(*reader, frames, oversamplingFactor, data.data->fileData, &data.data->availableFrames);
AudioReaderPtr reader = createAudioReader(file, promise->fileId.isReverse(), &readError); const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime;
if (readError) { logger.logFileTime(waitDuration, loadDuration, frames, data.id.filename());
DBG("[sfizz] libsndfile errored for " << promise->fileId << " with message " << readError.message());
promise->dataStatus = FilePromise::DataStatus::Error;
continue;
}
const auto frames = static_cast<uint32_t>(reader->frames());
streamFromFile(*reader, frames, oversamplingFactor, promise->fileData, &promise->availableFrames);
promise->dataStatus = FilePromise::DataStatus::Ready;
const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime;
logger.logFileTime(waitDuration, loadDuration, frames, promise->fileId.filename());
threadsLoading--; data.data->status = FileData::Status::Done;
semFilledPromiseQueueAvailable.wait(); std::lock_guard<SpinMutex> guard { lastUsedMutex };
filledPromiseQueue.push(promise); if (absl::c_find(lastUsedFiles, data.id) == lastUsedFiles.end())
lastUsedFiles.push_back(data.id);
promise.reset();
} while (1);
} }
void sfz::FilePool::clear() void sfz::FilePool::clear()
{ {
emptyFileLoadingQueues(); emptyFileLoadingQueues();
preloadedFiles.clear(); preloadedFiles.clear();
temporaryFilePromises.clear();
promisesToClear.clear();
}
void sfz::FilePool::cleanupPromises() noexcept
{
const std::unique_lock<SpinMutex> lock { promiseGuard, std::try_to_lock };
if (!lock.owns_lock())
return;
// The garbage collection cleared the data from these so we can move them
// back to the empty queue
auto promiseWaiting = [](FilePromisePtr& p) { return p->waiting(); };
auto moveToEmpty = [&](FilePromisePtr& p) { return emptyPromises.push_back(p); };
swapAndPopAll(promisesToClear, promiseWaiting, moveToEmpty);
// Remove the promises from the filled queue and put them in a linear
// storage
FilePromisePtr promise;
while (filledPromiseQueue.try_pop(promise)) {
semFilledPromiseQueueAvailable.post();
temporaryFilePromises.push_back(promise);
}
auto promiseUsedOnce = [](FilePromisePtr& p) { return p.use_count() == 1; };
auto moveToClear = [&](FilePromisePtr& p) { return promisesToClear.push_back(p); };
if (swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear) > 0)
semClearingRequest.post();
} }
void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept
@ -485,10 +424,22 @@ void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept
preloadedFile.second.information.maxOffset + preloadSize preloadedFile.second.information.maxOffset + preloadSize
); );
}(); }();
fs::path file { rootDirectory / preloadedFile.first.filename() }; fs::path file { rootDirectory / preloadedFile.first.filename() };
AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse()); AudioReaderPtr reader = createAudioReader(file, preloadedFile.first.isReverse());
preloadedFile.second.preloadedData = readFromFile(*reader, framesToLoad, factor); preloadedFile.second.preloadedData = readFromFile(*reader, framesToLoad, factor);
preloadedFile.second.information.sampleRate *= samplerateChange; FileInformation& information = preloadedFile.second.information;
information.sampleRate *= samplerateChange;
information.end = static_cast<uint32_t>(samplerateChange * information.end);
information.loopBegin = static_cast<uint32_t>(samplerateChange * information.loopBegin);
information.loopEnd = static_cast<uint32_t>(samplerateChange * information.loopEnd);
if (preloadedFile.second.status == FileData::Status::Done) {
const auto realFrames =
preloadedFile.second.availableFrames.load() / static_cast<unsigned>(this->oversamplingFactor);
preloadedFile.second.fileData = readFromFile(*reader, realFrames, factor);
preloadedFile.second.availableFrames = realFrames * static_cast<unsigned>(factor);
}
} }
this->oversamplingFactor = factor; this->oversamplingFactor = factor;
@ -504,26 +455,69 @@ uint32_t sfz::FilePool::getPreloadSize() const noexcept
return preloadSize; return preloadSize;
} }
template <typename R>
bool is_ready(std::future<R> const& f)
{
return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
}
void sfz::FilePool::dispatchingJob() noexcept
{
QueuedFileData queuedData;
while (dispatchFlag) {
dispatchBarrier.wait();
if (emptyQueueFlag) {
while (filesToLoad.try_pop(queuedData)) {
// pass
}
semEmptyQueueFinished.post();
emptyQueueFlag = false;
continue;
}
if (filesToLoad.try_pop(queuedData)) {
loadingJobs.push_back(
threadPool.enqueue([this](const QueuedFileData& data) { loadingJob(data); }, queuedData));
}
// Clear finished jobs
swapAndPopAll(loadingJobs, [](std::future<void>& future) {
return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
});
}
}
void sfz::FilePool::garbageJob() noexcept
{
while (garbageFlag) {
semGarbageBarrier.wait();
{
std::lock_guard<SpinMutex> guard { garbageMutex };
for (auto& g: garbageToCollect)
g.reset();
garbageToCollect.clear();
}
}
}
void sfz::FilePool::emptyFileLoadingQueues() noexcept void sfz::FilePool::emptyFileLoadingQueues() noexcept
{ {
emptyQueue = true; ASSERT(dispatchFlag);
workerBarrier.post(); emptyQueueFlag = true;
semEmptyQueueFinished.wait(); std::error_code ec;
dispatchBarrier.post(ec);
if (!ec)
semEmptyQueueFinished.wait();
} }
void sfz::FilePool::waitForBackgroundLoading() noexcept void sfz::FilePool::waitForBackgroundLoading() noexcept
{ {
// TODO: validate that this is enough, otherwise we will need an atomic count for (auto& job : loadingJobs)
// of the files we need to load still. job.wait();
// Spinlocking on the size of the background queue loadingJobs.clear();
while (!promiseQueue.was_empty()){
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
// Spinlocking on the threads possibly logging in the background
while (threadsLoading > 0) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
} }
void sfz::FilePool::raiseCurrentThreadPriority() noexcept void sfz::FilePool::raiseCurrentThreadPriority() noexcept
@ -548,8 +542,7 @@ void sfz::FilePool::raiseCurrentThreadPriority() noexcept
policy = SCHED_RR; policy = SCHED_RR;
const int minprio = sched_get_priority_min(policy); const int minprio = sched_get_priority_min(policy);
const int maxprio = sched_get_priority_max(policy); const int maxprio = sched_get_priority_max(policy);
param.sched_priority = minprio + param.sched_priority = minprio + config::backgroundLoaderPthreadPriority * (maxprio - minprio) / 100;
config::backgroundLoaderPthreadPriority * (maxprio - minprio) / 100;
if (pthread_setschedparam(thread, policy, &param) != 0) { if (pthread_setschedparam(thread, policy, &param) != 0) {
DBG("[sfizz] Cannot set current thread scheduling parameters"); DBG("[sfizz] Cannot set current thread scheduling parameters");
@ -579,3 +572,40 @@ void sfz::FilePool::setRamLoading(bool loadInRam) noexcept
setPreloadSize(preloadSize); setPreloadSize(preloadSize);
} }
} }
void sfz::FilePool::triggerGarbageCollection() noexcept
{
const std::unique_lock<SpinMutex> lastUsedLock { lastUsedMutex, std::try_to_lock };
const std::unique_lock<SpinMutex> garbageLock { garbageMutex, std::try_to_lock };
if (!lastUsedLock.owns_lock() || !garbageLock.owns_lock())
return;
const auto now = std::chrono::high_resolution_clock::now();
swapAndPopAll(lastUsedFiles, [&](const FileId& id) {
if (garbageToCollect.size() == garbageToCollect.capacity())
return false;
auto& data = preloadedFiles[id];
if (data.status == FileData::Status::Preloaded)
return true;
if (data.status != FileData::Status::Done)
return false;
if (data.readerCount != 0)
return false;
const auto secondsIdle = std::chrono::duration_cast<std::chrono::seconds>(now - data.lastViewerLeftAt).count();
if (secondsIdle < config::fileClearingPeriod)
return false;
data.availableFrames = 0;
data.status = FileData::Status::Preloaded;
garbageToCollect.push_back(std::move(data.fileData));
return true;
});
std::error_code ec;
semGarbageBarrier.post(ec);
ASSERT(!ec);
}

View file

@ -42,7 +42,8 @@
#include "Logger.h" #include "Logger.h"
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <mutex> #include <future>
#include "utility/SpinMutex.h"
namespace sfz { namespace sfz {
using FileAudioBuffer = AudioBuffer<float, 2, config::defaultAlignment, using FileAudioBuffer = AudioBuffer<float, 2, config::defaultAlignment,
@ -62,62 +63,104 @@ struct FileInformation {
}; };
// Strict C++11 disallows member initialization if aggregate initialization is to be used... // Strict C++11 disallows member initialization if aggregate initialization is to be used...
struct FileDataHandle struct FileData
{ {
FileAudioBufferPtr preloadedData; enum class Status { Invalid, Preloaded, Streaming, Done };
FileInformation information; FileData() = default;
}; FileData(FileAudioBuffer preloaded, FileInformation info)
: preloadedData(std::move(preloaded)), information(std::move(info))
{
struct FilePromise }
{
AudioSpan<const float> getData() AudioSpan<const float> getData()
{ {
if (dataStatus == DataStatus::Ready) if (availableFrames > preloadedData.getNumFrames())
return AudioSpan<const float>(fileData);
else if (availableFrames > preloadedData->getNumFrames())
return AudioSpan<const float>(fileData).first(availableFrames); return AudioSpan<const float>(fileData).first(availableFrames);
else else
return AudioSpan<const float>(*preloadedData); return AudioSpan<const float>(preloadedData);
} }
void reset() FileData(const FileData& other) = default;
FileData& operator=(const FileData& other) = default;
FileData(FileData&& other)
{ {
fileData.reset(); ASSERT(other.readerCount == 0); // Probably should not be moving this...
preloadedData.reset(); information = std::move(other.information);
fileId = FileId {}; preloadedData = std::move(other.preloadedData);
availableFrames = 0; fileData = std::move(other.fileData);
dataStatus = DataStatus::Wait; availableFrames = other.availableFrames.load();
oversamplingFactor = config::defaultOversamplingFactor; lastViewerLeftAt = other.lastViewerLeftAt;
sampleRate = config::defaultSampleRate; status = other.status.load();
} }
FileData& operator=(FileData&& other)
enum class DataStatus {
Wait = 0,
Ready,
Error,
};
bool waiting() const { return dataStatus == DataStatus::Wait; }
void sleepUntilComplete()
{ {
while (waiting()) ASSERT(other.readerCount == 0); // Probably should not be moving this...
std::this_thread::sleep_for(std::chrono::milliseconds(50)); information = std::move(other.information);
preloadedData = std::move(other.preloadedData);
fileData = std::move(other.fileData);
availableFrames = other.availableFrames.load();
lastViewerLeftAt = other.lastViewerLeftAt;
status = other.status.load();
return *this;
} }
FileId fileId {}; FileAudioBuffer preloadedData;
FileAudioBufferPtr preloadedData {}; FileInformation information;
FileAudioBuffer fileData {}; FileAudioBuffer fileData {};
float sampleRate { config::defaultSampleRate }; std::atomic<Status> status { Status::Invalid };
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
std::atomic<size_t> availableFrames { 0 }; std::atomic<size_t> availableFrames { 0 };
std::atomic<DataStatus> dataStatus { DataStatus::Wait }; std::atomic<int> readerCount { 0 };
std::chrono::time_point<std::chrono::high_resolution_clock> creationTime; std::chrono::time_point<std::chrono::high_resolution_clock> lastViewerLeftAt;
LEAK_DETECTOR(FilePromise); LEAK_DETECTOR(FileData);
};
class FileDataHolder {
public:
FileDataHolder() = default;
FileDataHolder(const FileDataHolder&) = delete;
FileDataHolder& operator=(const FileDataHolder&) = delete;
FileDataHolder(FileDataHolder&& other)
{
this->data = other.data;
other.data = nullptr;
}
FileDataHolder& operator=(FileDataHolder&& other)
{
this->data = other.data;
other.data = nullptr;
return *this;
}
FileDataHolder(FileData* data) : data(data)
{
if (!data)
return;
data->readerCount += 1;
}
void reset()
{
if (!data)
return;
data->readerCount -= 1;
data->lastViewerLeftAt = std::chrono::high_resolution_clock::now();
data = nullptr;
}
~FileDataHolder()
{
ASSERT(!data || data->readerCount > 0);
reset();
}
FileData& operator*() { return *data; }
FileData* operator->() { return data; }
explicit operator bool() const { return data != nullptr; }
private:
FileData* data { nullptr };
LEAK_DETECTOR(FileDataHolder);
}; };
using FilePromisePtr = std::shared_ptr<FilePromise>;
/** /**
* @brief This is a singleton-designed class that holds all the preloaded data * @brief This is a singleton-designed class that holds all the preloaded data
* as well as functions to request new file data and collect the file handles to * as well as functions to request new file data and collect the file handles to
@ -188,7 +231,7 @@ public:
* @param fileId * @param fileId
* @return A handle on the file data * @return A handle on the file data
*/ */
absl::optional<sfz::FileDataHandle> loadFile(const FileId& fileId) noexcept; FileDataHolder loadFile(const FileId& fileId) noexcept;
/** /**
* @brief Check that the sample exists. If not, try to find it in a case insensitive way. * @brief Check that the sample exists. If not, try to find it in a case insensitive way.
@ -214,19 +257,12 @@ public:
*/ */
void clear(); void clear();
/** /**
* @brief Moves the filled promises to a linear storage, and checks * @brief Get a handle on a file, which triggers background loading
* said linear storage for promises that are not used anymore.
*
* This function has to be called on the audio thread.
*/
void cleanupPromises() noexcept;
/**
* @brief Get a file promise
* *
* @param fileId the file to preload * @param fileId the file to preload
* @return FilePromisePtr a file promise * @return FileDataHolder a file data handle
*/ */
FilePromisePtr getFilePromise(const FileId& fileId) noexcept; FileDataHolder getFilePromise(const FileId& fileId) noexcept;
/** /**
* @brief Change the preloading size. This will trigger a full * @brief Change the preloading size. This will trigger a full
* reload of all samples, so don't call it on the audio thread. * reload of all samples, so don't call it on the audio thread.
@ -277,37 +313,59 @@ public:
* @param loadInRam * @param loadInRam
*/ */
void setRamLoading(bool loadInRam) noexcept; void setRamLoading(bool loadInRam) noexcept;
/**
* @brief Prepares unused data to be freed on a background thread.
* This should be called regularly by the Synth, otherwise memory
* risk building up.
*/
void triggerGarbageCollection() noexcept;
private: private:
Logger& logger; Logger& logger;
fs::path rootDirectory; fs::path rootDirectory;
void loadingThread() noexcept;
void clearingThread();
void tryToClearPromises();
atomic_queue::AtomicQueue2<FilePromisePtr, config::maxVoices> promiseQueue;
atomic_queue::AtomicQueue2<FilePromisePtr, config::maxVoices> filledPromiseQueue;
RTSemaphore semFilledPromiseQueueAvailable { config::maxVoices };
bool loadInRam { config::loadInRam }; bool loadInRam { config::loadInRam };
uint32_t preloadSize { config::preloadSize }; uint32_t preloadSize { config::preloadSize };
Oversampling oversamplingFactor { config::defaultOversamplingFactor }; Oversampling oversamplingFactor { config::defaultOversamplingFactor };
// Signals
volatile bool quitThread { false };
volatile bool emptyQueue { false };
RTSemaphore semEmptyQueueFinished;
std::atomic<int> threadsLoading { 0 };
RTSemaphore workerBarrier;
RTSemaphore semClearingRequest;
// File promises data structures along with their guards. // Signals
std::vector<FilePromisePtr> emptyPromises; volatile bool dispatchFlag { true };
std::vector<FilePromisePtr> temporaryFilePromises; volatile bool garbageFlag { true };
std::vector<FilePromisePtr> promisesToClear; volatile bool emptyQueueFlag { false };
SpinMutex promiseGuard; RTSemaphore dispatchBarrier;
RTSemaphore semEmptyQueueFinished;
RTSemaphore semGarbageBarrier;
// Structures for the background loaders
struct QueuedFileData
{
using TimePoint = std::chrono::time_point<std::chrono::high_resolution_clock>;
QueuedFileData() = default;
QueuedFileData(FileId id, FileData* data, TimePoint queuedTime)
: id(id), data(data), queuedTime(queuedTime) {}
QueuedFileData(const QueuedFileData&) = default;
QueuedFileData& operator=(const QueuedFileData&) = default;
QueuedFileData(QueuedFileData&&) = default;
QueuedFileData& operator=(QueuedFileData&&) = default;
FileId id {};
FileData* data { nullptr };
TimePoint queuedTime {};
};
atomic_queue::AtomicQueue2<QueuedFileData, config::maxVoices> filesToLoad;
void dispatchingJob() noexcept;
void garbageJob() noexcept;
void loadingJob(QueuedFileData data) noexcept;
std::vector<std::future<void>> loadingJobs;
std::thread dispatchThread { &FilePool::dispatchingJob, this };
std::thread garbageThread { &FilePool::garbageJob, this };
SpinMutex lastUsedMutex;
std::vector<FileId> lastUsedFiles;
SpinMutex garbageMutex;
std::vector<FileAudioBuffer> garbageToCollect;
// Preloaded data // Preloaded data
absl::flat_hash_map<FileId, FileDataHandle> preloadedFiles; absl::flat_hash_map<FileId, FileData> preloadedFiles;
absl::flat_hash_map<FileId, FileDataHandle> loadedFiles; absl::flat_hash_map<FileId, FileData> loadedFiles;
std::vector<std::thread> threadPool { };
LEAK_DETECTOR(FilePool); LEAK_DETECTOR(FilePool);
}; };
} }

View file

@ -674,7 +674,7 @@ public:
} }
private: private:
std::array<uint32_t, N> seeds_ {}; std::array<uint32_t, N> seeds_ {{}};
float mean_ { 0 }; float mean_ { 0 };
float gain_ { 0 }; float gain_ { 0 };
}; };

View file

@ -840,6 +840,15 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
if (resources.synthConfig.freeWheeling) if (resources.synthConfig.freeWheeling)
resources.filePool.waitForBackgroundLoading(); resources.filePool.waitForBackgroundLoading();
const auto now = std::chrono::high_resolution_clock::now();
const auto timeSinceLastCollection =
std::chrono::duration_cast<std::chrono::seconds>(now - lastGarbageCollection);
if (timeSinceLastCollection.count() > config::fileClearingPeriod) {
lastGarbageCollection = now;
resources.filePool.triggerGarbageCollection();
}
const std::unique_lock<SpinMutex> lock { callbackGuard, std::try_to_lock }; const std::unique_lock<SpinMutex> lock { callbackGuard, std::try_to_lock };
if (!lock.owns_lock()) if (!lock.owns_lock())
return; return;
@ -860,7 +869,6 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
{ // Main render block { // Main render block
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
tempMixSpan->fill(0.0f); tempMixSpan->fill(0.0f);
resources.filePool.cleanupPromises();
// Ramp out whatever is in the buffer at this point; should only be killed voice data // Ramp out whatever is in the buffer at this point; should only be killed voice data
linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames)); linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames));

View file

@ -941,6 +941,8 @@ private:
Duration dispatchDuration { 0 }; Duration dispatchDuration { 0 };
std::chrono::time_point<std::chrono::high_resolution_clock> lastGarbageCollection;
Parser parser; Parser parser;
fs::file_time_type modificationTime { }; fs::file_time_type modificationTime { };

View file

@ -95,13 +95,13 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event
setupOscillatorUnison(); setupOscillatorUnison();
} else { } else {
currentPromise = resources.filePool.getFilePromise(region->sampleId); currentPromise = resources.filePool.getFilePromise(region->sampleId);
if (currentPromise == nullptr) { if (!currentPromise) {
switchState(State::cleanMeUp); switchState(State::cleanMeUp);
return; return;
} }
updateLoopInformation(); updateLoopInformation();
speedRatio = static_cast<float>(currentPromise->sampleRate / this->sampleRate); speedRatio = static_cast<float>(currentPromise->information.sampleRate / this->sampleRate);
sourcePosition = region->getOffset(currentPromise->oversamplingFactor); sourcePosition = region->getOffset(resources.filePool.getOversamplingFactor());
} }
// do Scala retuning and reconvert the frequency into a 12TET key number // do Scala retuning and reconvert the frequency into a 12TET key number
@ -545,7 +545,7 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
if (numSamples == 0) if (numSamples == 0)
return; return;
if (currentPromise == nullptr) { if (!currentPromise) {
DBG("[Voice] Missing promise during fillWithData"); DBG("[Voice] Missing promise during fillWithData");
return; return;
} }
@ -647,17 +647,17 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
else { else {
// cut short the voice at the instant of reaching end of sample // cut short the voice at the instant of reaching end of sample
const auto sampleEnd = min( const auto sampleEnd = min(
static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)), static_cast<int>(currentPromise->information.end),
static_cast<int>(source.getNumFrames()) static_cast<int>(source.getNumFrames())
) - 1; ) - 1;
for (unsigned i = 0; i < numSamples; ++i) { for (unsigned i = 0; i < numSamples; ++i) {
if ((*indices)[i] >= sampleEnd) { if ((*indices)[i] >= sampleEnd) {
#ifndef NDEBUG #ifndef NDEBUG
// Check for underflow // Check for underflow
if (source.getNumFrames() - 1 < region->trueSampleEnd(currentPromise->oversamplingFactor)) { if (source.getNumFrames() - 1 < currentPromise->information.end) {
DBG("[sfizz] Underflow: source available samples " DBG("[sfizz] Underflow: source available samples "
<< source.getNumFrames() << "/" << source.getNumFrames() << "/"
<< region->trueSampleEnd(currentPromise->oversamplingFactor) << currentPromise->information.end
<< " for sample " << region->sampleId); << " for sample " << region->sampleId);
} }
#endif #endif
@ -1091,16 +1091,13 @@ void sfz::Voice::updateLoopInformation() noexcept
if (!region->shouldLoop()) if (!region->shouldLoop())
return; return;
const auto& info = currentPromise->information;
const auto rate = info.sampleRate;
const auto factor = currentPromise->oversamplingFactor; loop.end = static_cast<int>(info.loopEnd);
const auto rate = currentPromise->sampleRate; loop.start = static_cast<int>(info.loopBegin);
loop.end = static_cast<int>(region->loopEnd(factor));
loop.start = static_cast<int>(region->loopStart(factor));
loop.size = loop.end + 1 - loop.start; loop.size = loop.end + 1 - loop.start;
loop.xfSize = static_cast<int>( loop.xfSize = static_cast<int>(lroundPositive(region->loopCrossfade * rate));
lroundPositive(region->loopCrossfade * static_cast<int>(factor) * rate)
);
loop.xfOutStart = loop.end + 1 - loop.xfSize; loop.xfOutStart = loop.end + 1 - loop.xfSize;
loop.xfInStart = loop.start - loop.xfSize; loop.xfInStart = loop.start - loop.xfSize;
} }

View file

@ -527,7 +527,7 @@ private:
*/ */
void updateLoopInformation() noexcept; void updateLoopInformation() noexcept;
FilePromisePtr currentPromise { nullptr }; FileDataHolder currentPromise;
int samplesPerBlock { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate }; float sampleRate { config::defaultSampleRate };

View file

@ -525,10 +525,10 @@ bool WavetablePool::createFileWave(FilePool& filePool, const std::string& filena
if (fileHandle->information.numChannels > 1) if (fileHandle->information.numChannels > 1)
DBG("[sfizz] Only the first channel of " << filename << " will be used to create the wavetable"); DBG("[sfizz] Only the first channel of " << filename << " will be used to create the wavetable");
auto audioData = fileHandle->preloadedData->getConstSpan(0); auto audioData = fileHandle->preloadedData.getConstSpan(0);
// an even size is required for FFT // an even size is required for FFT
static_assert(absl::remove_reference_t<decltype(*fileHandle->preloadedData)>::PaddingRight > 0, static_assert(absl::remove_reference_t<decltype(fileHandle->preloadedData)>::PaddingRight > 0,
"Right padding is required on the audio file buffer"); "Right padding is required on the audio file buffer");
if (audioData.size() & 1) if (audioData.size() & 1)
audioData = absl::MakeConstSpan(audioData.data(), audioData.size() + 1); audioData = absl::MakeConstSpan(audioData.data(), audioData.size() + 1);