diff --git a/HelpSource/Classes/Impulse.schelp b/HelpSource/Classes/Impulse.schelp index 667394723c9..32faa72dc9a 100644 --- a/HelpSource/Classes/Impulse.schelp +++ b/HelpSource/Classes/Impulse.schelp @@ -5,7 +5,6 @@ categories:: UGens>Generators>Deterministic Description:: - Outputs non-bandlimited single sample impulses. @@ -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:: diff --git a/server/plugins/LFUGens.cpp b/server/plugins/LFUGens.cpp index 1f3c09ece8b..3fcf63e1156 100644 --- a/server/plugins/LFUGens.cpp +++ b/server/plugins/LFUGens.cpp @@ -67,7 +67,7 @@ struct LFGauss : public Unit { }; struct Impulse : public Unit { - double mPhase, mPhaseOffset; + double mPhase, mPhaseOffset, mPhaseIncrement; float mFreqMul; }; @@ -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); @@ -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)) { + 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; } ////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/testsuite/classlibrary/TestCoreUGens.sc b/testsuite/classlibrary/TestCoreUGens.sc index e1f1142eb80..65128fe4200 100644 --- a/testsuite/classlibrary/TestCoreUGens.sc +++ b/testsuite/classlibrary/TestCoreUGens.sc @@ -381,6 +381,98 @@ TestCoreUGens : UnitTest { condvar.waitFor(1, { completed == tests.size }); } + test_impulse { + var funcs, results, frq, phs; + var rates = [\kr,\ar]; + var renderCond = Condition(); + + server.bootSync; + + frq = server.sampleRate / server.options.blockSize * 2.123; // 2.123 impulses per block + phs = 0; + + rates.do{ |rate| + + funcs = [{DC.ar(frq)}, {DC.kr(frq)}, frq].collect({ |in0| + [{DC.ar(phs)}, {DC.kr(phs)}, phs].collect{ |in1| + { Impulse.perform(rate, in0, in1) } + } + }).flat; + results = Array.newClear(funcs.size); + + funcs.do{ |f, i| + f.loadToFloatArray( + duration: server.options.blockSize / server.sampleRate * 3, // 3 blocks + action: { |arr| results[i] = arr; renderCond.test_(true).signal } + ); + renderCond.wait; renderCond.test_(false) + }; + + this.assert(results.every(_ == results[0]), + "Impulse.%: all rate combinations of identical unmodulated input values should have identical output".format(rate), + report: true); + }; + + /* Tests in response to historical bugs */ + + // Phase wrapping, initial phase offset + // https://github.com/supercollider/supercollider/pull/2864#issuecomment-299860789 + frq = 50; + { Impulse.kr(frq, 3.8) }.loadToFloatArray( + duration: frq.reciprocal, // render one freq period (should contain only 1 impulse) + action: { |arr| + this.assert(arr.sum == 1.0, "Impulse.kr: phase that is far out-of-range should wrap immediately in-range, and not cause multiple impulses to fire.", report: true); + this.assert(arr[0] != 1.0, "Impulse.kr: a phase offset other than 0 or 1 should not produce an impulse on the first output sample.", report: true); + renderCond.test_(true).signal; + } + ); + renderCond.wait; renderCond.test_(false); + + // Phase offset of 0,1,-1 should be equal on first sample + rates.do{ |rate| + var phases = [0, 1, -1]; + phases.do{ |phs| + { Impulse.perform(rate, frq, phs) }.loadToFloatArray( + duration: frq.reciprocal, // 1 freq period + action: { |arr| + this.assert(arr[0] == 1.0, + "Impulse.%: initial phase of % should produce and impulse on the first frame.".format(rate, phs), report: true); + renderCond.test_(true).signal; + } + ); + renderCond.wait; renderCond.test_(false); + } + }; + + // Freq = 0 should produce a single impulse on first frame + // https://github.com/supercollider/supercollider/pull/4150#issuecomment-582905976 + rates.do{ |rate| + { Impulse.perform(rate, 0) }.loadToFloatArray( + duration: server.options.blockSize / server.sampleRate * 3, // 3 blocks + action: { |arr| + this.assert(arr[0] == 1.0 and: { arr.sum == 1.0 }, + "Impulse.%: freq = 0 should produce a single impulse on the first frame and no more.".format(rate), report: true); + renderCond.test_(true).signal; + } + ); + renderCond.wait; renderCond.test_(false); + }; + + // Positive and negative freqs should produce the same output + rates.do{ |rate| + { Impulse.perform(rate, 100 * [1,-1]) }.loadToFloatArray( + duration: 5 * frq.reciprocal, + action: { |arr| + arr = arr.clump(2).flop; // de-interleave + this.assertArrayFloatEquals(arr[0], arr[1], + "Impulse.%: positive and negative frequencies should produce the same output.".format(rate), report: true); + renderCond.test_(true).signal; + } + ); + renderCond.wait; renderCond.test_(false); + }; + } + test_demand { var nodesToFree, tests, testNaN; diff --git a/testsuite/classlibrary/TestFilterUGens.sc b/testsuite/classlibrary/TestFilterUGens.sc index f41d6c404d9..03137c18ac6 100644 --- a/testsuite/classlibrary/TestFilterUGens.sc +++ b/testsuite/classlibrary/TestFilterUGens.sc @@ -87,6 +87,7 @@ TestFilterUGens : UnitTest { var delay_times = [1,64]; // in samples var numTests = delay_times.size * filters.size; var completed = 0; + var impFrq = 500; filters.do { arg filter; @@ -99,9 +100,14 @@ TestFilterUGens : UnitTest { { var deltime = delay_samples/SampleRate.ir; // should be silent - FP rounding errors are ok. - DelayN.ar(filter.ar(Impulse.ar(0)), deltime, deltime) - - filter.ar(DelayN.ar(Impulse.ar(0), deltime, deltime)); - }.loadToFloatArray(0.1, server, { + // Note: impulse phase offset to avoid errors associated with + // initialization sample bugs in filter UGens (and others). + // https://github.com/supercollider/rfcs/pull/19 + DelayN.ar(filter.ar(Impulse.ar(impFrq, 0.25)), deltime, deltime) + - filter.ar(DelayN.ar(Impulse.ar(impFrq, 0.25), deltime, deltime)); + // DelayN.ar(filter.ar(Impulse.ar(0)), deltime, deltime) + // - filter.ar(DelayN.ar(Impulse.ar(0), deltime, deltime)); + }.loadToFloatArray(2*impFrq.reciprocal, server, { arg data; this.assertArrayFloatEquals(data, 0, message, within:1e-10, report:true); completed = completed + 1;