I came across this piece of code here. I’ve found some interesting ideas in this Wiki, so I come back to it from time to time to steal things or adapt them.

This line of #27 is bugging me. The variable out is a sine wave here.

I used Desmos to graph the results of applying these three trig functions to sin(x) and the resulting waveforms are really interesting.

I’m wondering:

Is there a better way to do this in SC that’s more efficient/elegant than using the sine and cosine methods?

How does this work? Or what are some resources that cover things like basic trig for DSP that might be useful? I haven’t really done any trig since high school and all I remember is what sine, cosine, and tangent are. I also know what happens if you raise them to a power and I understand their relationship to pi, but most of this math is pretty hazy for me.

How can I start to intuit the way these kinds of transformations might work? This probably is related to #2. Follow up question, how might I figure out what else I can do?

I’m not sure what you mean by this. Is there something you find inefficient or inelegant about them? If not, don’t worry about it.

The fact that they are specifically trig functions doesn’t actually matter that much in this case. When it comes to waveshapers, it’s the overall broad shape that matters most.

The function sin(x), when used as a waveshaper, functions like a saturator when the input signal is less than 0.5pi in peak amplitude. When the input peak amplitude exceeds that range, it acts like a wavefolder. It will sound fairly similar to .fold2 when driven at high amplitudes.

cos(x) acts somewhat like the “full-wave rectification” waveshaper x.abs when x is less than pi in peak amplitude. If it exceeds that range, it again acts like a wavefolder.

A phase offset of the form sin(x+a) is equivalent to a “bias” setting found in many waveshapers, which alters the timbre in a complex way dependent on the signal.

If the input signal is always a sine wave, applying a sine-wave wavefolder is actually a special case of phase modulation synthesis where the carrier has zero frequency.

As far as pure, stateless waveshaping goes, the most common functions are soft clipping, hard clipping, wavefolding, crossover distortion, and bitcrushing. There are other, Dark Waveshapers such as bitwise operations and the “inside out” operation x.sign - x.

But the choice of waveshaping function is perhaps less important than how you apply it and where it fits in the broader signal chain. I highly recommend modulating the input amplitude (drive) so that timbre changes over time. Also, a trick I use all the time is to alternate stages of EQ and waveshaping, so the linear and nonlinear stages interact.

The question is more “shouldn’t you use SinOsc instead of .sin?” for example. My understanding is that trig functions are heavy, so one might use a Taylor approximation or table lookup rather than calculating sine. It was that and I think, though I’m not at my computer, something like taking the sine of a phasor ugen ranging from 0-2pi in amplitude will have more severe aliasing artifacts than the native table lookup or FSinOsc which is, what, a resonant filter?

Thanks for the explanation, that’s super helpful! I’ve often included something like a gain parameter in synthdefs recently and then used .softclip or .tanh. So what you’re saying is these are all just forms of distortion essentially and I can just use the UGens I normally would use for different flavors of distortion (.round for bitcrush, .tanh or .softclip for soft clipping, .clip for hard clipping, .fold for wave folding, etc etc) and just raise the gain to see what happens. From there of course just shaping the parameters over time to add movement?

(
p = {
var phase = Phasor.ar(0, 440 * SampleDur.ir, 0, 2pi);
var sin = phase.sin;
var sinosc = SinOsc.ar(0, phase);
sin absdif: sinosc
}.plot(duration: 1);
)
p.plots[0].value.maxItem // 4.3213367462158e-07
p.plots[0].value.maxItem.ampdb // -127.28763778729 dB

Not really aliasing but rather, inaccuracies in the sample values raise the noise floor. However, this is quite near the bottom end of the ~140dB dynamic range of 24 bits so the probability of it being detectable to the ear is rather low. (The noise floor of a 16-bit CD quality recording is louder than this.)

But I don’t think SinOsc.ar(freq) and SinOsc.ar(0, Phasor.ar(0, freq * SampleDur.ir, 0, 2pi)) are analogous in the way they’re implemented. I noticed this a few weeks ago.

(
{
var a = SinOsc.ar(400);
var b = SinOsc.ar(0, Phasor.ar(0, 400 * SampleDur.ir * 2pi, 0, 2pi));
var c = a absdif: b;
RunningMax.ar(c).poll; //this should work since it's sampling absdif at audio rate, right?
[a,b,c]
}.plot;
)

You’re right that I did mess up the formula – it should have been Phasor.ar(0, freq * SampleDur.ir, 0, 1) * 2pi or Phasor.ar(0, freq * SampleDur.ir * 2pi, 0, 2pi).

However, the initial question was about differences between sin() and SinOsc, and it is valid to compare those with equivalent phase inputs, at any frequency. So my example looks like I meant 400 Hz while it doesn’t produce 400 Hz (true), but it does correctly report differences between the two methods of calculating the sine of the same phase.

Thanks James. The comparison results were a lot more significant when I tried it again (after multiplying frequency by 2pi), so I guess now it’s more a matter of considering trade off between FSinOsc (amplitude deviations), SinOsc (usually what I end up using and of course that’s the intent), Phasor, .sin, etc.
I feel like this is a good question to ask when you’re doing something with additive synthesis and a ton of sine waves, which I’m not doing.
I tend to obsess about efficiency and these questions I just find interesting.

That is, assuming C’s sin() function is more accurate than SinOsc’s wavetable lookup, the noise introduced by SinOsc is ~30 dB quieter than that of a 16-bit audio release. It’s negligible – really, it’s nothing to worry about.