Auto-enable oscillator=on when a wavetable is provided
This commit is contained in:
parent
6494cdac10
commit
85883481c9
10 changed files with 154 additions and 69 deletions
|
|
@ -55,7 +55,7 @@ void ADSREnvelope<Type>::reset(const EGDescription& desc, const Region& region,
|
||||||
shouldRelease = false;
|
shouldRelease = false;
|
||||||
freeRunning = (
|
freeRunning = (
|
||||||
(this->sustain == 0.0f)
|
(this->sustain == 0.0f)
|
||||||
|| (region.loopMode == SfzLoopMode::one_shot && (region.isGenerator() || region.oscillator))
|
|| (region.loopMode == SfzLoopMode::one_shot && region.isOscillator())
|
||||||
);
|
);
|
||||||
currentValue = this->start;
|
currentValue = this->start;
|
||||||
currentState = State::Delay;
|
currentState = State::Delay;
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,11 @@ namespace config {
|
||||||
static constexpr double amplitudeTriangle = 1.0;
|
static constexpr double amplitudeTriangle = 1.0;
|
||||||
static constexpr double amplitudeSaw = 0.8164965809277261; // sqrt(2)/sqrt(3)
|
static constexpr double amplitudeSaw = 0.8164965809277261; // sqrt(2)/sqrt(3)
|
||||||
static constexpr double amplitudeSquare = 0.8164965809277261; // should have been sqrt(2)?
|
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
|
Background file loading
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
#include "FilePool.h"
|
#include "FilePool.h"
|
||||||
#include "AudioReader.h"
|
#include "AudioReader.h"
|
||||||
#include "FileMetadata.h"
|
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "AudioBuffer.h"
|
#include "AudioBuffer.h"
|
||||||
#include "AudioSpan.h"
|
#include "AudioSpan.h"
|
||||||
|
|
@ -218,13 +217,21 @@ absl::optional<sfz::FileInformation> sfz::FilePool::getFileInformation(const Fil
|
||||||
|
|
||||||
SF_INSTRUMENT instrumentInfo {};
|
SF_INSTRUMENT instrumentInfo {};
|
||||||
|
|
||||||
|
FileMetadataReader mdReader;
|
||||||
|
bool mdReaderOpened = mdReader.open(file);
|
||||||
|
|
||||||
if (!reader->getInstrument(&instrumentInfo)) {
|
if (!reader->getInstrument(&instrumentInfo)) {
|
||||||
// if no instrument, then try extracting from embedded RIFF chunks (flac)
|
// if no instrument, then try extracting from embedded RIFF chunks (flac)
|
||||||
FileMetadataReader mdReader;
|
if (mdReaderOpened)
|
||||||
if (mdReader.open(file))
|
|
||||||
mdReader.extractRiffInstrument(instrumentInfo);
|
mdReader.extractRiffInstrument(instrumentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mdReaderOpened) {
|
||||||
|
WavetableInfo wt;
|
||||||
|
if (mdReader.extractWavetableInfo(wt))
|
||||||
|
returnedValue.wavetable = wt;
|
||||||
|
}
|
||||||
|
|
||||||
if (!fileId.isReverse()) {
|
if (!fileId.isReverse()) {
|
||||||
if (instrumentInfo.loop_count > 0) {
|
if (instrumentInfo.loop_count > 0) {
|
||||||
returnedValue.hasLoop = true;
|
returnedValue.hasLoop = true;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
#include "AudioBuffer.h"
|
#include "AudioBuffer.h"
|
||||||
#include "AudioSpan.h"
|
#include "AudioSpan.h"
|
||||||
#include "FileId.h"
|
#include "FileId.h"
|
||||||
|
#include "FileMetadata.h"
|
||||||
#include "SIMDHelpers.h"
|
#include "SIMDHelpers.h"
|
||||||
#include "utility/SpinMutex.h"
|
#include "utility/SpinMutex.h"
|
||||||
#include "ghc/fs_std.hpp"
|
#include "ghc/fs_std.hpp"
|
||||||
|
|
@ -55,6 +56,7 @@ struct FileInformation {
|
||||||
bool hasLoop { false };
|
bool hasLoop { false };
|
||||||
double sampleRate { config::defaultSampleRate };
|
double sampleRate { config::defaultSampleRate };
|
||||||
int numChannels { 0 };
|
int numChannels { 0 };
|
||||||
|
absl::optional<WavetableInfo> wavetable;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strict C++11 disallows member initialization if aggregate initialization is to be used...
|
// Strict C++11 disallows member initialization if aggregate initialization is to be used...
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
break;
|
break;
|
||||||
case hash("oscillator"):
|
case hash("oscillator"):
|
||||||
if (auto value = readBooleanFromOpcode(opcode))
|
if (auto value = readBooleanFromOpcode(opcode))
|
||||||
oscillator = *value;
|
oscillatorEnabled = *value ? OscillatorEnabled::On : OscillatorEnabled::Off;
|
||||||
break;
|
break;
|
||||||
case hash("oscillator_multi"):
|
case hash("oscillator_multi"):
|
||||||
setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange);
|
setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange);
|
||||||
|
|
|
||||||
|
|
@ -76,13 +76,28 @@ struct Region {
|
||||||
* @return false
|
* @return false
|
||||||
*/
|
*/
|
||||||
bool isGenerator() const noexcept { return sampleId.filename().size() > 0 ? sampleId.filename()[0] == '*' : 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)?
|
* @brief Is stereo (has stereo sample or is unison oscillator)?
|
||||||
*
|
*
|
||||||
* @return true
|
* @return true
|
||||||
* @return false
|
* @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)?
|
* @brief Is a looping region (at least potentially)?
|
||||||
*
|
*
|
||||||
|
|
@ -284,7 +299,9 @@ struct Region {
|
||||||
|
|
||||||
// Wavetable oscillator
|
// Wavetable oscillator
|
||||||
float oscillatorPhase { Default::oscillatorPhase };
|
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;
|
int oscillatorMulti = Default::oscillatorMulti;
|
||||||
float oscillatorDetune = Default::oscillatorDetune;
|
float oscillatorDetune = Default::oscillatorDetune;
|
||||||
absl::optional<int> oscillatorQuality;
|
absl::optional<int> oscillatorQuality;
|
||||||
|
|
|
||||||
|
|
@ -495,18 +495,25 @@ void sfz::Synth::finalizeSfzLoad()
|
||||||
while (currentRegionIndex < currentRegionCount) {
|
while (currentRegionIndex < currentRegionCount) {
|
||||||
auto region = regions[currentRegionIndex].get();
|
auto region = regions[currentRegionIndex].get();
|
||||||
|
|
||||||
if (!region->oscillator && !region->isGenerator()) {
|
absl::optional<FileInformation> fileInformation;
|
||||||
|
|
||||||
|
if (!region->isGenerator()) {
|
||||||
if (!resources.filePool.checkSampleId(region->sampleId)) {
|
if (!resources.filePool.checkSampleId(region->sampleId)) {
|
||||||
removeCurrentRegion();
|
removeCurrentRegion();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fileInformation = resources.filePool.getFileInformation(region->sampleId);
|
fileInformation = resources.filePool.getFileInformation(region->sampleId);
|
||||||
if (!fileInformation) {
|
if (!fileInformation) {
|
||||||
removeCurrentRegion();
|
removeCurrentRegion();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
region->hasWavetableSample = fileInformation->wavetable ||
|
||||||
|
fileInformation->end < config::wavetableMaxFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!region->isOscillator()) {
|
||||||
region->sampleEnd = std::min(region->sampleEnd, fileInformation->end);
|
region->sampleEnd = std::min(region->sampleEnd, fileInformation->end);
|
||||||
|
|
||||||
if (fileInformation->hasLoop) {
|
if (fileInformation->hasLoop) {
|
||||||
|
|
@ -539,12 +546,8 @@ void sfz::Synth::finalizeSfzLoad()
|
||||||
|
|
||||||
if (!resources.filePool.preloadFile(region->sampleId, maxOffset))
|
if (!resources.filePool.preloadFile(region->sampleId, maxOffset))
|
||||||
removeCurrentRegion();
|
removeCurrentRegion();
|
||||||
} else if (region->oscillator && !region->isGenerator()) {
|
}
|
||||||
if (!resources.filePool.checkSampleId(region->sampleId)) {
|
else if (!region->isGenerator()) {
|
||||||
removeCurrentRegion();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resources.wavePool.createFileWave(resources.filePool, std::string(region->sampleId.filename()))) {
|
if (!resources.wavePool.createFileWave(resources.filePool, std::string(region->sampleId.filename()))) {
|
||||||
removeCurrentRegion();
|
removeCurrentRegion();
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -58,25 +58,29 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event
|
||||||
if (delay < 0)
|
if (delay < 0)
|
||||||
delay = 0;
|
delay = 0;
|
||||||
|
|
||||||
if (region->isGenerator()) {
|
if (region->isOscillator()) {
|
||||||
const WavetableMulti* wave = nullptr;
|
const WavetableMulti* wave = nullptr;
|
||||||
switch (hash(region->sampleId.filename())) {
|
if (!region->isGenerator())
|
||||||
default:
|
wave = resources.wavePool.getFileWave(region->sampleId.filename());
|
||||||
case hash("*silence"):
|
else {
|
||||||
break;
|
switch (hash(region->sampleId.filename())) {
|
||||||
case hash("*sine"):
|
default:
|
||||||
wave = resources.wavePool.getWaveSin();
|
case hash("*silence"):
|
||||||
break;
|
break;
|
||||||
case hash("*triangle"): // fallthrough
|
case hash("*sine"):
|
||||||
case hash("*tri"):
|
wave = resources.wavePool.getWaveSin();
|
||||||
wave = resources.wavePool.getWaveTriangle();
|
break;
|
||||||
break;
|
case hash("*triangle"): // fallthrough
|
||||||
case hash("*square"):
|
case hash("*tri"):
|
||||||
wave = resources.wavePool.getWaveSquare();
|
wave = resources.wavePool.getWaveTriangle();
|
||||||
break;
|
break;
|
||||||
case hash("*saw"):
|
case hash("*square"):
|
||||||
wave = resources.wavePool.getWaveSaw();
|
wave = resources.wavePool.getWaveSquare();
|
||||||
break;
|
break;
|
||||||
|
case hash("*saw"):
|
||||||
|
wave = resources.wavePool.getWaveSaw();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const float phase = region->getPhase();
|
const float phase = region->getPhase();
|
||||||
const int quality = region->oscillatorQuality.value_or(Default::oscillatorQuality);
|
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);
|
osc.setQuality(quality);
|
||||||
}
|
}
|
||||||
setupOscillatorUnison();
|
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 {
|
} else {
|
||||||
currentPromise = resources.filePool.getFilePromise(region->sampleId);
|
currentPromise = resources.filePool.getFilePromise(region->sampleId);
|
||||||
if (currentPromise == nullptr) {
|
if (currentPromise == nullptr) {
|
||||||
|
|
@ -276,7 +270,7 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
|
|
||||||
{ // Fill buffer with raw data
|
{ // Fill buffer with raw data
|
||||||
ScopedTiming logger { dataDuration };
|
ScopedTiming logger { dataDuration };
|
||||||
if (region->isGenerator() || region->oscillator)
|
if (region->isOscillator())
|
||||||
fillWithGenerator(delayed_buffer);
|
fillWithGenerator(delayed_buffer);
|
||||||
else
|
else
|
||||||
fillWithData(delayed_buffer);
|
fillWithData(delayed_buffer);
|
||||||
|
|
|
||||||
103
tests/FilesT.cpp
103
tests/FilesT.cpp
|
|
@ -270,37 +270,90 @@ TEST_CASE("[Files] Channels (channels_multi.sfz)")
|
||||||
{
|
{
|
||||||
Synth synth;
|
Synth synth;
|
||||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels_multi.sfz");
|
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");
|
int regionNumber = 0;
|
||||||
REQUIRE(!synth.getRegionView(0)->isStereo());
|
const Region* region = nullptr;
|
||||||
REQUIRE(synth.getRegionView(0)->isGenerator());
|
|
||||||
REQUIRE(!synth.getRegionView(0)->oscillator);
|
|
||||||
|
|
||||||
REQUIRE(synth.getRegionView(1)->sampleId.filename() == "*sine");
|
// generator only
|
||||||
REQUIRE(synth.getRegionView(1)->isStereo());
|
region = synth.getRegionView(regionNumber++);
|
||||||
REQUIRE(synth.getRegionView(1)->isGenerator());
|
REQUIRE(region->sampleId.filename() == "*sine");
|
||||||
REQUIRE(!synth.getRegionView(1)->oscillator);
|
REQUIRE(!region->isStereo());
|
||||||
|
REQUIRE(region->isGenerator());
|
||||||
|
REQUIRE(region->isOscillator());
|
||||||
|
REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto);
|
||||||
|
|
||||||
REQUIRE(synth.getRegionView(2)->sampleId.filename() == "ramp_wave.wav");
|
// generator with multi
|
||||||
REQUIRE(!synth.getRegionView(2)->isStereo());
|
region = synth.getRegionView(regionNumber++);
|
||||||
REQUIRE(!synth.getRegionView(2)->isGenerator());
|
REQUIRE(region->sampleId.filename() == "*sine");
|
||||||
REQUIRE(synth.getRegionView(2)->oscillator);
|
REQUIRE(region->isStereo());
|
||||||
|
REQUIRE(region->isGenerator());
|
||||||
|
REQUIRE(region->isOscillator());
|
||||||
|
REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::Auto);
|
||||||
|
|
||||||
REQUIRE(synth.getRegionView(3)->sampleId.filename() == "ramp_wave.wav");
|
// explicit wavetable
|
||||||
REQUIRE(synth.getRegionView(3)->isStereo());
|
region = synth.getRegionView(regionNumber++);
|
||||||
REQUIRE(!synth.getRegionView(3)->isGenerator());
|
REQUIRE(region->sampleId.filename() == "ramp_wave.wav");
|
||||||
REQUIRE(synth.getRegionView(3)->oscillator);
|
REQUIRE(!region->isStereo());
|
||||||
|
REQUIRE(!region->isGenerator());
|
||||||
|
REQUIRE(region->isOscillator());
|
||||||
|
REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::On);
|
||||||
|
|
||||||
REQUIRE(synth.getRegionView(4)->sampleId.filename() == "*sine");
|
// explicit wavetable with multi
|
||||||
REQUIRE(!synth.getRegionView(4)->isStereo());
|
region = synth.getRegionView(regionNumber++);
|
||||||
REQUIRE(synth.getRegionView(4)->isGenerator());
|
REQUIRE(region->sampleId.filename() == "ramp_wave.wav");
|
||||||
REQUIRE(!synth.getRegionView(4)->oscillator);
|
REQUIRE(region->isStereo());
|
||||||
|
REQUIRE(!region->isGenerator());
|
||||||
|
REQUIRE(region->isOscillator());
|
||||||
|
REQUIRE(region->oscillatorEnabled == Region::OscillatorEnabled::On);
|
||||||
|
|
||||||
REQUIRE(synth.getRegionView(5)->sampleId.filename() == "*sine");
|
// explicit disabled wavetable
|
||||||
REQUIRE(!synth.getRegionView(5)->isStereo());
|
region = synth.getRegionView(regionNumber++);
|
||||||
REQUIRE(synth.getRegionView(5)->isGenerator());
|
REQUIRE(region->sampleId.filename() == "ramp_wave.wav");
|
||||||
REQUIRE(!synth.getRegionView(5)->oscillator);
|
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")
|
TEST_CASE("[Files] sw_default")
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,9 @@
|
||||||
<region> sample=*sine oscillator_multi=3
|
<region> sample=*sine oscillator_multi=3
|
||||||
<region> sample=ramp_wave.wav oscillator=on
|
<region> sample=ramp_wave.wav oscillator=on
|
||||||
<region> sample=ramp_wave.wav oscillator=on oscillator_multi=3
|
<region> sample=ramp_wave.wav oscillator=on oscillator_multi=3
|
||||||
|
<region> sample=ramp_wave.wav oscillator=off
|
||||||
|
<region> sample=ramp_wave.wav oscillator=off oscillator_multi=3
|
||||||
|
<region> sample=ramp_wave.wav
|
||||||
|
<region> sample=snare.wav
|
||||||
<region> sample=*sine oscillator_multi=1
|
<region> sample=*sine oscillator_multi=1
|
||||||
<region> sample=*sine oscillator_multi=2
|
<region> sample=*sine oscillator_multi=2
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue