yep, this is the case in puredata’s osc~ also, and it uses linear interpolation. so i concur that this stuff is mostly or entirely attributable to interpolation error. in a nutshell, these errors will produce more or fewer odd-order distortion harmonics depending on the relationship between per-sample phase increment and table size (so they will sweep around in pitch, amplitude, and distribution as you move the cos freq), and those harmonics will alias.
(did you try just looking at the super-low osc~ without the HPF?)
the code for the production and consumption of the cosine table is a little odd, since it kinda abuses the bit layout of 32-bit IEEE floats for efficient interpolation (? haven’t quite grokked it tbh. and given that, i can’t really rule out an error - maybe worth noting what kind of processor you’re using. also suspicious that there seems to be more error at the extremes of the cos waveform - the bounds of the lookup table.)
tab size defined: https://github.com/pure-data/pure-data/blob/master/src/m_pd.h#L620
tab computed: https://github.com/pure-data/pure-data/blob/master/src/d_osc.c#L204
lookup: https://github.com/pure-data/pure-data/blob/master/src/d_osc.c#L263
i’m pretty sure cos~ uses the same table, just applying it as a transfer function instead of looping over it. somehow not spotting that code right now.
so if you want to try a higher-quality (and slower) cos approximation, might be best with a simple custom object (store phase increment and phase as double, and just skip the table and use standard cos() function.)
you can also generate a much cleaner sine with a self-oscillating 2-pole filter, as is done in supercollider’s FSinOsc Ugen. (with important caveat that the amplitude falls off at low frequencies, IIRC? and it can blow up if you modulate it with big discontinuities.)
source for that, which you could adapt to pd’s raw filter interfaces (too lazy to look for it online r/n):
void FSinOsc_Ctor(FSinOsc* unit) {
if (INRATE(0) == calc_ScalarRate)
SETCALC(FSinOsc_next_i);
else
SETCALC(FSinOsc_next);
unit->m_freq = ZIN0(0);
float iphase = ZIN0(1);
float w = unit->m_freq * unit->mRate->mRadiansPerSample;
unit->m_b1 = 2. * cos(w);
unit->m_y1 = sin(iphase);
unit->m_y2 = sin(iphase - w);
ZOUT0(0) = unit->m_y1;
}
void FSinOsc_next(FSinOsc* unit, int inNumSamples) {
float* out = ZOUT(0);
double freq = ZIN0(0);
double b1;
if (freq != unit->m_freq) {
unit->m_freq = freq;
double w = freq * unit->mRate->mRadiansPerSample;
unit->m_b1 = b1 = 2.f * cos(w);
} else {
b1 = unit->m_b1;
}
double y0;
double y1 = unit->m_y1;
double y2 = unit->m_y2;
// Print("y %g %g b1 %g\n", y1, y2, b1);
// Print("%d %d\n", unit->mRate->mFilterLoops, unit->mRate->mFilterRemain);
LOOP(unit->mRate->mFilterLoops, ZXP(out) = y0 = b1 * y1 - y2; ZXP(out) = y2 = b1 * y0 - y1;
ZXP(out) = y1 = b1 * y2 - y0;);
LOOP(unit->mRate->mFilterRemain, ZXP(out) = y0 = b1 * y1 - y2; y2 = y1; y1 = y0;);
// Print("y %g %g b1 %g\n", y1, y2, b1);
unit->m_y1 = y1;
unit->m_y2 = y2;
}
also maybe worth noting that hip~ appears to just have a single pole, producing a -6db/oct rolloff that will not reject DC very well at low cutoff freqs:
hip~ source: https://github.com/pure-data/pure-data/blob/master/src/d_filter.c#L55
and i think some of the artefacts could arise from the way pd zaps denormals in the filter history (again, worth noting your processor arch) - in particular i’m suspicious of these when it comes to the relatively loud whine at subharmonics of the samplerate: when the input is crossing zero, “potential future denormals” are entering the filter history, and maybe getting too aggressively zapped every other sample (or whatever.)
i would try just commenting out lines 103, 104 in d_filters.c to see what happens? (zapping denormals is an optimization, not supposed to affect accuracy, but my gut feeling is it’s kinda bad practice to deal with them by testing bits instead of using the processor’s own flush-to-zero instructions.)
m_pd.h
748:static inline int PD_BIGORSMALL(t_float f) /* exponent outside (-64,64) */
771:static inline int PD_BIGORSMALL(t_float f) /* exponent outside (-512,512) */
781:#define PD_BIGORSMALL(f) 0
789:#define PD_BIGORSMALL(f) ((((*(unsigned int*)&(f))&0x60000000)==0) || \
793:#define PD_BIGORSMALL(f) ((f) > 1e150 || (f) < -1e150 \
anyways, interpolation and aliasing artefacts down around -80db aren’t exactly surprising with the kinds of corners that are often cut for efficiency in computer music systems.
but yeah, i too enjoyed hearing them in that video and the general exploration of the microscopic glitch landscape! (though i don’t know that i’d describe even these basic DSP blocks as exactly “simple” when you get down to the details.)