#include "global_pre.hpp" #include #include #include #include #include "alikins.hpp" #include "ui.hpp" #include "global_ui.hpp" namespace rack_plugin_Alikins { struct SpecificValue : Module { enum ParamIds { VALUE1_PARAM, OCTAVE_PARAM, NUM_PARAMS }; enum InputIds { VALUE1_INPUT, NUM_INPUTS }; enum OutputIds { VALUE1_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; SpecificValue() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} float A440_octave = 4.0f; void step() override; // TODO: toJson/fromJson for saving values float volt_value; float hz_value; float period_value; float cents_value; }; struct NoteInfo { std::string name; float offset_cents; }; // TODO: mv to header float A440_VOLTAGE = 4.75f; int A440_MIDI_NUMBER = 69; // TODO: support other enharmonic names Db G♭ A♯? std::vector note_name_vec = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; std::unordered_map gen_note_name_map() { float volt = -10.0f; std::string fs = note_name_vec[4]; std::unordered_map note_name_map; // FIXME: add a map of note name (including enharmonic) to voltage offset from C // then just iterate over it for each octave for (int i = -10; i <= 10; i++) { for (int j = 0; j < 12; j++) { // debug("oct=%d note=%s volt=%f ", i, note_name_vec[j].c_str(), volt); note_name_map[stringf("%s%d", note_name_vec[j].c_str(), i)] = volt; volt += (1.0f / 12.0f); } } return note_name_map; } std::unordered_map note_name_to_volts_map = gen_note_name_map(); // FIXME: can/should be inline // FIXME: likely should be a NoteInfo type/struct/object // These are assuming A440 == A4 == 4.75v float freq_to_cv(float freq, float a440_octave) { float volts = log2f(freq / 440.0f * powf(2.0f, A440_VOLTAGE)) - a440_octave; // debug("freq_to_vc freq=%f a440_octave=%f volts=%f A440_voltage=%f", freq, a440_octave, volts, A440_VOLTAGE); return volts; } float cv_to_freq(float volts, float a440_octave) { float freq = 440.0f / powf(2.0f, A440_VOLTAGE) * powf(2.0f, volts + a440_octave); // debug("cv_to_freq freq=%f a440_octave=%f volts=%f A440_voltage=%f", freq, a440_octave, volts, A440_VOLTAGE); return freq; } // can return negative float volts_of_nearest_note(float volts) { float res = roundf( (volts * 12.0f) ) / 12.0f; return res; } int volts_to_note(float volts) { int res = abs(static_cast( roundf( (volts * 12.0f) ) ) ) % 12; // debug("volts_to_note volts=%f res=%d", volts, res); return res; } int volts_to_octave(float volts, float a440_octave) { // debug("a440_octave=%f", a440_octave); int octave = floor(volts + a440_octave); // debug("volts_to_octaves volts=%f, a440_octave=%f, octave=%d", volts, a440_octave, octave); return octave; } float volts_to_note_cents(float volts, float a440_octave) { float nearest_note = volts_of_nearest_note(volts); float cent_volt = 1.0f / 12.0f / 100.0f; float offset_cents = (volts-nearest_note)/cent_volt; // debug("volts: %f volts_of_nearest: %f volts-volts_nearest: %f offset_cents %f", // volts, nearest_note, volts-nearest_note, offset_cents); return offset_cents; } int volts_to_midi(float volts, float a440_octave) { int midi_note = floor(volts * 12.0f + a440_octave) + 21; return midi_note; } void SpecificValue::step() { A440_octave = params[OCTAVE_PARAM].value; if (inputs[VALUE1_INPUT].active) { params[VALUE1_PARAM].value = inputs[VALUE1_INPUT].value; } volt_value = params[VALUE1_PARAM].value; outputs[VALUE1_OUTPUT].value = volt_value; } struct FloatField : TextField { float value; SpecificValue *module; FloatField(SpecificValue *_module); void onAction(EventAction &e) override; void onChange(EventChange &e) override; float textToVolts(std::string field_text); std::string voltsToText(float param_volts); }; FloatField::FloatField(SpecificValue *_module) { module = _module; value = module->params[SpecificValue::VALUE1_PARAM].value; text = voltsToText(value); } // TODO: this is really data stuff, so could be in type/struct/class for the data (volt, freq/hz, period/seconds, note_name) // and instanced and provided to a generic ValueField widget that has-a data type converter thingy float FloatField::textToVolts(std::string field_text) { return atof(field_text.c_str()); } std::string FloatField::voltsToText(float param_volts){ return stringf("%0.3f", param_volts); } void FloatField::onChange(EventChange &e) { //debug("FloatField onChange text=%s param=%f", text.c_str(), module->params[SpecificValue::VALUE1_PARAM].value); if (this != RACK_PLUGIN_UI_FOCUSED_WIDGET) { std::string new_text = voltsToText(module->params[SpecificValue::VALUE1_PARAM].value); setText(new_text); } } void FloatField::onAction(EventAction &e) { //debug("FloatField onAction text=%s", text.c_str()); //update text first? TextField::onAction(e); float volts = textToVolts(text); //debug("FloatField setting volts=%f text=%s", volts, text.c_str()); module->params[SpecificValue::VALUE1_PARAM].value = volts; //debug("FloatField onAction2 text=%s volts=%f module->volt_values=%f", // text.c_str(), volts, module->volt_value); } struct HZFloatField : TextField { float value; SpecificValue *module; HZFloatField(SpecificValue *_module); void onChange(EventChange &e) override; void onAction(EventAction &e) override; float textToVolts(std::string field_text); std::string voltsToText(float param_volts); }; HZFloatField::HZFloatField(SpecificValue *_module) { module = _module; } float HZFloatField::textToVolts(std::string field_text) { float freq = strtof(text.c_str(), NULL); return freq_to_cv(freq, module->A440_octave); } std::string HZFloatField::voltsToText(float param_volts){ float freq = cv_to_freq(param_volts, module->A440_octave); std::string new_text = stringf("%0.*f", freq < 100 ? 4 : 3, freq); return new_text; } void HZFloatField::onChange(EventChange &e) { //debug("HZFloatField onChange text=%s param=%f", text.c_str(), module->params[SpecificValue::VALUE1_PARAM].value); //TextField::onChange(e); if (this != RACK_PLUGIN_UI_FOCUSED_WIDGET) { std::string new_text = voltsToText(module->params[SpecificValue::VALUE1_PARAM].value); setText(new_text); } } void HZFloatField::onAction(EventAction &e) { //debug("HZFloatField onAction text=%s", text.c_str()); //update text first? TextField::onAction(e); float volts = textToVolts(text); //debug("HZ FloatField onAction about to set VALUE*_PARAM to volts: %f", volts); module->params[SpecificValue::VALUE1_PARAM].value = volts; } struct SecondsFloatField : TextField { float value; SpecificValue *module; SecondsFloatField(SpecificValue *_module); void onAction(EventAction &e) override; void onChange(EventChange &e) override; float textToVolts(std::string field_text); std::string voltsToText(float param_volts); }; SecondsFloatField::SecondsFloatField(SpecificValue *_module) { module = _module; } float SecondsFloatField::textToVolts(std::string field_text) { float period = strtof(text.c_str(), NULL); float freq = 1.0f / period; return freq_to_cv(freq, module->A440_octave); } std::string SecondsFloatField::voltsToText(float param_volts){ float period = 1.0f / cv_to_freq(param_volts, module->A440_octave); std::string new_text = stringf("%0.*f", period < 100 ? 4 : 3, period); return new_text; } void SecondsFloatField::onChange(EventChange &e) { //debug("SecondsFloatField onChange text=%s param=%f", text.c_str(), module->params[SpecificValue::VALUE1_PARAM].value); //TextField::onChange(e); if (this != RACK_PLUGIN_UI_FOCUSED_WIDGET) { std::string new_text = voltsToText(module->params[SpecificValue::VALUE1_PARAM].value); setText(new_text); } } void SecondsFloatField::onAction(EventAction &e) { //debug("SecondsFloatField onAction text=%s", text.c_str()); //update text first? TextField::onAction(e); float volts = textToVolts(text); //debug("SecondsFloatField onAction about to set VALUE*_PARAM to volts: %f", volts); module->params[SpecificValue::VALUE1_PARAM].value = volts; } struct CentsField : TextField { float value; SpecificValue *module; CentsField(SpecificValue *_module); void onChange(EventChange &e) override; void onAction(EventAction &e) override; }; CentsField::CentsField(SpecificValue *_module) { module = _module; } void CentsField::onChange(EventChange &e) { // debug("CentsField onChange"); float cents = volts_to_note_cents(module->params[SpecificValue::VALUE1_PARAM].value, module->params[SpecificValue::OCTAVE_PARAM].value); // debug("CentsField onChange cents: %f", cents); if (this != RACK_PLUGIN_UI_FOCUSED_WIDGET || fabs(cents) >= 0.50f) { float cents = volts_to_note_cents(module->params[SpecificValue::VALUE1_PARAM].value, module->params[SpecificValue::OCTAVE_PARAM].value); std::string new_text = stringf("% 0.2f", cents); setText(new_text); } } void CentsField::onAction(EventAction &e) { TextField::onAction(e); float cents = strtof(text.c_str(), NULL); // figure what to tweak the current volts float cent_volt = 1.0f / 12.0f / 100.0f; float delta_volt = cents * cent_volt; float nearest_note_voltage = volts_of_nearest_note(module->params[SpecificValue::VALUE1_PARAM].value); //debug("volts: %f nearest_volts: %f", module->params[SpecificValue::VALUE1_PARAM].value, nearest_note_voltage); //debug("delta_volt: %+f nearest_note_voltage+delta_volt: %f", delta_volt, nearest_note_voltage, // nearest_note_voltage + delta_volt); module->params[SpecificValue::VALUE1_PARAM].value = nearest_note_voltage + delta_volt; } struct NoteNameField : TextField { float value; SpecificValue *module; NoteNameField(SpecificValue *_module); void onChange(EventChange &e) override; void onAction(EventAction &e) override; }; NoteNameField::NoteNameField(SpecificValue *_module) { module = _module; } void NoteNameField::onChange(EventChange &e) { //debug("NoteNameField onChange text=%s param=%f", text.c_str(), module->params[SpecificValue::VALUE1_PARAM].value); //TextField::onChange(e); if (this != RACK_PLUGIN_UI_FOCUSED_WIDGET) { float cv_volts = module->params[SpecificValue::VALUE1_PARAM].value; int octave = volts_to_octave(cv_volts, module->params[SpecificValue::OCTAVE_PARAM].value); int note_number = volts_to_note(cv_volts); // float semi_cents = volts_to_note_and_cents(cv_volts, module->params[SpecificValue::OCTAVE_PARAM].value); // note_info = volts_to_note_info(cv_volts, module->params[SpecificValue::OCTAVE_PARAM].value); // TODO: modf for oct/fract part, need to get +/- cents from chromatic notes std::string new_text = stringf("%s%d", note_name_vec[note_number].c_str(), octave); // debug("foo %f bar %f", ) setText(new_text); } } void NoteNameField::onAction(EventAction &e) { //debug("NoteNameField onAction"); TextField::onAction(e); // FIXME: Haven't tested but seems like this does a lot. // FIXME: I suspect just a array of structs with name/freq in it and a linear search makes more sense // but lets c++ stuff auto search = note_name_to_volts_map.find(text); if(search != note_name_to_volts_map.end()) { /* debug("note_name_to_volts_map[%s] = %f (%f) %f", text.c_str(), note_name_to_volts_map[text], (note_name_to_volts_map[text] - module->A440_octave), module->A440_octave ); */ module->params[SpecificValue::VALUE1_PARAM].value = note_name_to_volts_map[text] - module->A440_octave; return; } else { // TODO: change the text color to indicate bogus name? debug("%s was NOT A VALID note name", text.c_str()); return; } } struct SmallPurpleTrimpot : Trimpot { SmallPurpleTrimpot(); }; SmallPurpleTrimpot::SmallPurpleTrimpot() : Trimpot() { setSVG(SVG::load(assetPlugin(plugin, "res/SmallPurpleTrimpot.svg"))); shadow->blurRadius = 0.0; shadow->opacity = 0.10; shadow->box.pos = Vec(0.0, box.size.y * 0.1); } struct PurpleTrimpot : Trimpot { Module *module; bool initialized = false; PurpleTrimpot(); void step() override; void reset() override; void randomize() override; }; PurpleTrimpot::PurpleTrimpot() : Trimpot() { setSVG(SVG::load(assetPlugin(plugin, "res/PurpleTrimpot.svg"))); shadow->blurRadius = 0.0; shadow->opacity = 0.10; shadow->box.pos = Vec(0.0, box.size.y * 0.05); } // FIXME: if we are getting moving inputs and we are hovering // over the trimpot, we kind of jitter arround. // maybe run this via an onChange()? void PurpleTrimpot::step() { //debug("paramId=%d this->initialized: %d initialized: %d this->value: %f value: %f param.value: %f", // paramId, this->initialized, initialized, this->value, value, module->params[paramId].value); if (this->value != module->params[paramId].value) { if (this != RACK_PLUGIN_UI_HOVERED_WIDGET && this->initialized) { // this->value = module->params[paramId].value; setValue(module->params[paramId].value); } else { module->params[paramId].value = this->value; this->initialized |= true; } EventChange e; onChange(e); } Trimpot::step(); } void PurpleTrimpot::reset() { this->initialized = false; Trimpot::reset(); } void PurpleTrimpot::randomize() { reset(); setValue(rescale(randomUniform(), 0.0f, 1.0f, minValue, maxValue)); } struct SpecificValueWidget : ModuleWidget { SpecificValueWidget(SpecificValue *module); void step() override; void onChange(EventChange &e) override; float prev_volts = 0.0f; float prev_octave = 4.0f; float prev_input = 0.0f; FloatField *volts_field; HZFloatField *hz_field; SecondsFloatField *period_field; NoteNameField *note_name_field; CentsField *cents_field; }; SpecificValueWidget::SpecificValueWidget(SpecificValue *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/SpecificValue.svg"))); // TODO: widget with these children? float y_baseline = 45.0f; Vec volt_field_size = Vec(70.0f, 22.0f); Vec hz_field_size = Vec(70.0, 22.0f); Vec seconds_field_size = Vec(70.0, 22.0f); float x_pos = 10.0f; // debug("adding field %d", i); y_baseline = 45.0f; volts_field = new FloatField(module); volts_field->box.pos = Vec(x_pos, y_baseline); volts_field->box.size = volt_field_size; volts_field->value = module->params[SpecificValue::VALUE1_PARAM].value; addChild(volts_field); y_baseline = 90.0f; float h_pos = x_pos; hz_field = new HZFloatField(module); hz_field->box.pos = Vec(x_pos, y_baseline); hz_field->box.size = hz_field_size; hz_field->value = module->hz_value; addChild(hz_field); y_baseline = 135.0f; period_field = new SecondsFloatField(module); period_field->box.pos = Vec(h_pos, y_baseline); period_field->box.size = seconds_field_size; period_field->value = module->period_value; addChild(period_field); y_baseline = 180.0f; note_name_field = new NoteNameField(module); note_name_field->box.pos = Vec(x_pos, y_baseline); note_name_field->box.size = Vec(70.0f, 22.0f); note_name_field->value = module->volt_value; addChild(note_name_field); y_baseline += note_name_field->box.size.y; y_baseline += 5.0f; // y_baseline += 20.0f; cents_field = new CentsField(module); cents_field->box.pos = Vec(x_pos, y_baseline); cents_field->box.size = Vec(55.0f, 22.0f); cents_field->value = module->cents_value; addChild(cents_field); // y_baseline += period_field->box.size.y; y_baseline += 20.0f; float middle = box.size.x / 2.0f; float in_port_x = 15.0f; y_baseline += 24.0f + 12.0f; Port *value_in_port = Port::create( Vec(in_port_x, y_baseline), Port::INPUT, module, SpecificValue::VALUE1_INPUT); //value_in_port->box.pos = Vec(middle - value_in_port->box.size.x / 2, y_baseline); value_in_port->box.pos = Vec(2.0f, y_baseline); inputs.push_back(value_in_port); addChild(value_in_port); // octave trimpot SmallPurpleTrimpot *octaveTrimpot = ParamWidget::create( Vec(middle, y_baseline + 2.5f), module, SpecificValue::OCTAVE_PARAM, 0.0f, 8.0f, 4.0f); params.push_back(octaveTrimpot); octaveTrimpot->box.pos = Vec(middle - octaveTrimpot->box.size.x / 2, y_baseline + 2.5f); octaveTrimpot->snap = true; addChild(octaveTrimpot); float out_port_x = middle + 24.0f; Port *value_out_port = Port::create( Vec(out_port_x, y_baseline), Port::OUTPUT, module, SpecificValue::VALUE1_OUTPUT); outputs.push_back(value_out_port); value_out_port->box.pos = Vec(box.size.x - value_out_port->box.size.x - 2.0f, y_baseline); addChild(value_out_port); y_baseline += value_out_port->box.size.y; y_baseline += 16.0f; PurpleTrimpot *trimpot = ParamWidget::create( Vec(middle - 24.0f, y_baseline + 2.5f), module, SpecificValue::VALUE1_PARAM, -10.0f, 10.0f, 0.0f); //debug(" trimpot: dv: %f v: %f p.value: %f", trimpot->defaultValue, trimpot->value, // module->params[SpecificValue::VALUE1_PARAM].value); params.push_back(trimpot); addChild(trimpot); } void SpecificValueWidget::step() { ModuleWidget::step(); if (prev_volts != module->params[SpecificValue::VALUE1_PARAM].value || prev_octave != module->params[SpecificValue::OCTAVE_PARAM].value || prev_input != module->params[SpecificValue::VALUE1_INPUT].value) { // debug("SpVWidget step - emitting EventChange / onChange prev_volts=%f param=%f", // prev_volts, module->params[SpecificValue::VALUE1_PARAM].value); prev_volts = module->params[SpecificValue::VALUE1_PARAM].value; prev_octave = module->params[SpecificValue::OCTAVE_PARAM].value; prev_input = module->params[SpecificValue::VALUE1_INPUT].value; EventChange e; onChange(e); } } void SpecificValueWidget::onChange(EventChange &e) { // debug("SpvWidget onChange"); ModuleWidget::onChange(e); volts_field->onChange(e); hz_field->onChange(e); period_field->onChange(e); note_name_field->onChange(e); cents_field->onChange(e); } } // namespace rack_plugin_Alikins using namespace rack_plugin_Alikins; RACK_PLUGIN_MODEL_INIT(Alikins, SpecificValue) { Model *modelSpecificValue = Model::create( "Alikins", "SpecificValue", "Specific Values", UTILITY_TAG); return modelSpecificValue; }