diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index d5471a12..59a2047a 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -274,7 +274,7 @@ void sfz::FilePool::tryToClearPromises() std::this_thread::sleep_for(std::chrono::milliseconds(1)); for (auto& promise: promisesToClear) { - if (promise->dataReady) + if (promise->dataStatus != FilePromise::DataStatus::Wait) promise->reset(); } } @@ -313,11 +313,12 @@ void sfz::FilePool::loadingThread() noexcept SndfileHandle sndFile(file.string().c_str()); if (sndFile.error() != 0) { DBG("[sfizz] libsndfile errored for " << promise->filename << " with message " << sndFile.strError()); + promise->dataStatus = FilePromise::DataStatus::Error; continue; } const auto frames = static_cast(sndFile.frames()); streamFromFile(sndFile, frames, oversamplingFactor, promise->fileData, &promise->availableFrames); - promise->dataReady = true; + promise->dataStatus = FilePromise::DataStatus::Ready; const auto loadDuration = std::chrono::high_resolution_clock::now() - loadStartTime; logger.logFileTime(waitDuration, loadDuration, frames, promise->filename); @@ -352,7 +353,7 @@ void sfz::FilePool::cleanupPromises() noexcept auto clearedIterator = promisesToClear.begin(); auto clearedSentinel = promisesToClear.rbegin(); while (clearedIterator < clearedSentinel.base()) { - if (clearedIterator->get()->dataReady == false) { + if (clearedIterator->get()->dataStatus == FilePromise::DataStatus::Wait) { emptyPromises.push_back(*clearedIterator); std::iter_swap(clearedIterator, clearedSentinel); ++clearedSentinel; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index c75e6c75..dfebd5b1 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -55,7 +55,7 @@ struct FilePromise { AudioSpan getData() { - if (dataReady) + if (dataStatus == DataStatus::Ready) return AudioSpan(fileData); else if (availableFrames > preloadedData->getNumFrames()) return AudioSpan(fileData).first(availableFrames); @@ -69,18 +69,30 @@ struct FilePromise preloadedData.reset(); filename = ""; availableFrames = 0; - dataReady = false; + dataStatus = DataStatus::Wait; oversamplingFactor = config::defaultOversamplingFactor; sampleRate = config::defaultSampleRate; } + void waitCompletion() + { + while (dataStatus == DataStatus::Wait) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + enum class DataStatus { + Wait = 0, + Ready, + Error, + }; + absl::string_view filename {}; AudioBufferPtr preloadedData {}; AudioBuffer fileData {}; float sampleRate { config::defaultSampleRate }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; std::atomic availableFrames { 0 }; - std::atomic dataReady { false }; + std::atomic dataStatus { DataStatus::Wait }; std::chrono::time_point creationTime; LEAK_DETECTOR(FilePromise); diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 8d8f14ee..4f05f314 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -98,6 +98,9 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) // Wavetable oscillator case hash("oscillator_phase"): setValueFromOpcode(opcode, oscillatorPhase, Default::oscillatorPhaseRange); + case hash("oscillator"): + if (auto value = readBooleanFromOpcode(opcode)) + oscillator = *value; break; // Instrument settings: voice lifecycle diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 3ef284d0..ebacc207 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -228,6 +228,7 @@ struct Region { // Wavetable oscillator float oscillatorPhase { Default::oscillatorPhase }; + bool oscillator = false; // Instrument settings: voice lifecycle uint32_t group { Default::group }; // group diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 930e1c9e..e370fc79 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -306,6 +306,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) return false; resources.filePool.setRootDirectory(parser.originalDirectory()); + resources.wavePool.clearFileWaves(); auto currentRegion = regions.begin(); auto lastRegion = regions.rbegin(); @@ -324,7 +325,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) while (currentRegion < lastRegion.base()) { auto region = currentRegion->get(); - if (!region->isGenerator()) { + if (!region->oscillator && !region->isGenerator()) { if (!resources.filePool.checkSample(region->sample)) { removeCurrentRegion(); continue; @@ -360,6 +361,9 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) if (!resources.filePool.preloadFile(region->sample, maxOffset)) removeCurrentRegion(); } + else if (region->oscillator && !region->isGenerator()) { + resources.wavePool.createFileWave(resources.filePool, region->sample); + } for (auto note = 0; note < 128; note++) { if (region->keyRange.containsWithEnd(note) || diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 743c00f7..2b150b42 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -58,7 +58,19 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value break; } waveOscillator.setWavetable(wave); + } else if (region->oscillator) { + const WavetableMulti* wave = resources.wavePool.getFileWave(region->sample); + waveOscillator.setWavetable(wave); + } else { + currentPromise = resources.filePool.getFilePromise(region->sample); + if (currentPromise == nullptr) { + reset(); + return; + } + speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); + } + if (region->oscillator || region->isGenerator()) { float phase; const float phaseParam = region->oscillatorPhase; if (phaseParam >= 0) { @@ -69,14 +81,8 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value phase = phaseDist(Random::randomGenerator); } waveOscillator.setPhase(phase); - } else { - currentPromise = resources.filePool.getFilePromise(region->sample); - if (currentPromise == nullptr) { - reset(); - return; - } - speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); } + pitchRatio = region->getBasePitchVariation(number, value); baseVolumedB = region->getBaseVolumedB(number); @@ -294,7 +300,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept { // Fill buffer with raw data ScopedTiming logger { dataDuration }; - if (region->isGenerator()) + if (region->isGenerator() || region->oscillator) fillWithGenerator(delayed_buffer); else fillWithData(delayed_buffer); diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index 355b9fca..9576af8a 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -5,9 +5,9 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "Wavetables.h" +#include "FilePool.h" #include "MathHelpers.h" #include -#include namespace sfz { @@ -279,6 +279,31 @@ void WavetableMulti::fillExtra() } } +//------------------------------------------------------------------------------ + +/** + * @brief Harmonic profile which takes its values from a table. + */ +class TabulatedHarmonicProfile : public HarmonicProfile { +public: + explicit TabulatedHarmonicProfile(absl::Span> harmonics) + : _harmonics(harmonics) + { + } + + std::complex getHarmonic(size_t index) const override + { + if (index >= _harmonics.size()) + return {}; + + return _harmonics[index]; + } + +private: + absl::Span> _harmonics; +}; + +//------------------------------------------------------------------------------ WavetablePool::WavetablePool() { @@ -313,4 +338,65 @@ const WavetableMulti* WavetablePool::getWaveSquare() return &wave; } +const WavetableMulti* WavetablePool::getFileWave(const std::string& filename) +{ + auto it = _fileWaves.find(filename); + if (it == _fileWaves.end()) + return nullptr; + + return it->second.get(); +} + +void WavetablePool::clearFileWaves() +{ + _fileWaves.clear(); +} + +const WavetableMulti* WavetablePool::createFileWave(FilePool& filePool, const std::string& filename) +{ + if (const WavetableMulti* wave = getFileWave(filename)) + return wave; + + if (!filePool.preloadFile(filename, 0)) + return nullptr; + + FilePromisePtr fp = filePool.getFilePromise(filename); + if (!fp) + return nullptr; + + fp->waitCompletion(); + if (fp->dataStatus == FilePromise::DataStatus::Error) + return nullptr; + + // use 1 channel only, maybe warn if file has more channels + auto audioData = fp->fileData.getSpan(0); + size_t fftSize = audioData.size(); + size_t specSize = fftSize / 2 + 1; + + typedef std::complex cpx; + std::unique_ptr spec { new cpx[specSize] }; + + kiss_fftr_cfg cfg = kiss_fftr_alloc(fftSize, false, nullptr, nullptr); + if (!cfg) + throw std::bad_alloc(); + + kiss_fftr(cfg, audioData.data(), reinterpret_cast(spec.get())); + kiss_fftr_free(cfg); + + // scale transform, and normalize amplitude and phase + const std::complex k = std::polar(2.0 / fftSize, -M_PI / 2); + for (size_t i = 0; i < specSize; ++i) + spec[i] *= k; + + TabulatedHarmonicProfile hp { + absl::Span> { spec.get(), specSize } + }; + + auto wave = std::make_shared( + WavetableMulti::createForHarmonicProfile(hp, 1.0)); + + _fileWaves[filename] = wave; + return wave.get(); +} + } // namespace sfz diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index bdc715bc..30640e16 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -9,10 +9,12 @@ #include "LeakDetector.h" #include "Buffer.h" #include +#include #include #include namespace sfz { +class FilePool; class WavetableMulti; @@ -185,15 +187,23 @@ private: }; /** - * @brief Holds predefined wavetables. + * @brief Holds predefined and loaded wavetables. * */ struct WavetablePool { WavetablePool(); + + const WavetableMulti* getFileWave(const std::string& filename); + const WavetableMulti* createFileWave(FilePool& filePool, const std::string& filename); + void clearFileWaves(); + static const WavetableMulti* getWaveSin(); static const WavetableMulti* getWaveTriangle(); static const WavetableMulti* getWaveSaw(); static const WavetableMulti* getWaveSquare(); + +private: + absl::flat_hash_map> _fileWaves; }; } // namespace sfz