Changed the logic for the file loading
This commit is contained in:
parent
1536ead90b
commit
630220e7e0
9 changed files with 188 additions and 247 deletions
|
|
@ -31,26 +31,25 @@
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <sndfile.hh>
|
#include <sndfile.hh>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <mutex>
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
std::unique_ptr<sfz::AudioBuffer<T>> readFromFile(SndfileHandle& sndFile, int numFrames)
|
std::unique_ptr<sfz::AudioBuffer<T>> readFromFile(SndfileHandle& sndFile, uint32_t numFrames)
|
||||||
{
|
{
|
||||||
auto returnedBuffer = std::make_unique<sfz::AudioBuffer<T>>(sndFile.channels(), numFrames);
|
auto returnedBuffer = std::make_unique<sfz::AudioBuffer<T>>(sndFile.channels(), numFrames);
|
||||||
if (sndFile.channels() == 1) {
|
if (sndFile.channels() == 1) {
|
||||||
sndFile.readf(returnedBuffer->channelWriter(0), numFrames);
|
sndFile.readf(returnedBuffer->channelWriter(0), numFrames);
|
||||||
} else if (sndFile.channels() == 2) {
|
} else if (sndFile.channels() == 2) {
|
||||||
auto tempReadBuffer = std::make_unique<sfz::AudioBuffer<float>>(1, 2 * numFrames);
|
auto tempReadBuffer = std::make_unique<sfz::AudioBuffer<T>>(1, 2 * numFrames);
|
||||||
sndFile.readf(tempReadBuffer->channelWriter(0), numFrames);
|
sndFile.readf(tempReadBuffer->channelWriter(0), numFrames);
|
||||||
sfz::readInterleaved<float>(tempReadBuffer->getSpan(0), returnedBuffer->getSpan(0), returnedBuffer->getSpan(1));
|
sfz::readInterleaved<T>(tempReadBuffer->getSpan(0), returnedBuffer->getSpan(0), returnedBuffer->getSpan(1));
|
||||||
}
|
}
|
||||||
return returnedBuffer;
|
return returnedBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
absl::optional<sfz::FilePool::FileInformation> sfz::FilePool::getFileInformation(const std::string& filename, uint32_t offset) noexcept
|
absl::optional<sfz::FilePool::FileInformation> sfz::FilePool::getFileInformation(const std::string& filename) noexcept
|
||||||
{
|
{
|
||||||
fs::path file { rootDirectory / filename };
|
fs::path file{ rootDirectory / filename };
|
||||||
if (!fs::exists(file))
|
if (!fs::exists(file))
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
|
@ -63,6 +62,7 @@ absl::optional<sfz::FilePool::FileInformation> sfz::FilePool::getFileInformation
|
||||||
FileInformation returnedValue;
|
FileInformation returnedValue;
|
||||||
returnedValue.end = static_cast<uint32_t>(sndFile.frames());
|
returnedValue.end = static_cast<uint32_t>(sndFile.frames());
|
||||||
returnedValue.sampleRate = static_cast<double>(sndFile.samplerate());
|
returnedValue.sampleRate = static_cast<double>(sndFile.samplerate());
|
||||||
|
returnedValue.numChannels = sndFile.channels();
|
||||||
|
|
||||||
SF_INSTRUMENT instrumentInfo;
|
SF_INSTRUMENT instrumentInfo;
|
||||||
sndFile.command(SFC_GET_INSTRUMENT, &instrumentInfo, sizeof(instrumentInfo));
|
sndFile.command(SFC_GET_INSTRUMENT, &instrumentInfo, sizeof(instrumentInfo));
|
||||||
|
|
@ -71,113 +71,111 @@ absl::optional<sfz::FilePool::FileInformation> sfz::FilePool::getFileInformation
|
||||||
returnedValue.loopEnd = instrumentInfo.loops[0].end;
|
returnedValue.loopEnd = instrumentInfo.loops[0].end;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Large offsets will require large preloading; is this OK in practice?
|
|
||||||
const auto preloadedSize = [&]() {
|
|
||||||
if (config::preloadSize == 0)
|
|
||||||
return returnedValue.end;
|
|
||||||
else
|
|
||||||
return std::min(returnedValue.end, offset + static_cast<uint32_t>(config::preloadSize));
|
|
||||||
}();
|
|
||||||
|
|
||||||
if (preloadedData.contains(filename)) {
|
|
||||||
auto alreadyPreloaded = preloadedData[filename];
|
|
||||||
if (preloadedSize > alreadyPreloaded->getNumFrames()) {
|
|
||||||
// FIXME: Okay, ideally here you would have a double indirection so that we can update _all_ the preloaded
|
|
||||||
// files in previous regions to account for the new offset
|
|
||||||
//
|
|
||||||
// Before this next command, some old regions and the file pool hold a shared pointer to the same audio buffer.
|
|
||||||
// This audio buffer is OK for the old regions, but too small for the new one.
|
|
||||||
// By resetting the filepool data to a new, longer audiobuffer, we are creating 2 copies of the same audio data.
|
|
||||||
// The filepool and the new regions have the longer copy, and the older regions have the shorter copy.
|
|
||||||
// This is not entirely optimal, but is it better to write a double shared pointer ?
|
|
||||||
// std::shared_ptr<std::shared_ptr<sfz::AudioBuffer>>> is a bit ugly...
|
|
||||||
alreadyPreloaded.reset(readFromFile<float>(sndFile, preloadedSize).release());
|
|
||||||
}
|
|
||||||
returnedValue.preloadedData = alreadyPreloaded;
|
|
||||||
} else {
|
|
||||||
returnedValue.preloadedData = readFromFile<float>(sndFile, preloadedSize);
|
|
||||||
preloadedData[filename] = returnedValue.preloadedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnedValue;
|
return returnedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::enqueueLoading(Voice* voice, const std::string* sample, int numFrames, unsigned ticket) noexcept
|
bool sfz::FilePool::preloadFile(const std::string& filename, uint32_t maxOffset) noexcept
|
||||||
{
|
{
|
||||||
if (!loadingQueue.try_enqueue({ voice, sample, numFrames, ticket })) {
|
fs::path file{ rootDirectory / filename };
|
||||||
DBG("Problem enqueuing a file read for file " << sample);
|
if (!fs::exists(file))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SndfileHandle sndFile(reinterpret_cast<const char*>(file.c_str()));
|
||||||
|
if (sndFile.channels() != 1 && sndFile.channels() != 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// FIXME: Large offsets will require large preloading; is this OK in practice? Apparently sforzando does the same
|
||||||
|
const uint32_t frames = sndFile.frames();
|
||||||
|
const auto framesToLoad = [&]() {
|
||||||
|
if (preloadSize == 0)
|
||||||
|
return frames;
|
||||||
|
else
|
||||||
|
return min(frames, maxOffset + preloadSize);
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (preloadedFiles.contains(filename)) {
|
||||||
|
if (framesToLoad > preloadedFiles[filename].preloadedData->getNumFrames()) {
|
||||||
|
preloadedFiles[filename].preloadedData = readFromFile<float>(sndFile, framesToLoad);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
preloadedFiles.insert_or_assign(filename, { readFromFile<float>(sndFile, framesToLoad), static_cast<float>(sndFile.samplerate()) * static_cast<float>(oversamplingFactor) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sfz::FilePromisePtr sfz::FilePool::getFilePromise(const std::string& filename) noexcept
|
||||||
|
{
|
||||||
|
auto promise = std::make_shared<FilePromise>();
|
||||||
|
const auto preloaded = preloadedFiles.find(filename);
|
||||||
|
if (preloaded != preloadedFiles.end()) {
|
||||||
|
promise->filename = preloaded->first;
|
||||||
|
promise->preloadedData = preloaded->second.preloadedData;
|
||||||
|
promise->sampleRate = preloaded->second.sampleRate;
|
||||||
|
promise->oversamplingFactor = oversamplingFactor;
|
||||||
|
temporaryFilePromises.push_back(promise);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept
|
||||||
|
{
|
||||||
|
// Update all the preloaded sizes
|
||||||
|
for (auto& preloadedFile: preloadedFiles) {
|
||||||
|
const auto numFrames = preloadedFile.second.preloadedData->getNumFrames();
|
||||||
|
const uint32_t maxOffset = numFrames > this->preloadSize ? numFrames - this->preloadSize : 0;
|
||||||
|
fs::path file{ rootDirectory / std::string(preloadedFile.first) };
|
||||||
|
SndfileHandle sndFile(reinterpret_cast<const char*>(file.c_str()));
|
||||||
|
preloadedFile.second.preloadedData = readFromFile<float>(sndFile, preloadSize + maxOffset);
|
||||||
|
}
|
||||||
|
this->preloadSize = preloadSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::loadingThread() noexcept
|
void sfz::FilePool::loadingThread() noexcept
|
||||||
{
|
{
|
||||||
while (!quitThread) {
|
while (!quitThread) {
|
||||||
if (emptyQueue) {
|
std::this_thread::sleep_for(1ms);
|
||||||
while(loadingQueue.pop()) {}
|
for (auto& promise : temporaryFilePromises) {
|
||||||
emptyQueue = false;
|
if (!promise->dataReady) {
|
||||||
continue;
|
fs::path file{ rootDirectory / std::string(promise->filename) };
|
||||||
}
|
SndfileHandle sndFile(reinterpret_cast<const char*>(file.c_str()));
|
||||||
|
if (sndFile.error() != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
FileLoadingInformation fileToLoad {};
|
DBG("Loading file for " << *promise->filename << " in the background");
|
||||||
if (!loadingQueue.wait_dequeue_timed(fileToLoad, 200ms)) {
|
const uint32_t frames = sndFile.frames();
|
||||||
continue;
|
promise->fileData = readFromFile<float>(sndFile, frames);
|
||||||
}
|
promise->dataReady = true;
|
||||||
|
|
||||||
if (fileToLoad.voice == nullptr) {
|
|
||||||
DBG("Background thread error: voice is null.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileToLoad.sample == nullptr) {
|
|
||||||
DBG("Background thread error: sample is null.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DBG("Background loading of: " << *fileToLoad.sample);
|
|
||||||
fs::path file { rootDirectory / *fileToLoad.sample };
|
|
||||||
if (!fs::exists(file)) {
|
|
||||||
DBG("Background thread: no file " << *fileToLoad.sample << " exists.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
SndfileHandle sndFile(reinterpret_cast<const char*>(file.c_str()));
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> guard { fileHandleMutex };
|
|
||||||
fileHandles.emplace_back(readFromFile<float>(sndFile, fileToLoad.numFrames));
|
|
||||||
fileToLoad.voice->setFileData(fileHandles.back(), fileToLoad.ticket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sfz::FilePool::garbageThread() noexcept
|
|
||||||
{
|
|
||||||
while (!quitThread) {
|
|
||||||
fileHandleMutex.lock();
|
|
||||||
for (auto handle = fileHandles.begin(); handle < fileHandles.end();) {
|
|
||||||
if (handle->use_count() == 1) {
|
|
||||||
handle->reset();
|
|
||||||
std::iter_swap(handle, fileHandles.end() - 1);
|
|
||||||
fileHandles.pop_back();
|
|
||||||
} else {
|
|
||||||
handle++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileHandleMutex.unlock();
|
|
||||||
std::this_thread::sleep_for(200ms);
|
promisesToClean.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::clear()
|
void sfz::FilePool::clear()
|
||||||
{
|
{
|
||||||
preloadedData.clear();
|
preloadedFiles.clear();
|
||||||
fileHandles.clear();
|
temporaryFilePromises.clear();
|
||||||
while (loadingQueue.pop()) {
|
promisesToClean.clear();
|
||||||
// Pop the queue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::FilePool::emptyFileLoadingQueue() noexcept
|
void sfz::FilePool::cleanupPromises() noexcept
|
||||||
{
|
{
|
||||||
emptyQueue = true;
|
if (temporaryFilePromises.empty())
|
||||||
while (emptyQueue)
|
return;
|
||||||
std::this_thread::sleep_for(1ms);
|
|
||||||
|
auto promise = temporaryFilePromises.begin();
|
||||||
|
auto sentinel = temporaryFilePromises.end() - 1;
|
||||||
|
while (promise != temporaryFilePromises.end()) {
|
||||||
|
if (promise->use_count() == 1) {
|
||||||
|
promisesToClean.push_back(*promise);
|
||||||
|
std::iter_swap(promise, sentinel);
|
||||||
|
sentinel--;
|
||||||
|
temporaryFilePromises.pop_back();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
promise++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,36 @@
|
||||||
#include "Defaults.h"
|
#include "Defaults.h"
|
||||||
#include "LeakDetector.h"
|
#include "LeakDetector.h"
|
||||||
#include "AudioBuffer.h"
|
#include "AudioBuffer.h"
|
||||||
#include "Voice.h"
|
#include "SIMDHelpers.h"
|
||||||
#include "ghc/fs_std.hpp"
|
#include "ghc/fs_std.hpp"
|
||||||
#include "moodycamel/readerwriterqueue.h"
|
#include "moodycamel/readerwriterqueue.h"
|
||||||
#include <absl/container/flat_hash_map.h>
|
#include <absl/container/flat_hash_map.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <absl/types/optional.h>
|
#include <absl/types/optional.h>
|
||||||
#include <string_view>
|
#include "absl/strings/string_view.h"
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
using AudioBufferPtr = std::shared_ptr<AudioBuffer<float>>;
|
||||||
|
|
||||||
|
|
||||||
|
struct PreloadedFileHandle
|
||||||
|
{
|
||||||
|
std::shared_ptr<AudioBuffer<float>> preloadedData {};
|
||||||
|
float sampleRate { config::defaultSampleRate };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FilePromise
|
||||||
|
{
|
||||||
|
absl::string_view filename {};
|
||||||
|
AudioBufferPtr preloadedData {};
|
||||||
|
std::unique_ptr<AudioBuffer<float>> fileData {};
|
||||||
|
float sampleRate { config::defaultSampleRate };
|
||||||
|
std::atomic<bool> dataReady { false };
|
||||||
|
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
|
||||||
|
};
|
||||||
|
|
||||||
|
using FilePromisePtr = std::shared_ptr<FilePromise>;
|
||||||
/**
|
/**
|
||||||
* @brief This is a singleton-designed class that holds all the preloaded
|
* @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
|
* data as well as functions to request new file data and collect the file
|
||||||
|
|
@ -53,6 +73,8 @@ namespace sfz {
|
||||||
* to 1. A garbage collection thread then runs regularly to clear the memory of all file
|
* to 1. A garbage collection thread then runs regularly to clear the memory of all file
|
||||||
* handles with a reference count of 1.
|
* handles with a reference count of 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
class FilePool {
|
class FilePool {
|
||||||
public:
|
public:
|
||||||
FilePool() { }
|
FilePool() { }
|
||||||
|
|
@ -61,7 +83,6 @@ public:
|
||||||
{
|
{
|
||||||
quitThread = true;
|
quitThread = true;
|
||||||
fileLoadingThread.join();
|
fileLoadingThread.join();
|
||||||
garbageCollectionThread.join();
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Set the root directory from which to search for files to load
|
* @brief Set the root directory from which to search for files to load
|
||||||
|
|
@ -74,65 +95,50 @@ public:
|
||||||
*
|
*
|
||||||
* @return size_t
|
* @return size_t
|
||||||
*/
|
*/
|
||||||
size_t getNumPreloadedSamples() const noexcept { return preloadedData.size(); }
|
size_t getNumPreloadedSamples() const noexcept { return preloadedFiles.size(); }
|
||||||
|
|
||||||
struct FileInformation {
|
struct FileInformation {
|
||||||
uint32_t end { Default::sampleEndRange.getEnd() };
|
uint32_t end { Default::sampleEndRange.getEnd() };
|
||||||
uint32_t loopBegin { Default::loopRange.getStart() };
|
uint32_t loopBegin { Default::loopRange.getStart() };
|
||||||
uint32_t loopEnd { Default::loopRange.getEnd() };
|
uint32_t loopEnd { Default::loopRange.getEnd() };
|
||||||
double sampleRate { config::defaultSampleRate };
|
double sampleRate { config::defaultSampleRate };
|
||||||
std::shared_ptr<AudioBuffer<float>> preloadedData;
|
int numChannels { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get metadata information about a file as well as the first chunk of data
|
* @brief Get metadata information about a file.
|
||||||
*
|
*
|
||||||
* If the same file was already preloaded and with a compatible offset, the handle
|
* @param filename
|
||||||
* is shared between the regions. Otherwise, a new handle is created (the others keep
|
* @return absl::optional<FileInformation>
|
||||||
* the old preloaded file).
|
*/
|
||||||
|
absl::optional<FileInformation> getFileInformation(const std::string& filename) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check that a file is preloaded with the proper offset bounds
|
||||||
*
|
*
|
||||||
* @param filename
|
* @param filename
|
||||||
* @param offset the maximum offset to consider for preloading. The total preloaded
|
* @param offset the maximum offset to consider for preloading. The total preloaded
|
||||||
* size will be preloadedSize + offset
|
* size will be preloadSize + offset
|
||||||
* @return absl::optional<FileInformation>
|
* @return true if the preloading went fine
|
||||||
|
* @return false if something went wrong ()
|
||||||
*/
|
*/
|
||||||
absl::optional<FileInformation> getFileInformation(const std::string& filename, uint32_t offset) noexcept;
|
bool preloadFile(const std::string& filename, uint32_t maxOffset) noexcept;
|
||||||
/**
|
|
||||||
* @brief Queue a full loading operation for a given voice.
|
|
||||||
*
|
|
||||||
* The goal of the ticket is to avoid file loading operations that for some reason
|
|
||||||
* finish too late a "replace" a proper sample with an obsolete one for a voice.
|
|
||||||
*
|
|
||||||
* @param voice the voice to give the full file data to
|
|
||||||
* @param sample the sample file
|
|
||||||
* @param numFrames the number of frames to load from the file
|
|
||||||
* @param ticket an ideally unique ticket number for this file.
|
|
||||||
*/
|
|
||||||
void enqueueLoading(Voice* voice, const std::string* sample, int numFrames, unsigned ticket) noexcept;
|
|
||||||
/**
|
/**
|
||||||
* @brief Clear all preloaded files.
|
* @brief Clear all preloaded files.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void clear();
|
void clear();
|
||||||
/**
|
|
||||||
* @brief Empty the file loading queue. This function will lock and wait
|
void cleanupPromises() noexcept;
|
||||||
* for the background thread to finish its business, so don't call it from
|
FilePromisePtr getFilePromise(const std::string& filename) noexcept;
|
||||||
* the audio thread.
|
void setPreloadSize(uint32_t preloadSize) noexcept;
|
||||||
*
|
|
||||||
*/
|
|
||||||
void emptyFileLoadingQueue() noexcept;
|
|
||||||
private:
|
private:
|
||||||
fs::path rootDirectory;
|
fs::path rootDirectory;
|
||||||
struct FileLoadingInformation {
|
|
||||||
Voice* voice;
|
|
||||||
const std::string* sample;
|
|
||||||
int numFrames;
|
|
||||||
unsigned ticket;
|
|
||||||
};
|
|
||||||
|
|
||||||
moodycamel::BlockingReaderWriterQueue<FileLoadingInformation> loadingQueue { config::numVoices };
|
|
||||||
void loadingThread() noexcept;
|
void loadingThread() noexcept;
|
||||||
void garbageThread() noexcept;
|
|
||||||
|
|
||||||
|
uint32_t preloadSize { config::preloadSize };
|
||||||
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
|
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
|
||||||
|
|
||||||
// Signals
|
// Signals
|
||||||
|
|
@ -140,10 +146,11 @@ private:
|
||||||
bool emptyQueue { false };
|
bool emptyQueue { false };
|
||||||
|
|
||||||
std::mutex fileHandleMutex;
|
std::mutex fileHandleMutex;
|
||||||
std::vector<std::shared_ptr<AudioBuffer<float>>> fileHandles;
|
|
||||||
absl::flat_hash_map<absl::string_view, std::shared_ptr<AudioBuffer<float>>> preloadedData;
|
std::vector<FilePromisePtr> temporaryFilePromises;
|
||||||
|
std::vector<FilePromisePtr> promisesToClean;
|
||||||
|
absl::flat_hash_map<absl::string_view, PreloadedFileHandle> preloadedFiles;
|
||||||
std::thread fileLoadingThread { &FilePool::loadingThread, this };
|
std::thread fileLoadingThread { &FilePool::loadingThread, this };
|
||||||
std::thread garbageCollectionThread { &FilePool::garbageThread, this };
|
|
||||||
LEAK_DETECTOR(FilePool);
|
LEAK_DETECTOR(FilePool);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -659,22 +659,6 @@ uint32_t sfz::Region::trueSampleEnd() const noexcept
|
||||||
return min(sampleEnd, loopRange.getEnd());
|
return min(sampleEnd, loopRange.getEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sfz::Region::canUsePreloadedData() const noexcept
|
|
||||||
{
|
|
||||||
if (preloadedData == nullptr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return trueSampleEnd() < static_cast<uint32_t>(preloadedData->getNumFrames());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sfz::Region::isStereo() const noexcept
|
|
||||||
{
|
|
||||||
if (isGenerator())
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
return (this->preloadedData->getNumChannels() == 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<class T, class U>
|
template<class T, class U>
|
||||||
float crossfadeIn(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
|
float crossfadeIn(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -149,13 +149,7 @@ struct Region {
|
||||||
* @return false
|
* @return false
|
||||||
*/
|
*/
|
||||||
void registerTempo(float secondsPerQuarter) noexcept;
|
void registerTempo(float secondsPerQuarter) noexcept;
|
||||||
/**
|
|
||||||
* @brief Is the underlying region sample a stereo one?
|
|
||||||
*
|
|
||||||
* @return true
|
|
||||||
* @return false
|
|
||||||
*/
|
|
||||||
bool isStereo() const noexcept;
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the base pitch of the region depending on which note has been
|
* @brief Get the base pitch of the region depending on which note has been
|
||||||
* pressed and at which velocity.
|
* pressed and at which velocity.
|
||||||
|
|
@ -339,8 +333,7 @@ struct Region {
|
||||||
EGDescription pitchEG;
|
EGDescription pitchEG;
|
||||||
EGDescription filterEG;
|
EGDescription filterEG;
|
||||||
|
|
||||||
double sampleRate { config::defaultSampleRate };
|
bool isStereo { false };
|
||||||
std::shared_ptr<AudioBuffer<float>> preloadedData { nullptr };
|
|
||||||
private:
|
private:
|
||||||
const MidiState& midiState;
|
const MidiState& midiState;
|
||||||
bool keySwitched { true };
|
bool keySwitched { true };
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& filename)
|
||||||
auto region = currentRegion->get();
|
auto region = currentRegion->get();
|
||||||
|
|
||||||
if (!region->isGenerator()) {
|
if (!region->isGenerator()) {
|
||||||
auto fileInformation = resources.filePool.getFileInformation(region->sample, region->offset + region->offsetRandom);
|
auto fileInformation = resources.filePool.getFileInformation(region->sample);
|
||||||
if (!fileInformation) {
|
if (!fileInformation) {
|
||||||
DBG("Removing the region with sample " << region->sample);
|
DBG("Removing the region with sample " << region->sample);
|
||||||
std::iter_swap(currentRegion, lastRegion);
|
std::iter_swap(currentRegion, lastRegion);
|
||||||
|
|
@ -236,8 +236,12 @@ bool sfz::Synth::loadSfzFile(const fs::path& filename)
|
||||||
}
|
}
|
||||||
region->sampleEnd = std::min(region->sampleEnd, fileInformation->end);
|
region->sampleEnd = std::min(region->sampleEnd, fileInformation->end);
|
||||||
region->loopRange.shrinkIfSmaller(fileInformation->loopBegin, fileInformation->loopEnd);
|
region->loopRange.shrinkIfSmaller(fileInformation->loopBegin, fileInformation->loopEnd);
|
||||||
region->preloadedData = fileInformation->preloadedData;
|
if (fileInformation->numChannels == 2)
|
||||||
region->sampleRate = fileInformation->sampleRate;
|
region->isStereo = true;
|
||||||
|
|
||||||
|
// TODO: adjust with LFO targets
|
||||||
|
const auto maxOffset { region->offset + region->offsetRandom };
|
||||||
|
resources.filePool.preloadFile(region->sample, maxOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto note = 0; note < 128; note++) {
|
for (auto note = 0; note < 128; note++) {
|
||||||
|
|
@ -313,7 +317,7 @@ int sfz::Synth::getNumActiveVoices() const noexcept
|
||||||
void sfz::Synth::garbageCollect() noexcept
|
void sfz::Synth::garbageCollect() noexcept
|
||||||
{
|
{
|
||||||
for (auto& voice : voices) {
|
for (auto& voice : voices) {
|
||||||
voice->garbageCollect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,6 +352,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
ScopedFTZ ftz;
|
ScopedFTZ ftz;
|
||||||
buffer.fill(0.0f);
|
buffer.fill(0.0f);
|
||||||
|
|
||||||
|
resources.filePool.cleanupPromises();
|
||||||
|
|
||||||
AtomicGuard callbackGuard { inCallback };
|
AtomicGuard callbackGuard { inCallback };
|
||||||
if (!canEnterCallback)
|
if (!canEnterCallback)
|
||||||
return;
|
return;
|
||||||
|
|
@ -387,10 +393,6 @@ void sfz::Synth::noteOn(int delay, int channel, int noteNumber, uint8_t velocity
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
voice->startVoice(region, delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
voice->startVoice(region, delay, channel, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
||||||
if (!region->isGenerator()) {
|
|
||||||
voice->expectFileData(fileTicket);
|
|
||||||
resources.filePool.enqueueLoading(voice, ®ion->sample, region->trueSampleEnd(), fileTicket++);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -420,10 +422,6 @@ void sfz::Synth::noteOff(int delay, int channel, int noteNumber, uint8_t velocit
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
voice->startVoice(region, delay, channel, noteNumber, replacedVelocity, Voice::TriggerType::NoteOff);
|
voice->startVoice(region, delay, channel, noteNumber, replacedVelocity, Voice::TriggerType::NoteOff);
|
||||||
if (!region->isGenerator()) {
|
|
||||||
voice->expectFileData(fileTicket);
|
|
||||||
resources.filePool.enqueueLoading(voice, ®ion->sample, region->trueSampleEnd(), fileTicket++);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,10 +447,6 @@ void sfz::Synth::cc(int delay, int channel, int ccNumber, uint8_t ccValue) noexc
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
voice->startVoice(region, delay, channel, ccNumber, ccValue, Voice::TriggerType::CC);
|
voice->startVoice(region, delay, channel, ccNumber, ccValue, Voice::TriggerType::CC);
|
||||||
if (!region->isGenerator()) {
|
|
||||||
voice->expectFileData(fileTicket);
|
|
||||||
resources.filePool.enqueueLoading(voice, ®ion->sample, region->trueSampleEnd(), fileTicket++);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -533,10 +527,9 @@ void sfz::Synth::resetVoices(int numVoices)
|
||||||
std::this_thread::sleep_for(1ms);
|
std::this_thread::sleep_for(1ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
resources.filePool.emptyFileLoadingQueue();
|
|
||||||
voices.clear();
|
voices.clear();
|
||||||
for (int i = 0; i < numVoices; ++i)
|
for (int i = 0; i < numVoices; ++i)
|
||||||
voices.push_back(std::make_unique<Voice>(midiState));
|
voices.push_back(std::make_unique<Voice>(midiState, resources));
|
||||||
|
|
||||||
for (auto& voice: voices) {
|
for (auto& voice: voices) {
|
||||||
voice->setSampleRate(this->sampleRate);
|
voice->setSampleRate(this->sampleRate);
|
||||||
|
|
@ -554,7 +547,6 @@ void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
||||||
std::this_thread::sleep_for(1ms);
|
std::this_thread::sleep_for(1ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
resources.filePool.emptyFileLoadingQueue();
|
|
||||||
for (auto& voice: voices)
|
for (auto& voice: voices)
|
||||||
voice->reset();
|
voice->reset();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "Resources.h"
|
#include "Resources.h"
|
||||||
#include "Parser.h"
|
#include "Parser.h"
|
||||||
|
#include "Voice.h"
|
||||||
#include "Region.h"
|
#include "Region.h"
|
||||||
#include "LeakDetector.h"
|
#include "LeakDetector.h"
|
||||||
#include "MidiState.h"
|
#include "MidiState.h"
|
||||||
|
|
@ -353,11 +354,6 @@ private:
|
||||||
std::vector<Opcode> masterOpcodes;
|
std::vector<Opcode> masterOpcodes;
|
||||||
std::vector<Opcode> groupOpcodes;
|
std::vector<Opcode> groupOpcodes;
|
||||||
|
|
||||||
// Singletons passed as references to the voices
|
|
||||||
// TODO: these should probably go in a global singleton holder along with a buffer distribution and LFO/EG stuff...
|
|
||||||
Resources resources;
|
|
||||||
MidiState midiState;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Find a voice that is not currently playing
|
* @brief Find a voice that is not currently playing
|
||||||
*
|
*
|
||||||
|
|
@ -396,6 +392,10 @@ private:
|
||||||
std::atomic<bool> canEnterCallback { true };
|
std::atomic<bool> canEnterCallback { true };
|
||||||
std::atomic<bool> inCallback { false };
|
std::atomic<bool> inCallback { false };
|
||||||
|
|
||||||
|
// Singletons passed as references to the voices
|
||||||
|
Resources resources;
|
||||||
|
MidiState midiState;
|
||||||
|
|
||||||
LEAK_DETECTOR(Synth);
|
LEAK_DETECTOR(Synth);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@
|
||||||
#include "absl/algorithm/container.h"
|
#include "absl/algorithm/container.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
sfz::Voice::Voice(const MidiState& midiState)
|
sfz::Voice::Voice(const sfz::MidiState& midiState, sfz::Resources& resources)
|
||||||
: midiState(midiState)
|
: midiState(midiState), resources(resources)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,6 +44,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int channel, int number,
|
||||||
triggerValue = value;
|
triggerValue = value;
|
||||||
|
|
||||||
this->region = region;
|
this->region = region;
|
||||||
|
state = State::playing;
|
||||||
|
|
||||||
ASSERT(delay >= 0);
|
ASSERT(delay >= 0);
|
||||||
if (delay < 0)
|
if (delay < 0)
|
||||||
|
|
@ -51,8 +52,16 @@ void sfz::Voice::startVoice(Region* region, int delay, int channel, int number,
|
||||||
|
|
||||||
// DBG("Starting voice with " << region->sample);
|
// DBG("Starting voice with " << region->sample);
|
||||||
|
|
||||||
state = State::playing;
|
if (!region->isGenerator()) {
|
||||||
speedRatio = static_cast<float>(region->sampleRate / this->sampleRate);
|
currentPromise = resources.filePool.getFilePromise(region->sample);
|
||||||
|
if (currentPromise == nullptr) {
|
||||||
|
DBG("[Voice] Could not fetch the file promise for sample " << region->sample);
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
speedRatio = static_cast<float>(currentPromise->sampleRate / this->sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
pitchRatio = region->getBasePitchVariation(number, value);
|
pitchRatio = region->getBasePitchVariation(number, value);
|
||||||
|
|
||||||
baseVolumedB = region->getBaseVolumedB(number);
|
baseVolumedB = region->getBaseVolumedB(number);
|
||||||
|
|
@ -118,15 +127,6 @@ void sfz::Voice::prepareEGEnvelope(int delay, uint8_t velocity) noexcept
|
||||||
normalizePercents(region->amplitudeEG.getStart(midiState.cc, velocity)));
|
normalizePercents(region->amplitudeEG.getStart(midiState.cc, velocity)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Voice::setFileData(std::shared_ptr<AudioBuffer<float>> file, unsigned ticket) noexcept
|
|
||||||
{
|
|
||||||
if (ticket != this->ticket)
|
|
||||||
return;
|
|
||||||
|
|
||||||
fileData = std::move(file);
|
|
||||||
dataReady.store(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool sfz::Voice::isFree() const noexcept
|
bool sfz::Voice::isFree() const noexcept
|
||||||
{
|
{
|
||||||
return (region == nullptr);
|
return (region == nullptr);
|
||||||
|
|
@ -245,7 +245,7 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
else
|
else
|
||||||
fillWithData(delayed_buffer);
|
fillWithData(delayed_buffer);
|
||||||
|
|
||||||
if (region->isStereo())
|
if (region->isStereo)
|
||||||
processStereo(buffer);
|
processStereo(buffer);
|
||||||
else
|
else
|
||||||
processMono(buffer);
|
processMono(buffer);
|
||||||
|
|
@ -352,12 +352,13 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto source { [&]() {
|
auto source { [&]() {
|
||||||
if (region->canUsePreloadedData())
|
// if (region->trueSampleEnd() < currentPromise->preloadedData->getNumFrames())
|
||||||
return AudioSpan<const float>(*region->preloadedData);
|
// return AudioSpan<const float>(*currentPromise->preloadedData);
|
||||||
else if (!dataReady)
|
// else
|
||||||
return AudioSpan<const float>(*region->preloadedData);
|
if (!currentPromise->dataReady)
|
||||||
|
return AudioSpan<const float>(*currentPromise->preloadedData);
|
||||||
else
|
else
|
||||||
return AudioSpan<const float>(*fileData);
|
return AudioSpan<const float>(*currentPromise->fileData);
|
||||||
}() };
|
}() };
|
||||||
|
|
||||||
auto indices = indexSpan.first(buffer.getNumFrames());
|
auto indices = indexSpan.first(buffer.getNumFrames());
|
||||||
|
|
@ -483,30 +484,17 @@ sfz::Voice::TriggerType sfz::Voice::getTriggerType() const noexcept
|
||||||
|
|
||||||
void sfz::Voice::reset() noexcept
|
void sfz::Voice::reset() noexcept
|
||||||
{
|
{
|
||||||
dataReady.store(false);
|
|
||||||
fileData.reset();
|
|
||||||
state = State::idle;
|
state = State::idle;
|
||||||
if (region != nullptr) {
|
if (region != nullptr) {
|
||||||
DBG("Reset voice with sample " << region->sample);
|
DBG("Reset voice with sample " << region->sample);
|
||||||
}
|
}
|
||||||
region = nullptr;
|
region = nullptr;
|
||||||
|
currentPromise.reset();
|
||||||
sourcePosition = 0;
|
sourcePosition = 0;
|
||||||
floatPositionOffset = 0.0f;
|
floatPositionOffset = 0.0f;
|
||||||
noteIsOff = false;
|
noteIsOff = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Voice::garbageCollect() noexcept
|
|
||||||
{
|
|
||||||
if (state == State::idle && region == nullptr) {
|
|
||||||
fileData.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sfz::Voice::expectFileData(unsigned ticket)
|
|
||||||
{
|
|
||||||
this->ticket = ticket;
|
|
||||||
}
|
|
||||||
|
|
||||||
float sfz::Voice::getMeanSquaredAverage() const noexcept
|
float sfz::Voice::getMeanSquaredAverage() const noexcept
|
||||||
{
|
{
|
||||||
return powerHistory.getAverage();
|
return powerHistory.getAverage();
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
#include "Region.h"
|
#include "Region.h"
|
||||||
#include "AudioBuffer.h"
|
#include "AudioBuffer.h"
|
||||||
#include "MidiState.h"
|
#include "MidiState.h"
|
||||||
|
#include "Resources.h"
|
||||||
#include "AudioSpan.h"
|
#include "AudioSpan.h"
|
||||||
#include "LeakDetector.h"
|
#include "LeakDetector.h"
|
||||||
#include <absl/types/span.h>
|
#include <absl/types/span.h>
|
||||||
|
|
@ -50,7 +51,7 @@ public:
|
||||||
*
|
*
|
||||||
* @param midiState
|
* @param midiState
|
||||||
*/
|
*/
|
||||||
Voice(const MidiState& midiState);
|
Voice(const MidiState& midiState, Resources& resources);
|
||||||
enum class TriggerType {
|
enum class TriggerType {
|
||||||
NoteOn,
|
NoteOn,
|
||||||
NoteOff,
|
NoteOff,
|
||||||
|
|
@ -98,23 +99,6 @@ public:
|
||||||
*/
|
*/
|
||||||
void startVoice(Region* region, int delay, int channel, int number, uint8_t value, TriggerType triggerType) noexcept;
|
void startVoice(Region* region, int delay, int channel, int number, uint8_t value, TriggerType triggerType) noexcept;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Tells the voice that it should expect to receive a file at some point using the
|
|
||||||
* setFileData() function. The ticket is a unique identifier that will prevent the file data
|
|
||||||
* to be set "too late"; if the voice receives the file for an older ticket, it will discard
|
|
||||||
* it.
|
|
||||||
*
|
|
||||||
* @param ticket
|
|
||||||
*/
|
|
||||||
void expectFileData(unsigned ticket);
|
|
||||||
/**
|
|
||||||
* @brief Sets the file data for a given ticket. The voice can freely release and destroy the
|
|
||||||
* shared pointer, as it will be garbage collected by the file pool afterwards.
|
|
||||||
*
|
|
||||||
* @param file
|
|
||||||
* @param ticket
|
|
||||||
*/
|
|
||||||
void setFileData(std::shared_ptr<AudioBuffer<float>> file, unsigned ticket) noexcept;
|
|
||||||
/**
|
/**
|
||||||
* @brief Register a note-off event; this may trigger a release.
|
* @brief Register a note-off event; this may trigger a release.
|
||||||
*
|
*
|
||||||
|
|
@ -221,11 +205,6 @@ public:
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void reset() noexcept;
|
void reset() noexcept;
|
||||||
/**
|
|
||||||
* @brief Clear the loaded file data if it's not useful anymore
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
void garbageCollect() noexcept;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the mean squared power of the last rendered block. This is used
|
* @brief Get the mean squared power of the last rendered block. This is used
|
||||||
|
|
@ -309,9 +288,7 @@ private:
|
||||||
int sourcePosition { 0 };
|
int sourcePosition { 0 };
|
||||||
int initialDelay { 0 };
|
int initialDelay { 0 };
|
||||||
|
|
||||||
std::atomic<bool> dataReady { false };
|
FilePromisePtr currentPromise { nullptr };
|
||||||
std::shared_ptr<AudioBuffer<float>> fileData { nullptr };
|
|
||||||
unsigned ticket { 0 };
|
|
||||||
|
|
||||||
Buffer<float> tempBuffer1;
|
Buffer<float> tempBuffer1;
|
||||||
Buffer<float> tempBuffer2;
|
Buffer<float> tempBuffer2;
|
||||||
|
|
@ -326,6 +303,8 @@ private:
|
||||||
float sampleRate { config::defaultSampleRate };
|
float sampleRate { config::defaultSampleRate };
|
||||||
|
|
||||||
const MidiState& midiState;
|
const MidiState& midiState;
|
||||||
|
Resources& resources;
|
||||||
|
|
||||||
ADSREnvelope<float> egEnvelope;
|
ADSREnvelope<float> egEnvelope;
|
||||||
LinearEnvelope<float> volumeEnvelope; // dB events but the envelope output is linear gain
|
LinearEnvelope<float> volumeEnvelope; // dB events but the envelope output is linear gain
|
||||||
LinearEnvelope<float> amplitudeEnvelope; // linear events
|
LinearEnvelope<float> amplitudeEnvelope; // linear events
|
||||||
|
|
|
||||||
|
|
@ -257,9 +257,9 @@ TEST_CASE("[Files] Channels (channels.sfz)")
|
||||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels.sfz");
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels.sfz");
|
||||||
REQUIRE(synth.getNumRegions() == 2);
|
REQUIRE(synth.getNumRegions() == 2);
|
||||||
REQUIRE(synth.getRegionView(0)->sample == "mono_sample.wav");
|
REQUIRE(synth.getRegionView(0)->sample == "mono_sample.wav");
|
||||||
REQUIRE(!synth.getRegionView(0)->isStereo());
|
REQUIRE(!synth.getRegionView(0)->isStereo);
|
||||||
REQUIRE(synth.getRegionView(1)->sample == "stereo_sample.wav");
|
REQUIRE(synth.getRegionView(1)->sample == "stereo_sample.wav");
|
||||||
REQUIRE(synth.getRegionView(1)->isStereo());
|
REQUIRE(synth.getRegionView(1)->isStereo);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("[Files] sw_default")
|
TEST_CASE("[Files] sw_default")
|
||||||
|
|
@ -315,4 +315,4 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines")
|
||||||
REQUIRE( synth.getRegionView(2)->amplitudeCC );
|
REQUIRE( synth.getRegionView(2)->amplitudeCC );
|
||||||
REQUIRE( synth.getRegionView(2)->amplitudeCC->first == 10 );
|
REQUIRE( synth.getRegionView(2)->amplitudeCC->first == 10 );
|
||||||
REQUIRE( synth.getRegionView(2)->amplitudeCC->second == 34.0f );
|
REQUIRE( synth.getRegionView(2)->amplitudeCC->second == 34.0f );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue