From 85883481c996d3886115bc6a429ca7f088471606 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 22 Sep 2020 15:53:33 +0200 Subject: [PATCH] Auto-enable oscillator=on when a wavetable is provided --- src/sfizz/ADSREnvelope.cpp | 2 +- src/sfizz/Config.h | 5 ++ src/sfizz/FilePool.cpp | 13 +++- src/sfizz/FilePool.h | 2 + src/sfizz/Region.cpp | 2 +- src/sfizz/Region.h | 21 +++++- src/sfizz/Synth.cpp | 19 +++--- src/sfizz/Voice.cpp | 52 +++++++-------- tests/FilesT.cpp | 103 ++++++++++++++++++++++------- tests/TestFiles/channels_multi.sfz | 4 ++ 10 files changed, 154 insertions(+), 69 deletions(-) diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 419a8f55..803adbb6 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -55,7 +55,7 @@ void ADSREnvelope::reset(const EGDescription& desc, const Region& region, shouldRelease = false; freeRunning = ( (this->sustain == 0.0f) - || (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator)) + || (region.loopMode == SfzLoopMode::one_shot && region.isOscillator()) ); currentValue = this->start; currentState = State::Delay; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 54ba4871..442a165c 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -113,6 +113,11 @@ namespace config { static constexpr double amplitudeTriangle = 1.0; static constexpr double amplitudeSaw = 0.8164965809277261; // sqrt(2)/sqrt(3) static constexpr double amplitudeSquare = 0.8164965809277261; // should have been sqrt(2)? + /** + Frame count high limit, for automatically loading a sound file as wavetable. + Set to 3000 according to Cakewalk. + */ + static constexpr unsigned wavetableMaxFrames = 3000; /** Background file loading */ diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 6e27153b..8b4ca8a3 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -25,7 +25,6 @@ #include "FilePool.h" #include "AudioReader.h" -#include "FileMetadata.h" #include "Buffer.h" #include "AudioBuffer.h" #include "AudioSpan.h" @@ -218,13 +217,21 @@ absl::optional sfz::FilePool::getFileInformation(const Fil SF_INSTRUMENT instrumentInfo {}; + FileMetadataReader mdReader; + bool mdReaderOpened = mdReader.open(file); + if (!reader->getInstrument(&instrumentInfo)) { // if no instrument, then try extracting from embedded RIFF chunks (flac) - FileMetadataReader mdReader; - if (mdReader.open(file)) + if (mdReaderOpened) mdReader.extractRiffInstrument(instrumentInfo); } + if (mdReaderOpened) { + WavetableInfo wt; + if (mdReader.extractWavetableInfo(wt)) + returnedValue.wavetable = wt; + } + if (!fileId.isReverse()) { if (instrumentInfo.loop_count > 0) { returnedValue.hasLoop = true; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 9d099550..dcef362c 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -31,6 +31,7 @@ #include "AudioBuffer.h" #include "AudioSpan.h" #include "FileId.h" +#include "FileMetadata.h" #include "SIMDHelpers.h" #include "utility/SpinMutex.h" #include "ghc/fs_std.hpp" @@ -55,6 +56,7 @@ struct FileInformation { bool hasLoop { false }; double sampleRate { config::defaultSampleRate }; int numChannels { 0 }; + absl::optional wavetable; }; // Strict C++11 disallows member initialization if aggregate initialization is to be used... diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index f2097395..7ec6619d 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -145,7 +145,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) break; case hash("oscillator"): if (auto value = readBooleanFromOpcode(opcode)) - oscillator = *value; + oscillatorEnabled = *value ? OscillatorEnabled::On : OscillatorEnabled::Off; break; case hash("oscillator_multi"): setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 4c9dabba..1d402bbe 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -76,13 +76,28 @@ struct Region { * @return false */ bool isGenerator() const noexcept { return sampleId.filename().size() > 0 ? sampleId.filename()[0] == '*' : false; } + /** + * @brief Is an oscillator (generator or wavetable)? + * + * @return true + * @return false + */ + bool isOscillator() const noexcept + { + if (isGenerator()) + return true; + else if (oscillatorEnabled != OscillatorEnabled::Auto) + return oscillatorEnabled == OscillatorEnabled::On; + else + return hasWavetableSample; + } /** * @brief Is stereo (has stereo sample or is unison oscillator)? * * @return true * @return false */ - bool isStereo() const noexcept { return hasStereoSample || ((oscillator || isGenerator()) && oscillatorMulti >= 3); } + bool isStereo() const noexcept { return hasStereoSample || (isOscillator() && oscillatorMulti >= 3); } /** * @brief Is a looping region (at least potentially)? * @@ -284,7 +299,9 @@ struct Region { // Wavetable oscillator float oscillatorPhase { Default::oscillatorPhase }; - bool oscillator = false; + enum class OscillatorEnabled { Auto = -1, Off = 0, On = 1 }; + OscillatorEnabled oscillatorEnabled = OscillatorEnabled::Auto; // oscillator + bool hasWavetableSample = false; // (set according to sample file) int oscillatorMulti = Default::oscillatorMulti; float oscillatorDetune = Default::oscillatorDetune; absl::optional oscillatorQuality; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 02335014..e63803c1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -495,18 +495,25 @@ void sfz::Synth::finalizeSfzLoad() while (currentRegionIndex < currentRegionCount) { auto region = regions[currentRegionIndex].get(); - if (!region->oscillator && !region->isGenerator()) { + absl::optional fileInformation; + + if (!region->isGenerator()) { if (!resources.filePool.checkSampleId(region->sampleId)) { removeCurrentRegion(); continue; } - const auto fileInformation = resources.filePool.getFileInformation(region->sampleId); + fileInformation = resources.filePool.getFileInformation(region->sampleId); if (!fileInformation) { removeCurrentRegion(); continue; } + region->hasWavetableSample = fileInformation->wavetable || + fileInformation->end < config::wavetableMaxFrames; + } + + if (!region->isOscillator()) { region->sampleEnd = std::min(region->sampleEnd, fileInformation->end); if (fileInformation->hasLoop) { @@ -539,12 +546,8 @@ void sfz::Synth::finalizeSfzLoad() if (!resources.filePool.preloadFile(region->sampleId, maxOffset)) removeCurrentRegion(); - } else if (region->oscillator && !region->isGenerator()) { - if (!resources.filePool.checkSampleId(region->sampleId)) { - removeCurrentRegion(); - continue; - } - + } + else if (!region->isGenerator()) { if (!resources.wavePool.createFileWave(resources.filePool, std::string(region->sampleId.filename()))) { removeCurrentRegion(); continue; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d080db37..499e21b2 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -58,25 +58,29 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event if (delay < 0) delay = 0; - if (region->isGenerator()) { + if (region->isOscillator()) { const WavetableMulti* wave = nullptr; - switch (hash(region->sampleId.filename())) { - default: - case hash("*silence"): - break; - case hash("*sine"): - wave = resources.wavePool.getWaveSin(); - break; - case hash("*triangle"): // fallthrough - case hash("*tri"): - wave = resources.wavePool.getWaveTriangle(); - break; - case hash("*square"): - wave = resources.wavePool.getWaveSquare(); - break; - case hash("*saw"): - wave = resources.wavePool.getWaveSaw(); - break; + if (!region->isGenerator()) + wave = resources.wavePool.getFileWave(region->sampleId.filename()); + else { + switch (hash(region->sampleId.filename())) { + default: + case hash("*silence"): + break; + case hash("*sine"): + wave = resources.wavePool.getWaveSin(); + break; + case hash("*triangle"): // fallthrough + case hash("*tri"): + wave = resources.wavePool.getWaveTriangle(); + break; + case hash("*square"): + wave = resources.wavePool.getWaveSquare(); + break; + case hash("*saw"): + wave = resources.wavePool.getWaveSaw(); + break; + } } const float phase = region->getPhase(); const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality); @@ -86,16 +90,6 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event osc.setQuality(quality); } setupOscillatorUnison(); - } else if (region->oscillator) { - const WavetableMulti* wave = resources.wavePool.getFileWave(region->sampleId.filename()); - const float phase = region->getPhase(); - const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality); - for (WavetableOscillator& osc : waveOscillators) { - osc.setWavetable(wave); - osc.setPhase(phase); - osc.setQuality(quality); - } - setupOscillatorUnison(); } else { currentPromise = resources.filePool.getFilePromise(region->sampleId); if (currentPromise == nullptr) { @@ -276,7 +270,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept { // Fill buffer with raw data ScopedTiming logger { dataDuration }; - if (region->isGenerator() || region->oscillator) + if (region->isOscillator()) fillWithGenerator(delayed_buffer); else fillWithData(delayed_buffer); diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index e5a7c77f..d0d0b064 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -270,37 +270,90 @@ TEST_CASE("[Files] Channels (channels_multi.sfz)") { Synth synth; synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels_multi.sfz"); - REQUIRE(synth.getNumRegions() == 6); + REQUIRE(synth.getNumRegions() == 10); - REQUIRE(synth.getRegionView(0)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(0)->isStereo()); - REQUIRE(synth.getRegionView(0)->isGenerator()); - REQUIRE(!synth.getRegionView(0)->oscillator); + int regionNumber = 0; + const Region* region = nullptr; - REQUIRE(synth.getRegionView(1)->sampleId.filename() == "*sine"); - REQUIRE(synth.getRegionView(1)->isStereo()); - REQUIRE(synth.getRegionView(1)->isGenerator()); - REQUIRE(!synth.getRegionView(1)->oscillator); + // generator only + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(!region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); - REQUIRE(synth.getRegionView(2)->sampleId.filename() == "ramp_wave.wav"); - REQUIRE(!synth.getRegionView(2)->isStereo()); - REQUIRE(!synth.getRegionView(2)->isGenerator()); - REQUIRE(synth.getRegionView(2)->oscillator); + // generator with multi + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); - REQUIRE(synth.getRegionView(3)->sampleId.filename() == "ramp_wave.wav"); - REQUIRE(synth.getRegionView(3)->isStereo()); - REQUIRE(!synth.getRegionView(3)->isGenerator()); - REQUIRE(synth.getRegionView(3)->oscillator); + // explicit wavetable + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::On); - REQUIRE(synth.getRegionView(4)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(4)->isStereo()); - REQUIRE(synth.getRegionView(4)->isGenerator()); - REQUIRE(!synth.getRegionView(4)->oscillator); + // explicit wavetable with multi + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::On); - REQUIRE(synth.getRegionView(5)->sampleId.filename() == "*sine"); - REQUIRE(!synth.getRegionView(5)->isStereo()); - REQUIRE(synth.getRegionView(5)->isGenerator()); - REQUIRE(!synth.getRegionView(5)->oscillator); + // explicit disabled wavetable + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(!region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Off); + + // explicit disabled wavetable with multi + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(!region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Off); + + // implicit wavetable (sound file < 3000 frames) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "ramp_wave.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); + + // implicit non-wavetable (sound file >= 3000 frames) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "snare.wav"); + REQUIRE(!region->isStereo()); + REQUIRE(!region->isGenerator()); + REQUIRE(!region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); + + // generator with multi=1 (single) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(!region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); + + // generator with multi=2 (ring modulation) + region = synth.getRegionView(regionNumber++); + REQUIRE(region->sampleId.filename() == "*sine"); + REQUIRE(!region->isStereo()); + REQUIRE(region->isGenerator()); + REQUIRE(region->isOscillator()); + REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto); } TEST_CASE("[Files] sw_default") diff --git a/tests/TestFiles/channels_multi.sfz b/tests/TestFiles/channels_multi.sfz index 8146ba45..ee6d6acf 100644 --- a/tests/TestFiles/channels_multi.sfz +++ b/tests/TestFiles/channels_multi.sfz @@ -2,5 +2,9 @@ sample=*sine oscillator_multi=3 sample=ramp_wave.wav oscillator=on sample=ramp_wave.wav oscillator=on oscillator_multi=3 + sample=ramp_wave.wav oscillator=off + sample=ramp_wave.wav oscillator=off oscillator_multi=3 + sample=ramp_wave.wav + sample=snare.wav sample=*sine oscillator_multi=1 sample=*sine oscillator_multi=2