Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add oscillator unison #161

Merged
merged 5 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.