New sample VCO code for three VCOs, and detailed docs on how and why they work.

I had been wondering about that issue - how audible is this, and do people care? I know it’s quite audible, because even my ancient ears (which have been tested, and there is very little at 8k!) can easily hear it.

This morning I was thinking about this issue. Obviously I’m distressed that people keep releasing VCOs with really bad aliasing, but then I started to think about the VCOs that become very popular over time. VCOs from Vult, Lindenberg, Befaco, Bogaudio, VCV. I have tested all of these and they all have effective alias suppression. Some have more than others or have different artifacts, but they all have dramatically less aliasing than a naive VCO like our Demo VCO1. I haven’t measure the Mutable Instruments, but I’m almost certain I’ll find it is good, also.

So, I’m hopeful that in general people don’t like aliasing, and ultimately will gravitate to the VCOs where the dev has invested the time in getting rid of it.

3 Likes

Thank you. I think there are a lot of people starting out ever day, so maybe we can help them.

Have you read our old paper on making efficient plugins? Depending on your area of expertise there may be something useful in there. https://github.com/squinkylabs/SquinkyVCV/blob/master/docs/efficient-plugins.md

1 Like

Good stuff! Unfortunately, i don’t have enough time to dedicate to coding right now. I let the experts like you do their job.

2 Likes

Yes and no… It’s one of those tricky things and psychoacoustics is a world of study. As an old HIFI nut at the deep end I have a bit of experience with people, listening and gear. It’s audible, actually quite audible, but you have to train yourself to hear the exact difference. Looking at a spectrum analyzer and correlating that with what you hear helps greatly. Making A/B comparisons between VCO’s helps. For many it’s just sticking a VCO in a patch and “yeah, that sounds Ok”, but the accumulated noise in a big patch, compared to a clean one and yeah, pretty much everyone will hear the difference. But to some it matters a whole lot more than others. Some LOVE noise and distortion! Also, these days people live happily for decades with a crappy little Sonos speaker and they’ve never actually heard good sound. So it’s quite the tricky and subjective area. But some of us REALLY care, don’t worry :slight_smile:

You’re not alone. But… as long as there are GOOD ones we’ll be Ok. Mostly agree with your list, but not all of it. It’s a really touchy subject and people can get quite offended, especially if they can’t really hear it themselves.

Well yeah. I’m more like: There’s always people who don’t care, and thankfully also people who care highly about this. Just think of the last group when you make something and forget about the rest :slight_smile: I can assure you that the same issue drives people in the HIFI manufacturing business batty for the same reason. But the people who can appreciate quality makes it worth it in the end. Battle on my friend…

4 Likes

I have found two schools, those that care and those that dont, but I have never seen a complaint about a lack of the aliased sound.

Please keep up the the good work you are doing.

3 Likes

So, once you understand the theory how to implement a VCO… what are your chances of coming up with a new and fun sounding one? It feels like so much ground has already been explored by experts, there’s no way beginners can come up with a unique concept to make theirs stand out and worth using.

2 Likes

Well, I’m not saying they are great. but my Chebyshev is quite a unique vco that has been out for I think a year? And Saws, while it’s not unique exactly is probably the closest emulation of a Roland Super Saw, and super light on the CPU. Ev3, while a copy of the Befaco is at least a lot different from other VCOs.

I don’t usually like to boast (total lie) … But I think there are over 20 Squinky Labs modules in the plugin manager, and for better or for worse they are pretty unique. In non-VCO, there are Chopper, LFN, Grey Code, Growler, those may be totally unique. Shaper, is a lot like some other wave shapers, but way more versatile, and no alisasing. Stairway is not really like any other VCV filters, and is kind of popular. Certainly our sequencers, again for better or worse, are unlike any other VCO sequencer.

For at least a year our first module, Booty Shifter, was the only frequency shifter in VCV (although now Surge has one that is really good).

Whew - look - you baited me into boasting :wink:

OK, back to your question. From time to time there are some pretty different VCOs in VCV, I’ve seen some with really crazy waveforms, That vector one from Hora. A lot of the Bogaudios are pretty unique.

VCO is a tough nut to crack for sure. Just to get started in the game I believe you should be able to make something that doesn’t use a ton a CPU and doesn’t alias a lot, Right there it’s pretty tough (although not too tough if you borrow some bits from there and there). But who knows who will come up with the next amazing one?

4 Likes

One answer to that question might be to study the amazing Energy VCO. Based on ring modulation, there are also other great VCO’s in Rack based on phase distortion and of course wavetables. I think there’s still good space to innovate in the space of those more esoteric oscillator techniques.

But also at the more basic level. As an example, I would love a traditional VCO with the following properties:

  • Low aliasing and low CPU consumption.
  • An octave switcher
  • A tune knob with selectable free-tune or semi-tone-tune.
  • Waveforms: Square, shark-tooth, sawtooth, sine, triangle.
  • Inbuilt wave-folder on the sine.
  • Inbuilt de-tuner, creating two copies of the primary osc.
  • A sub-oscillator output linked to the primary, going 1 or 2 octaves below. Square or sawtooth wave.
  • Linear FM, sync, PWM.
  • Make it musical, with it’s own character, carefully selecting the distribution of harmonics (look at the Lindenberg in a spectrum analyzer).
  • Doesn’t matter if the panel is a bit on the wide side.

For inspiration, these two oscillators together cover almost every point I’ve mentioned:

I would almost think that most of this functionality can be cobbled together from existing open-source code, and then some loving attention to detail so the thing sounds good. I’ll be your happy tester :slight_smile:

5 Likes

@Vortico: have you had a chance to look at this repo and tutorials? Any errors that should be fixed up?

Sure. Here’s some feedback.

VCO-1

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L10 A constant PORT_MAX_CHANNELS is defined in engine/Port.hpp.

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L36-L37 I recommend to avoid specifying 0 in the first element of initializer lists. It misleads some programmers. Use = {}

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L58 I prefer to count up instead of down for iterators. Or better yet, use dsp::ClockDivider and if (clockDivider.process()).

In general, your VCO-1 oscillator isn’t going to behave well with audio-rate FM because it’s only computing the pitch every 4 samples. That’s a tradeoff that developers need to make a conscious decision about. For an oscillator like this, I’d prefer to just compute parameters on every frame, since it’s pretty simple in this case. If a fast exp2 approximation is used, it would actually probably be faster to process parameters every sample, since reading/writing the phaseAdvance array could have greater latency than a simple pure function like fast_exp2().

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L38-L42 The variables currentPolyphony and output* don’t need to be class-scoped. You’re just wasting memory and read/write cycles. They can have local function scope.

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L88 This isn’t standard C++11. Needs to be std::log2.

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L110 What if phaseAccumulators[i] >= 2.f? Bug!

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L125 Tiny nitpick: M_PI is a double literal, so you’re doing a float->double->float conversion here. Using float(M_PI) will increase the speed of this line.

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L172-L181 Pretty much all developers should be generating these files from their SVG using helper.py.

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L207 Nitpick: I’d use const std::string& or just std::string instead of const char* because this is C++. If you’re writing a low-level C-like function, it’d fine to use char buffers, but there’s no disadvantage of using C++ strings here.

https://github.com/squinkylabs/Demo/blob/master/src/VCO1.cpp#L194-L199 In order to be consistent with the addInput(createInput(...)) lines above, I’d change your method to a helper function in the root namespace so that you can call addChild(createLabel(...)).

VCO-2 & 3

Other than the suggestions inherited from VCO-1, MinBLEP and SIMD MinBLEP code is never pretty so as long as they work and don’t have bugs, this code is fine.

3 Likes

Thanks so much for taking the time to provide this valuable feedback! I’ll incorporate most of it into the project. Still digesting some of it :wink:

1 Like

I think there is just one difference of opinion here. While of course I agree that there are some non-linear FM cases where sub-sampling the V/Octave input will change the sound, I have always found that technique to have a big impact on CPU usage. On my own “real” plugins I’ve often seen 2X improvements.

These plugins (Demo VCOx) are of course pretty simple plugins, and you ask a very reasonable question – basically “for these plugins, sub sampling may not lower the CPU usage much, and may in fact raise it.”

As an experiment I made a VCO4 that does not sub-sample the CV, and does not save the phase increment to a variable. I found that for 1-4 voices it was about the same, and for 16 voices the sub-sampling was a significant improvement, although still a bit less than a 2X improvement.

Since the Mind-Meld mixer is the only other plugin that I know of that sub-samples the CV, I measured it, too. This was quite easy, since there is a context menu item to turn this feature on and off.

On the mind-meld the improvement is about 1.6X on my computer. Which is significant as this is a pretty heavy plugin.

I’ll go edit the comments in the source for VCO1 and detail these trade-offs a bit more. I thought I had done so already, but apparently I only did it in my mind.

As a follow up experiment, I tried re-building all these plugins with -march=native, thinking that maybe register pressure could explain some of these results. I was surprised that for this test it didn’t make much difference. The mind-meld saw a significant, but not earth shaking improvement, the rest only showed a minor improvement.

Anyway, for sure the source code needs to be updated to explain the trade-offs.

Data here:

2 Likes

I’ve folded in most of the suggested improvements to the code, and added more comments around the decision to sub-sample the CV input. Thanks again for all the great feedback and suggestions.

1 Like

In the case of the float_4 variables declared in VCO3 (struct and methods), would it be recommended to always declare them using alignas(16), so that they are always properly aligned? In the case of that code I think it’s ok since there are no other types of local variables, but if afterwards, someone were to declare another simple float before the float_4, then alignment would be off.

In other words, to avoid a potential situation like this:

struct foo {
    float f1;
    float_4 val4;// not aligned?
}

Should it be good practice to do it like this?:

struct foo2 {
    float f1;
    alignas(16) float_4 val4;
}

Even if we were to say that we should not use the alignas() and just sort our variables from widest to narrowest (i.e. from float_4 at the top and then all the way down to char), I think the problem would re-appear when we declare an array of structs.

struct foo3 {
    float_4 val4;
    float f1;
}

foo3 fooarr[8];

so only the first and fifth elements of this array have their float_4 properly aligned if I understand correctly.

P.S. Thanks also for the comments regarding the Demo code by Bruce. I was surprised and happy to learn about the float(M_PI) thing, and I will be doing this also from now on :slight_smile: I tested it in the on-line assembler viewer you once referenced in another thread, quite neat!

Vectors like float_4 are already aligned types. There is no need to specify their alignment with alignas.

1 Like

I see now that it is more when using .store(...) and .load(...) in simd that we should strive to have those pointers referencing 16-byte-aligned memory locations. Good to know that float_4 is implicitly aligned.

Related to my previous reply, I just tried the following code in the compiler explorer, and to my surprise the sizeof() returns 32 and not 17, so it seems making an array will be ok also :slight_smile:

#include <pmmintrin.h>

struct foo {
    __m128 f1;
    int8_t c1;
};

int main(void) {
    return sizeof(foo);
}

If we replace the __m128 with an array of 4 floats, the sizeof will be 20 (and still not 17!). Sorry if this obvious to many, but I find it too fascinating to not share this :slight_smile:

I’m not sure that follows. That example seems to imply that structure get padded to be an even multiple of 32, not that float arrays are always aligned to 128?

Indeed, there were two points related to my initial comment, my main one was about alignment of the float_4 type, which Andrew answered, and then in my examples I also alluded to how a vector would behave vs alignment, i.e. that if it’s fully packed, not all elements would be aligned, but now with the last experiment, I saw that it does indeed get padded such that all elements of the array have all their constituents aligned as well.

1 Like

There are two rules with C/C++ compilers:

  • The alignment of a struct is that of its maximally aligned member. E.g. if a struct contains a char, float, and float_4, it will be aligned to 16 bytes.
  • The size of every type is a multiple of its alignment. E.g. the struct above contains 1+4+16=21 bytes of members, but its size must be rounded up to 32 because its alignment is 16.
2 Likes