Merge pull request #425 from paulfd/simpler-file-loading
Changes to the background loading
This commit is contained in:
commit
2fc6aab964
10 changed files with 456 additions and 260 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;
|
||||||
|
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
|
||||||
|
|
@ -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 { 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, ¶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 +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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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