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

Topic/impulse rewrite #4150

Merged
merged 7 commits into from
Aug 23, 2022
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
47 changes: 37 additions & 10 deletions HelpSource/Classes/Impulse.schelp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ categories:: UGens>Generators>Deterministic


Description::

Outputs non-bandlimited single sample impulses.


Expand All @@ -14,26 +13,54 @@ classmethods::
method::ar, kr

argument::freq

Frequency in Hertz.
Frequency in Hertz. strong::freq:: may be negative.


argument::phase

Phase offset in cycles (0..1).

Phase offset in cycles (0..1). Staying in this range offers a slight efficiency
advantage, though phase offsets outside this range are supported and wrapped
internally.

argument::mul

The output will be multiplied by this value.


argument::add

This value will be added to the output.

discussion::
An Impulse with frequency 0 returns a single impulse.
Discussion::
code::Impulse:: will output an impulse on the first sample (assuming no phase
offset).

When the initial code::freq = 0::, a single impulse is output on first sample,
followed by silence until the frequency changes.

Discussion::
code::Impulse:: will output a code::1.0:: on the first sample (assuming no
phase offset).

If the initial code::freq = 0::, a single impulse is output on first sample,
followed by silence until the frequency changes.

Supported rate combinations for code::(freq, phase):: are
code::(a,a)::, code::(a,k)::, code::(a,i)::,
code::(k,k)::, code::(k,i)::,
code::(i,k)::, code::(i,i)::.


Internally, code::Impulse:: is based on a wrapping phasor: when the phase wraps,
an impulse is output. Any strong::phase:: offset is added and wrapped before
the phase increment (determined by strong::freq::) is applied. Therefore, it is
the phase increment (freq) that triggers an impulse, not the phase offset. For
example, if you wanted to drive and impulse train directly by the phase,
code::Impulse:: would not support that. However, a small UGen network could
achieve this result:
code::
({ var f = 1000;
HPZ1.ar(HPZ1.ar(Phasor.ar(rate: f * SampleDur.ir))) > 1e-5
}.plot(0.005)
);
::

Examples::

Expand Down
264 changes: 195 additions & 69 deletions server/plugins/LFUGens.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ struct LFGauss : public Unit {
};

struct Impulse : public Unit {
double mPhase, mPhaseOffset;
double mPhase, mPhaseOffset, mPhaseIncrement;
float mFreqMul;
};

Expand Down Expand Up @@ -202,9 +202,13 @@ void VarSaw_next_a(VarSaw* unit, int inNumSamples);
void VarSaw_next_k(VarSaw* unit, int inNumSamples);
void VarSaw_Ctor(VarSaw* unit);

void Impulse_next_a(Impulse* unit, int inNumSamples);
void Impulse_next_aa(Impulse* unit, int inNumSamples);
void Impulse_next_ak(Impulse* unit, int inNumSamples);
void Impulse_next_ai(Impulse* unit, int inNumSamples);
void Impulse_next_kk(Impulse* unit, int inNumSamples);
void Impulse_next_k(Impulse* unit, int inNumSamples);
void Impulse_next_ki(Impulse* unit, int inNumSamples);
void Impulse_next_ik(Impulse* unit, int inNumSamples);
void Impulse_next_ii(Impulse* unit, int inNumSamples);
void Impulse_Ctor(Impulse* unit);

void SyncSaw_next_aa(SyncSaw* unit, int inNumSamples);
Expand Down Expand Up @@ -793,109 +797,231 @@ void LFGauss_Ctor(LFGauss* unit) {

//////////////////////////////////////////////////////////////////////////////////////////////////

void Impulse_next_a(Impulse* unit, int inNumSamples) {
// detect if phasor is out-of-bounds, trigger and wrap [0, 1]
static inline float Impulse_testWrapPhase(double prev_inc, double& phase) {
if (prev_inc < 0.f) { // negative freqs
if (phase <= 0.f) {
phase += 1.f;
if (phase <= 0.f) { // catch large phase jumps
phase -= sc_ceil(phase);
}
return 1.f;
} else {
return 0.f;
}
} else { // positive freqs
if (phase >= 1.f) {
phase -= 1.f;
if (phase >= 1.f) {
phase -= sc_floor(phase);
}
return 1.f;
} else {
return 0.f;
}
}
}

void Impulse_next_ii(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
float* freq = ZIN(0);
double phase = unit->mPhase;
double inc = unit->mPhaseIncrement;

float freqmul = unit->mFreqMul;
LOOP1(inNumSamples, ZXP(out) = Impulse_testWrapPhase(inc, phase); phase += inc;);

unit->mPhase = phase;
}

void Impulse_next_ik(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
double phase = unit->mPhase;

double inc = unit->mPhaseIncrement;

double prev_off = unit->mPhaseOffset;
double off = ZIN0(1);
double phaseSlope = CALCSLOPE(off, prev_off);
bool phOffChanged = phaseSlope != 0.f;

LOOP1(
inNumSamples, float z; if (phase >= 1.f) {
phase -= 1.f;
z = 1.f;
} else { z = 0.f; } phase += ZXP(freq) * freqmul;
ZXP(out) = z;);
inNumSamples, ZXP(out) = Impulse_testWrapPhase(inc, phase);

if (phOffChanged) {
phase += phaseSlope;
Impulse_testWrapPhase(inc, phase);
} phase += inc;);

unit->mPhase = phase;
unit->mPhaseOffset = off;
}

/* phase mod - jrh 03 */

void Impulse_next_ak(Impulse* unit, int inNumSamples) {
void Impulse_next_ki(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
float* freq = ZIN(0);
double phaseOffset = ZIN0(1);

float freqmul = unit->mFreqMul;
double phase = unit->mPhase;
double prev_phaseOffset = unit->mPhaseOffset;
double phaseSlope = CALCSLOPE(phaseOffset, prev_phaseOffset);
phase += prev_phaseOffset;

LOOP1(
inNumSamples, float z; phase += phaseSlope; if (phase >= 1.f) {
phase -= 1.f;
z = 1.f;
} else { z = 0.f; } phase += ZXP(freq) * freqmul;
ZXP(out) = z;);
double prev_inc = unit->mPhaseIncrement;
double inc = ZIN0(0) * unit->mFreqMul;
double incSlope = CALCSLOPE(inc, prev_inc);

LOOP1(inNumSamples, ZXP(out) = Impulse_testWrapPhase(prev_inc, phase);

prev_inc += incSlope; phase += prev_inc;);

unit->mPhase = phase - phaseOffset;
unit->mPhaseOffset = phaseOffset;
unit->mPhase = phase;
unit->mPhaseIncrement = inc;
}

void Impulse_next_kk(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
float freq = ZIN0(0) * unit->mFreqMul;
double phaseOffset = ZIN0(1);

double phase = unit->mPhase;
double prev_phaseOffset = unit->mPhaseOffset;
double phaseSlope = CALCSLOPE(phaseOffset, prev_phaseOffset);
phase += prev_phaseOffset;

double prev_inc = unit->mPhaseIncrement;
double inc = ZIN0(0) * unit->mFreqMul;
double incSlope = CALCSLOPE(inc, prev_inc);

double prev_off = unit->mPhaseOffset;
double off = ZIN0(1);
double phaseSlope = CALCSLOPE(off, prev_off);
bool phOffChanged = phaseSlope != 0.f;

LOOP1(
inNumSamples, float z; phase += phaseSlope; if (phase >= 1.f) {
phase -= 1.f;
z = 1.f;
} else { z = 0.f; } phase += freq;
ZXP(out) = z;);
inNumSamples, ZXP(out) = Impulse_testWrapPhase(prev_inc, phase);

unit->mPhase = phase - phaseOffset;
unit->mPhaseOffset = phaseOffset;
}
if (phOffChanged) {
phase += phaseSlope;
Impulse_testWrapPhase(prev_inc, phase);
} prev_inc += incSlope;
phase += prev_inc;);

unit->mPhase = phase;
unit->mPhaseOffset = off;
unit->mPhaseIncrement = inc;
}

void Impulse_next_k(Impulse* unit, int inNumSamples) {
void Impulse_next_ak(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
float freq = ZIN0(0) * unit->mFreqMul;

double phase = unit->mPhase;

double inc = unit->mPhaseIncrement;
float* freqIn = ZIN(0);
float freqMul = unit->mFreqMul;

double prev_off = unit->mPhaseOffset;
double off = ZIN0(1);
double offSlope = CALCSLOPE(off, prev_off);
bool offChanged = offSlope != 0.f;

LOOP1(
inNumSamples, float z; if (phase >= 1.f) {
phase -= 1.f;
z = 1.f;
} else { z = 0.f; } phase += freq;
ZXP(out) = z;);
inNumSamples, float z = Impulse_testWrapPhase(inc, phase); if (offChanged) {
phase += offSlope;
Impulse_testWrapPhase(inc, phase);
} inc = ZXP(freqIn) * freqMul;
ZXP(out) = z; phase += inc;);

unit->mPhase = phase;
unit->mPhaseOffset = off;
unit->mPhaseIncrement = inc;
}

void Impulse_next_aa(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
double phase = unit->mPhase;

double inc = unit->mPhaseIncrement;
float* freqin = ZIN(0);
float freqmul = unit->mFreqMul;

double prev_off = unit->mPhaseOffset;
float* offIn = ZIN(1);

LOOP1(inNumSamples, float z = Impulse_testWrapPhase(inc, phase); float off = ZXP(offIn);
float offInc = off - prev_off; phase += offInc; Impulse_testWrapPhase(inc, phase);
inc = ZXP(freqin) * freqmul; ZXP(out) = z;

phase += inc; prev_off = off;);

unit->mPhase = phase;
unit->mPhaseOffset = prev_off;
unit->mPhaseIncrement = inc;
}

void Impulse_next_ai(Impulse* unit, int inNumSamples) {
float* out = ZOUT(0);
double phase = unit->mPhase;

double inc = unit->mPhaseIncrement;
float* freqin = ZIN(0);
float freqmul = unit->mFreqMul;

LOOP1(inNumSamples, float z = Impulse_testWrapPhase(inc, phase); inc = ZXP(freqin) * freqmul; ZXP(out) = z;
phase += inc;);

unit->mPhase = phase;
unit->mPhaseIncrement = inc;
}

// Impulse is based on a wrapping phasor. When the phase wraps, an impulse is
// output. Phase _increments_ according to its frequency and an additional phase
// _offset_ is applied.
// Order of operations:
// 1. Phase _offset_ is applied to the current phase (if offset has changed).
// 2. Phase is wrapped into range.
// 3. Phase _increment_ is added (according to the frequency).
// 4. Phase is checked for being out of range, in which case a trigger is fired
// and the phase is again wrapped.
// Therefore, phase increment (freq) triggers an impulse, but not phase offset.
void Impulse_Ctor(Impulse* unit) {
unit->mPhase = ZIN0(1);
unit->mPhaseOffset = ZIN0(1);
unit->mFreqMul = unit->mRate->mSampleDur;
unit->mPhaseIncrement = ZIN0(0) * unit->mFreqMul;

if (INRATE(0) == calc_FullRate) {
if (INRATE(1) != calc_ScalarRate) {
SETCALC(Impulse_next_ak);
unit->mPhase = 1.f;
double initOff = unit->mPhaseOffset;
double initInc = unit->mPhaseIncrement;
double initPhase = sc_wrap(initOff, 0.0, 1.0);

// Initial phase offset of 0 means output of 1 on first sample.
// Set phase to wrap point to trigger impulse on first sample
if (initPhase == 0.0 && initInc >= 0.0) {
initPhase = 1.0; // positive frequency trigger/wrap position
}
unit->mPhase = initPhase;

UnitCalcFunc func;
switch (INRATE(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to address it, but when this reaches the stage of c++ style review, I expect there to be a comment about the number of calculation functions. There have been some discussions lately about the topic of handling the permutations.

If you remove all the i functions, then we go from 9 down to 4 -- reducing the maintenance load by a factor of 2.22, while the performance gain of the i functions would be minuscule. So I'd start there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, 9 is overkill. I've trimmed it to 7 (dropping a-rate phase offset for k- and i-rate frequencies). If needed we could also drop a-rate phase offset altogether. I kept it because I recall a user on the scsynth forums being surprised it wasn't supported.
I agree we could drop i-rate altogether, falling back on a k-rate minimum for both args. Although one of the goals of this PR was support more rates. If maintenance is the issue though, i-rate is the easiest to introspect.

case calc_FullRate:
switch (INRATE(1)) {
case calc_ScalarRate:
func = (UnitCalcFunc)Impulse_next_ai;
break;
case calc_BufRate:
func = (UnitCalcFunc)Impulse_next_ak;
break;
case calc_FullRate:
func = (UnitCalcFunc)Impulse_next_aa;
break;
}
break;
case calc_BufRate:
if (INRATE(1) == calc_ScalarRate) {
func = (UnitCalcFunc)Impulse_next_ki;
} else {
SETCALC(Impulse_next_a);
func = (UnitCalcFunc)Impulse_next_kk;
}
} else {
if (INRATE(1) != calc_ScalarRate) {
SETCALC(Impulse_next_kk);
unit->mPhase = 1.f;
break;
case calc_ScalarRate:
if (INRATE(1) == calc_ScalarRate) {
func = (UnitCalcFunc)Impulse_next_ii;
} else {
SETCALC(Impulse_next_k);
func = (UnitCalcFunc)Impulse_next_ik;
}
break;
}
unit->mCalcFunc = func;
func(unit, 1);


unit->mPhaseOffset = 0.f;
unit->mFreqMul = unit->mRate->mSampleDur;
if (unit->mPhase == 0.f)
unit->mPhase = 1.f;

ZOUT0(0) = 0.f;
unit->mPhase = initPhase;
unit->mPhaseOffset = initOff;
unit->mPhaseIncrement = initInc;
}

//////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
Loading