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 <sndfile.hh>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
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);
|
||||
if (sndFile.channels() == 1) {
|
||||
sndFile.readf(returnedBuffer->channelWriter(0), numFrames);
|
||||
} 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);
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
return {};
|
||||
|
||||
|
|
@ -63,6 +62,7 @@ absl::optional<sfz::FilePool::FileInformation> sfz::FilePool::getFileInformation
|
|||
FileInformation returnedValue;
|
||||
returnedValue.end = static_cast<uint32_t>(sndFile.frames());
|
||||
returnedValue.sampleRate = static_cast<double>(sndFile.samplerate());
|
||||
returnedValue.numChannels = sndFile.channels();
|
||||
|
||||
SF_INSTRUMENT 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 })) {
|
||||
DBG("Problem enqueuing a file read for file " << sample);
|
||||
fs::path file{ rootDirectory / filename };
|
||||
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
|
||||
{
|
||||
while (!quitThread) {
|
||||
if (emptyQueue) {
|
||||
while(loadingQueue.pop()) {}
|
||||
emptyQueue = false;
|
||||
continue;
|
||||
}
|
||||
std::this_thread::sleep_for(1ms);
|
||||
for (auto& promise : temporaryFilePromises) {
|
||||
if (!promise->dataReady) {
|
||||
fs::path file{ rootDirectory / std::string(promise->filename) };
|
||||
SndfileHandle sndFile(reinterpret_cast<const char*>(file.c_str()));
|
||||
if (sndFile.error() != 0)
|
||||
continue;
|
||||
|
||||
FileLoadingInformation fileToLoad {};
|
||||
if (!loadingQueue.wait_dequeue_timed(fileToLoad, 200ms)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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++;
|
||||
DBG("Loading file for " << *promise->filename << " in the background");
|
||||
const uint32_t frames = sndFile.frames();
|
||||
promise->fileData = readFromFile<float>(sndFile, frames);
|
||||
promise->dataReady = true;
|
||||
}
|
||||
}
|
||||
fileHandleMutex.unlock();
|
||||
std::this_thread::sleep_for(200ms);
|
||||
|
||||
promisesToClean.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::FilePool::clear()
|
||||
{
|
||||
preloadedData.clear();
|
||||
fileHandles.clear();
|
||||
while (loadingQueue.pop()) {
|
||||
// Pop the queue
|
||||
}
|
||||
preloadedFiles.clear();
|
||||
temporaryFilePromises.clear();
|
||||
promisesToClean.clear();
|
||||
}
|
||||
|
||||
void sfz::FilePool::emptyFileLoadingQueue() noexcept
|
||||
void sfz::FilePool::cleanupPromises() noexcept
|
||||
{
|
||||
emptyQueue = true;
|
||||
while (emptyQueue)
|
||||
std::this_thread::sleep_for(1ms);
|
||||
if (temporaryFilePromises.empty())
|
||||
return;
|
||||
|
||||
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 "LeakDetector.h"
|
||||
#include "AudioBuffer.h"
|
||||
#include "Voice.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include "ghc/fs_std.hpp"
|
||||
#include "moodycamel/readerwriterqueue.h"
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
#include <mutex>
|
||||
#include <absl/types/optional.h>
|
||||
#include <string_view>
|
||||
#include "absl/strings/string_view.h"
|
||||
#include <thread>
|
||||
|
||||
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
|
||||
* 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
|
||||
* handles with a reference count of 1.
|
||||
*/
|
||||
|
||||
|
||||
class FilePool {
|
||||
public:
|
||||
FilePool() { }
|
||||
|
|
@ -61,7 +83,6 @@ public:
|
|||
{
|
||||
quitThread = true;
|
||||
fileLoadingThread.join();
|
||||
garbageCollectionThread.join();
|
||||
}
|
||||
/**
|
||||
* @brief Set the root directory from which to search for files to load
|
||||
|
|
@ -74,65 +95,50 @@ public:
|
|||
*
|
||||
* @return size_t
|
||||
*/
|
||||
size_t getNumPreloadedSamples() const noexcept { return preloadedData.size(); }
|
||||
size_t getNumPreloadedSamples() const noexcept { return preloadedFiles.size(); }
|
||||
|
||||
struct FileInformation {
|
||||
uint32_t end { Default::sampleEndRange.getEnd() };
|
||||
uint32_t loopBegin { Default::loopRange.getStart() };
|
||||
uint32_t loopEnd { Default::loopRange.getEnd() };
|
||||
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
|
||||
* is shared between the regions. Otherwise, a new handle is created (the others keep
|
||||
* the old preloaded file).
|
||||
* @param filename
|
||||
* @return absl::optional<FileInformation>
|
||||
*/
|
||||
absl::optional<FileInformation> getFileInformation(const std::string& filename) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Check that a file is preloaded with the proper offset bounds
|
||||
*
|
||||
* @param filename
|
||||
* @param offset the maximum offset to consider for preloading. The total preloaded
|
||||
* size will be preloadedSize + offset
|
||||
* @return absl::optional<FileInformation>
|
||||
* size will be preloadSize + offset
|
||||
* @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;
|
||||
/**
|
||||
* @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;
|
||||
bool preloadFile(const std::string& filename, uint32_t maxOffset) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Clear all preloaded files.
|
||||
*
|
||||
*/
|
||||
void clear();
|
||||
/**
|
||||
* @brief Empty the file loading queue. This function will lock and wait
|
||||
* for the background thread to finish its business, so don't call it from
|
||||
* the audio thread.
|
||||
*
|
||||
*/
|
||||
void emptyFileLoadingQueue() noexcept;
|
||||
|
||||
void cleanupPromises() noexcept;
|
||||
FilePromisePtr getFilePromise(const std::string& filename) noexcept;
|
||||
void setPreloadSize(uint32_t preloadSize) noexcept;
|
||||
private:
|
||||
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 garbageThread() noexcept;
|
||||
|
||||
uint32_t preloadSize { config::preloadSize };
|
||||
Oversampling oversamplingFactor { config::defaultOversamplingFactor };
|
||||
|
||||
// Signals
|
||||
|
|
@ -140,10 +146,11 @@ private:
|
|||
bool emptyQueue { false };
|
||||
|
||||
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 garbageCollectionThread { &FilePool::garbageThread, this };
|
||||
LEAK_DETECTOR(FilePool);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -659,22 +659,6 @@ uint32_t sfz::Region::trueSampleEnd() const noexcept
|
|||
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>
|
||||
float crossfadeIn(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -149,13 +149,7 @@ struct Region {
|
|||
* @return false
|
||||
*/
|
||||
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
|
||||
* pressed and at which velocity.
|
||||
|
|
@ -339,8 +333,7 @@ struct Region {
|
|||
EGDescription pitchEG;
|
||||
EGDescription filterEG;
|
||||
|
||||
double sampleRate { config::defaultSampleRate };
|
||||
std::shared_ptr<AudioBuffer<float>> preloadedData { nullptr };
|
||||
bool isStereo { false };
|
||||
private:
|
||||
const MidiState& midiState;
|
||||
bool keySwitched { true };
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& filename)
|
|||
auto region = currentRegion->get();
|
||||
|
||||
if (!region->isGenerator()) {
|
||||
auto fileInformation = resources.filePool.getFileInformation(region->sample, region->offset + region->offsetRandom);
|
||||
auto fileInformation = resources.filePool.getFileInformation(region->sample);
|
||||
if (!fileInformation) {
|
||||
DBG("Removing the region with sample " << region->sample);
|
||||
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->loopRange.shrinkIfSmaller(fileInformation->loopBegin, fileInformation->loopEnd);
|
||||
region->preloadedData = fileInformation->preloadedData;
|
||||
region->sampleRate = fileInformation->sampleRate;
|
||||
if (fileInformation->numChannels == 2)
|
||||
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++) {
|
||||
|
|
@ -313,7 +317,7 @@ int sfz::Synth::getNumActiveVoices() const noexcept
|
|||
void sfz::Synth::garbageCollect() noexcept
|
||||
{
|
||||
for (auto& voice : voices) {
|
||||
voice->garbageCollect();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +352,8 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
ScopedFTZ ftz;
|
||||
buffer.fill(0.0f);
|
||||
|
||||
resources.filePool.cleanupPromises();
|
||||
|
||||
AtomicGuard callbackGuard { inCallback };
|
||||
if (!canEnterCallback)
|
||||
return;
|
||||
|
|
@ -387,10 +393,6 @@ void sfz::Synth::noteOn(int delay, int channel, int noteNumber, uint8_t velocity
|
|||
continue;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
resources.filePool.emptyFileLoadingQueue();
|
||||
voices.clear();
|
||||
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) {
|
||||
voice->setSampleRate(this->sampleRate);
|
||||
|
|
@ -554,7 +547,6 @@ void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
|||
std::this_thread::sleep_for(1ms);
|
||||
}
|
||||
|
||||
resources.filePool.emptyFileLoadingQueue();
|
||||
for (auto& voice: voices)
|
||||
voice->reset();
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
#pragma once
|
||||
#include "Resources.h"
|
||||
#include "Parser.h"
|
||||
#include "Voice.h"
|
||||
#include "Region.h"
|
||||
#include "LeakDetector.h"
|
||||
#include "MidiState.h"
|
||||
|
|
@ -353,11 +354,6 @@ private:
|
|||
std::vector<Opcode> masterOpcodes;
|
||||
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
|
||||
*
|
||||
|
|
@ -396,6 +392,10 @@ private:
|
|||
std::atomic<bool> canEnterCallback { true };
|
||||
std::atomic<bool> inCallback { false };
|
||||
|
||||
// Singletons passed as references to the voices
|
||||
Resources resources;
|
||||
MidiState midiState;
|
||||
|
||||
LEAK_DETECTOR(Synth);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@
|
|||
#include "absl/algorithm/container.h"
|
||||
#include <memory>
|
||||
|
||||
sfz::Voice::Voice(const MidiState& midiState)
|
||||
: midiState(midiState)
|
||||
sfz::Voice::Voice(const sfz::MidiState& midiState, sfz::Resources& resources)
|
||||
: midiState(midiState), resources(resources)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +44,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int channel, int number,
|
|||
triggerValue = value;
|
||||
|
||||
this->region = region;
|
||||
state = State::playing;
|
||||
|
||||
ASSERT(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);
|
||||
|
||||
state = State::playing;
|
||||
speedRatio = static_cast<float>(region->sampleRate / this->sampleRate);
|
||||
if (!region->isGenerator()) {
|
||||
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);
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return (region == nullptr);
|
||||
|
|
@ -245,7 +245,7 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
else
|
||||
fillWithData(delayed_buffer);
|
||||
|
||||
if (region->isStereo())
|
||||
if (region->isStereo)
|
||||
processStereo(buffer);
|
||||
else
|
||||
processMono(buffer);
|
||||
|
|
@ -352,12 +352,13 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
|||
return;
|
||||
|
||||
auto source { [&]() {
|
||||
if (region->canUsePreloadedData())
|
||||
return AudioSpan<const float>(*region->preloadedData);
|
||||
else if (!dataReady)
|
||||
return AudioSpan<const float>(*region->preloadedData);
|
||||
// if (region->trueSampleEnd() < currentPromise->preloadedData->getNumFrames())
|
||||
// return AudioSpan<const float>(*currentPromise->preloadedData);
|
||||
// else
|
||||
if (!currentPromise->dataReady)
|
||||
return AudioSpan<const float>(*currentPromise->preloadedData);
|
||||
else
|
||||
return AudioSpan<const float>(*fileData);
|
||||
return AudioSpan<const float>(*currentPromise->fileData);
|
||||
}() };
|
||||
|
||||
auto indices = indexSpan.first(buffer.getNumFrames());
|
||||
|
|
@ -483,30 +484,17 @@ sfz::Voice::TriggerType sfz::Voice::getTriggerType() const noexcept
|
|||
|
||||
void sfz::Voice::reset() noexcept
|
||||
{
|
||||
dataReady.store(false);
|
||||
fileData.reset();
|
||||
state = State::idle;
|
||||
if (region != nullptr) {
|
||||
DBG("Reset voice with sample " << region->sample);
|
||||
}
|
||||
region = nullptr;
|
||||
currentPromise.reset();
|
||||
sourcePosition = 0;
|
||||
floatPositionOffset = 0.0f;
|
||||
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
|
||||
{
|
||||
return powerHistory.getAverage();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
#include "Region.h"
|
||||
#include "AudioBuffer.h"
|
||||
#include "MidiState.h"
|
||||
#include "Resources.h"
|
||||
#include "AudioSpan.h"
|
||||
#include "LeakDetector.h"
|
||||
#include <absl/types/span.h>
|
||||
|
|
@ -50,7 +51,7 @@ public:
|
|||
*
|
||||
* @param midiState
|
||||
*/
|
||||
Voice(const MidiState& midiState);
|
||||
Voice(const MidiState& midiState, Resources& resources);
|
||||
enum class TriggerType {
|
||||
NoteOn,
|
||||
NoteOff,
|
||||
|
|
@ -98,23 +99,6 @@ public:
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -221,11 +205,6 @@ public:
|
|||
*
|
||||
*/
|
||||
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
|
||||
|
|
@ -309,9 +288,7 @@ private:
|
|||
int sourcePosition { 0 };
|
||||
int initialDelay { 0 };
|
||||
|
||||
std::atomic<bool> dataReady { false };
|
||||
std::shared_ptr<AudioBuffer<float>> fileData { nullptr };
|
||||
unsigned ticket { 0 };
|
||||
FilePromisePtr currentPromise { nullptr };
|
||||
|
||||
Buffer<float> tempBuffer1;
|
||||
Buffer<float> tempBuffer2;
|
||||
|
|
@ -326,6 +303,8 @@ private:
|
|||
float sampleRate { config::defaultSampleRate };
|
||||
|
||||
const MidiState& midiState;
|
||||
Resources& resources;
|
||||
|
||||
ADSREnvelope<float> egEnvelope;
|
||||
LinearEnvelope<float> volumeEnvelope; // dB events but the envelope output is linear gain
|
||||
LinearEnvelope<float> amplitudeEnvelope; // linear events
|
||||
|
|
|
|||
|
|
@ -257,9 +257,9 @@ TEST_CASE("[Files] Channels (channels.sfz)")
|
|||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels.sfz");
|
||||
REQUIRE(synth.getNumRegions() == 2);
|
||||
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)->isStereo());
|
||||
REQUIRE(synth.getRegionView(1)->isStereo);
|
||||
}
|
||||
|
||||
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->first == 10 );
|
||||
REQUIRE( synth.getRegionView(2)->amplitudeCC->second == 34.0f );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue