Problems instantiating LFO phase/start positions

Hi all, first time posting so my apologies if I fail to provide adequate information for my particular question. I have a single module in the Libary: Planetary LFOs by HawthornLabs, which creates LFO’s based on the length of each planet’s revolution around the sun. Currently the 9 LFO’s all start at zero and then begin to oscillate at their various (very slow) frequencies.

I’m attempting to code start positions for them so they don’t all start at zero every time and am doing so by calculating the amount of time passed since the epoch and doing modulo math to figure out where in their revolution they would be right now. (a stretch goal is to actually figure out where the planets were on 1/1/1970 but currently it assumes magical planetary alignment on that date)

In a sandbox file I can generate the offsets and then multiply them by 2*pi and a gain of 5.0f to get the current starting point:

#include <ctime>
#include <chrono>
#include <iostream>

int timeSinceEpoch = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
long planetDur[9] = {7603200, 19414080, 31553280, 59356800, 374198400, 928540800, 2642889600, 5166720000, 7824384000};
double phase[9];
float _2PI = 2.f * M_PI;

int main(){
    for (int i = 0; i < 9; i ++){
        phase[i] = ((double)(timeSinceEpoch % planetDur[i]) / (double)planetDur[i])*_2PI;
        std::cout << phase[i]
                  << ", "
                  << 5.0f * sin(phase[i])
                  << '\n';
    };
}

Which results in:

5.1842, -4.45374
0.0449624, 0.224736
2.38472, 3.43326
2.75846, 1.86916
4.20805, -4.37747
5.54104, -3.37935
4.15427, -4.24126
2.125, 4.2516
1.40321, 4.92995

and those numbers look plausible. Especially the second set of numbers would seem to suggest that the LFO’s should start all over the +/- 5V range when the module is instantiated.

However, when I put my local build of the module into VCV Rack, I’m getting 1.7v from the fastest LFO (Mercury) then 0.7v, 0.48v, and it keeps descending towards zero as the frequency of the LFO decreases. This feels like I’m screwing up my math somehow, but I can’t figure out where the mismatch is from my sandbox file and the module file. Here’s the module file:

#include "plugin.hpp"
#include <chrono>
#include <ctime>

struct PlanetaryLFOs : Module {
	enum ParamId {
		SPEED_PARAM,
		PARAMS_LEN
	};
	enum InputId {
		INPUTS_LEN
	};
	enum OutputId {
		ENUMS(TR, 9),
		ENUMS(LFO, 9),
		OUTPUTS_LEN
	};
	enum LightIds {
		ENUMS(LIGHT, 6),
		LIGHTS_LEN
	};

	double mercury = 0.000000131523569023569; 		// was 7603200					
	double venus = 0.0000000515090078953007;		// was 19414080
	double earth = 0.0000000316924262707395;		// was 31553280
	double mars = 0.0000000168472693945765;			// was 59356800
	double jupiter = 0.00000000267237914432558;		// was 374198400
	double saturn = 0.00000000107695859998828;		// was 928540800
	double uranus = 0.000000000378373731539902;		// was 2642889600
	double neptune = 0.000000000193546389198563;	// was 5166720000
	double pluto = 0.000000000127805588273786;		// was 7824384000

	long timeSinceEpoch = std::chrono::duration_cast<std::chrono::hours>(std::chrono::system_clock::now().time_since_epoch()).count();
	long planetDur[9] = {7603200, 19414080, 31553280, 59356800, 374198400, 928540800, 2642889600, 5166720000, 7824384000}; 						// duration of each planet's year in seconds, used against timeSinceEpoch to figure out starting phase.
	double phase[9];

	// float realtime = 1.f;
	// float inADay = 365.2f;
	// float inAnHour = 8764.8f;
	// float inAMin = 525888.f;
	// float inASec = 31553280.f;
	// float inAMs = 31553280000.f;

	int counter = 2;
	int speedKnob = 1;
	float timeCompression = 1.f;
	float _gain = 5.f;
	float _2PI = 2.f * M_PI;

	dsp::PulseGenerator trig[9];

	double planet[9] = {mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto};
	double freq[9] = {mercury, venus, earth, mars, jupiter, saturn, uranus, neptune, pluto};

	PlanetaryLFOs() {
		for (int i = 0; i < 9; i ++){phase[i] = ((double)(timeSinceEpoch % planetDur[i]) / (double)planetDur[i])*_2PI;};
		config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
		configParam(SPEED_PARAM, 1, 1100, 1, "Time Compression");
		configOutput(TR + 0, "Mercury Trig");
		configOutput(TR + 1, "Venus Trig");
		configOutput(TR + 2, "Earth Trig");
		configOutput(TR + 3, "Mars Trig");
		configOutput(TR + 4, "Jupiter Trig");
		configOutput(TR + 5, "Saturn Trig");
		configOutput(TR + 6, "Uranus Trig");
		configOutput(TR + 7, "Neptune Trig");
		configOutput(TR + 8, "Pluto Trig");
		configOutput(LFO + 0, "Mercury LFO");
		configOutput(LFO + 1, "Venus LFO");
		configOutput(LFO + 2, "Earth LFO");
		configOutput(LFO + 3, "Mars LFO");
		configOutput(LFO + 4, "Jupiter LFO");
		configOutput(LFO + 5, "Saturn LFO");
		configOutput(LFO + 6, "Uranus LFO");
		configOutput(LFO + 7, "Neptune LFO");
		configOutput(LFO + 8, "Pluto LFO");
	}

	void process(const ProcessArgs& args) override {
		if (counter % 2 == 0){	// LFO's are slow, so run every other sample (could do half and half)
			for (int i = 0; i < 9; i ++){
				if (outputs[LFO + i].isConnected() || outputs[TR + i].isConnected()) {
					freq[i] = planet[i] * timeCompression;
					double phase_increment = _2PI * freq[i] / args.sampleRate;		// calc phase increment
					phase[i] += phase_increment;									// push osc forward 2 steps
					if (phase[i] >= _2PI){
						phase[i] -= _2PI;
						trig[i].trigger(0.01f);
					};
					double sine_output = _gain * sin(phase[i]);
					outputs[LFO + i].setVoltage(sine_output);
					outputs[TR + i].setVoltage(trig[i].process(args.sampleTime) ? 10.0f : 0.0f);
				};
			};
		};
		if (counter % 8 == 0){	// check speed knob every 8 samples
			if (speedKnob != params[SPEED_PARAM].getValue())
				updateTimeCompression();
		};
		counter++;
		if (counter > args.sampleRate)
			counter = 1;
	}

	void updateTimeCompression(){
		speedKnob = params[SPEED_PARAM].getValue();
		if (speedKnob <= 2){
			timeCompression = 1.f;
			lights[LIGHT + 0].setSmoothBrightness(1.f, 0.1f);
		} else if (speedKnob > 2 && speedKnob <=218){
			timeCompression = scaleKnobValue(speedKnob, 1, 218, 1.f, 365.2f);
			refreshLights();
		} else if (speedKnob > 218 && speedKnob <= 222){
			timeCompression = 365.2f;
			lights[LIGHT + 1].setSmoothBrightness(1.f, 0.1f);
		} else if (speedKnob > 222 && speedKnob <= 428){
			timeCompression = scaleKnobValue(speedKnob, 222, 428, 365.2f, 8764.8f);
			refreshLights();
		} else if (speedKnob > 428 && speedKnob <= 432){
			timeCompression = 8764.8f;
			lights[LIGHT + 2].setSmoothBrightness(1.f, 0.1f);
		} else if (speedKnob > 432 && speedKnob <= 648){
			timeCompression = scaleKnobValue(speedKnob, 432, 648, 8764.8f, 525888.f);
			refreshLights();
		} else if (speedKnob > 648 && speedKnob <= 652){
			timeCompression = 525888.f;
			lights[LIGHT + 3].setSmoothBrightness(1.f, 0.1f);
		} else if (speedKnob > 652 && speedKnob <= 878){
			timeCompression = scaleKnobValue(speedKnob, 652, 878, 525888.f, 31553280.f);
			refreshLights();
		} else if (speedKnob > 878 && speedKnob <= 882){
			timeCompression = 31553280.f;
			lights[LIGHT + 4].setSmoothBrightness(1.f, 0.1f);
		} else if (speedKnob > 882 && speedKnob < 1097){
			timeCompression = scaleKnobValue(speedKnob, 882, 1097, 31553280.f, 31553280000.f);
			refreshLights();
		} else if (speedKnob >= 1097){
			timeCompression = 31553280000.f;
			lights[LIGHT + 5].setSmoothBrightness(1.f, 0.1f);
		} else {
			timeCompression = 1.f;
			refreshLights();
		};
	}

	float scaleKnobValue(float speedKnob, int knobMin, int knobMax, float scaleMin, float scaleMax){
		float knobPercent = (speedKnob - knobMin)/(knobMax - knobMin);
		return (knobPercent) * (scaleMax - scaleMin) + scaleMin;
	}

	void refreshLights(){
		for(int i = 0; i<6; i++){
			lights[LIGHT + i].setSmoothBrightness(0.f, 0.1f);
		};
	}
};

I’ve left out the Struct portion in hopes of this post not being a mile long… Thanks all in advance

P.S. I’ve gotten one user reporting the LFO’s freeze sometimes and they think it might be how I’m only processing the LFO’s once every other sample, to try to cut down on processor load. I welcome any comments on my code, this is my first C++ project.

There’s nothing wrong with processing every other sample. See here for more on that.

you might get more help on this issue if you posted a link to a github repo where ppl could try this out?

1 Like

Thanks @Squinky, I think your docs on Github gave me the idea of skipping samples in the first place, so thank you for all the shared knowledge! Moving to lookup tables is on my to-do list for this module as well. FYI, the link for your Chebyshev-every-4-samples seems to be broken: Demo/docs/efficient-plugins.md at main · squinkylabs/Demo · GitHub

I…need to read up on best practices for how to use Github while working on new versions. I started late last night and just duplicated my local copy and started there. Not the best way I could have handled it, I’m sure. I assume I just branch it and work on the branch (and a quick Google confirmed this.)

1 Like

Ok, I believe I haven’t screwed anything up with the source control :stuck_out_tongue:

Trying what the kids would call a “no scope” here: sandbox is int timeSinceEpoch = std::chrono::duration_cast<std::chrono::seconds> but the module file has std::chrono::duration_cast<std::chrono::hours>; could it be as simple as that? You may be doing that conversion elsewhere but I didn’t spot it on a quick read, and the planetDur array seems to be seconds in both cases.

1 Like

facepalm Yep! Must’ve changed the code in the sandbox and not copy/pasted it back. UGH. Thank you…

1 Like

Sometimes all it takes is fresh eyes! :slight_smile: Cool module, by the way. I’m thinking about a quad performance that it may be perfect for!

2 Likes

Ooh, love working in quad. Best of luck, let me know how it goes! Interested in any feedback on the module you might have, as well. Thanks again :slight_smile:

1 Like

For sure/will do!

oh, tx. Yeah, that whole repo is gone. I should update… in my defense, that article is many years old… Someone else has been taking care of those modules. Over here: GitHub - kockie69/SquinkyVCV-main

2 Likes

my rule at work - put the time units in the variable name! We have so many things that are seconds and so many that are milliseconds…

4 Likes