#include "Oscillator.h" namespace serum { void Oscillator::reset() { for (auto& v : voices) v = SubVoice {}; activeUnison = 1; initializedUnison = 0; paramsValid = false; } void Oscillator::noteOn (double freqHz, const OscParams& p, juce::uint32 seed) { jassert (freqHz > 0.0); rng.setSeed (seed); initializedUnison = 0; paramsValid = false; setParams (p); } void Oscillator::setParams (const OscParams& p) noexcept { const int uni = juce::jlimit (1, kMaxUnison, p.unison); const bool countChanged = ! paramsValid || uni != activeUnison; const bool detuneChanged = countChanged || p.detune != cachedParams.detune; const bool panChanged = countChanged || p.pan != cachedParams.pan || p.spread != cachedParams.spread; if (! detuneChanged && ! panChanged) return; const double basePhase = (double) p.phase * kTwoPi; for (int v = initializedUnison; v < uni; ++v) { // Even phase spacing distributes the initial unison phases. double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0; double random = rng.nextFloat() * (double) p.randPhase * kTwoPi; voices[(size_t) v].phase = basePhase + offset + random; } initializedUnison = juce::jmax (initializedUnison, uni); const float unisonLevel = 1.0f / std::sqrt ((float) uni); for (int v = 0; v < uni; ++v) { auto& sv = voices[(size_t) v]; if (detuneChanged) { // Detune: linear spread in cents, 0..50 cents at full depth. double detuneCents = 0.0; if (uni > 1) detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0)); sv.detuneRatio = std::pow (2.0, detuneCents / 1200.0); } if (panChanged) { // Stereo spread. float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f; panPos = juce::jlimit (-1.0f, 1.0f, panPos + p.pan); // Constant-power pan. const float panAngle = (panPos + 1.0f) * 0.5f * juce::MathConstants::halfPi; sv.panL = std::cos (panAngle); sv.panR = std::sin (panAngle); } if (countChanged) { // Gain scaling with a centre emphasis for odd unison counts. float lvl = unisonLevel; if ((uni & 1) && v == uni / 2) lvl *= 1.3f; sv.level = lvl; } } activeUnison = uni; cachedParams = p; paramsValid = true; } float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept { const float amt = p.warpAmt; switch ((WarpMode) p.warp) { case WarpMode::None: return phase; case WarpMode::BendPlus: return phase + amt * 0.5f * std::sin (phase * (float) kTwoPi); case WarpMode::BendMinus: return phase - amt * 0.5f * std::sin (phase * (float) kTwoPi); case WarpMode::Sync: return std::fmod (phase * (1.0f + amt * 7.0f), 1.0f); case WarpMode::Asym: if (amt < 0.001f) return phase; return std::pow (phase, 1.0f + amt * 3.0f); case WarpMode::Mirror: { const float mirror = 2.0f * std::abs (phase - 0.5f); return phase + (mirror - phase) * amt; } case WarpMode::PWM: return phase; case WarpMode::Fold: return phase; default: return phase; } } float Oscillator::warpSample (float sample, const OscParams& p) const noexcept { const float amt = p.warpAmt; switch ((WarpMode) p.warp) { case WarpMode::PWM: { const float threshold = (2.0f * amt - 1.0f) * 0.9f; return std::tanh ((sample - threshold) * 4.0f); } case WarpMode::Fold: return std::sin (sample * (1.0f + amt * 5.0f) * 1.5707963267948966f); default: return sample; } } void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double freqHz, float& outL, float& outR) noexcept { if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0) return; const int uni = activeUnison; const float framePos = p.wtPos * 255.0f; const double phaseInc = kTwoPi * freqHz / sr; float accL = 0.0f; float accR = 0.0f; for (int v = 0; v < uni; ++v) { auto& sv = voices[(size_t) v]; sv.phase += phaseInc * sv.detuneRatio; sv.phase -= std::floor (sv.phase * (1.0 / kTwoPi)) * kTwoPi; if (sv.phase >= kTwoPi) sv.phase -= kTwoPi; if (sv.phase < 0.0) sv.phase += kTwoPi; float phase01 = (float) (sv.phase * (1.0 / kTwoPi)); float sample = wt.readSafe (framePos, warpPhase (phase01, p)); sample = warpSample (sample, p); const float gain = sv.level * p.level; accL += sample * gain * sv.panL; accR += sample * gain * sv.panR; } outL += accL; outR += accR; } } // namespace serum