BogaudioModules

BogaudioModules for VCV Rack
Log | Files | Refs | README | LICENSE

Ranalyzer.cpp (13839B)


      1 
      2 #include "Ranalyzer.hpp"
      3 
      4 #define TRIGGER_ON_LOAD "triggerOnLoad"
      5 #define DISPLAY_TRACES "display_traces"
      6 #define DISPLAY_TRACES_ALL "all"
      7 #define DISPLAY_TRACES_TEST_RETURN "test_return"
      8 #define DISPLAY_TRACES_ANALYSIS "analysis"
      9 #define WINDOW_TYPE "window_type"
     10 #define WINDOW_TYPE_NONE "none"
     11 #define WINDOW_TYPE_TAPER "taper"
     12 #define WINDOW_TYPE_HAMMING "hamming"
     13 #define WINDOW_TYPE_KAISER "Kaiser"
     14 
     15 void Ranalyzer::reset() {
     16 	_trigger.reset();
     17 	_triggerPulseGen.process(10.0f);
     18 	_eocPulseGen.process(10.0f);
     19 	_core.resetChannels();
     20 	_chirp.reset();
     21 	_run = false;
     22 }
     23 
     24 void Ranalyzer::sampleRateChange() {
     25 	reset();
     26 	_sampleRate = APP->engine->getSampleRate();
     27 	_sampleTime = 1.0f / _sampleRate;
     28 	_maxFrequency = roundf(maxFrequencyNyquistRatio * _sampleRate);
     29 	_chirp.setSampleRate(_sampleRate);
     30 	_rangeMinHz = 0.0f;
     31 	_rangeMaxHz = 0.5f * _sampleRate;
     32 	if (_sampleRate >= 96000.0f) {
     33 		_core.setParams(_sampleRate, 1, AnalyzerCore::QUALITY_FIXED_32K, AnalyzerCore::WINDOW_NONE);
     34 	}
     35 	else {
     36 		_core.setParams(_sampleRate, 1, AnalyzerCore::QUALITY_FIXED_16K, AnalyzerCore::WINDOW_NONE);
     37 	}
     38 	setWindow(_windowType);
     39 	_run = false;
     40 	_flush = false;
     41 	if (!_initialDelay) {
     42 		_initialDelay = new Timer(_sampleRate, initialDelaySeconds);
     43 	}
     44 }
     45 
     46 json_t* Ranalyzer::saveToJson(json_t* root) {
     47 	frequencyPlotToJson(root);
     48 	frequencyRangeToJson(root);
     49 	amplitudePlotToJson(root);
     50 	json_object_set_new(root, TRIGGER_ON_LOAD, json_boolean(_triggerOnLoad));
     51 
     52 	switch (_displayTraces) {
     53 		case ALL_TRACES: {
     54 			json_object_set_new(root, DISPLAY_TRACES, json_string(DISPLAY_TRACES_ALL));
     55 			break;
     56 		}
     57 		case TEST_RETURN_TRACES: {
     58 			json_object_set_new(root, DISPLAY_TRACES, json_string(DISPLAY_TRACES_TEST_RETURN));
     59 			break;
     60 		}
     61 		case ANALYSIS_TRACES: {
     62 			json_object_set_new(root, DISPLAY_TRACES, json_string(DISPLAY_TRACES_ANALYSIS));
     63 			break;
     64 		}
     65 	}
     66 
     67 	switch (_windowType) {
     68 		case NONE_WINDOW_TYPE: {
     69 			json_object_set_new(root, WINDOW_TYPE, json_string(WINDOW_TYPE_NONE));
     70 			break;
     71 		}
     72 		case TAPER_WINDOW_TYPE: {
     73 			json_object_set_new(root, WINDOW_TYPE, json_string(WINDOW_TYPE_TAPER));
     74 			break;
     75 		}
     76 		case HAMMING_WINDOW_TYPE: {
     77 			json_object_set_new(root, WINDOW_TYPE, json_string(WINDOW_TYPE_HAMMING));
     78 			break;
     79 		}
     80 		case KAISER_WINDOW_TYPE: {
     81 			json_object_set_new(root, WINDOW_TYPE, json_string(WINDOW_TYPE_KAISER));
     82 			break;
     83 		}
     84 	}
     85 
     86 	return root;
     87 }
     88 
     89 void Ranalyzer::loadFromJson(json_t* root) {
     90 	frequencyPlotFromJson(root);
     91 	frequencyRangeFromJson(root);
     92 	amplitudePlotFromJson(root);
     93 
     94 	json_t* t = json_object_get(root, TRIGGER_ON_LOAD);
     95 	if (t) {
     96 		_triggerOnLoad = json_boolean_value(t);
     97 	}
     98 
     99 	json_t* dt = json_object_get(root, DISPLAY_TRACES);
    100 	if (dt) {
    101 		std::string dts = json_string_value(dt);
    102 		if (dts == DISPLAY_TRACES_ALL) {
    103 			setDisplayTraces(ALL_TRACES);
    104 		}
    105 		else if (dts == DISPLAY_TRACES_TEST_RETURN) {
    106 			setDisplayTraces(TEST_RETURN_TRACES);
    107 		}
    108 		else if (dts == DISPLAY_TRACES_ANALYSIS) {
    109 			setDisplayTraces(ANALYSIS_TRACES);
    110 		}
    111 	}
    112 
    113 	json_t* wt = json_object_get(root, WINDOW_TYPE);
    114 	if (wt) {
    115 		std::string wts = json_string_value(wt);
    116 		if (wts == WINDOW_TYPE_NONE) {
    117 			setWindow(NONE_WINDOW_TYPE);
    118 		}
    119 		else if (wts == WINDOW_TYPE_TAPER) {
    120 			setWindow(TAPER_WINDOW_TYPE);
    121 		}
    122 		else if (wts == WINDOW_TYPE_HAMMING) {
    123 			setWindow(HAMMING_WINDOW_TYPE);
    124 		}
    125 		else if (wts == WINDOW_TYPE_KAISER) {
    126 			setWindow(KAISER_WINDOW_TYPE);
    127 		}
    128 	}
    129 }
    130 
    131 void Ranalyzer::modulate() {
    132 	_rangeMinHz = 0.0f;
    133 	_rangeMaxHz = 0.5f * _sampleRate;
    134 	if (_range < 0.0f) {
    135 		_rangeMaxHz *= 1.0f + _range;
    136 	}
    137 	else if (_range > 0.0f) {
    138 		_rangeMinHz = _range * _rangeMaxHz;
    139 	}
    140 
    141 	_exponential = params[EXPONENTIAL_PARAM].getValue() > 0.5f;
    142 	_loop = params[LOOP_PARAM].getValue() > 0.5f;
    143 	_returnSampleDelay = clamp((int)roundf(params[DELAY_PARAM].getValue()), 2, maxResponseDelay);
    144 
    145 	_frequency1 = clamp(params[FREQUENCY1_PARAM].getValue(), 0.0f, 1.0f);
    146 	_frequency1 *= _frequency1;
    147 	_frequency1 *= _maxFrequency - minFrequency;
    148 	_frequency1 += minFrequency;
    149 
    150 	_frequency2 = clamp(params[FREQUENCY2_PARAM].getValue(), 0.0f, 1.0f);
    151 	_frequency2 *= _frequency2;
    152 	_frequency2 *= _maxFrequency - minFrequency;
    153 	_frequency2 += minFrequency;
    154 }
    155 
    156 void Ranalyzer::processAll(const ProcessArgs& args) {
    157 	bool maybeTriggerOnLoad = false;
    158 	if (_initialDelay && !_initialDelay->next()) {
    159 		maybeTriggerOnLoad = true;
    160 		delete _initialDelay;
    161 		_initialDelay = NULL;
    162 	}
    163 
    164 	bool triggered = _trigger.process(params[TRIGGER_PARAM].getValue()*5.0f + inputs[TRIGGER_INPUT].getVoltage());
    165 	if (!_run) {
    166 		if (triggered || (!_initialDelay && _loop) || (maybeTriggerOnLoad && _triggerOnLoad)) {
    167 			_run = true;
    168 			_outBufferCount = _currentReturnSampleDelay = _returnSampleDelay;
    169 			_chirp.reset();
    170 			_cycleN = _core.size();
    171 			_cycleI = 0;
    172 			_chirp.setParams(_frequency1, _frequency2, _core.size() / (double)_sampleRate, !_exponential);
    173 			_triggerPulseGen.trigger(0.001f);
    174 			_useTestInput = inputs[TEST_INPUT].isConnected();
    175 		}
    176 	}
    177 
    178 	float out = 0.0f;
    179 	if (_run) {
    180 		if (_useTestInput) {
    181 			out = inputs[TEST_INPUT].getVoltage();
    182 		}
    183 		else {
    184 			out = _chirp.next() * 5.0f;
    185 		}
    186 
    187 		_inputBuffer.push(out);
    188 		if (_outBufferCount > 0) {
    189 			--_outBufferCount;
    190 		}
    191 		else {
    192 			float w = _window ? _window->at(_cycleI - _currentReturnSampleDelay) : 1.0f;
    193 			_core.stepChannelSample(0, w * _inputBuffer.value(_currentReturnSampleDelay - 1));
    194 			_core.stepChannelSample(1, w * inputs[RETURN_INPUT].getVoltage());
    195 		}
    196 
    197 		++_cycleI;
    198 		if (_cycleI >= _cycleN) {
    199 			_run = false;
    200 			_flush = true;
    201 			_analysisBufferCount = _currentReturnSampleDelay;
    202 		}
    203 	}
    204 	if (_flush) {
    205 		float w = _window ? _window->at(_cycleN - _analysisBufferCount) : 1.0f;
    206 		_core.stepChannelSample(0, w * _inputBuffer.value((_run ? _currentReturnSampleDelay : _analysisBufferCount) - 1));
    207 		_core.stepChannelSample(1, w * inputs[RETURN_INPUT].getVoltage());
    208 		--_analysisBufferCount;
    209 		if (_analysisBufferCount < 1) {
    210 			_flush = false;
    211 			_eocPulseGen.trigger(0.001f);
    212 		}
    213 	}
    214 
    215 	outputs[SEND_OUTPUT].setVoltage(out);
    216 	outputs[TRIGGER_OUTPUT].setVoltage(_triggerPulseGen.process(_sampleTime) * 5.0f);
    217 	outputs[EOC_OUTPUT].setVoltage(_eocPulseGen.process(_sampleTime) * 5.0f);
    218 }
    219 
    220 void Ranalyzer::setDisplayTraces(Traces traces) {
    221 	_displayTraces = traces;
    222 	if (_channelDisplayListener) {
    223 		switch (_displayTraces) {
    224 			case ALL_TRACES: {
    225 				_channelDisplayListener->displayChannels(true, true, true);
    226 				break;
    227 			}
    228 			case TEST_RETURN_TRACES: {
    229 				_channelDisplayListener->displayChannels(true, true, false);
    230 				break;
    231 			}
    232 			case ANALYSIS_TRACES: {
    233 				_channelDisplayListener->displayChannels(false, false, true);
    234 				break;
    235 			}
    236 		}
    237 	}
    238 }
    239 
    240 void Ranalyzer::setChannelDisplayListener(ChannelDisplayListener* listener) {
    241 	_channelDisplayListener = listener;
    242 }
    243 
    244 void Ranalyzer::setWindow(WindowType wt) {
    245 	if (!_window || _windowType != wt || _window->size() != _core.size()) {
    246 		if (_window) {
    247 			delete _window;
    248 			_window = NULL;
    249 		}
    250 
    251 		_windowType = wt;
    252 		switch (_windowType) {
    253 			case NONE_WINDOW_TYPE: break;
    254 			case TAPER_WINDOW_TYPE: {
    255 				_window = new PlanckTaperWindow(_core.size(), (int)(_core.size() * 0.03f));
    256 				break;
    257 			}
    258 			case HAMMING_WINDOW_TYPE: {
    259 				_window = new HammingWindow(_core.size());
    260 				break;
    261 			}
    262 			case KAISER_WINDOW_TYPE: {
    263 				_window = new KaiserWindow(_core.size());
    264 				break;
    265 			}
    266 		}
    267 	}
    268 }
    269 
    270 
    271 struct AnalysisBinsReader : AnalyzerDisplay::BinsReader {
    272 	float* _testBins;
    273 	float* _responseBins;
    274 
    275 	AnalysisBinsReader(float* testBins, float* responseBins) : _testBins(testBins), _responseBins(responseBins) {}
    276 
    277 	float at(int i) override {
    278 		float test = AnalyzerDisplay::binValueToDb(_testBins[i]);
    279 		float response = AnalyzerDisplay::binValueToDb(_responseBins[i]);
    280 		return AnalyzerDisplay::dbToBinValue(response - test);
    281 	}
    282 
    283 	static std::unique_ptr<BinsReader> factory(AnalyzerCore& core) {
    284 		assert(core._nChannels == 3);
    285 		return std::unique_ptr<BinsReader>(new AnalysisBinsReader(core.getBins(0), core.getBins(1)));
    286 	}
    287 };
    288 
    289 
    290 struct RanalyzerDisplay : AnalyzerDisplay, ChannelDisplayListener {
    291 	RanalyzerDisplay(Ranalyzer* module, Vec size, bool drawInset)
    292 	: AnalyzerDisplay(module, size, drawInset)
    293 	{}
    294 
    295 	void displayChannels(bool c0, bool c1, bool c2) override {
    296 		displayChannel(0, c0);
    297 		displayChannel(1, c1);
    298 		displayChannel(2, c2);
    299 	}
    300 
    301 	void drawHeader(const DrawArgs& args, float rangeMinHz, float rangeMaxHz) override {
    302 		nvgSave(args.vg);
    303 
    304 		const int textY = -4;
    305 		const int charPx = 5;
    306 		int x = _insetAround + 2;
    307 
    308 		std::string s = format("Bin width %0.1f HZ", APP->engine->getSampleRate() / (float)(_module->_core._size / _module->_core._binAverageN));
    309 		drawText(args, s.c_str(), x, _insetTop + textY);
    310 		x += s.size() * charPx + 20;
    311 
    312 		const char* labels[3] = { "TEST", "RESPONSE", "ANALYSIS" };
    313 		for (int i = 0; i < 3; ++i) {
    314 			if (_displayChannel[i]) {
    315 				auto color = _channelColors[i % channelColorsN];
    316 				nvgStrokeColor(args.vg, color);
    317 				nvgStrokeWidth(args.vg, std::max(1.0f, 3.0f - getZoom()));
    318 				nvgBeginPath(args.vg);
    319 				float lineY = _insetTop - 7.0f;
    320 				nvgMoveTo(args.vg, x, lineY);
    321 				x += 10.0f;
    322 				nvgLineTo(args.vg, x, lineY);
    323 				x += 3.0f;
    324 				nvgStroke(args.vg);
    325 
    326 				drawText(args, labels[i], x, _insetTop + textY, 0.0, &color);
    327 				x += strlen(labels[i]) * charPx + 20;
    328 			}
    329 		}
    330 
    331 		nvgRestore(args.vg);
    332 	}
    333 };
    334 
    335 
    336 struct RanalyzerWidget : AnalyzerBaseWidget {
    337 	static constexpr int hp = 45;
    338 
    339 	RanalyzerWidget(Ranalyzer* module) {
    340 		setModule(module);
    341 		box.size = Vec(RACK_GRID_WIDTH * hp, RACK_GRID_HEIGHT);
    342 		setPanel(box.size, "Ranalyzer", false);
    343 
    344 		{
    345 			auto inset = Vec(75, 1);
    346 			auto size = Vec(box.size.x - inset.x - 1, 378);
    347 			auto display = new RanalyzerDisplay(module, size, false);
    348 			display->box.pos = inset;
    349 			display->box.size = size;
    350 			if (module) {
    351 				display->setChannelBinsReaderFactory(2, AnalysisBinsReader::factory);
    352 				module->setChannelDisplayListener(display);
    353 				display->channelLabel(0, "Test");
    354 				display->channelLabel(1, "Response");
    355 				display->channelLabel(2, "Analysis");
    356 			}
    357 			addChild(display);
    358 		}
    359 
    360 		// generated by svg_widgets.rb
    361 		auto frequency1ParamPosition = Vec(24.5, 42.0);
    362 		auto frequency2ParamPosition = Vec(24.5, 103.5);
    363 		auto triggerParamPosition = Vec(18.0, 154.0);
    364 		auto exponentialParamPosition = Vec(23.0, 213.0);
    365 		auto loopParamPosition = Vec(62.0, 213.0);
    366 		auto delayParamPosition = Vec(29.5, 249.5);
    367 
    368 		auto triggerInputPosition = Vec(40.5, 151.0);
    369 		auto testInputPosition = Vec(30.5, 181.0);
    370 		auto returnInputPosition = Vec(40.5, 323.0);
    371 
    372 		auto triggerOutputPosition = Vec(10.5, 286.0);
    373 		auto eocOutputPosition = Vec(40.5, 286.0);
    374 		auto sendOutputPosition = Vec(10.5, 323.0);
    375 		// end generated by svg_widgets.rb
    376 
    377 		{
    378 			auto w = createParam<Knob26>(frequency1ParamPosition, module, Ranalyzer::FREQUENCY1_PARAM);
    379 			auto k = dynamic_cast<BGKnob*>(w);
    380 			k->skinChanged("dark");
    381 			addParam(w);
    382 		}
    383 		{
    384 			auto w = createParam<Knob26>(frequency2ParamPosition, module, Ranalyzer::FREQUENCY2_PARAM);
    385 			auto k = dynamic_cast<BGKnob*>(w);
    386 			k->skinChanged("dark");
    387 			addParam(w);
    388 		}
    389 		addParam(createParam<Button18>(triggerParamPosition, module, Ranalyzer::TRIGGER_PARAM));
    390 		addParam(createParam<IndicatorButtonGreen9>(exponentialParamPosition, module, Ranalyzer::EXPONENTIAL_PARAM));
    391 		addParam(createParam<IndicatorButtonGreen9>(loopParamPosition, module, Ranalyzer::LOOP_PARAM));
    392 		addParam(createParam<Knob16>(delayParamPosition, module, Ranalyzer::DELAY_PARAM));
    393 
    394 		addInput(createInput<Port24>(triggerInputPosition, module, Ranalyzer::TRIGGER_INPUT));
    395 		addInput(createInput<Port24>(testInputPosition, module, Ranalyzer::TEST_INPUT));
    396 		addInput(createInput<Port24>(returnInputPosition, module, Ranalyzer::RETURN_INPUT));
    397 
    398 		addOutput(createOutput<Port24>(triggerOutputPosition, module, Ranalyzer::TRIGGER_OUTPUT));
    399 		addOutput(createOutput<Port24>(eocOutputPosition, module, Ranalyzer::EOC_OUTPUT));
    400 		addOutput(createOutput<Port24>(sendOutputPosition, module, Ranalyzer::SEND_OUTPUT));
    401 	}
    402 
    403 	void contextMenu(Menu* menu) override {
    404 		auto a = dynamic_cast<Ranalyzer*>(module);
    405 		assert(a);
    406 
    407 		menu->addChild(new MenuLabel());
    408 		{
    409 			OptionsMenuItem* mi = new OptionsMenuItem("Display traces");
    410 			mi->addItem(OptionMenuItem("All", [a]() { return a->_displayTraces == Ranalyzer::ALL_TRACES; }, [a]() { a->setDisplayTraces(Ranalyzer::ALL_TRACES); }));
    411 			mi->addItem(OptionMenuItem("Analysis only", [a]() { return a->_displayTraces == Ranalyzer::ANALYSIS_TRACES; }, [a]() { a->setDisplayTraces(Ranalyzer::ANALYSIS_TRACES); }));
    412 			mi->addItem(OptionMenuItem("Test/return only", [a]() { return a->_displayTraces == Ranalyzer::TEST_RETURN_TRACES; }, [a]() { a->setDisplayTraces(Ranalyzer::TEST_RETURN_TRACES); }));
    413 			OptionsMenuItem::addToMenu(mi, menu);
    414 		}
    415 		{
    416 			OptionsMenuItem* mi = new OptionsMenuItem("Window");
    417 			mi->addItem(OptionMenuItem("None", [a]() { return a->_windowType == Ranalyzer::NONE_WINDOW_TYPE; }, [a]() { a->setWindow(Ranalyzer::NONE_WINDOW_TYPE); }));
    418 			mi->addItem(OptionMenuItem("Taper", [a]() { return a->_windowType == Ranalyzer::TAPER_WINDOW_TYPE; }, [a]() { a->setWindow(Ranalyzer::TAPER_WINDOW_TYPE); }));
    419 			mi->addItem(OptionMenuItem("Hamming", [a]() { return a->_windowType == Ranalyzer::HAMMING_WINDOW_TYPE; }, [a]() { a->setWindow(Ranalyzer::HAMMING_WINDOW_TYPE); }));
    420 			mi->addItem(OptionMenuItem("Kaiser", [a]() { return a->_windowType == Ranalyzer::KAISER_WINDOW_TYPE; }, [a]() { a->setWindow(Ranalyzer::KAISER_WINDOW_TYPE); }));
    421 			OptionsMenuItem::addToMenu(mi, menu);
    422 		}
    423 		addFrequencyPlotContextMenu(menu);
    424 		addFrequencyRangeContextMenu(menu);
    425 		addAmplitudePlotContextMenu(menu, false);
    426 		menu->addChild(new BoolOptionMenuItem("Trigger on load", [a]() { return &a->_triggerOnLoad; }));
    427 	}
    428 };
    429 
    430 Model* modelRanalyzer = createModel<Ranalyzer, RanalyzerWidget>("Bogaudio-Ranalyzer", "RANALYZER", "Swept-sine frequency response analyzer", "Visual");