Skip to content

Commit

Permalink
Merge pull request #161 from jpcima/oscillator-multi
Browse files Browse the repository at this point in the history
Add oscillator unison
  • Loading branch information
jpcima authored Apr 7, 2020
2 parents 4e7f153 + c4c4278 commit 332c752
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 28 deletions.
1 change: 1 addition & 0 deletions src/sfizz/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ namespace config {
constexpr int filtersInPool { maxVoices * 2 };
constexpr int filtersPerVoice { 2 };
constexpr int eqsPerVoice { 3 };
constexpr int oscillatorsPerVoice { 9 };
constexpr float noiseVariance { 0.25f };
/**
Minimum interval in frames between recomputations of coefficients of the
Expand Down
4 changes: 4 additions & 0 deletions src/sfizz/Defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ namespace Default
// Wavetable oscillator
constexpr float oscillatorPhase { 0.0 };
constexpr Range<float> oscillatorPhaseRange { -1.0, 360.0 };
constexpr int oscillatorMulti { 1 };
constexpr Range<int> oscillatorMultiRange { 1, config::oscillatorsPerVoice };
constexpr float oscillatorDetune { 0 };
constexpr Range<float> oscillatorDetuneRange { -9600, 9600 };

// Instrument setting: voice lifecycle
constexpr uint32_t group { 0 };
Expand Down
4 changes: 2 additions & 2 deletions src/sfizz/Range.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class Range {
{

}
Type getStart() const noexcept { return _start; }
Type getEnd() const noexcept { return _end; }
constexpr Type getStart() const noexcept { return _start; }
constexpr Type getEnd() const noexcept { return _end; }
/**
* @brief Get the range as an std::pair of the endpoints
*
Expand Down
6 changes: 6 additions & 0 deletions src/sfizz/Region.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
if (auto value = readBooleanFromOpcode(opcode))
oscillator = *value;
break;
case hash("oscillator_multi"):
setValueFromOpcode(opcode, oscillatorMulti, Default::oscillatorMultiRange);
break;
case hash("oscillator_detune"):
setValueFromOpcode(opcode, oscillatorDetune, Default::oscillatorDetuneRange);
break;

// Instrument settings: voice lifecycle
case hash("group"): // fallthrough
Expand Down
11 changes: 10 additions & 1 deletion src/sfizz/Region.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ struct Region {
* @return false
*/
bool isGenerator() const noexcept { return sample.size() > 0 ? sample[0] == '*' : false; }
/**
* @brief Is stereo (has stereo sample or is unison oscillator)?
*
* @return true
* @return false
*/
bool isStereo() const noexcept { return hasStereoSample || ((oscillator || isGenerator()) && oscillatorMulti >= 3); }
/**
* @brief Is a looping region (at least potentially)?
*
Expand Down Expand Up @@ -235,6 +242,8 @@ struct Region {
// Wavetable oscillator
float oscillatorPhase { Default::oscillatorPhase };
bool oscillator = false;
int oscillatorMulti = Default::oscillatorMulti;
float oscillatorDetune = Default::oscillatorDetune;

// Instrument settings: voice lifecycle
uint32_t group { Default::group }; // group
Expand Down Expand Up @@ -318,7 +327,7 @@ struct Region {
EGDescription pitchEG;
EGDescription filterEG;

bool isStereo { false };
bool hasStereoSample { false };

// Effects
std::vector<float> gainToEffect;
Expand Down
4 changes: 2 additions & 2 deletions src/sfizz/Synth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file)
}

if (fileInformation->numChannels == 2)
region->isStereo = true;
region->hasStereoSample = true;

// TODO: adjust with LFO targets
const auto maxOffset = region->offset + region->offsetRandom;
Expand Down Expand Up @@ -785,7 +785,7 @@ void sfz::Synth::pitchWheel(int delay, int pitch) noexcept
{
ASSERT(pitch <= 8192);
ASSERT(pitch >= -8192);
const auto normalizedPitch = normalizeBend(pitch);
const auto normalizedPitch = normalizeBend(float(pitch));

ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
resources.midiState.pitchBendEvent(delay, normalizedPitch);
Expand Down
109 changes: 96 additions & 13 deletions src/sfizz/Voice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ sfz::Voice::Voice(sfz::Resources& resources)
filters.reserve(config::filtersPerVoice);
equalizers.reserve(config::eqsPerVoice);

waveOscillator.init(sampleRate);
for (WavetableOscillator& osc : waveOscillators)
osc.init(sampleRate);
}

void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept
Expand Down Expand Up @@ -59,12 +60,18 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
wave = resources.wavePool.getWaveSaw();
break;
}
waveOscillator.setWavetable(wave);
waveOscillator.setPhase(region->getPhase());
for (WavetableOscillator& osc : waveOscillators) {
osc.setWavetable(wave);
osc.setPhase(region->getPhase());
}
setupOscillatorUnison();
} else if (region->oscillator) {
const WavetableMulti* wave = resources.wavePool.getFileWave(region->sample);
waveOscillator.setWavetable(wave);
waveOscillator.setPhase(region->getPhase());
for (WavetableOscillator& osc : waveOscillators) {
osc.setWavetable(wave);
osc.setPhase(region->getPhase());
}
setupOscillatorUnison();
} else {
currentPromise = resources.filePool.getFilePromise(region->sample);
if (currentPromise == nullptr) {
Expand All @@ -83,7 +90,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
ASSERT((filters.capacity() - filters.size()) >= region->filters.size());
ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size());

const unsigned numChannels = region->isStereo ? 2 : 1;
const unsigned numChannels = region->isStereo() ? 2 : 1;
for (auto& filter: region->filters) {
auto newFilter = resources.filterPool.getFilter(filter, numChannels, number, value);
if (newFilter)
Expand Down Expand Up @@ -187,7 +194,8 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept
{
this->sampleRate = sampleRate;

waveOscillator.init(sampleRate);
for (WavetableOscillator& osc : waveOscillators)
osc.init(sampleRate);
}

void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept
Expand Down Expand Up @@ -218,7 +226,7 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
fillWithData(delayed_buffer);
}

if (region->isStereo) {
if (region->isStereo()) {
ampStageStereo(buffer);
panStageStereo(buffer);
filterStageStereo(buffer);
Expand Down Expand Up @@ -518,9 +526,10 @@ void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
absl::c_generate(leftSpan, [&](){ return noiseDist(Random::randomGenerator); });
absl::c_generate(rightSpan, [&](){ return noiseDist(Random::randomGenerator); });
} else {
const auto numSamples = buffer.getNumFrames();
auto frequencies = resources.bufferPool.getBuffer(numSamples);
auto bends = resources.bufferPool.getBuffer(numSamples);
const auto numFrames = buffer.getNumFrames();

auto frequencies = resources.bufferPool.getBuffer(numFrames);
auto bends = resources.bufferPool.getBuffer(numFrames);
if (!frequencies || !bends)
return;

Expand All @@ -544,8 +553,25 @@ void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
applyGain<float>(*bends, *frequencies);
}

waveOscillator.processModulated(frequencies->data(), leftSpan.data(), buffer.getNumFrames());
copy<float>(leftSpan, rightSpan);
if (waveUnisonSize == 1) {
WavetableOscillator& osc = waveOscillators[0];
osc.processModulated(frequencies->data(), 1.0, leftSpan.data(), buffer.getNumFrames());
copy<float>(leftSpan, rightSpan);
}
else {
buffer.fill(0.0f);

auto tempSpan = resources.bufferPool.getBuffer(numFrames);
if (!tempSpan)
return;

for (unsigned i = 0, n = waveUnisonSize; i < n; ++i) {
WavetableOscillator& osc = waveOscillators[i];
osc.processModulated(frequencies->data(), waveDetuneRatio[i], tempSpan->data(), numFrames);
sfz::multiplyAdd<float>(waveLeftGain[i], *tempSpan, leftSpan);
sfz::multiplyAdd<float>(waveRightGain[i], *tempSpan, rightSpan);
}
}
}
}

Expand Down Expand Up @@ -620,3 +646,60 @@ void sfz::Voice::setMaxEQsPerVoice(size_t numFilters)
ASSERT(equalizers.size() == 0);
equalizers.reserve(numFilters);
}

void sfz::Voice::setupOscillatorUnison()
{
int m = region->oscillatorMulti;
float d = region->oscillatorDetune;

// 3-9: unison mode, 1: normal/RM, 2: PM/FM
// TODO(jpc) RM/FM/PM synthesis
if (m < 3) {
waveUnisonSize = 1;
waveDetuneRatio[0] = 1.0;
waveLeftGain[0] = 1.0;
waveRightGain[0] = 1.0;
return;
}

// oscillator count, aka. unison size
waveUnisonSize = m;

// detune (cents)
float detunes[config::oscillatorsPerVoice];
detunes[0] = 0.0;
detunes[1] = -d;
detunes[2] = +d;
for (int i = 3; i < m; ++i) {
int n = (i - 1) / 2;
detunes[i] = d * ((i & 1) ? -0.25f : +0.25f) * float(n);
}

// detune (ratio)
for (int i = 0; i < m; ++i)
waveDetuneRatio[i] = std::exp2(detunes[i] * (0.01f / 12.0f));

// gains
waveLeftGain[0] = 0.0;
waveRightGain[m - 1] = 0.0;
for (int i = 0; i < m - 1; ++i) {
float g = 1.0f - float(i) / float(m - 1);
waveLeftGain[m - 1 - i] = g;
waveRightGain[i] = g;
}

#if 0
fprintf(stderr, "\n");
fprintf(stderr, "# Left:\n");
for (int i = m - 1; i >= 0; --i) {
if (waveLeftGain[i] != 0)
fprintf(stderr, "[%d] %10g cents, %10g dB\n", i, detunes[i], 20.0f * std::log10(waveLeftGain[i]));
}
fprintf(stderr, "\n");
fprintf(stderr, "# Right:\n");
for (int i = 0; i < m; ++i) {
if (waveRightGain[i] != 0)
fprintf(stderr, "[%d] %10g cents, %10g dB\n", i, detunes[i], 20.0f * std::log10(waveRightGain[i]));
}
#endif
}
12 changes: 11 additions & 1 deletion src/sfizz/Voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ class Voice {
void panStageStereo(AudioSpan<float> buffer) noexcept;
void filterStageMono(AudioSpan<float> buffer) noexcept;
void filterStageStereo(AudioSpan<float> buffer) noexcept;
/**
* @brief Initialize frequency and gain coefficients for the oscillators.
*/
void setupOscillatorUnison();

Region* region { nullptr };

Expand Down Expand Up @@ -284,7 +288,13 @@ class Voice {
ADSREnvelope<float> egEnvelope;
float bendStepFactor { centsFactor(1) };

WavetableOscillator waveOscillator;
WavetableOscillator waveOscillators[config::oscillatorsPerVoice];

// unison of oscillators
unsigned waveUnisonSize { 0 };
float waveDetuneRatio[config::oscillatorsPerVoice] { };
float waveLeftGain[config::oscillatorsPerVoice] { };
float waveRightGain[config::oscillatorsPerVoice] { };

Duration dataDuration;
Duration amplitudeDuration;
Expand Down
8 changes: 4 additions & 4 deletions src/sfizz/Wavetables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ void WavetableOscillator::setPhase(float phase)
_phase = phase;
}

void WavetableOscillator::process(float frequency, float* output, unsigned nframes)
void WavetableOscillator::process(float frequency, float detuneRatio, float* output, unsigned nframes)
{
float phase = _phase;
float phaseInc = frequency * _sampleInterval;
float phaseInc = frequency * (detuneRatio * _sampleInterval);

const WavetableMulti& multi = *_multi;
unsigned tableSize = multi.tableSize();
Expand All @@ -56,7 +56,7 @@ void WavetableOscillator::process(float frequency, float* output, unsigned nfram
_phase = phase;
}

void WavetableOscillator::processModulated(const float* frequencies, float* output, unsigned nframes)
void WavetableOscillator::processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes)
{
float phase = _phase;
float sampleInterval = _sampleInterval;
Expand All @@ -66,7 +66,7 @@ void WavetableOscillator::processModulated(const float* frequencies, float* outp

for (unsigned i = 0; i < nframes; ++i) {
float frequency = frequencies[i];
float phaseInc = frequency * sampleInterval;
float phaseInc = frequency * (detuneRatio * sampleInterval);
absl::Span<const float> table = multi.getTableForFrequency(frequency);

float position = phase * tableSize;
Expand Down
4 changes: 2 additions & 2 deletions src/sfizz/Wavetables.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ class WavetableOscillator {
/**
Compute a cycle of the oscillator, with constant frequency.
*/
void process(float frequency, float* output, unsigned nframes);
void process(float frequency, float detuneRatio, float* output, unsigned nframes);

/**
Compute a cycle of the oscillator, with varying frequency.
*/
void processModulated(const float* frequencies, float* output, unsigned nframes);
void processModulated(const float* frequencies, float detuneRatio, float* output, unsigned nframes);

private:
/**
Expand Down
2 changes: 1 addition & 1 deletion tests/DemoWavetables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ int DemoApp::processAudio(jack_nframes_t nframes, void* cbdata)
self->fSweepCurrent = sweepCurrent;

// compute oscillator
osc.processModulated(frequency, left, nframes);
osc.processModulated(frequency, 1.0, left, nframes);
std::memcpy(right, left, nframes * sizeof(float));

return 0;
Expand Down
41 changes: 39 additions & 2 deletions tests/FilesT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,46 @@ 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] Channels (channels_multi.sfz)")
{
sfz::Synth synth;
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/channels_multi.sfz");
REQUIRE(synth.getNumRegions() == 6);

REQUIRE(synth.getRegionView(0)->sample == "*sine");
REQUIRE(!synth.getRegionView(0)->isStereo());
REQUIRE(synth.getRegionView(0)->isGenerator());
REQUIRE(!synth.getRegionView(0)->oscillator);

REQUIRE(synth.getRegionView(1)->sample == "*sine");
REQUIRE(synth.getRegionView(1)->isStereo());
REQUIRE(synth.getRegionView(1)->isGenerator());
REQUIRE(!synth.getRegionView(1)->oscillator);

REQUIRE(synth.getRegionView(2)->sample == "ramp_wave.wav");
REQUIRE(!synth.getRegionView(2)->isStereo());
REQUIRE(!synth.getRegionView(2)->isGenerator());
REQUIRE(synth.getRegionView(2)->oscillator);

REQUIRE(synth.getRegionView(3)->sample == "ramp_wave.wav");
REQUIRE(synth.getRegionView(3)->isStereo());
REQUIRE(!synth.getRegionView(3)->isGenerator());
REQUIRE(synth.getRegionView(3)->oscillator);

REQUIRE(synth.getRegionView(4)->sample == "*sine");
REQUIRE(!synth.getRegionView(4)->isStereo());
REQUIRE(synth.getRegionView(4)->isGenerator());
REQUIRE(!synth.getRegionView(4)->oscillator);

REQUIRE(synth.getRegionView(5)->sample == "*sine");
REQUIRE(!synth.getRegionView(5)->isStereo());
REQUIRE(synth.getRegionView(5)->isGenerator());
REQUIRE(!synth.getRegionView(5)->oscillator);
}

TEST_CASE("[Files] sw_default")
Expand Down
6 changes: 6 additions & 0 deletions tests/TestFiles/channels_multi.sfz
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<region> sample=*sine
<region> sample=*sine oscillator_multi=3
<region> sample=ramp_wave.wav oscillator=on
<region> sample=ramp_wave.wav oscillator=on oscillator_multi=3
<region> sample=*sine oscillator_multi=1
<region> sample=*sine oscillator_multi=2
Binary file added tests/TestFiles/ramp_wave.wav
Binary file not shown.

0 comments on commit 332c752

Please sign in to comment.