// Mutable Instruments Streams emulation for VCV Rack // Copyright (C) 2020 Tyler Coy // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #pragma once #include #include #include "cv_scaler.hpp" #include "ui.hpp" #include namespace streams { using namespace rack; class DigitalEngine { public: static constexpr int kSampleRate = 31089; template struct ChannelFrame { // Parameters float shape_knob; float mod_knob; float level_mod_knob; float response_knob; bool function_button; // Inputs float excite_in[block_size]; float signal_in[block_size]; float level_adc_in[block_size]; // Outputs float dac_out[block_size]; float pwm_out[block_size]; // Lights float led_green[4]; float led_red[4]; }; template struct Frame { ChannelFrame ch1; ChannelFrame ch2; bool metering_button; }; DigitalEngine() { Reset(); } void Reset(void) { adc_.Init(); cv_scaler_.Init(&adc_); processor_[0].Init(0); processor_[1].Init(1); ui_.Init(&adc_, &cv_scaler_, &processor_[0]); pwm_value_[0] = 0; pwm_value_[1] = 0; for (int i = 0; i < 4; i++) { led_lpf_[i].reset(); led_lpf_[i].setLambda(kLambdaLEDs); } } const UiSettings& ui_settings(void) { return ui_.settings(); } void ApplySettings(const UiSettings& settings) { ui_.ApplySettings(settings); } void SyncUI(const DigitalEngine& other) { ui_.SyncUI(other.ui_); for (int i = 0; i < 4; i++) { led_lpf_[i].reset(); } } void Randomize(void) { UiSettings settings; settings.alternate[0] = random::u32() & 1; settings.alternate[1] = random::u32() & 1; int modulus0 = (settings.alternate[0]) ? 1 + PROCESSOR_FUNCTION_FILTER_CONTROLLER : 1 + PROCESSOR_FUNCTION_COMPRESSOR; int modulus1 = (settings.alternate[1]) ? 1 + PROCESSOR_FUNCTION_FILTER_CONTROLLER : 1 + PROCESSOR_FUNCTION_COMPRESSOR; settings.function[0] = random::u32() % modulus0; settings.function[1] = random::u32() % modulus1; settings.monitor_mode = ui_.settings().monitor_mode; settings.linked = false; ApplySettings(settings); } template void Process(Frame& frame) { ProcessUI(frame); for (int i = 0; i < block_size; i++) { float ch1_signal_adc = clamp(frame.ch1.signal_in[i], 0.f, kVdda); float ch2_signal_adc = clamp(frame.ch2.signal_in[i], 0.f, kVdda); float ch1_excite_adc = clamp(frame.ch1.excite_in[i], 0.f, kVdda); float ch2_excite_adc = clamp(frame.ch2.excite_in[i], 0.f, kVdda); float ch1_level_adc = clamp(frame.ch1.level_adc_in[i], 0.f, kVdda); float ch2_level_adc = clamp(frame.ch2.level_adc_in[i], 0.f, kVdda); adc_.cvs_[0] = std::round(0xFFFF * ch1_excite_adc / kVdda); adc_.cvs_[1] = std::round(0xFFFF * ch1_signal_adc / kVdda); adc_.cvs_[2] = std::round(0xFFFF * ch1_level_adc / kVdda); adc_.cvs_[3] = std::round(0xFFFF * ch2_excite_adc / kVdda); adc_.cvs_[4] = std::round(0xFFFF * ch2_signal_adc / kVdda); adc_.cvs_[5] = std::round(0xFFFF * ch2_level_adc / kVdda); uint16_t gain[2] = {0, 0}; uint16_t frequency[2] = {kPWMPeriod, kPWMPeriod}; for (int i = 0; i < 2; i++) { processor_[i].Process( cv_scaler_.audio_sample(i), cv_scaler_.excite_sample(i), &gain[i], &frequency[i]); // Filter PWM value frequency[i] = kPWMPeriod - frequency[i]; pwm_value_[i] += (frequency[i] - pwm_value_[i]) >> 4; } frame.ch1.dac_out[i] = kVoltsPerLSB * cv_scaler_.ScaleGain(0, gain[0]); frame.ch2.dac_out[i] = kVoltsPerLSB * cv_scaler_.ScaleGain(1, gain[1]); frame.ch1.pwm_out[i] = (kVdda / kPWMPeriod) * pwm_value_[0]; frame.ch2.pwm_out[i] = (kVdda / kPWMPeriod) * pwm_value_[1]; } } protected: using float_4 = simd::float_4; AdcEmulator adc_; CvScaler cv_scaler_; Processor processor_[2]; Ui ui_; uint16_t pwm_value_[2]; dsp::TExponentialFilter led_lpf_[4]; template void ProcessUI(Frame& frame) { float timestep = block_size * 1.f / kSampleRate; adc_.pots_[0] = std::round(0xFFFF * frame.ch1.shape_knob); adc_.pots_[1] = std::round(0xFFFF * frame.ch1.mod_knob); adc_.pots_[2] = std::round(0xFFFF * frame.ch2.shape_knob); adc_.pots_[3] = std::round(0xFFFF * frame.ch2.mod_knob); ui_.switches().SetPin(SWITCH_MODE_1, frame.ch1.function_button); ui_.switches().SetPin(SWITCH_MODE_2, frame.ch2.function_button); ui_.switches().SetPin(SWITCH_MONITOR, frame.metering_button); ui_.Poll(timestep * 1e6); ui_.DoEvents(); for (int i = 0; i < 4; i++) { auto led = float_4( ui_.leds().intensity_green(i), ui_.leds().intensity_red(i), ui_.leds().intensity_green(i + 4), ui_.leds().intensity_red(i + 4)); led = led_lpf_[i].process(timestep, led); frame.ch1.led_green[i] = led[0]; frame.ch1.led_red[i] = led[1]; frame.ch2.led_green[i] = led[2]; frame.ch2.led_red[i] = led[3]; } } static constexpr int kUiPollRate = 4000; static constexpr float kVdda = 3.3f; static constexpr int kPWMPeriod = 65535; static constexpr float kDacVref = 2.5f; static constexpr float kVoltsPerLSB = kDacVref / 65536.f; // The VU meter flickers when monitoring LEVEL or OUT when there is an // audio signal at the LEVEL input. Due to human persistence of vision, // the only noticable effect on the hardware module is a slight dimming of // the LEDs. However, the flickering is very apparent on the software module // due to the low UI refresh rate. We solve this by applying a lowpass // filter to the LED brightness. This lambda value is simply hand-tuned // to match hardware. static constexpr float kLambdaLEDs = 1.5e-3 * kSampleRate; }; }