using .isconnected() after module state restore with dataFromJson

Hello, I made a simple mute module, and I wanted to let the user choose to save module state in the patch, with a context menu entry.
So I used dataToJson and dataFromJson.
It works correctly with small patches, but with with more complex patches, the state is saved (I looked at patch.json), but when VCV is reloaded the module state isn’t reverted.
What I’m doing wrong?
Code follows.
‘stateRestore’ is the context menu variable and it works good with all patches.
‘mute’ variable isn’t reverted with complex patches

Thanks

#include "plugin.hpp"

struct TestModule : Module {
	enum ParamId {
		PARAMS_LEN
	};
	enum InputId {
		TRIG_INPUT,
		IN_INPUT,
		INPUTS_LEN
	};
	enum OutputId {
		OUT_OUTPUT,
		OUTPUTS_LEN
	};
	enum LightId {
		OFF_LIGHT,
		ON_LIGHT,
		LIGHTS_LEN
	};

	bool stateRestore = true;

	bool mute = false;
	bool changes = true;

	bool trigConnection = false;
	bool prevTrigConnection = true;

	float trigValue = 0;
	float prevTrigValue = 0;

	TestModule() {
		config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
		configInput(TRIG_INPUT, "Trig/Gate");
		configInput(IN_INPUT, "IN");
		configOutput(OUT_OUTPUT, "OUT");
	}

	void onReset() override {
		stateRestore = true;
		mute = false;
		changes = true;
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();
		json_object_set_new(rootJ, "StateRestore", json_boolean(stateRestore));
		json_object_set_new(rootJ, "Mute", json_boolean(mute));
		return rootJ;
	}
	
	void dataFromJson(json_t* rootJ) override {
		json_t* stateRestoreJ = json_object_get(rootJ, "StateRestore");
		if (stateRestoreJ)
			stateRestore = json_boolean_value(stateRestoreJ);

		if (stateRestore) {
			json_t* muteJ = json_object_get(rootJ, "Mute");
			if (muteJ)
				mute = json_boolean_value(muteJ);
		}
	}

	void process(const ProcessArgs& args) override {

		trigConnection = inputs[TRIG_INPUT].isConnected();

		if (trigConnection != prevTrigConnection) {
			changes = true;
			if (trigConnection)
				mute = false;
			prevTrigConnection = trigConnection;
		}

		if (trigConnection) {

			trigValue = inputs[TRIG_INPUT].getVoltage();
			if (trigValue >= 1 && prevTrigValue < 1){
				mute = !mute;
				changes = true;
			}
			prevTrigValue = trigValue;

			if (changes) {
				if (mute) {
					lights[OFF_LIGHT].setBrightness(1.f);
					lights[ON_LIGHT].setBrightness(0.f);
				} else {
					lights[OFF_LIGHT].setBrightness(0.f);
					lights[ON_LIGHT].setBrightness(1.f);
				}
			}

		} else {

			if (changes) {
				lights[OFF_LIGHT].setBrightness(0.f);
				lights[ON_LIGHT].setBrightness(0.f);
				mute = true;
			}

		}

		if (mute) {
			if (changes)
				outputs[OUT_OUTPUT].setVoltage(0);
		} else {
			outputs[OUT_OUTPUT].setVoltage(inputs[IN_INPUT].getVoltage());
		}

		if (changes)
			changes = false;

	}
};

struct TestModuleWidget : ModuleWidget {
	TestModuleWidget(TestModule* module) {
		setModule(module);
		setPanel(createPanel(asset::plugin(pluginInstance, "res/TestModule.svg")));

		addChild(createWidget<ScrewBlack>(Vec(0, 0)));
		addChild(createWidget<ScrewBlack>(Vec(box.size.x - RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 26)), module, TestModule::TRIG_INPUT));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 55.5)), module, TestModule::IN_INPUT));

		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(7.62, 109)), module, TestModule::OUT_OUTPUT));

		addChild(createLightCentered<SmallLight<GreenLight>>(mm2px(Vec(12, 105.5)), module, TestModule::ON_LIGHT));
		addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(12, 114.5)), module, TestModule::OFF_LIGHT));
	}

	void appendContextMenu(Menu* menu) override {
		TestModule* module = dynamic_cast<TestModule*>(this->module);

		menu->addChild(new MenuSeparator());
		menu->addChild(createBoolPtrMenuItem("Restore State on Load", "", &module->stateRestore));
	}
};

Model* modelTestModule = createModel<TestModule, TestModuleWidget>("TestModule");

Hello @sickozell,

First of all, welcome to the community! Let me see if I can help you out. Your code looks a bit unusual to me, and I’m wondering if maybe I can help clarify what I would do in your situation.

dataToJson and dataFromJson are called by VCV Rack automatically at the appropriate time. For example, when you load a patch, dataFromJson is called. When you make changes to a module, such as changing a parameter value, dataToJson is called. Typically you wouldn’t need a flag that says, “Hey VCV, the user wishes to save the state.” Saving the state is pretty much standard practice. Same with restoring a state. It’s unusual to add code that allows a user to restore a previous state. Previous states are automatically restored when the patch is loaded.

I hope that isn’t confusing. Let me state is another way.

Place code in dataToJson() for saving stuff that isn’t automatically saved for you. Parameters (knobs, switches, and stuff) are saved for you, and so you don’t have to worry about it.

Place code in dataFromJson() to load stuff that you saved with dataToJson(). Again, it’s perfectly normal for this to be empty.

Let’s talk a bit about your specific case! It looks like you have an input, called TRIG_INPUT, which toggles whether the mute is active (muting) or inactive (not muting).

I think you would always want to save this state? I don’t see much reason that user would ever want to not save the state. Therefore, I think it would be safe to remove the context menu “Restore State on Load” option. Most modules will restore state on load. As a user, this is definitely desired behavior! :slight_smile:

There’s another thing that jumps out to me. Typically, if you are dealing with a trigger style input (one that transitions from 0 to 1, then back to 0 quickly), it’s common to use a “dsp::SchmittTrigger” object. The SchmittTrigger is a helper that frees you from having to use a “previous” variable, like you did with prevTrigConnection.

Here’s my rewrite of your code. Please forgive me if I made any mistakes. I haven’t tested this code:

#include "plugin.hpp"

struct TestModule : Module {
	enum ParamId {
		PARAMS_LEN
	};
	enum InputId {
		TOGGLE_MUTE_INPUT,
		IN_INPUT,
		INPUTS_LEN
	};
	enum OutputId {
		OUT_OUTPUT,
		OUTPUTS_LEN
	};
	enum LightId {
		OFF_LIGHT,
		ON_LIGHT,
		LIGHTS_LEN
	};

	bool mute = false;
	dsp::SchmittTrigger muteTrigger;

	TestModule() {
		config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
		configInput(TOGGLE_MUTE_INPUT, "Trig/Gate");
		configInput(IN_INPUT, "IN");
		configOutput(OUT_OUTPUT, "OUT");
	}

	void onReset() override {
		mute = false;
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();
		json_object_set_new(rootJ, "Mute", json_boolean(mute));
		return rootJ;
	}
	
	void dataFromJson(json_t* rootJ) override {
		json_t* muteJ = json_object_get(rootJ, "Mute");
		if (muteJ) mute = json_boolean_value(muteJ);
	}

	void process(const ProcessArgs& args) override {

		if (muteTrigger.process(rescale(inputs[TOGGLE_MUTE_INPUT].getVoltage(), 0.0f, 10.0f, 0.f, 1.f))) {
			mute = !mute;
		}

		lights[OFF_LIGHT].setBrightness(mute);
		lights[ON_LIGHT].setBrightness(! mute);

		if (mute) {
			outputs[OUT_OUTPUT].setVoltage(0);
		} else {
			outputs[OUT_OUTPUT].setVoltage(inputs[IN_INPUT].getVoltage());
		}
	}
};

struct TestModuleWidget : ModuleWidget {
	TestModuleWidget(TestModule* module) {
		setModule(module);
		setPanel(createPanel(asset::plugin(pluginInstance, "res/TestModule.svg")));

		addChild(createWidget<ScrewBlack>(Vec(0, 0)));
		addChild(createWidget<ScrewBlack>(Vec(box.size.x - RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 26)), module, TestModule::TRIG_INPUT));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 55.5)), module, TestModule::IN_INPUT));

		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(7.62, 109)), module, TestModule::OUT_OUTPUT));

		addChild(createLightCentered<SmallLight<GreenLight>>(mm2px(Vec(12, 105.5)), module, TestModule::ON_LIGHT));
		addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(12, 114.5)), module, TestModule::OFF_LIGHT));
	}
};

Model* modelTestModule = createModel<TestModule, TestModuleWidget>("TestModule");
4 Likes

and, btw, those JSON functions do work with complex patches :wink:

2 Likes

Thanks @clone45 for your welcome and quick reply. You’ve been very clear, but let me explain: I thought about the ‘Save State’ option if someone would like to dont’ revert last state as usually hardware modules do. I remember to have read it somewhere, and if it’s true, I thought maybe someone would like it ( not me and most of people in effect :wink: ). But this aspect is not so important.

Unfortunately, I’m a goat in C++ programming, and more unfortunately I haven’t ‘enough life-time’ to learn it well, so I have to code in my only known… bad way.
I tried your code and it works good in all situations, so I rewrote it without SchmittTrigger (with previous state variable), and it worked good too.

I’m sure what I wrote: my starting code restores state in vcv build environment and regular windows installation, but only with very few modules patches. When I add more, it doesn’t restore its state on reload.

yes @Squinky , I understand it sounds impossible. I also thought it could be the fault of the windows version of vcv. or maybe pc performance, don’t know.

At the moment my plan is to add features to this test module step by step, as my starting intentions. For example lights off when no trig cable is connected, and a reset input. I have to say that the goal is not this simple mute/unmute module, but is to implement a good working state-restore in my other existing modules :slight_smile:

btw: Voxglitch and SquinkyLabs are great plugins !

1 Like

Because restoring state is the default in VCV Rack, having an option to do that seems redundant, but if you flip the meaning, and rename it to “Initialize on start” I think it might make more sense. Initialize, I think, should always initialize to “factory defaults”, and so you have an option to do that “on startup” or not.

Thanks @EnigmaCurry , it’s a good idea, and I’ve done that.

I started my testings, but didn’t find a solution. The issue I found, starts already with a patch like this.

When I added the 10th VCO (sometimes less, sometimes more) and restarted vcv, the previous state of mute module was not restored.

Follows the code I used. It differs from the one written by @clone45 in the way that if no trigger input is connected, both the lights stay off.
I tested on a windows pc either with asio, wasapi or directsound driver. I thought also that the fault would be from my build environment, but my other modules released in the official library have the same problem.
I didn’t notice any problem with plugin from others.
I really can’t figure out why this happens.

#include "plugin.hpp"

struct TestModule : Module {
	enum ParamId {
		PARAMS_LEN
	};
	enum InputId {
		TOGGLE_MUTE_INPUT,
		IN_INPUT,
		INPUTS_LEN
	};
	enum OutputId {
		OUT_OUTPUT,
		OUTPUTS_LEN
	};
	enum LightId {
		OFF_LIGHT,
		ON_LIGHT,
		LIGHTS_LEN
	};

	bool initStart = false;

	bool mute = false;

	//float trigValue = 0;
	//float prevTrigValue = 0;
	dsp::SchmittTrigger muteTrigger;

	TestModule() {
		config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
		configInput(TOGGLE_MUTE_INPUT, "Trig/Gate");
		configInput(IN_INPUT, "IN");
		configOutput(OUT_OUTPUT, "OUT");
	}

	void onReset() override {
		initStart = false;
		mute = false;
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();
		json_object_set_new(rootJ, "InitStart", json_boolean(initStart));
		json_object_set_new(rootJ, "Mute", json_boolean(mute));
		return rootJ;
	}
	
	void dataFromJson(json_t* rootJ) override {
		json_t* initStartJ = json_object_get(rootJ, "InitStart");
		if (initStartJ)
			initStart = json_boolean_value(initStartJ);

		if (!initStart) {
			json_t* muteJ = json_object_get(rootJ, "Mute");
			if (muteJ)
				mute = json_boolean_value(muteJ);
		}
	}

	void process(const ProcessArgs& args) override {

		if (inputs[TOGGLE_MUTE_INPUT].isConnected()) {

			//trigValue = inputs[TOGGLE_MUTE_INPUT].getVoltage();
			//if (trigValue >= 1 && prevTrigValue < 1){
			//	mute = !mute;
			//}
			//prevTrigValue = trigValue;

			if (muteTrigger.process(rescale(inputs[TOGGLE_MUTE_INPUT].getVoltage(), 0.0f, 10.0f, 0.f, 1.f))) {
				mute = !mute;
			}
			lights[OFF_LIGHT].setBrightness(mute);
			lights[ON_LIGHT].setBrightness(!mute);
		} else {
			if (mute)
				mute = false;
			lights[OFF_LIGHT].setBrightness(0);
			lights[ON_LIGHT].setBrightness(0);
		}
		if (mute) {
			outputs[OUT_OUTPUT].setVoltage(0);
		} else {
			outputs[OUT_OUTPUT].setVoltage(inputs[IN_INPUT].getVoltage());
		}
	}
};

struct TestModuleWidget : ModuleWidget {
	TestModuleWidget(TestModule* module) {
		setModule(module);
		setPanel(createPanel(asset::plugin(pluginInstance, "res/TestModule.svg")));

		addChild(createWidget<ScrewBlack>(Vec(0, 0)));
		addChild(createWidget<ScrewBlack>(Vec(box.size.x - RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 26)), module, TestModule::TOGGLE_MUTE_INPUT));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 55.5)), module, TestModule::IN_INPUT));

		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(7.62, 109)), module, TestModule::OUT_OUTPUT));

		addChild(createLightCentered<SmallLight<GreenLight>>(mm2px(Vec(12, 105.5)), module, TestModule::ON_LIGHT));
		addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(12, 114.5)), module, TestModule::OFF_LIGHT));
	}

	void appendContextMenu(Menu* menu) override {
		TestModule* module = dynamic_cast<TestModule*>(this->module);

		menu->addChild(new MenuSeparator());
		menu->addChild(createBoolPtrMenuItem("Initialize on Start", "", &module->initStart));
	}
	
};

Model* modelTestModule = createModel<TestModule, TestModuleWidget>("TestModule");

Hi @sickozell ,

Hmm… I’m not seeing anything that jumps out to me. However, let’s do some experiments! If you comment out

Test #1:

/*
	void onReset() override {
		initStart = false;
		mute = false;
	}
*/

… then does it load correctly?

Test #2:

When you shut down rack, rack creates a snapshot of the path in /Rack2/autosave/patch.json. Could you check that snapshot and make sure that the InitStart and Mute are saving correctly?

Test #3:

The version of OnReset() that you are using is deprecated.

Try…

  void onReset(const ResetEvent &e) override
  {
    initStart = false;
    mute = false;
    Module::onReset(e);
  }

I would try commenting out most of your process() and see if it works. Specifically I wonder if

if (inputs[TOGGLE_MUTE_INPUT].isConnected())

is strictly always true on startup? Because if its not, your process immediately sets mute = false. So comment that out and maybe it works?

1 Like

@EnigmaCurry Very clever!

Now it works :slight_smile:
commenting onReset didn’t solve, but I changed it as @clone45 suggested.
Then I checked that InitStart and Mute were stored correctly, so dataToJson was working fine.
@EnigmaCurry had intuition.
In effect, also visually, when the patch is loaded, it seems that cables are actually wired after the initialization of modules.
So I rewrote the code with variables storing the connection state and the previous one.
Now it works everypatch! THANKS!

void process(const ProcessArgs& args) override {

		trigConnection = inputs[TOGGLE_MUTE_INPUT].isConnected();
		if (trigConnection) {

			if (muteTrigger.process(rescale(inputs[TOGGLE_MUTE_INPUT].getVoltage(), 0.0f, 10.0f, 0.f, 1.f))) {
				mute = !mute;
			}
			
			lights[OFF_LIGHT].setBrightness(mute);
			lights[ON_LIGHT].setBrightness(!mute);
		} else {
			if (prevTrigConnection && mute)
				mute = false;
			lights[OFF_LIGHT].setBrightness(0);
			lights[ON_LIGHT].setBrightness(0);
		}
		prevTrigConnection = trigConnection;

		if (mute) {
			outputs[OUT_OUTPUT].setVoltage(0);
		} else {
			outputs[OUT_OUTPUT].setVoltage(inputs[IN_INPUT].getVoltage());
		}
	}
3 Likes

so now that it works can you change the title of this thread?

Sorry, @Squinky : solution flagged, should I modify title too?

1 Like

Yes, the title isn’t accurate

1 Like