Files
serumalt/Source/SynthVoice.h
T
biggy cd26beca42 refactor(osc): support held-note parameter updates without phase reset
Add Oscillator::setParams() which updates detune, pan, and gain for
already-initialised sub-voices without resetting their phases. This
preserves phase continuity when unison or spread is LFO-modulated at
control rate. noteOn() now just seeds phases and delegates to
setParams().

Precompute panL/panR in SubVoice instead of calling cos/sin per
sample. Add paramsValid and cachedParams to skip redundant work.

Replace SynthVoice's lastUnisonA/B tracking with a single
oscillatorsNeedNoteOn flag: noteOn sets it, render clears it after
the first block. Add SubLevel and NoiseLevel to the per-voice
modulation targets so they can be modulated like other parameters.
2026-09-09 14:33:21 +02:00

99 lines
2.8 KiB
C++

#pragma once
#include <JuceHeader.h>
#include "Params.h"
#include "Wavetable.h"
#include "Oscillator.h"
#include "SubOscillator.h"
#include "NoiseOscillator.h"
#include "FilterBank.h"
#include "Envelope.h"
#include "ModulationMatrix.h"
#include "MacroControls.h"
namespace serum
{
// ===========================================================================
// Everything a voice needs to render one block. Global (non per-voice) values
// are provided by the engine; the voice adds its own per-voice modulation.
// ===========================================================================
struct RenderContext
{
double sampleRate = 44100.0;
const WavetableLibrary* wavetables = nullptr;
float lfoValues[kNumLfos] { 0.0f, 0.0f, 0.0f, 0.0f };
float macroValues[kNumMacros] { 0.0f, 0.0f, 0.0f, 0.0f };
float modWheel = 0.0f;
float pitchBend = 0.0f;
float pitchBendRange = 2.0f;
const ModulationMatrix* matrix = nullptr;
const MacroControls* macros = nullptr;
OscParams oscA;
OscParams oscB;
bool subOn = false;
int subShape = 0;
int subOct = -1;
float subLevel = 0.0f;
bool noiseOn = false;
int noiseType = 0;
float noiseLevel = 0.0f;
FilterBankParams filters;
float envAttack[4] { 0.01f, 0.01f, 0.01f, 0.01f };
float envDecay[4] { 0.2f, 0.2f, 0.2f, 0.2f };
float envSustain[4]{ 0.7f, 0.7f, 0.7f, 0.7f };
float envRelease[4]{ 0.3f, 0.3f, 0.3f, 0.3f };
float envCurve[4] { 0.5f, 0.5f, 0.5f, 0.5f };
};
// ===========================================================================
// One synthesis voice: two oscillators + sub + noise -> filter bank -> amp
// envelope. Four envelopes provide per-voice modulation sources.
// ===========================================================================
class SynthVoice
{
public:
void prepare (double sampleRate, int maxBlockSize);
void reset();
void noteOn (int noteNumber, float velocity, double freqHz, juce::uint32 seed);
void noteOff();
void render (float* outL, float* outR, int numSamples, const RenderContext& ctx) noexcept;
bool isActive() const noexcept { return active; }
bool isReleased() const noexcept { return released; }
int getNote() const noexcept { return note; }
juce::uint64 getNoteId() const noexcept { return noteId; }
private:
Oscillator oscA, oscB;
SubOscillator sub;
NoiseOscillator noise;
FilterBank filters;
std::array<Envelope, 4> env;
int note = -1;
float velocity = 0.0f;
double baseFreq = 0.0;
bool active = false;
bool released = false;
juce::uint64 noteId = 0;
bool oscillatorsNeedNoteOn = false;
juce::uint32 seed = 0;
float noteRandom = 0.5f;
juce::AudioBuffer<float> scratch; // 2 channels
};
} // namespace serum