Deep change of the background loadingThe goal was to reduce the number of background threads in big sessions (multiple instances) and avoid loading the same file alot on repeated notes (e.g. drum tracks).- All instances use a common thread pool- Each instance has a dispatching job that starts background loader, and a garbage job that clears data that has been unused for some time- The "FilePromise" object has disappeared. Upon request, Voices get a very thin reference-counting handler to the file data. When the handler is released the reference count is decreased and a "last used" timestamp is added to the file.- The background file loading job checks i) if the file is already loaded, or ii) if some other thread is already loading it. If any case is validated, it exits assuming the background loading is already happening.- The garbage job has to be triggered regularly by e.g. the synth. In the RT thread the triggerGarbageCollection() method will check the list of previously loaded file Ids to see if any has not been used for a while. If so, the memory is set to be discarded by a background thread, otherwise it'll wait for the next ping.Only tested on Linux for now.
This commit is contained in:
parent
4e13493ab4
commit
8ee4105b39
9 changed files with 450 additions and 259 deletions
100
src/external/threadpool/ThreadPool.h
vendored
Normal file
100
src/external/threadpool/ThreadPool.h
vendored
Normal 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;
|
||||||
|
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
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 { sfz::config::numBackgroundThreads };
|
||||||
|
|
||||||
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())
|
loadingJobs.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
|
||||||
|
|
@ -358,118 +349,67 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept
|
||||||
// Update all the preloaded sizes
|
// Update all the preloaded sizes
|
||||||
for (auto& preloadedFile : preloadedFiles) {
|
for (auto& preloadedFile : preloadedFiles) {
|
||||||
const auto maxOffset = preloadedFile.second.information.maxOffset;
|
const auto maxOffset = preloadedFile.second.information.maxOffset;
|
||||||
|
const auto numFrames = preloadedFile.second.preloadedData.getNumFrames() / static_cast<int>(oversamplingFactor);
|
||||||
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, preloadSize + maxOffset, oversamplingFactor);
|
preloadedFile.second.preloadedData = readFromFile(*reader, preloadSize + maxOffset, oversamplingFactor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if (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 +425,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 +456,71 @@ 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
|
||||||
|
{
|
||||||
|
std::chrono::seconds counter { 0 }; // This avoids waiting to long on the thread
|
||||||
|
constexpr std::chrono::milliseconds timeAtom { 100 };
|
||||||
|
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 +545,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, ¶m) != 0) {
|
if (pthread_setschedparam(thread, policy, ¶m) != 0) {
|
||||||
DBG("[sfizz] Cannot set current thread scheduling parameters");
|
DBG("[sfizz] Cannot set current thread scheduling parameters");
|
||||||
|
|
@ -579,3 +575,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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,51 @@ 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
|
||||||
|
{
|
||||||
|
FileId id;
|
||||||
|
FileData* data;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> 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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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 { };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue