Changed the logic for the file loading

This commit is contained in:
Paul Ferrand 2019-12-01 20:18:59 +01:00
parent 1536ead90b
commit 630220e7e0
9 changed files with 188 additions and 247 deletions

View file

@ -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++;
}
}
} }

View file

@ -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);
}; };
} }

View file

@ -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)
{ {

View file

@ -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 };

View file

@ -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, &region->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, &region->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, &region->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();

View file

@ -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);
}; };

View file

@ -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();

View file

@ -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

View file

@ -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 );
} }