@@ -1,117 +1,496 @@ | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#include <string> | |||||
#include <algorithm> | |||||
#include "plugin.hpp" | #include "plugin.hpp" | ||||
#include "Streams/streams.hpp" | |||||
namespace streams | |||||
{ | |||||
struct StreamsChannelMode | |||||
{ | |||||
ProcessorFunction function; | |||||
bool alternate; | |||||
std::string label; | |||||
}; | |||||
static constexpr int kNumChannelModes = 10; | |||||
static const StreamsChannelMode kChannelModeTable[kNumChannelModes] = | |||||
{ | |||||
{PROCESSOR_FUNCTION_ENVELOPE, false, "Envelope"}, | |||||
{PROCESSOR_FUNCTION_VACTROL, false, "Vactrol"}, | |||||
{PROCESSOR_FUNCTION_FOLLOWER, false, "Follower"}, | |||||
{PROCESSOR_FUNCTION_COMPRESSOR, false, "Compressor"}, | |||||
{PROCESSOR_FUNCTION_ENVELOPE, true, "AR envelope"}, | |||||
{PROCESSOR_FUNCTION_VACTROL, true, "Plucked vactrol"}, | |||||
{PROCESSOR_FUNCTION_FOLLOWER, true, "Cutoff controller"}, | |||||
{PROCESSOR_FUNCTION_COMPRESSOR, true, "Slow compressor"}, | |||||
{PROCESSOR_FUNCTION_FILTER_CONTROLLER, true, "Direct VCF controller"}, | |||||
{PROCESSOR_FUNCTION_LORENZ_GENERATOR, false, "Lorenz generator"}, | |||||
}; | |||||
struct StreamsMonitorMode | |||||
{ | |||||
MonitorMode mode; | |||||
std::string label; | |||||
}; | |||||
static constexpr int kNumMonitorModes = 4; | |||||
struct Streams : Module { | |||||
enum ParamIds { | |||||
BUTTON_1_PARAM, | |||||
BUTTON_2_PARAM, | |||||
SHAPE_1_PARAM, | |||||
SHAPE_2_PARAM, | |||||
MOD_1_PARAM, | |||||
MOD_2_PARAM, | |||||
METER_PARAM, | |||||
KNOB_1_PARAM, | |||||
LEVEL_MOD_1_PARAM, | |||||
LEVEL_MOD_2_PARAM, | |||||
KNOB_2_PARAM, | |||||
NUM_PARAMS | |||||
}; | |||||
enum InputIds { | |||||
EXCITE_1_INPUT, | |||||
IN_1_INPUT, | |||||
IN_2_INPUT, | |||||
EXCITE_2_INPUT, | |||||
LEVEL_1_INPUT, | |||||
LEVEL_2_INPUT, | |||||
NUM_INPUTS | |||||
}; | |||||
enum OutputIds { | |||||
OUT_1_OUTPUT, | |||||
OUT_2_OUTPUT, | |||||
NUM_OUTPUTS | |||||
}; | |||||
enum LightIds { | |||||
METER_1_LIGHT, | |||||
CIRCLE2329_LIGHT, | |||||
CIRCLE2323_LIGHT, | |||||
CIRCLE2331_LIGHT, | |||||
CIRCLE2325_LIGHT, | |||||
CIRCLE2333_LIGHT, | |||||
CIRCLE2327_LIGHT, | |||||
CIRCLE2335_LIGHT, | |||||
NUM_LIGHTS | |||||
}; | |||||
Streams() { | |||||
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||||
configParam(BUTTON_1_PARAM, 0.f, 1.f, 0.f, ""); | |||||
configParam(BUTTON_2_PARAM, 0.f, 1.f, 0.f, ""); | |||||
configParam(SHAPE_1_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(SHAPE_2_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(MOD_1_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(MOD_2_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(METER_PARAM, 0.f, 1.f, 0.f, ""); | |||||
configParam(KNOB_1_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(LEVEL_MOD_1_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(LEVEL_MOD_2_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
configParam(KNOB_2_PARAM, 0.f, 1.f, 0.5f, ""); | |||||
} | |||||
void process(const ProcessArgs& args) override { | |||||
lights[METER_1_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2329_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2323_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2331_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2325_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2333_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2327_LIGHT].setBrightness(1); | |||||
lights[CIRCLE2335_LIGHT].setBrightness(1); | |||||
} | |||||
static const StreamsMonitorMode kMonitorModeTable[kNumMonitorModes] = | |||||
{ | |||||
{MONITOR_MODE_EXCITE_IN, "Excite"}, | |||||
{MONITOR_MODE_VCA_CV, "Level"}, | |||||
{MONITOR_MODE_AUDIO_IN, "In"}, | |||||
{MONITOR_MODE_OUTPUT, "Out"}, | |||||
}; | }; | ||||
} | |||||
struct Streams : Module | |||||
{ | |||||
enum ParamIds | |||||
{ | |||||
CH1_SHAPE_PARAM, | |||||
CH1_MOD_PARAM, | |||||
CH1_LEVEL_MOD_PARAM, | |||||
CH1_RESPONSE_PARAM, | |||||
CH2_SHAPE_PARAM, | |||||
CH2_MOD_PARAM, | |||||
CH2_LEVEL_MOD_PARAM, | |||||
CH2_RESPONSE_PARAM, | |||||
CH1_FUNCTION_BUTTON_PARAM, | |||||
CH2_FUNCTION_BUTTON_PARAM, | |||||
METERING_BUTTON_PARAM, | |||||
NUM_PARAMS | |||||
}; | |||||
enum InputIds | |||||
{ | |||||
CH1_EXCITE_INPUT, | |||||
CH1_SIGNAL_INPUT, | |||||
CH1_LEVEL_INPUT, | |||||
CH2_EXCITE_INPUT, | |||||
CH2_SIGNAL_INPUT, | |||||
CH2_LEVEL_INPUT, | |||||
NUM_INPUTS | |||||
}; | |||||
enum OutputIds | |||||
{ | |||||
CH1_SIGNAL_OUTPUT, | |||||
CH2_SIGNAL_OUTPUT, | |||||
NUM_OUTPUTS | |||||
}; | |||||
enum LightIds | |||||
{ | |||||
CH1_LIGHT_1_G, | |||||
CH1_LIGHT_1_R, | |||||
CH1_LIGHT_2_G, | |||||
CH1_LIGHT_2_R, | |||||
CH1_LIGHT_3_G, | |||||
CH1_LIGHT_3_R, | |||||
CH1_LIGHT_4_G, | |||||
CH1_LIGHT_4_R, | |||||
CH2_LIGHT_1_G, | |||||
CH2_LIGHT_1_R, | |||||
CH2_LIGHT_2_G, | |||||
CH2_LIGHT_2_R, | |||||
CH2_LIGHT_3_G, | |||||
CH2_LIGHT_3_R, | |||||
CH2_LIGHT_4_G, | |||||
CH2_LIGHT_4_R, | |||||
NUM_LIGHTS | |||||
}; | |||||
static constexpr int kNumEngines = 16; | |||||
streams::StreamsEngine engine_[kNumEngines]; | |||||
float brightness_[NUM_LIGHTS][kNumEngines]; | |||||
int prev_num_channels_; | |||||
Streams() | |||||
{ | |||||
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||||
configParam(CH1_SHAPE_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH1_MOD_PARAM, 0.f, 1.f, 0.5f); | |||||
configParam(CH1_LEVEL_MOD_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH2_SHAPE_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH2_MOD_PARAM, 0.f, 1.f, 0.5f); | |||||
configParam(CH2_LEVEL_MOD_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH1_RESPONSE_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH2_RESPONSE_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH1_FUNCTION_BUTTON_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(CH2_FUNCTION_BUTTON_PARAM, 0.f, 1.f, 0.f); | |||||
configParam(METERING_BUTTON_PARAM, 0.f, 1.f, 0.f); | |||||
onReset(); | |||||
} | |||||
void onReset() override | |||||
{ | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].Reset(); | |||||
for (int i = 0; i < NUM_LIGHTS; i++) | |||||
{ | |||||
brightness_[c][i] = 0.f; | |||||
} | |||||
} | |||||
prev_num_channels_ = 1; | |||||
onSampleRateChange(); | |||||
} | |||||
void onSampleRateChange() override | |||||
{ | |||||
float sample_rate = APP->engine->getSampleRate(); | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].SetSampleRate(sample_rate); | |||||
} | |||||
} | |||||
json_t* dataToJson() override | |||||
{ | |||||
streams::UiSettings settings = engine_[0].ui_settings(); | |||||
json_t* root_j = json_object(); | |||||
json_object_set_new(root_j, "function1", json_integer(settings.function[0])); | |||||
json_object_set_new(root_j, "function2", json_integer(settings.function[1])); | |||||
json_object_set_new(root_j, "alternate1", json_integer(settings.alternate[0])); | |||||
json_object_set_new(root_j, "alternate2", json_integer(settings.alternate[1])); | |||||
json_object_set_new(root_j, "monitor_mode", json_integer(settings.monitor_mode)); | |||||
json_object_set_new(root_j, "linked", json_integer(settings.linked)); | |||||
return root_j; | |||||
} | |||||
void dataFromJson(json_t* root_j) override | |||||
{ | |||||
json_t* function1_j = json_object_get(root_j, "function1"); | |||||
json_t* function2_j = json_object_get(root_j, "function2"); | |||||
json_t* alternate1_j = json_object_get(root_j, "alternate1"); | |||||
json_t* alternate2_j = json_object_get(root_j, "alternate2"); | |||||
json_t* monitor_mode_j = json_object_get(root_j, "monitor_mode"); | |||||
json_t* linked_j = json_object_get(root_j, "linked"); | |||||
streams::UiSettings settings = {}; | |||||
if (function1_j) settings.function[0] = json_integer_value(function1_j); | |||||
if (function2_j) settings.function[1] = json_integer_value(function2_j); | |||||
if (alternate1_j) settings.alternate[0] = json_integer_value(alternate1_j); | |||||
if (alternate2_j) settings.alternate[1] = json_integer_value(alternate2_j); | |||||
if (monitor_mode_j) settings.monitor_mode = json_integer_value(monitor_mode_j); | |||||
if (linked_j) settings.linked = json_integer_value(linked_j); | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].ApplySettings(settings); | |||||
} | |||||
} | |||||
void onRandomize() override | |||||
{ | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].Randomize(); | |||||
} | |||||
} | |||||
void ToggleLink() | |||||
{ | |||||
streams::UiSettings settings = engine_[0].ui_settings(); | |||||
settings.linked ^= 1; | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].ApplySettings(settings); | |||||
} | |||||
} | |||||
void SetChannelMode(int channel, int mode_id) | |||||
{ | |||||
streams::UiSettings settings = engine_[0].ui_settings(); | |||||
settings.function[channel] = streams::kChannelModeTable[mode_id].function; | |||||
settings.alternate[channel] = streams::kChannelModeTable[mode_id].alternate; | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].ApplySettings(settings); | |||||
} | |||||
} | |||||
void SetMonitorMode(int mode_id) | |||||
{ | |||||
streams::UiSettings settings = engine_[0].ui_settings(); | |||||
settings.monitor_mode = streams::kMonitorModeTable[mode_id].mode; | |||||
for (int c = 0; c < kNumEngines; c++) | |||||
{ | |||||
engine_[c].ApplySettings(settings); | |||||
} | |||||
} | |||||
int function(int channel) | |||||
{ | |||||
return engine_[0].ui_settings().function[channel]; | |||||
} | |||||
int alternate(int channel) | |||||
{ | |||||
return engine_[0].ui_settings().alternate[channel]; | |||||
} | |||||
struct StreamsWidget : ModuleWidget { | |||||
StreamsWidget(Streams* module) { | |||||
setModule(module); | |||||
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Streams.svg"))); | |||||
addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0))); | |||||
addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); | |||||
addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||||
addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||||
addParam(createParamCentered<TL1105>(mm2px(Vec(24.715, 15.025)), module, Streams::BUTTON_1_PARAM)); | |||||
addParam(createParamCentered<TL1105>(mm2px(Vec(36.135, 15.025)), module, Streams::BUTTON_2_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(11.065, 21.055)), module, Streams::SHAPE_1_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(49.785, 21.055)), module, Streams::SHAPE_2_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(11.065, 44.555)), module, Streams::MOD_1_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(49.785, 44.555)), module, Streams::MOD_2_PARAM)); | |||||
addParam(createParamCentered<TL1105>(mm2px(Vec(30.425, 46.775)), module, Streams::METER_PARAM)); | |||||
addParam(createParamCentered<Trimpot>(mm2px(Vec(30.425, 60.745)), module, Streams::KNOB_1_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSRed>(mm2px(Vec(11.065, 68.045)), module, Streams::LEVEL_MOD_1_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSGreen>(mm2px(Vec(49.785, 68.045)), module, Streams::LEVEL_MOD_2_PARAM)); | |||||
addParam(createParamCentered<Trimpot>(mm2px(Vec(30.425, 75.345)), module, Streams::KNOB_2_PARAM)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.506, 96.615)), module, Streams::EXCITE_1_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(23.116, 96.615)), module, Streams::IN_1_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(37.726, 96.615)), module, Streams::IN_2_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(52.335, 96.615)), module, Streams::EXCITE_2_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.506, 111.225)), module, Streams::LEVEL_1_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(52.335, 111.225)), module, Streams::LEVEL_2_INPUT)); | |||||
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(23.116, 111.225)), module, Streams::OUT_1_OUTPUT)); | |||||
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(37.726, 111.225)), module, Streams::OUT_2_OUTPUT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(24.715, 22.005)), module, Streams::METER_1_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(36.135, 22.005)), module, Streams::CIRCLE2329_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(24.715, 27.725)), module, Streams::CIRCLE2323_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(36.135, 27.725)), module, Streams::CIRCLE2331_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(24.715, 33.445)), module, Streams::CIRCLE2325_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(36.135, 33.445)), module, Streams::CIRCLE2333_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(24.715, 39.166)), module, Streams::CIRCLE2327_LIGHT)); | |||||
addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(36.135, 39.166)), module, Streams::CIRCLE2335_LIGHT)); | |||||
} | |||||
bool linked() | |||||
{ | |||||
return engine_[0].ui_settings().linked; | |||||
} | |||||
int monitor_mode() | |||||
{ | |||||
return engine_[0].ui_settings().monitor_mode; | |||||
} | |||||
void process(const ProcessArgs& args) override | |||||
{ | |||||
int num_channels = std::max(inputs[CH1_SIGNAL_INPUT].getChannels(), | |||||
inputs[CH2_SIGNAL_INPUT].getChannels()); | |||||
num_channels = std::max(num_channels, 1); | |||||
if (num_channels > prev_num_channels_) | |||||
{ | |||||
for (int c = prev_num_channels_; c < num_channels; c++) | |||||
{ | |||||
engine_[c].SyncUI(engine_[0]); | |||||
} | |||||
} | |||||
prev_num_channels_ = num_channels; | |||||
// Reuse the same frame object for multiple engines because the params | |||||
// aren't touched. | |||||
streams::StreamsEngine::Frame frame; | |||||
frame.ch1.shape_knob = params[CH1_SHAPE_PARAM] .getValue(); | |||||
frame.ch1.mod_knob = params[CH1_MOD_PARAM] .getValue(); | |||||
frame.ch1.level_mod_knob = params[CH1_LEVEL_MOD_PARAM].getValue(); | |||||
frame.ch1.response_knob = params[CH1_RESPONSE_PARAM] .getValue(); | |||||
frame.ch2.shape_knob = params[CH2_SHAPE_PARAM] .getValue(); | |||||
frame.ch2.mod_knob = params[CH2_MOD_PARAM] .getValue(); | |||||
frame.ch2.level_mod_knob = params[CH2_LEVEL_MOD_PARAM].getValue(); | |||||
frame.ch2.response_knob = params[CH2_RESPONSE_PARAM] .getValue(); | |||||
frame.ch1.signal_in_connected = inputs[CH1_SIGNAL_INPUT].isConnected(); | |||||
frame.ch1.level_cv_connected = inputs[CH1_LEVEL_INPUT] .isConnected(); | |||||
frame.ch2.signal_in_connected = inputs[CH2_SIGNAL_INPUT].isConnected(); | |||||
frame.ch2.level_cv_connected = inputs[CH2_LEVEL_INPUT] .isConnected(); | |||||
frame.ch1.function_button = params[CH1_FUNCTION_BUTTON_PARAM].getValue(); | |||||
frame.ch2.function_button = params[CH2_FUNCTION_BUTTON_PARAM].getValue(); | |||||
frame.metering_button = params[METERING_BUTTON_PARAM].getValue(); | |||||
bool lights_updated = false; | |||||
for (int c = 0; c < num_channels; c++) | |||||
{ | |||||
frame.ch1.excite_in = inputs[CH1_EXCITE_INPUT].getPolyVoltage(c); | |||||
frame.ch1.signal_in = inputs[CH1_SIGNAL_INPUT].getPolyVoltage(c); | |||||
frame.ch1.level_cv = inputs[CH1_LEVEL_INPUT] .getPolyVoltage(c); | |||||
frame.ch2.excite_in = inputs[CH2_EXCITE_INPUT].getPolyVoltage(c); | |||||
frame.ch2.signal_in = inputs[CH2_SIGNAL_INPUT].getPolyVoltage(c); | |||||
frame.ch2.level_cv = inputs[CH2_LEVEL_INPUT] .getPolyVoltage(c); | |||||
engine_[c].Process(frame); | |||||
outputs[CH1_SIGNAL_OUTPUT].setVoltage(frame.ch1.signal_out, c); | |||||
outputs[CH2_SIGNAL_OUTPUT].setVoltage(frame.ch2.signal_out, c); | |||||
if (frame.lights_updated) | |||||
{ | |||||
brightness_[CH1_LIGHT_1_G][c] = frame.ch1.led_green[0]; | |||||
brightness_[CH1_LIGHT_2_G][c] = frame.ch1.led_green[1]; | |||||
brightness_[CH1_LIGHT_3_G][c] = frame.ch1.led_green[2]; | |||||
brightness_[CH1_LIGHT_4_G][c] = frame.ch1.led_green[3]; | |||||
brightness_[CH1_LIGHT_1_R][c] = frame.ch1.led_red[0]; | |||||
brightness_[CH1_LIGHT_2_R][c] = frame.ch1.led_red[1]; | |||||
brightness_[CH1_LIGHT_3_R][c] = frame.ch1.led_red[2]; | |||||
brightness_[CH1_LIGHT_4_R][c] = frame.ch1.led_red[3]; | |||||
brightness_[CH2_LIGHT_1_G][c] = frame.ch2.led_green[0]; | |||||
brightness_[CH2_LIGHT_2_G][c] = frame.ch2.led_green[1]; | |||||
brightness_[CH2_LIGHT_3_G][c] = frame.ch2.led_green[2]; | |||||
brightness_[CH2_LIGHT_4_G][c] = frame.ch2.led_green[3]; | |||||
brightness_[CH2_LIGHT_1_R][c] = frame.ch2.led_red[0]; | |||||
brightness_[CH2_LIGHT_2_R][c] = frame.ch2.led_red[1]; | |||||
brightness_[CH2_LIGHT_3_R][c] = frame.ch2.led_red[2]; | |||||
brightness_[CH2_LIGHT_4_R][c] = frame.ch2.led_red[3]; | |||||
} | |||||
lights_updated |= frame.lights_updated; | |||||
} | |||||
outputs[CH1_SIGNAL_OUTPUT].setChannels(num_channels); | |||||
outputs[CH2_SIGNAL_OUTPUT].setChannels(num_channels); | |||||
if (lights_updated) | |||||
{ | |||||
// Drive lights according to maximum brightness across engines | |||||
for (int i = 0; i < NUM_LIGHTS; i++) | |||||
{ | |||||
float brightness = 0.f; | |||||
for (int c = 0; c < num_channels; c++) | |||||
{ | |||||
brightness = std::max(brightness_[i][c], brightness); | |||||
} | |||||
lights[i].setBrightness(brightness); | |||||
} | |||||
} | |||||
} | |||||
}; | }; | ||||
struct StreamsWidget : ModuleWidget | |||||
{ | |||||
StreamsWidget(Streams* module) | |||||
{ | |||||
setModule(module); | |||||
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Streams.svg"))); | |||||
addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0))); | |||||
addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); | |||||
addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||||
addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(11.065, 128.75 - 107.695)), module, Streams::CH1_SHAPE_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(11.065, 128.75 - 84.196)), module, Streams::CH1_MOD_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSRed> (mm2px(Vec(11.065, 128.75 - 60.706)), module, Streams::CH1_LEVEL_MOD_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(49.785, 128.75 - 107.695)), module, Streams::CH2_SHAPE_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(49.785, 128.75 - 84.196)), module, Streams::CH2_MOD_PARAM)); | |||||
addParam(createParamCentered<Rogan1PSGreen>(mm2px(Vec(49.785, 128.75 - 60.706)), module, Streams::CH2_LEVEL_MOD_PARAM)); | |||||
addParam(createParamCentered<Trimpot>(mm2px(Vec(30.425, 128.75 - 68.006)), module, Streams::CH1_RESPONSE_PARAM)); | |||||
addParam(createParamCentered<Trimpot>(mm2px(Vec(30.425, 128.75 - 53.406)), module, Streams::CH2_RESPONSE_PARAM)); | |||||
addParam(createParamCentered<TL1105>(mm2px(Vec(24.715, 128.75 - 113.726)), module, Streams::CH1_FUNCTION_BUTTON_PARAM)); | |||||
addParam(createParamCentered<TL1105>(mm2px(Vec(36.135, 128.75 - 113.726)), module, Streams::CH2_FUNCTION_BUTTON_PARAM)); | |||||
addParam(createParamCentered<TL1105>(mm2px(Vec(30.425, 128.75 - 81.976)), module, Streams::METERING_BUTTON_PARAM)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec( 8.506, 128.75 - 32.136)), module, Streams::CH1_EXCITE_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(23.116, 128.75 - 32.136)), module, Streams::CH1_SIGNAL_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec( 8.506, 128.75 - 17.526)), module, Streams::CH1_LEVEL_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(52.335, 128.75 - 32.136)), module, Streams::CH2_EXCITE_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(37.726, 128.75 - 32.136)), module, Streams::CH2_SIGNAL_INPUT)); | |||||
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(52.335, 128.75 - 17.526)), module, Streams::CH2_LEVEL_INPUT)); | |||||
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(23.116, 128.75 - 17.526)), module, Streams::CH1_SIGNAL_OUTPUT)); | |||||
addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(37.726, 128.75 - 17.526)), module, Streams::CH2_SIGNAL_OUTPUT)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(24.715, 128.75 - 106.746)), module, Streams::CH1_LIGHT_1_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(24.715, 128.75 - 101.026)), module, Streams::CH1_LIGHT_2_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(24.715, 128.75 - 95.305)), module, Streams::CH1_LIGHT_3_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(24.715, 128.75 - 89.585)), module, Streams::CH1_LIGHT_4_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(36.135, 128.75 - 106.746)), module, Streams::CH2_LIGHT_1_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(36.135, 128.75 - 101.026)), module, Streams::CH2_LIGHT_2_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(36.135, 128.75 - 95.305)), module, Streams::CH2_LIGHT_3_G)); | |||||
addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(36.135, 128.75 - 89.585)), module, Streams::CH2_LIGHT_4_G)); | |||||
} | |||||
void appendContextMenu(Menu* menu) override | |||||
{ | |||||
Streams* module = dynamic_cast<Streams*>(this->module); | |||||
struct LinkItem : MenuItem | |||||
{ | |||||
Streams* module; | |||||
void onAction(const event::Action & e) override | |||||
{ | |||||
module->ToggleLink(); | |||||
} | |||||
}; | |||||
struct ChannelModeItem : MenuItem | |||||
{ | |||||
Streams* module; | |||||
int channel; | |||||
int mode; | |||||
void onAction(const event::Action& e) override | |||||
{ | |||||
module->SetChannelMode(channel, mode); | |||||
} | |||||
}; | |||||
struct MonitorModeItem : MenuItem | |||||
{ | |||||
Streams* module; | |||||
int mode; | |||||
void onAction(const event::Action& e) override | |||||
{ | |||||
module->SetMonitorMode(mode); | |||||
} | |||||
}; | |||||
menu->addChild(new MenuSeparator); | |||||
LinkItem* link_item = createMenuItem<LinkItem>( | |||||
"Link channels", CHECKMARK(module->linked())); | |||||
link_item->module = module; | |||||
menu->addChild(link_item); | |||||
menu->addChild(new MenuSeparator); | |||||
menu->addChild(createMenuLabel("Channel 1")); | |||||
for (int i = 0; i < streams::kNumChannelModes; i++) | |||||
{ | |||||
auto mode_item = createMenuItem<ChannelModeItem>( | |||||
streams::kChannelModeTable[i].label, CHECKMARK( | |||||
module->function(0) == streams::kChannelModeTable[i].function && | |||||
module->alternate(0) == streams::kChannelModeTable[i].alternate)); | |||||
mode_item->module = module; | |||||
mode_item->channel = 0; | |||||
mode_item->mode = i; | |||||
menu->addChild(mode_item); | |||||
} | |||||
menu->addChild(new MenuSeparator); | |||||
menu->addChild(createMenuLabel("Channel 2")); | |||||
for (int i = 0; i < streams::kNumChannelModes; i++) | |||||
{ | |||||
auto mode_item = createMenuItem<ChannelModeItem>( | |||||
streams::kChannelModeTable[i].label, CHECKMARK( | |||||
module->function(1) == streams::kChannelModeTable[i].function && | |||||
module->alternate(1) == streams::kChannelModeTable[i].alternate)); | |||||
mode_item->module = module; | |||||
mode_item->channel = 1; | |||||
mode_item->mode = i; | |||||
menu->addChild(mode_item); | |||||
} | |||||
menu->addChild(new MenuSeparator); | |||||
menu->addChild(createMenuLabel("Meter")); | |||||
for (int i = 0; i < streams::kNumMonitorModes; i++) | |||||
{ | |||||
auto mode_item = createMenuItem<MonitorModeItem>( | |||||
streams::kMonitorModeTable[i].label, CHECKMARK( | |||||
module->monitor_mode() == streams::kMonitorModeTable[i].mode)); | |||||
mode_item->module = module; | |||||
mode_item->mode = i; | |||||
menu->addChild(mode_item); | |||||
} | |||||
} | |||||
}; | |||||
Model* modelStreams = createModel<Streams, StreamsWidget>("Streams"); | |||||
Model* modelStreams = createModel<Streams, StreamsWidget>("Streams"); |
@@ -0,0 +1,540 @@ | |||||
// Anti-aliasing filters for common sample rates | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#pragma once | |||||
#include "sos.hpp" | |||||
namespace streams | |||||
{ | |||||
/*[[[cog | |||||
import math | |||||
import aafilter | |||||
# We design our filters to keep aliasing out of this band | |||||
audio_bw = 20000 | |||||
# We assume the client process generates no frequency content above this | |||||
# multiple of the original bandwidth | |||||
max_bw_mult = 3 | |||||
rpass = 0.1 # Maximum passband ripple in dB | |||||
rstop = 100 # Minimum stopband attenuation in dB | |||||
# Generate filters for these sampling rates | |||||
common_rates = [ | |||||
8000, | |||||
11025, 12000, | |||||
22050, 24000, | |||||
44100, 48000, | |||||
88200, 96000, | |||||
176400, 192000, | |||||
352800, 384000, | |||||
705600, 768000 | |||||
] | |||||
# Oversample to at least this frequency | |||||
min_oversampled_rate = audio_bw * 2 * 2 | |||||
up_filters = list() | |||||
down_filters = list() | |||||
oversampling_factors = dict() | |||||
max_num_sections = 0 | |||||
# For each sample rate, design a pair of upsampling and downsampling filters. | |||||
# For the upsampling filter, the stopband must be placed such that the client's | |||||
# multiplied bandwidth won't reach into the aliased audio band. For the | |||||
# downsampling filter, the stopband must be placed such that all foldover falls | |||||
# above the audio band. | |||||
for fs in common_rates: | |||||
os = math.ceil(min_oversampled_rate / fs) | |||||
oversampling_factors[fs] = os | |||||
fpass = min(audio_bw, 0.475 * fs) | |||||
critical_bw = fpass if fpass >= audio_bw else fs / 2 | |||||
up_fstop = min(fs * os / 2, (fs * os - critical_bw) / max_bw_mult) | |||||
down_fstop = min(fs * os / 2, fs - critical_bw) | |||||
up = aafilter.design(fs, os, fpass, up_fstop, rpass, rstop) | |||||
down = aafilter.design(fs, os, fpass, down_fstop, rpass, rstop) | |||||
max_num_sections = max(max_num_sections, len(up.sections), len(down.sections)) | |||||
up_filters.append(up) | |||||
down_filters.append(down) | |||||
cog.outl('static constexpr int kMaxNumSections = {};' | |||||
.format(max_num_sections)) | |||||
]]]*/ | |||||
static constexpr int kMaxNumSections = 8; | |||||
//[[[end]]] | |||||
inline int SampleRateID(float sample_rate) | |||||
{ | |||||
if (false) {} | |||||
/*[[[cog | |||||
for fs in sorted(common_rates, reverse=True): | |||||
cog.outl('else if ({} <= sample_rate) return {};'.format(fs, fs)) | |||||
cog.outl('else return {};'.format(min(common_rates))) | |||||
]]]*/ | |||||
else if (768000 <= sample_rate) return 768000; | |||||
else if (705600 <= sample_rate) return 705600; | |||||
else if (384000 <= sample_rate) return 384000; | |||||
else if (352800 <= sample_rate) return 352800; | |||||
else if (192000 <= sample_rate) return 192000; | |||||
else if (176400 <= sample_rate) return 176400; | |||||
else if (96000 <= sample_rate) return 96000; | |||||
else if (88200 <= sample_rate) return 88200; | |||||
else if (48000 <= sample_rate) return 48000; | |||||
else if (44100 <= sample_rate) return 44100; | |||||
else if (24000 <= sample_rate) return 24000; | |||||
else if (22050 <= sample_rate) return 22050; | |||||
else if (12000 <= sample_rate) return 12000; | |||||
else if (11025 <= sample_rate) return 11025; | |||||
else if (8000 <= sample_rate) return 8000; | |||||
else return 8000; | |||||
//[[[end]]] | |||||
} | |||||
inline int OversamplingFactor(float sample_rate) | |||||
{ | |||||
switch (SampleRateID(sample_rate)) | |||||
{ | |||||
default: | |||||
/*[[[cog | |||||
for fs in sorted(common_rates): | |||||
cog.outl('case {}: return {};'.format(fs, oversampling_factors[fs])) | |||||
]]]*/ | |||||
case 8000: return 10; | |||||
case 11025: return 8; | |||||
case 12000: return 7; | |||||
case 22050: return 4; | |||||
case 24000: return 4; | |||||
case 44100: return 2; | |||||
case 48000: return 2; | |||||
case 88200: return 1; | |||||
case 96000: return 1; | |||||
case 176400: return 1; | |||||
case 192000: return 1; | |||||
case 352800: return 1; | |||||
case 384000: return 1; | |||||
case 705600: return 1; | |||||
case 768000: return 1; | |||||
//[[[end]]] | |||||
} | |||||
} | |||||
template <typename T> | |||||
class AAFilter | |||||
{ | |||||
public: | |||||
void Init(float sample_rate) | |||||
{ | |||||
InitFilter(sample_rate); | |||||
} | |||||
T Process(T in) | |||||
{ | |||||
return filter_.Process(in); | |||||
} | |||||
protected: | |||||
SOSFilter<T, kMaxNumSections> filter_; | |||||
virtual void InitFilter(float sample_rate) = 0; | |||||
}; | |||||
template <typename T> | |||||
class UpsamplingAAFilter : public AAFilter<T> | |||||
{ | |||||
void InitFilter(float sample_rate) override | |||||
{ | |||||
switch (SampleRateID(sample_rate)) | |||||
{ | |||||
default: | |||||
/*[[[cog | |||||
aafilter.print_filter_cases(up_filters) | |||||
]]]*/ | |||||
case 8000: // o = 10, fp = 3799, fst = 25333, cost = 160000 | |||||
{ | |||||
const SOSCoefficients kFilter8000x10[2] = | |||||
{ | |||||
{ {4.63786610e-04, 8.16220909e-04, 4.63786610e-04, }, {-1.63450649e+00, 6.81471340e-01, } }, | |||||
{ {1.00000000e+00, 9.17818354e-01, 1.00000000e+00, }, {-1.74936370e+00, 8.57701633e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(2, kFilter8000x10); | |||||
break; | |||||
} | |||||
case 11025: // o = 8, fp = 5236, fst = 27562, cost = 264600 | |||||
{ | |||||
const SOSCoefficients kFilter11025x8[3] = | |||||
{ | |||||
{ {8.58405971e-05, 1.10355095e-04, 8.58405971e-05, }, {-1.68369279e+00, 7.17693063e-01, } }, | |||||
{ {1.00000000e+00, -4.51272752e-01, 1.00000000e+00, }, {-1.70761645e+00, 7.97046177e-01, } }, | |||||
{ {1.00000000e+00, -9.69385103e-01, 1.00000000e+00, }, {-1.77709771e+00, 9.25148961e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter11025x8); | |||||
break; | |||||
} | |||||
case 12000: // o = 7, fp = 5700, fst = 26000, cost = 252000 | |||||
{ | |||||
const SOSCoefficients kFilter12000x7[3] = | |||||
{ | |||||
{ {1.23289409e-04, 1.76631634e-04, 1.23289409e-04, }, {-1.63990095e+00, 6.83607830e-01, } }, | |||||
{ {1.00000000e+00, -1.84350251e-01, 1.00000000e+00, }, {-1.65709238e+00, 7.72217183e-01, } }, | |||||
{ {1.00000000e+00, -7.46080513e-01, 1.00000000e+00, }, {-1.72410914e+00, 9.15596208e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter12000x7); | |||||
break; | |||||
} | |||||
case 22050: // o = 4, fp = 10473, fst = 25725, cost = 264600 | |||||
{ | |||||
const SOSCoefficients kFilter22050x4[3] = | |||||
{ | |||||
{ {8.28104239e-04, 1.49680255e-03, 8.28104239e-04, }, {-1.37972564e+00, 5.03689463e-01, } }, | |||||
{ {1.00000000e+00, 9.23962985e-01, 1.00000000e+00, }, {-1.31894849e+00, 6.44142088e-01, } }, | |||||
{ {1.00000000e+00, 3.95355727e-01, 1.00000000e+00, }, {-1.31864199e+00, 8.66452582e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter22050x4); | |||||
break; | |||||
} | |||||
case 24000: // o = 4, fp = 11400, fst = 28000, cost = 288000 | |||||
{ | |||||
const SOSCoefficients kFilter24000x4[3] = | |||||
{ | |||||
{ {8.28104239e-04, 1.49680255e-03, 8.28104239e-04, }, {-1.37972564e+00, 5.03689463e-01, } }, | |||||
{ {1.00000000e+00, 9.23962985e-01, 1.00000000e+00, }, {-1.31894849e+00, 6.44142088e-01, } }, | |||||
{ {1.00000000e+00, 3.95355727e-01, 1.00000000e+00, }, {-1.31864199e+00, 8.66452582e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter24000x4); | |||||
break; | |||||
} | |||||
case 44100: // o = 2, fp = 20000, fst = 22733, cost = 529200 | |||||
{ | |||||
const SOSCoefficients kFilter44100x2[6] = | |||||
{ | |||||
{ {1.79111485e-03, 3.36261548e-03, 1.79111485e-03, }, {-1.13743427e+00, 3.66260569e-01, } }, | |||||
{ {1.00000000e+00, 1.20719512e+00, 1.00000000e+00, }, {-9.11565008e-01, 5.12543165e-01, } }, | |||||
{ {1.00000000e+00, 6.02914008e-01, 1.00000000e+00, }, {-6.39374195e-01, 6.90602186e-01, } }, | |||||
{ {1.00000000e+00, 2.52534955e-01, 1.00000000e+00, }, {-4.37984152e-01, 8.26937992e-01, } }, | |||||
{ {1.00000000e+00, 7.75467885e-02, 1.00000000e+00, }, {-3.22061575e-01, 9.15513587e-01, } }, | |||||
{ {1.00000000e+00, 6.28451771e-03, 1.00000000e+00, }, {-2.73474858e-01, 9.74748983e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(6, kFilter44100x2); | |||||
break; | |||||
} | |||||
case 48000: // o = 2, fp = 20000, fst = 25333, cost = 480000 | |||||
{ | |||||
const SOSCoefficients kFilter48000x2[5] = | |||||
{ | |||||
{ {1.56483717e-03, 2.92030174e-03, 1.56483717e-03, }, {-1.17455774e+00, 3.85298764e-01, } }, | |||||
{ {1.00000000e+00, 1.15074177e+00, 1.00000000e+00, }, {-9.70672689e-01, 5.26603999e-01, } }, | |||||
{ {1.00000000e+00, 5.31582897e-01, 1.00000000e+00, }, {-7.26313285e-01, 7.02981269e-01, } }, | |||||
{ {1.00000000e+00, 1.94109007e-01, 1.00000000e+00, }, {-5.54517652e-01, 8.45646275e-01, } }, | |||||
{ {1.00000000e+00, 5.47965468e-02, 1.00000000e+00, }, {-4.79572665e-01, 9.52220684e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(5, kFilter48000x2); | |||||
break; | |||||
} | |||||
case 88200: // o = 1, fp = 20000, fst = 22733, cost = 529200 | |||||
{ | |||||
const SOSCoefficients kFilter88200x1[6] = | |||||
{ | |||||
{ {1.79111485e-03, 3.36261548e-03, 1.79111485e-03, }, {-1.13743427e+00, 3.66260569e-01, } }, | |||||
{ {1.00000000e+00, 1.20719512e+00, 1.00000000e+00, }, {-9.11565008e-01, 5.12543165e-01, } }, | |||||
{ {1.00000000e+00, 6.02914008e-01, 1.00000000e+00, }, {-6.39374195e-01, 6.90602186e-01, } }, | |||||
{ {1.00000000e+00, 2.52534955e-01, 1.00000000e+00, }, {-4.37984152e-01, 8.26937992e-01, } }, | |||||
{ {1.00000000e+00, 7.75467885e-02, 1.00000000e+00, }, {-3.22061575e-01, 9.15513587e-01, } }, | |||||
{ {1.00000000e+00, 6.28451771e-03, 1.00000000e+00, }, {-2.73474858e-01, 9.74748983e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(6, kFilter88200x1); | |||||
break; | |||||
} | |||||
case 96000: // o = 1, fp = 20000, fst = 25333, cost = 480000 | |||||
{ | |||||
const SOSCoefficients kFilter96000x1[5] = | |||||
{ | |||||
{ {1.56483717e-03, 2.92030174e-03, 1.56483717e-03, }, {-1.17455774e+00, 3.85298764e-01, } }, | |||||
{ {1.00000000e+00, 1.15074177e+00, 1.00000000e+00, }, {-9.70672689e-01, 5.26603999e-01, } }, | |||||
{ {1.00000000e+00, 5.31582897e-01, 1.00000000e+00, }, {-7.26313285e-01, 7.02981269e-01, } }, | |||||
{ {1.00000000e+00, 1.94109007e-01, 1.00000000e+00, }, {-5.54517652e-01, 8.45646275e-01, } }, | |||||
{ {1.00000000e+00, 5.47965468e-02, 1.00000000e+00, }, {-4.79572665e-01, 9.52220684e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(5, kFilter96000x1); | |||||
break; | |||||
} | |||||
case 176400: // o = 1, fp = 20000, fst = 52133, cost = 529200 | |||||
{ | |||||
const SOSCoefficients kFilter176400x1[3] = | |||||
{ | |||||
{ {6.91751141e-04, 1.23689749e-03, 6.91751141e-04, }, {-1.40714871e+00, 5.20902227e-01, } }, | |||||
{ {1.00000000e+00, 8.42431018e-01, 1.00000000e+00, }, {-1.35717505e+00, 6.56002263e-01, } }, | |||||
{ {1.00000000e+00, 2.97097489e-01, 1.00000000e+00, }, {-1.36759134e+00, 8.70920336e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter176400x1); | |||||
break; | |||||
} | |||||
case 192000: // o = 1, fp = 20000, fst = 57333, cost = 576000 | |||||
{ | |||||
const SOSCoefficients kFilter192000x1[3] = | |||||
{ | |||||
{ {5.02504803e-04, 8.78421990e-04, 5.02504803e-04, }, {-1.45413648e+00, 5.51330003e-01, } }, | |||||
{ {1.00000000e+00, 6.85942380e-01, 1.00000000e+00, }, {-1.42143582e+00, 6.77242054e-01, } }, | |||||
{ {1.00000000e+00, 1.15756990e-01, 1.00000000e+00, }, {-1.44850505e+00, 8.78995879e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter192000x1); | |||||
break; | |||||
} | |||||
case 352800: // o = 1, fp = 20000, fst = 110933, cost = 1058400 | |||||
{ | |||||
const SOSCoefficients kFilter352800x1[3] = | |||||
{ | |||||
{ {7.63562466e-05, 9.37911276e-05, 7.63562466e-05, }, {-1.69760825e+00, 7.28764991e-01, } }, | |||||
{ {1.00000000e+00, -5.40096033e-01, 1.00000000e+00, }, {-1.72321786e+00, 8.05120281e-01, } }, | |||||
{ {1.00000000e+00, -1.04012920e+00, 1.00000000e+00, }, {-1.79287839e+00, 9.28245030e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter352800x1); | |||||
break; | |||||
} | |||||
case 384000: // o = 1, fp = 20000, fst = 121333, cost = 1152000 | |||||
{ | |||||
const SOSCoefficients kFilter384000x1[3] = | |||||
{ | |||||
{ {6.23104401e-05, 6.94740629e-05, 6.23104401e-05, }, {-1.72153665e+00, 7.48079159e-01, } }, | |||||
{ {1.00000000e+00, -6.96283878e-01, 1.00000000e+00, }, {-1.74951535e+00, 8.19207305e-01, } }, | |||||
{ {1.00000000e+00, -1.16050137e+00, 1.00000000e+00, }, {-1.81879173e+00, 9.33631596e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(3, kFilter384000x1); | |||||
break; | |||||
} | |||||
case 705600: // o = 1, fp = 20000, fst = 228533, cost = 1411200 | |||||
{ | |||||
const SOSCoefficients kFilter705600x1[2] = | |||||
{ | |||||
{ {1.08339911e-04, 1.50243615e-04, 1.08339911e-04, }, {-1.77824462e+00, 7.96098482e-01, } }, | |||||
{ {1.00000000e+00, -5.03405956e-02, 1.00000000e+00, }, {-1.87131112e+00, 9.11379528e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(2, kFilter705600x1); | |||||
break; | |||||
} | |||||
case 768000: // o = 1, fp = 20000, fst = 249333, cost = 1536000 | |||||
{ | |||||
const SOSCoefficients kFilter768000x1[2] = | |||||
{ | |||||
{ {8.80491172e-05, 1.13851506e-04, 8.80491172e-05, }, {-1.79584317e+00, 8.11038264e-01, } }, | |||||
{ {1.00000000e+00, -2.19769620e-01, 1.00000000e+00, }, {-1.88421935e+00, 9.18189356e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(2, kFilter768000x1); | |||||
break; | |||||
} | |||||
//[[[end]]] | |||||
} | |||||
} | |||||
}; | |||||
template <typename T> | |||||
class DownsamplingAAFilter : public AAFilter<T> | |||||
{ | |||||
void InitFilter(float sample_rate) override | |||||
{ | |||||
switch (SampleRateID(sample_rate)) | |||||
{ | |||||
default: | |||||
/*[[[cog | |||||
aafilter.print_filter_cases(down_filters) | |||||
]]]*/ | |||||
case 8000: // o = 10, fp = 3799, fst = 4000, cost = 640000 | |||||
{ | |||||
const SOSCoefficients kFilter8000x10[8] = | |||||
{ | |||||
{ {1.74724987e-05, -2.65793181e-06, 1.74724987e-05, }, {-1.83684224e+00, 8.46022748e-01, } }, | |||||
{ {1.00000000e+00, -1.60455772e+00, 1.00000000e+00, }, {-1.85073181e+00, 8.75957566e-01, } }, | |||||
{ {1.00000000e+00, -1.80816772e+00, 1.00000000e+00, }, {-1.86939499e+00, 9.16147406e-01, } }, | |||||
{ {1.00000000e+00, -1.86608225e+00, 1.00000000e+00, }, {-1.88504252e+00, 9.49754529e-01, } }, | |||||
{ {1.00000000e+00, -1.88843627e+00, 1.00000000e+00, }, {-1.89555097e+00, 9.72128817e-01, } }, | |||||
{ {1.00000000e+00, -1.89828300e+00, 1.00000000e+00, }, {-1.90199243e+00, 9.85446639e-01, } }, | |||||
{ {1.00000000e+00, -1.90275515e+00, 1.00000000e+00, }, {-1.90608719e+00, 9.93153182e-01, } }, | |||||
{ {1.00000000e+00, -1.90451538e+00, 1.00000000e+00, }, {-1.90935079e+00, 9.98002792e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(8, kFilter8000x10); | |||||
break; | |||||
} | |||||
case 11025: // o = 8, fp = 5236, fst = 5512, cost = 705600 | |||||
{ | |||||
const SOSCoefficients kFilter11025x8[8] = | |||||
{ | |||||
{ {2.28458309e-05, 6.85495861e-06, 2.28458309e-05, }, {-1.79651426e+00, 8.10683491e-01, } }, | |||||
{ {1.00000000e+00, -1.41042952e+00, 1.00000000e+00, }, {-1.80827124e+00, 8.47262510e-01, } }, | |||||
{ {1.00000000e+00, -1.70583736e+00, 1.00000000e+00, }, {-1.82413424e+00, 8.96543400e-01, } }, | |||||
{ {1.00000000e+00, -1.79296606e+00, 1.00000000e+00, }, {-1.83751071e+00, 9.37903530e-01, } }, | |||||
{ {1.00000000e+00, -1.82697868e+00, 1.00000000e+00, }, {-1.84658043e+00, 9.65516086e-01, } }, | |||||
{ {1.00000000e+00, -1.84202935e+00, 1.00000000e+00, }, {-1.85227474e+00, 9.81981088e-01, } }, | |||||
{ {1.00000000e+00, -1.84887890e+00, 1.00000000e+00, }, {-1.85613708e+00, 9.91518889e-01, } }, | |||||
{ {1.00000000e+00, -1.85157727e+00, 1.00000000e+00, }, {-1.85962400e+00, 9.97525116e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(8, kFilter11025x8); | |||||
break; | |||||
} | |||||
case 12000: // o = 7, fp = 5700, fst = 6000, cost = 672000 | |||||
{ | |||||
const SOSCoefficients kFilter12000x7[8] = | |||||
{ | |||||
{ {2.79174308e-05, 1.56664250e-05, 2.79174308e-05, }, {-1.76770492e+00, 7.86069996e-01, } }, | |||||
{ {1.00000000e+00, -1.25883414e+00, 1.00000000e+00, }, {-1.77670663e+00, 8.27280516e-01, } }, | |||||
{ {1.00000000e+00, -1.62177587e+00, 1.00000000e+00, }, {-1.78888474e+00, 8.82894847e-01, } }, | |||||
{ {1.00000000e+00, -1.73200235e+00, 1.00000000e+00, }, {-1.79920477e+00, 9.29653670e-01, } }, | |||||
{ {1.00000000e+00, -1.77543744e+00, 1.00000000e+00, }, {-1.80628315e+00, 9.60912839e-01, } }, | |||||
{ {1.00000000e+00, -1.79473107e+00, 1.00000000e+00, }, {-1.81087559e+00, 9.79568474e-01, } }, | |||||
{ {1.00000000e+00, -1.80352658e+00, 1.00000000e+00, }, {-1.81426259e+00, 9.90380916e-01, } }, | |||||
{ {1.00000000e+00, -1.80699414e+00, 1.00000000e+00, }, {-1.81775362e+00, 9.97192369e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(8, kFilter12000x7); | |||||
break; | |||||
} | |||||
case 22050: // o = 4, fp = 10473, fst = 11025, cost = 705600 | |||||
{ | |||||
const SOSCoefficients kFilter22050x4[8] = | |||||
{ | |||||
{ {9.74314780e-05, 1.37711747e-04, 9.74314780e-05, }, {-1.59261637e+00, 6.47353056e-01, } }, | |||||
{ {1.00000000e+00, -2.94219878e-01, 1.00000000e+00, }, {-1.56519364e+00, 7.15705529e-01, } }, | |||||
{ {1.00000000e+00, -9.81960881e-01, 1.00000000e+00, }, {-1.52839642e+00, 8.07626243e-01, } }, | |||||
{ {1.00000000e+00, -1.23949615e+00, 1.00000000e+00, }, {-1.49778996e+00, 8.84625474e-01, } }, | |||||
{ {1.00000000e+00, -1.34882542e+00, 1.00000000e+00, }, {-1.47786684e+00, 9.35956597e-01, } }, | |||||
{ {1.00000000e+00, -1.39893677e+00, 1.00000000e+00, }, {-1.46698417e+00, 9.66536770e-01, } }, | |||||
{ {1.00000000e+00, -1.42210887e+00, 1.00000000e+00, }, {-1.46262788e+00, 9.84243409e-01, } }, | |||||
{ {1.00000000e+00, -1.43130155e+00, 1.00000000e+00, }, {-1.46352911e+00, 9.95397324e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(8, kFilter22050x4); | |||||
break; | |||||
} | |||||
case 24000: // o = 4, fp = 11400, fst = 12000, cost = 768000 | |||||
{ | |||||
const SOSCoefficients kFilter24000x4[8] = | |||||
{ | |||||
{ {9.74314780e-05, 1.37711747e-04, 9.74314780e-05, }, {-1.59261637e+00, 6.47353056e-01, } }, | |||||
{ {1.00000000e+00, -2.94219878e-01, 1.00000000e+00, }, {-1.56519364e+00, 7.15705529e-01, } }, | |||||
{ {1.00000000e+00, -9.81960881e-01, 1.00000000e+00, }, {-1.52839642e+00, 8.07626243e-01, } }, | |||||
{ {1.00000000e+00, -1.23949615e+00, 1.00000000e+00, }, {-1.49778996e+00, 8.84625474e-01, } }, | |||||
{ {1.00000000e+00, -1.34882542e+00, 1.00000000e+00, }, {-1.47786684e+00, 9.35956597e-01, } }, | |||||
{ {1.00000000e+00, -1.39893677e+00, 1.00000000e+00, }, {-1.46698417e+00, 9.66536770e-01, } }, | |||||
{ {1.00000000e+00, -1.42210887e+00, 1.00000000e+00, }, {-1.46262788e+00, 9.84243409e-01, } }, | |||||
{ {1.00000000e+00, -1.43130155e+00, 1.00000000e+00, }, {-1.46352911e+00, 9.95397324e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(8, kFilter24000x4); | |||||
break; | |||||
} | |||||
case 44100: // o = 2, fp = 20000, fst = 24100, cost = 441000 | |||||
{ | |||||
const SOSCoefficients kFilter44100x2[5] = | |||||
{ | |||||
{ {2.47147477e-03, 4.68008071e-03, 2.47147477e-03, }, {-1.08909166e+00, 3.42723010e-01, } }, | |||||
{ {1.00000000e+00, 1.29826448e+00, 1.00000000e+00, }, {-8.40340328e-01, 5.00534399e-01, } }, | |||||
{ {1.00000000e+00, 7.43776254e-01, 1.00000000e+00, }, {-5.49893538e-01, 6.91539899e-01, } }, | |||||
{ {1.00000000e+00, 4.24723977e-01, 1.00000000e+00, }, {-3.48795082e-01, 8.41459476e-01, } }, | |||||
{ {1.00000000e+00, 2.89331378e-01, 1.00000000e+00, }, {-2.57028674e-01, 9.51166241e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(5, kFilter44100x2); | |||||
break; | |||||
} | |||||
case 48000: // o = 2, fp = 20000, fst = 28000, cost = 480000 | |||||
{ | |||||
const SOSCoefficients kFilter48000x2[5] = | |||||
{ | |||||
{ {1.56483717e-03, 2.92030174e-03, 1.56483717e-03, }, {-1.17455774e+00, 3.85298764e-01, } }, | |||||
{ {1.00000000e+00, 1.15074177e+00, 1.00000000e+00, }, {-9.70672689e-01, 5.26603999e-01, } }, | |||||
{ {1.00000000e+00, 5.31582897e-01, 1.00000000e+00, }, {-7.26313285e-01, 7.02981269e-01, } }, | |||||
{ {1.00000000e+00, 1.94109007e-01, 1.00000000e+00, }, {-5.54517652e-01, 8.45646275e-01, } }, | |||||
{ {1.00000000e+00, 5.47965468e-02, 1.00000000e+00, }, {-4.79572665e-01, 9.52220684e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(5, kFilter48000x2); | |||||
break; | |||||
} | |||||
case 88200: // o = 1, fp = 20000, fst = 44100, cost = 88200 | |||||
{ | |||||
const SOSCoefficients kFilter88200x1[1] = | |||||
{ | |||||
{ {4.47760494e-01, 8.95513661e-01, 4.47760494e-01, }, {5.33267789e-01, 2.57766861e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter88200x1); | |||||
break; | |||||
} | |||||
case 96000: // o = 1, fp = 20000, fst = 48000, cost = 96000 | |||||
{ | |||||
const SOSCoefficients kFilter96000x1[1] = | |||||
{ | |||||
{ {4.08937060e-01, 8.17865642e-01, 4.08937060e-01, }, {3.98731881e-01, 2.37007882e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter96000x1); | |||||
break; | |||||
} | |||||
case 176400: // o = 1, fp = 20000, fst = 88200, cost = 176400 | |||||
{ | |||||
const SOSCoefficients kFilter176400x1[1] = | |||||
{ | |||||
{ {1.95938020e-01, 3.91858763e-01, 1.95938020e-01, }, {-4.62313019e-01, 2.46047822e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter176400x1); | |||||
break; | |||||
} | |||||
case 192000: // o = 1, fp = 20000, fst = 96000, cost = 192000 | |||||
{ | |||||
const SOSCoefficients kFilter192000x1[1] = | |||||
{ | |||||
{ {1.74603587e-01, 3.49188678e-01, 1.74603587e-01, }, {-5.65216145e-01, 2.63611998e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter192000x1); | |||||
break; | |||||
} | |||||
case 352800: // o = 1, fp = 20000, fst = 176400, cost = 352800 | |||||
{ | |||||
const SOSCoefficients kFilter352800x1[1] = | |||||
{ | |||||
{ {6.99874107e-02, 1.39948456e-01, 6.99874107e-02, }, {-1.16347041e+00, 4.43393682e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter352800x1); | |||||
break; | |||||
} | |||||
case 384000: // o = 1, fp = 20000, fst = 192000, cost = 384000 | |||||
{ | |||||
const SOSCoefficients kFilter384000x1[1] = | |||||
{ | |||||
{ {6.09620331e-02, 1.21896769e-01, 6.09620331e-02, }, {-1.22760212e+00, 4.71422957e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter384000x1); | |||||
break; | |||||
} | |||||
case 705600: // o = 1, fp = 20000, fst = 352800, cost = 705600 | |||||
{ | |||||
const SOSCoefficients kFilter705600x1[1] = | |||||
{ | |||||
{ {2.13438638e-02, 4.26550556e-02, 2.13438638e-02, }, {-1.57253460e+00, 6.57877382e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter705600x1); | |||||
break; | |||||
} | |||||
case 768000: // o = 1, fp = 20000, fst = 384000, cost = 768000 | |||||
{ | |||||
const SOSCoefficients kFilter768000x1[1] = | |||||
{ | |||||
{ {1.83197956e-02, 3.66063440e-02, 1.83197956e-02, }, {-1.60702602e+00, 6.80271956e-01, } }, | |||||
}; | |||||
AAFilter<T>::filter_.Init(1, kFilter768000x1); | |||||
break; | |||||
} | |||||
//[[[end]]] | |||||
} | |||||
} | |||||
}; | |||||
} |
@@ -0,0 +1,89 @@ | |||||
# Anti-aliasing filter design | |||||
# 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 <https://www.gnu.org/licenses/>. | |||||
from scipy import signal | |||||
import math | |||||
import cog | |||||
# fs: base sampling rate in Hz | |||||
# os: oversampling factor | |||||
# fpass: passband corner in Hz | |||||
# fstop: stopband corner in Hz | |||||
# rpass: passband ripple in dB | |||||
# rstop: stopband attenuation in dB | |||||
def design(fs, os, fpass, fstop, rpass, rstop): | |||||
wp = fpass / (fs * os) | |||||
ws = min(0.5, fstop / (fs * os)) | |||||
n, wc = signal.ellipord(wp*2, ws*2, rpass, rstop) | |||||
# We are using second-order sections, so if the filter order would have | |||||
# been odd, we can bump it up by 1 for 'free' | |||||
n = 2 * int(math.ceil(n / 2)) | |||||
# Non-oversampled sampling rates result in 0-order filters, since there | |||||
# is no spectral content above fs/2. Bump these up to order 2 so we | |||||
# get some rolloff. | |||||
n = max(2, n) | |||||
z, p, k = signal.ellip(n, rpass, rstop, wc, output='zpk') | |||||
if n % 2 == 0: | |||||
# DC gain is -rpass for even-order filters, so amplify by rpass | |||||
k *= math.pow(10, rpass / 20) | |||||
sos = signal.zpk2sos(z, p, k) | |||||
cascade = FilterDescription((fs, os, n, wc/2, ws, sos)) | |||||
return cascade | |||||
class FilterDescription(tuple): | |||||
def __init__(self, desc): | |||||
(fs, os, n, wc, ws, sos) = desc | |||||
self.sample_rate = fs | |||||
self.oversampling = os | |||||
self.order = n | |||||
self.wpass = wc | |||||
self.wstop = ws | |||||
self.fpass = wc * fs * os | |||||
self.fstop = ws * fs * os | |||||
self.sections = sos | |||||
def print_filter_cases(filters): | |||||
for f in filters: | |||||
fs = f.sample_rate | |||||
factor = f.oversampling | |||||
num_sections = len(f.sections) | |||||
name = 'kFilter{}x{}'.format(fs, factor) | |||||
cost = fs * factor * num_sections | |||||
cog.outl('case {}: // o = {}, fp = {}, fst = {}, cost = {}' | |||||
.format(fs, factor, int(f.fpass), int(f.fstop), cost)) | |||||
cog.outl('{') | |||||
cog.outl(' const SOSCoefficients {}[{}] =' | |||||
.format(name, num_sections)) | |||||
cog.outl(' {') | |||||
print_coeff = lambda c: '{:.8e},'.format(c).ljust(17) | |||||
for s in f.sections: | |||||
b = ''.join([print_coeff(c) for c in s[:3]]) | |||||
a = ''.join([print_coeff(c) for c in s[4:]]) | |||||
cog.outl(' { {' + b + '}, {' + a + '} },') | |||||
cog.outl(' };') | |||||
cog.outl(' AAFilter<T>::filter_.Init({}, {});' | |||||
.format(num_sections, name)) | |||||
cog.outl(' break;') | |||||
cog.outl('}') |
@@ -0,0 +1,66 @@ | |||||
// Copyright 2013 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
// See http://creativecommons.org/licenses/MIT/ for more information. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// Driver for ADC. | |||||
#pragma once | |||||
#include "stmlib/stmlib.h" | |||||
namespace streams | |||||
{ | |||||
const uint8_t kNumPots = 4; | |||||
const uint8_t kNumCVs = 6; | |||||
class AdcEmulator | |||||
{ | |||||
public: | |||||
AdcEmulator() { } | |||||
~AdcEmulator() { } | |||||
void Init(void) | |||||
{ | |||||
} | |||||
int32_t pot(uint8_t index) const | |||||
{ | |||||
return pots_[index]; | |||||
} | |||||
int32_t cv(uint8_t index) const | |||||
{ | |||||
return cvs_[index]; | |||||
} | |||||
uint16_t pots_[kNumPots]; | |||||
uint16_t cvs_[kNumCVs]; | |||||
private: | |||||
DISALLOW_COPY_AND_ASSIGN(AdcEmulator); | |||||
}; | |||||
} // namespace streams |
@@ -0,0 +1,218 @@ | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#pragma once | |||||
#include "rack.hpp" | |||||
#include "aafilter.hpp" | |||||
namespace streams | |||||
{ | |||||
using namespace rack; | |||||
class AnalogEngine | |||||
{ | |||||
public: | |||||
struct ChannelFrame | |||||
{ | |||||
// Parameters | |||||
float level_mod_knob; | |||||
float response_knob; | |||||
// Inputs | |||||
float signal_in; | |||||
float level_cv; | |||||
float dac_cv; | |||||
float pwm_cv; | |||||
// Outputs | |||||
float signal_out; | |||||
float adc_out; | |||||
}; | |||||
struct Frame | |||||
{ | |||||
ChannelFrame ch1; | |||||
ChannelFrame ch2; | |||||
}; | |||||
AnalogEngine() | |||||
{ | |||||
Reset(); | |||||
SetSampleRate(1.f); | |||||
} | |||||
void Reset(void) | |||||
{ | |||||
v_out_ = 0.f; | |||||
} | |||||
void SetSampleRate(float sample_rate) | |||||
{ | |||||
sample_time_ = 1.f / sample_rate; | |||||
oversampling_ = OversamplingFactor(sample_rate); | |||||
float oversampled_rate = sample_rate * oversampling_; | |||||
up_filter_[0].Init(sample_rate); | |||||
up_filter_[1].Init(sample_rate); | |||||
down_filter_.Init(sample_rate); | |||||
auto cutoff = float_4(kDACFilterCutoff, | |||||
kDACFilterCutoff, | |||||
kPWMFilterCutoff, | |||||
kPWMFilterCutoff); | |||||
rc_lpf_.setCutoffFreq(cutoff / oversampled_rate); | |||||
} | |||||
void Process(Frame& frame) | |||||
{ | |||||
auto a_inputs = float_4(frame.ch1.signal_in, | |||||
frame.ch2.signal_in, | |||||
frame.ch1.level_cv, | |||||
frame.ch2.level_cv); | |||||
auto d_inputs = float_4(frame.ch1.dac_cv, | |||||
frame.ch2.dac_cv, | |||||
frame.ch1.pwm_cv, | |||||
frame.ch2.pwm_cv); | |||||
auto level_mod = float_4(frame.ch1.level_mod_knob, | |||||
frame.ch2.level_mod_knob, 0.f, 0.f); | |||||
auto response = float_4(frame.ch1.response_knob, | |||||
frame.ch2.response_knob, 0.f, 0.f); | |||||
float_4 output; | |||||
float timestep = sample_time_ / oversampling_; | |||||
for (int i = 0; i < oversampling_; i++) | |||||
{ | |||||
// Upsample and apply anti-aliasing filters | |||||
a_inputs = up_filter_[0].Process( | |||||
(i == 0) ? (a_inputs * oversampling_) : 0.f); | |||||
d_inputs = up_filter_[1].Process( | |||||
(i == 0) ? (d_inputs * oversampling_) : 0.f); | |||||
rc_lpf_.process(d_inputs); | |||||
d_inputs = rc_lpf_.lowpass(); | |||||
float_4 signal_in = a_inputs; | |||||
float_4 level_cv = _mm_movehl_ps(a_inputs.v, a_inputs.v); | |||||
float_4 dac_cv = d_inputs; | |||||
auto level = CalculateLevel(dac_cv, level_cv, level_mod, response); | |||||
signal_in *= level; | |||||
float_4 pwm_cv = _mm_movehl_ps(d_inputs.v, d_inputs.v); | |||||
pwm_cv *= kPWMCVInputR / (kPWMCVInputR + kPWMCVOutputR); | |||||
auto rad_per_s = -PinVoltageToLevel(pwm_cv) / kFilterCoreRC; | |||||
// Solve each VCF cell using the backward Euler method. | |||||
float_4 v_in = _mm_movelh_ps(signal_in.v, v_out_.v); | |||||
v_out_ = (v_in + v_out_) * simd::exp(rad_per_s * timestep) - v_in; | |||||
v_out_ = simd::clamp(v_out_, -kClampVoltage, kClampVoltage); | |||||
// Pre-downsample anti-alias filtering | |||||
// output will contain the lower two elements from level and the | |||||
// upper two elements from v_out_ | |||||
output = | |||||
_mm_shuffle_ps(level.v, v_out_.v, _MM_SHUFFLE(3, 2, 1, 0)); | |||||
output = down_filter_.Process(output); | |||||
} | |||||
frame.ch1.signal_out = output[2]; | |||||
frame.ch2.signal_out = output[3]; | |||||
// We don't care too much about aliasing in this signal, since it's | |||||
// only fed back to the digital section for VU metering. | |||||
output = LevelToPinVoltage(output); | |||||
frame.ch1.adc_out = output[0]; | |||||
frame.ch2.adc_out = output[1]; | |||||
} | |||||
protected: | |||||
using float_4 = simd::float_4; | |||||
float sample_time_; | |||||
int oversampling_; | |||||
UpsamplingAAFilter<float_4> up_filter_[2]; | |||||
DownsamplingAAFilter<float_4> down_filter_; | |||||
dsp::TRCFilter<float_4> rc_lpf_; | |||||
float_4 v_out_; | |||||
// Calculate level from the VCA control pin voltage | |||||
template <typename T> | |||||
T PinVoltageToLevel(T v_control) | |||||
{ | |||||
return simd::pow(10.f, v_control / (kVCAGainConstant * 20.f)); | |||||
} | |||||
// Calculate VCA control pin voltage from level | |||||
template <typename T> | |||||
T LevelToPinVoltage(T level) | |||||
{ | |||||
T volts = kVCAGainConstant * 20.f * simd::log10(level); | |||||
return simd::ifelse(level > 0.f, volts, kClampVoltage); | |||||
} | |||||
// Calculate level from the CV inputs and pots | |||||
template <typename T> | |||||
T CalculateLevel(T dac_cv, T level_cv, T level_mod, T response) | |||||
{ | |||||
T power = (kLevelResponseMinR + kLevelResponsePotR) / | |||||
(kLevelResponseMinR + (kLevelResponsePotR * response)); | |||||
T i_level = level_mod * level_cv / kLevelCVInputR; | |||||
T i_dac = dac_cv / (kDACCVOutputR + kDACCVInputR); | |||||
T i_in = i_level + i_dac + kVCAOffsetI; | |||||
T base = -i_in / kLevelRefI; | |||||
T level = simd::pow(base, power); | |||||
level = simd::ifelse(base > 0.f, level, 0.f); | |||||
return simd::fmin(level, kVCAMaxLevel); | |||||
} | |||||
// The 2164's gain constant is -33mV/dB | |||||
static constexpr float kVCAGainConstant = -33e-3f; | |||||
// The 2164's maximum gain is +20dB | |||||
static constexpr float kVCAMaxLevel = 10.f; | |||||
static constexpr float kLevelCVInputR = 100e3f; | |||||
static constexpr float kDACCVOutputR = 11e3f; | |||||
static constexpr float kDACCVInputR = 14e3f; | |||||
static constexpr float kVCAOffsetI = -10.f / 10e6f; | |||||
static constexpr float kPWMCVOutputR = 1.2e3f; | |||||
static constexpr float kPWMCVInputR = 2.5e3f; | |||||
// Level response VCA input current | |||||
static constexpr float kLevelRefI = -10.f / 200e3f; | |||||
// Level response potentiometer and series resistor | |||||
static constexpr float kLevelResponsePotR = 10e3f; | |||||
static constexpr float kLevelResponseMinR = 510.f; | |||||
// Opamp saturation voltage | |||||
static constexpr float kClampVoltage = 10.5f; | |||||
// 1N4148 forward voltage | |||||
static constexpr float kDiodeVoltage = 745e-3f; | |||||
static constexpr float kDACFilterCutoff = 12.7e3f; | |||||
static constexpr float kPWMFilterCutoff = 242.f; | |||||
static constexpr float kFilterCoreR = 100e3f; | |||||
static constexpr float kFilterCoreC = 33e-12f; | |||||
static constexpr float kFilterCoreRC = kFilterCoreR * kFilterCoreC; | |||||
}; | |||||
} |
@@ -0,0 +1,118 @@ | |||||
// Copyright 2014 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
// See http://creativecommons.org/licenses/MIT/ for more information. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// Discriminate an ADC signal into audio or CV ; and provide RMS stats. | |||||
#pragma once | |||||
#include "stmlib/stmlib.h" | |||||
namespace streams | |||||
{ | |||||
class AudioCvMeter | |||||
{ | |||||
public: | |||||
AudioCvMeter() { } | |||||
~AudioCvMeter() { } | |||||
void Init() | |||||
{ | |||||
peak_ = 0; | |||||
zero_crossing_interval_ = 0; | |||||
average_zero_crossing_interval_ = 0; | |||||
previous_sample_ = 0; | |||||
cv_ = false; | |||||
} | |||||
void Process(int32_t sample, uint32_t timestep_us) | |||||
{ | |||||
if ((sample >> 1) * previous_sample_ < 0 || | |||||
zero_crossing_interval_ >= (4096L * kHardwareTimestep_us) / timestep_us) | |||||
{ | |||||
int32_t error = zero_crossing_interval_ - average_zero_crossing_interval_; | |||||
average_zero_crossing_interval_ += error >> 3; | |||||
zero_crossing_interval_ = 0; | |||||
} | |||||
else | |||||
{ | |||||
++zero_crossing_interval_; | |||||
} | |||||
if (cv_ && average_zero_crossing_interval_ < (200L * kHardwareTimestep_us) / timestep_us) | |||||
{ | |||||
cv_ = false; | |||||
} | |||||
else if (!cv_ && average_zero_crossing_interval_ > (400L * kHardwareTimestep_us) / timestep_us) | |||||
{ | |||||
cv_ = true; | |||||
} | |||||
previous_sample_ = sample; | |||||
if (sample < 0) | |||||
{ | |||||
sample = -sample; | |||||
} | |||||
int32_t error = sample - peak_; | |||||
int32_t coefficient = 33; // 250ms at 1kHz | |||||
if (error > 0) | |||||
{ | |||||
coefficient = 809; // 10ms at 1kHz | |||||
} | |||||
coefficient = (coefficient * timestep_us) / kHardwareTimestep_us; | |||||
peak_ += error * coefficient >> 15; | |||||
} | |||||
inline bool cv() const | |||||
{ | |||||
return cv_; | |||||
} | |||||
inline int32_t peak() const | |||||
{ | |||||
return peak_; | |||||
} | |||||
private: | |||||
// Hardware Streams updates its LEDs at 4kHz (250us period), but we may be | |||||
// using a different rate here. We take this into account when filtering | |||||
// or interval-counting so that the software LEDs feel the same. | |||||
static constexpr uint32_t kHardwareTimestep_us = 250; | |||||
bool cv_; | |||||
int32_t peak_; | |||||
int32_t zero_crossing_interval_; | |||||
int32_t average_zero_crossing_interval_; | |||||
int32_t previous_sample_; | |||||
}; | |||||
} // namespace streams |
@@ -0,0 +1,138 @@ | |||||
// Copyright 2013 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
// See http://creativecommons.org/licenses/MIT/ for more information. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// Calibration settings. | |||||
#pragma once | |||||
#include "stmlib/stmlib.h" | |||||
#include "streams/gain.h" | |||||
#include "adc.hpp" | |||||
namespace streams | |||||
{ | |||||
const uint8_t kNumChannels = 2; | |||||
struct CalibrationSettings | |||||
{ | |||||
uint16_t excite_offset[kNumChannels]; | |||||
uint16_t audio_offset[kNumChannels]; | |||||
uint16_t dac_offset[kNumChannels]; | |||||
uint16_t padding[2]; | |||||
}; | |||||
class CvScaler | |||||
{ | |||||
public: | |||||
CvScaler() { } | |||||
~CvScaler() { } | |||||
void Init(AdcEmulator* adc) | |||||
{ | |||||
adc_ = adc; | |||||
for (uint8_t i = 0; i < 2; ++i) | |||||
{ | |||||
calibration_settings_.excite_offset[i] = 32768; | |||||
calibration_settings_.audio_offset[i] = 32768; | |||||
calibration_settings_.dac_offset[i] = | |||||
kDefaultOffset - (kDefaultOffset >> 4); | |||||
} | |||||
} | |||||
void CaptureAdcOffsets() | |||||
{ | |||||
for (uint8_t i = 0; i < 2; ++i) | |||||
{ | |||||
calibration_settings_.excite_offset[i] = adc_->cv(3 * i + 0); | |||||
calibration_settings_.audio_offset[i] = adc_->cv(3 * i + 1); | |||||
} | |||||
} | |||||
void SaveCalibrationData() | |||||
{ | |||||
} | |||||
// This class owns the calibration data and is reponsible for removing | |||||
// offsets. | |||||
inline int16_t audio_sample(uint8_t channel) const | |||||
{ | |||||
int32_t value = static_cast<int32_t>( | |||||
calibration_settings_.audio_offset[channel]) - \ | |||||
adc_->cv(channel * 3 + 1); | |||||
CLIP(value); | |||||
return value; | |||||
} | |||||
inline int16_t excite_sample(uint8_t channel) const | |||||
{ | |||||
int32_t value = static_cast<int32_t>( | |||||
calibration_settings_.excite_offset[channel]) - \ | |||||
adc_->cv(channel * 3); | |||||
CLIP(value); | |||||
return value; | |||||
} | |||||
inline int32_t gain_sample(uint8_t channel) const | |||||
{ | |||||
return -2 * adc_->cv(channel * 3 + 2); | |||||
} | |||||
inline int32_t raw_gain_sample(uint8_t channel) const | |||||
{ | |||||
return adc_->cv(channel * 3 + 2); | |||||
} | |||||
inline void set_dac_offset(uint8_t channel, uint16_t offset) | |||||
{ | |||||
calibration_settings_.dac_offset[channel] = offset; | |||||
} | |||||
inline uint16_t ScaleGain(uint8_t channel, uint16_t gain) const | |||||
{ | |||||
int32_t g = static_cast<int32_t>(gain); | |||||
g += static_cast<int32_t>(calibration_settings_.dac_offset[channel]); | |||||
if (g > 65535) | |||||
{ | |||||
g = 65535; | |||||
} | |||||
return static_cast<uint16_t>(g); | |||||
} | |||||
private: | |||||
AdcEmulator* adc_; | |||||
CalibrationSettings calibration_settings_; | |||||
DISALLOW_COPY_AND_ASSIGN(CvScaler); | |||||
}; | |||||
} // namespace streams |
@@ -0,0 +1,236 @@ | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#pragma once | |||||
#include <cmath> | |||||
#include "rack.hpp" | |||||
#include "cv_scaler.hpp" | |||||
#include "ui.hpp" | |||||
#include "eurorack/streams/processor.h" | |||||
namespace streams | |||||
{ | |||||
using namespace rack; | |||||
class DigitalEngine | |||||
{ | |||||
public: | |||||
static constexpr int kSampleRate = 31089; | |||||
template <int block_size> | |||||
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 <int block_size> | |||||
struct Frame | |||||
{ | |||||
ChannelFrame<block_size> ch1; | |||||
ChannelFrame<block_size> 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 <int block_size> | |||||
void Process(Frame<block_size>& 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<float_4> led_lpf_[4]; | |||||
template <int block_size> | |||||
void ProcessUI(Frame<block_size>& 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; | |||||
}; | |||||
} |
@@ -0,0 +1,105 @@ | |||||
// Copyright 2011 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// 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 <http://www.gnu.org/licenses/>. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// Event queue. | |||||
#pragma once | |||||
#include "stmlib/utils/ring_buffer.h" | |||||
namespace streams | |||||
{ | |||||
enum ControlType | |||||
{ | |||||
CONTROL_POT = 0, | |||||
CONTROL_ENCODER = 1, | |||||
CONTROL_ENCODER_CLICK = 2, | |||||
CONTROL_ENCODER_LONG_CLICK = 3, | |||||
CONTROL_SWITCH = 4, | |||||
CONTROL_SWITCH_HOLD = 5, | |||||
CONTROL_REFRESH = 0xff | |||||
}; | |||||
struct Event | |||||
{ | |||||
ControlType control_type; | |||||
uint16_t control_id; | |||||
int32_t data; | |||||
}; | |||||
template<uint16_t size = 32> | |||||
class EventQueue | |||||
{ | |||||
public: | |||||
EventQueue() { } | |||||
void Init() | |||||
{ | |||||
events_.Init(); | |||||
time_ = 0; | |||||
} | |||||
void Flush() | |||||
{ | |||||
events_.Flush(); | |||||
}; | |||||
void AddEvent(ControlType control_type, uint16_t id, int32_t data) | |||||
{ | |||||
Event e; | |||||
e.control_type = control_type; | |||||
e.control_id = id; | |||||
e.data = data; | |||||
events_.Overwrite(e); | |||||
Touch(); | |||||
} | |||||
void Touch() | |||||
{ | |||||
last_event_time_ = time_; | |||||
} | |||||
size_t available() | |||||
{ | |||||
return events_.readable(); | |||||
} | |||||
uint32_t idle_time() | |||||
{ | |||||
uint32_t now = time_; | |||||
return now - last_event_time_; | |||||
} | |||||
Event PullEvent() | |||||
{ | |||||
return events_.ImmediateRead(); | |||||
} | |||||
void StepTime(uint32_t step) | |||||
{ | |||||
time_ += step; | |||||
} | |||||
private: | |||||
uint32_t last_event_time_; | |||||
stmlib::RingBuffer<Event, size> events_; | |||||
uint32_t time_; | |||||
}; | |||||
} | |||||
@@ -0,0 +1,170 @@ | |||||
// Copyright 2013 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
// See http://creativecommons.org/licenses/MIT/ for more information. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// Driver for the 4 channels LEDs. | |||||
#pragma once | |||||
#include "stmlib/stmlib.h" | |||||
namespace streams | |||||
{ | |||||
const uint8_t kNumLeds = 8; | |||||
class LedsEmulator | |||||
{ | |||||
public: | |||||
LedsEmulator() { } | |||||
~LedsEmulator() { } | |||||
void Init() | |||||
{ | |||||
Clear(); | |||||
} | |||||
void set(uint8_t led, uint8_t red, uint8_t green) | |||||
{ | |||||
red_[led] = red; | |||||
green_[led] = green; | |||||
} | |||||
float intensity_red(uint8_t led) | |||||
{ | |||||
return red_[led] / 255.f; | |||||
} | |||||
float intensity_green(uint8_t led) | |||||
{ | |||||
return green_[led] / 255.f; | |||||
} | |||||
void Clear() | |||||
{ | |||||
std::fill(&red_[0], &red_[kNumLeds], 0); | |||||
std::fill(&green_[0], &green_[kNumLeds], 0); | |||||
} | |||||
void PaintBar(int32_t start, int32_t direction, int32_t db) | |||||
{ | |||||
if (db < 0) | |||||
{ | |||||
return; | |||||
} | |||||
if (db > 32767) | |||||
{ | |||||
db = 32767; | |||||
} | |||||
db <<= 1; | |||||
if (db >= 49152) | |||||
{ | |||||
set(start, (db - 49152) >> 6, 0); | |||||
set(start + direction, 255, 255); | |||||
set(start + 2 * direction, 0, 255); | |||||
set(start + 3 * direction, 0, 255); | |||||
} | |||||
else if (db >= 32768) | |||||
{ | |||||
set(start + direction, (db - 32768) >> 6, (db - 32768) >> 6); | |||||
set(start + 2 * direction, 0, 255); | |||||
set(start + 3 * direction, 0, 255); | |||||
} | |||||
else if (db >= 16384) | |||||
{ | |||||
set(start + 2 * direction, 0, (db - 16384) >> 6); | |||||
set(start + 3 * direction, 0, 255); | |||||
} | |||||
else | |||||
{ | |||||
set(start + 3 * direction, 0, db >> 6); | |||||
} | |||||
} | |||||
void PaintPositiveBar(uint8_t channel, int32_t db) | |||||
{ | |||||
PaintBar(channel * 4, +1, db); | |||||
} | |||||
void PaintNegativeBar(uint8_t channel, int32_t db) | |||||
{ | |||||
PaintBar(channel * 4 + 3, -1, -db); | |||||
} | |||||
void PaintCv(uint8_t channel, int32_t cv) | |||||
{ | |||||
uint8_t bank = channel * 4; | |||||
bool flip = false; | |||||
if (cv < 0) | |||||
{ | |||||
flip = true; | |||||
cv = -cv; | |||||
} | |||||
if (cv < 1024) | |||||
{ | |||||
cv = 0; | |||||
} | |||||
if (cv > 32767) | |||||
{ | |||||
cv = 32767; | |||||
} | |||||
for (uint8_t i = 0; i < 4; ++i) | |||||
{ | |||||
int16_t residual = (cv - (3 - i) * 8192); | |||||
if (residual < 0) | |||||
{ | |||||
residual = 0; | |||||
} | |||||
else if (residual > 8191) | |||||
{ | |||||
residual = 8191; | |||||
} | |||||
if (flip) | |||||
{ | |||||
set(bank + i, residual >> 5, 0); | |||||
} | |||||
else | |||||
{ | |||||
set(bank + i, 0, residual >> 5); | |||||
} | |||||
} | |||||
} | |||||
private: | |||||
uint8_t red_[kNumLeds]; | |||||
uint8_t green_[kNumLeds]; | |||||
DISALLOW_COPY_AND_ASSIGN(LedsEmulator); | |||||
}; | |||||
} // namespace streams |
@@ -0,0 +1,207 @@ | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#pragma once | |||||
#include "rack.hpp" | |||||
namespace streams | |||||
{ | |||||
template <int num_inputs, int num_outputs, int block_size, int buffer_size> | |||||
class SRCResampler | |||||
{ | |||||
public: | |||||
void Init(int outer_sample_rate, int inner_sample_rate, int quality) | |||||
{ | |||||
input_src.setRates(outer_sample_rate, inner_sample_rate); | |||||
output_src.setRates(inner_sample_rate, outer_sample_rate); | |||||
input_src.setQuality(quality); | |||||
output_src.setQuality(quality); | |||||
Reset(); | |||||
} | |||||
void Reset(void) | |||||
{ | |||||
input_src.refreshState(); | |||||
output_src.refreshState(); | |||||
input_buffer.clear(); | |||||
output_buffer.clear(); | |||||
} | |||||
using InputFrame = dsp::Frame<num_inputs>; | |||||
using OutputFrame = dsp::Frame<num_outputs>; | |||||
template <typename F> | |||||
OutputFrame Process(InputFrame& input_frame, F callback) | |||||
{ | |||||
if (!input_buffer.full()) | |||||
{ | |||||
input_buffer.push(input_frame); | |||||
} | |||||
if (output_buffer.empty()) | |||||
{ | |||||
// Resample input buffer | |||||
InputFrame input_frames[block_size] = {}; | |||||
int in_len = input_buffer.size(); | |||||
int out_len = block_size; | |||||
input_src.process( | |||||
input_buffer.startData(), &in_len, | |||||
input_frames, &out_len); | |||||
input_buffer.startIncr(in_len); | |||||
// Process the resampled signal | |||||
// We might not fill all of the input buffer if there is a | |||||
// deficiency, but this cannot be avoided due to imprecisions | |||||
// between the input and output SRC. | |||||
OutputFrame output_frames[block_size]; | |||||
callback(output_frames, input_frames); | |||||
// Resample output buffer | |||||
in_len = block_size; | |||||
out_len = output_buffer.capacity(); | |||||
output_src.process( | |||||
output_frames, &in_len, | |||||
output_buffer.endData(), &out_len); | |||||
output_buffer.endIncr(out_len); | |||||
} | |||||
// Set output | |||||
OutputFrame output_frame = {}; | |||||
if (!output_buffer.empty()) | |||||
{ | |||||
output_frame = output_buffer.shift(); | |||||
} | |||||
return output_frame; | |||||
} | |||||
protected: | |||||
dsp::SampleRateConverter<num_inputs> input_src; | |||||
dsp::SampleRateConverter<num_outputs> output_src; | |||||
dsp::DoubleRingBuffer<InputFrame, buffer_size> input_buffer; | |||||
dsp::DoubleRingBuffer<OutputFrame, buffer_size> output_buffer; | |||||
}; | |||||
template <int num_inputs, int num_outputs, int block_size, int buffer_size> | |||||
class InterpolatingResampler | |||||
{ | |||||
public: | |||||
void Init(int outer_sample_rate, int inner_sample_rate, int quality) | |||||
{ | |||||
(void)quality; | |||||
ratio_ = inner_sample_rate * 1.f / outer_sample_rate; | |||||
ratio_inverse_ = 1.f / ratio_; | |||||
} | |||||
void Reset(void) | |||||
{ | |||||
in_buffer_.clear(); | |||||
out_buffer_.clear(); | |||||
out_buffer_.endIncr(block_size); | |||||
in_phase_ = 1.f; | |||||
prev_input_ = {}; | |||||
prev_output_ = {}; | |||||
next_output_ = {}; | |||||
} | |||||
using InputFrame = dsp::Frame<num_inputs>; | |||||
using OutputFrame = dsp::Frame<num_outputs>; | |||||
template <typename F> | |||||
OutputFrame Process(InputFrame& input_frame, F callback) | |||||
{ | |||||
int pushed = -in_buffer_.size(); | |||||
while (in_phase_ <= 1.f) | |||||
{ | |||||
// Resample and push into input buffer | |||||
auto input = CrossfadeFrame(prev_input_, input_frame, in_phase_); | |||||
in_buffer_.push(input); | |||||
in_phase_ += ratio_inverse_; | |||||
} | |||||
in_phase_ -= 1.f; | |||||
prev_input_ = input_frame; | |||||
pushed += in_buffer_.size(); | |||||
// Process the resampled signal | |||||
while (in_buffer_.size() >= block_size) | |||||
{ | |||||
callback(out_buffer_.endData(), in_buffer_.startData()); | |||||
in_buffer_.startIncr(block_size); | |||||
out_buffer_.endIncr(block_size); | |||||
} | |||||
// Resample from output buffer | |||||
float phase = clamp(1.f - in_phase_ * ratio_, 0.f, 1.f); | |||||
while (pushed--) | |||||
{ | |||||
prev_output_ = next_output_; | |||||
next_output_ = out_buffer_.shift(); | |||||
} | |||||
return CrossfadeFrame(prev_output_, next_output_, phase); | |||||
} | |||||
protected: | |||||
float ratio_; | |||||
float ratio_inverse_; | |||||
dsp::DoubleRingBuffer<InputFrame, buffer_size> in_buffer_; | |||||
dsp::DoubleRingBuffer<OutputFrame, buffer_size> out_buffer_; | |||||
float in_phase_; | |||||
InputFrame prev_input_; | |||||
OutputFrame prev_output_; | |||||
OutputFrame next_output_; | |||||
template <typename T> | |||||
T Crossfade(T a, T b, float x) | |||||
{ | |||||
return a + (b - a) * x; | |||||
} | |||||
InputFrame CrossfadeFrame(InputFrame a, InputFrame b, float x) | |||||
{ | |||||
InputFrame result; | |||||
for (int i = 0; i < num_inputs; i++) | |||||
{ | |||||
result.samples[i] = Crossfade(a.samples[i], b.samples[i], x); | |||||
} | |||||
return result; | |||||
} | |||||
OutputFrame CrossfadeFrame(OutputFrame a, OutputFrame b, float x) | |||||
{ | |||||
OutputFrame result; | |||||
for (int i = 0; i < num_outputs; i++) | |||||
{ | |||||
result.samples[i] = Crossfade(a.samples[i], b.samples[i], x); | |||||
} | |||||
return result; | |||||
} | |||||
}; | |||||
} |
@@ -0,0 +1,118 @@ | |||||
// Cascaded second-order sections IIR filter | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#pragma once | |||||
namespace streams | |||||
{ | |||||
struct SOSCoefficients | |||||
{ | |||||
float b[3]; | |||||
float a[2]; | |||||
}; | |||||
template <typename T, int max_num_sections> | |||||
class SOSFilter | |||||
{ | |||||
public: | |||||
SOSFilter() | |||||
{ | |||||
Init(0); | |||||
} | |||||
SOSFilter(int num_sections) | |||||
{ | |||||
Init(num_sections); | |||||
} | |||||
void Init(int num_sections) | |||||
{ | |||||
num_sections_ = num_sections; | |||||
Reset(); | |||||
} | |||||
void Init(int num_sections, const SOSCoefficients* sections) | |||||
{ | |||||
num_sections_ = num_sections; | |||||
Reset(); | |||||
SetCoefficients(sections); | |||||
} | |||||
void Reset() | |||||
{ | |||||
for (int n = 0; n < num_sections_; n++) | |||||
{ | |||||
x_[n][0] = 0.f; | |||||
x_[n][1] = 0.f; | |||||
x_[n][2] = 0.f; | |||||
} | |||||
x_[num_sections_][0] = 0.f; | |||||
x_[num_sections_][1] = 0.f; | |||||
x_[num_sections_][2] = 0.f; | |||||
} | |||||
void SetCoefficients(const SOSCoefficients* sections) | |||||
{ | |||||
for (int n = 0; n < num_sections_; n++) | |||||
{ | |||||
sections_[n].b[0] = sections[n].b[0]; | |||||
sections_[n].b[1] = sections[n].b[1]; | |||||
sections_[n].b[2] = sections[n].b[2]; | |||||
sections_[n].a[0] = sections[n].a[0]; | |||||
sections_[n].a[1] = sections[n].a[1]; | |||||
} | |||||
} | |||||
T Process(T in) | |||||
{ | |||||
for (int n = 0; n < num_sections_; n++) | |||||
{ | |||||
// Shift x state | |||||
x_[n][2] = x_[n][1]; | |||||
x_[n][1] = x_[n][0]; | |||||
x_[n][0] = in; | |||||
T out = 0.f; | |||||
// Add x state | |||||
out += sections_[n].b[0] * x_[n][0]; | |||||
out += sections_[n].b[1] * x_[n][1]; | |||||
out += sections_[n].b[2] * x_[n][2]; | |||||
// Subtract y state | |||||
out -= sections_[n].a[0] * x_[n+1][0]; | |||||
out -= sections_[n].a[1] * x_[n+1][1]; | |||||
in = out; | |||||
} | |||||
// Shift final section x state | |||||
x_[num_sections_][2] = x_[num_sections_][1]; | |||||
x_[num_sections_][1] = x_[num_sections_][0]; | |||||
x_[num_sections_][0] = in; | |||||
return in; | |||||
} | |||||
protected: | |||||
int num_sections_; | |||||
SOSCoefficients sections_[max_num_sections]; | |||||
T x_[max_num_sections + 1][3]; | |||||
}; | |||||
} |
@@ -0,0 +1,237 @@ | |||||
// 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 <https://www.gnu.org/licenses/>. | |||||
#pragma once | |||||
#include <cmath> | |||||
#include "rack.hpp" | |||||
#include "resampler.hpp" | |||||
#include "analog_engine.hpp" | |||||
#include "digital_engine.hpp" | |||||
namespace streams | |||||
{ | |||||
using namespace rack; | |||||
class StreamsEngine | |||||
{ | |||||
public: | |||||
struct ChannelFrame | |||||
{ | |||||
// Parameters | |||||
float shape_knob; | |||||
float mod_knob; | |||||
float level_mod_knob; | |||||
float response_knob; | |||||
bool function_button; | |||||
// Inputs | |||||
float excite_in; | |||||
float signal_in; | |||||
float level_cv; | |||||
bool signal_in_connected; | |||||
bool level_cv_connected; | |||||
// Outputs | |||||
float signal_out; | |||||
// Lights | |||||
float led_green[4]; | |||||
float led_red[4]; | |||||
}; | |||||
struct Frame | |||||
{ | |||||
ChannelFrame ch1; | |||||
ChannelFrame ch2; | |||||
bool metering_button; | |||||
bool lights_updated; | |||||
}; | |||||
StreamsEngine() | |||||
{ | |||||
Reset(); | |||||
SetSampleRate(1.f); | |||||
} | |||||
void Reset(void) | |||||
{ | |||||
adc_lpf_.reset(); | |||||
resampler_.Reset(); | |||||
analog_engine_.Reset(); | |||||
digital_engine_.Reset(); | |||||
adc_feedback_[0] = 0.f; | |||||
adc_feedback_[1] = 0.f; | |||||
} | |||||
void SetSampleRate(float sample_rate) | |||||
{ | |||||
adc_lpf_.setCutoffFreq(kAdcFilterCutoff / sample_rate); | |||||
resampler_.Init(sample_rate, DigitalEngine::kSampleRate, 0); | |||||
analog_engine_.SetSampleRate(sample_rate); | |||||
} | |||||
void Randomize(void) | |||||
{ | |||||
digital_engine_.Randomize(); | |||||
} | |||||
void ApplySettings(const UiSettings& settings) | |||||
{ | |||||
digital_engine_.ApplySettings(settings); | |||||
} | |||||
const UiSettings& ui_settings(void) | |||||
{ | |||||
return digital_engine_.ui_settings(); | |||||
} | |||||
void SyncUI(const StreamsEngine& other) | |||||
{ | |||||
digital_engine_.SyncUI(other.digital_engine_); | |||||
} | |||||
void Process(Frame& frame) | |||||
{ | |||||
frame.lights_updated = false; | |||||
float ch1_signal_in = frame.ch1.signal_in_connected ? | |||||
frame.ch1.signal_in : kSignalInNormalV; | |||||
float ch2_signal_in = frame.ch2.signal_in_connected ? | |||||
frame.ch2.signal_in : kSignalInNormalV; | |||||
float ch1_level_cv = frame.ch1.level_cv_connected ? | |||||
frame.ch1.level_cv : kLevelNormalV; | |||||
float ch2_level_cv = frame.ch2.level_cv_connected ? | |||||
frame.ch2.level_cv : kLevelNormalV; | |||||
Rsmp::InputFrame d_input; | |||||
d_input.samples[0] = ch1_signal_in; | |||||
d_input.samples[1] = ch2_signal_in; | |||||
d_input.samples[2] = frame.ch1.excite_in; | |||||
d_input.samples[3] = frame.ch2.excite_in; | |||||
d_input.samples[4] = adc_feedback_[0]; | |||||
d_input.samples[5] = adc_feedback_[1]; | |||||
auto adc_input = simd::float_4::load(d_input.samples); | |||||
adc_lpf_.process(adc_input); | |||||
adc_input = adc_lpf_.lowpass(); | |||||
adc_input = kAdcFilterOffset + adc_input * kAdcFilterGain; | |||||
adc_input.store(d_input.samples); | |||||
DigitalEngine::Frame<kBlockSize> d_frame; | |||||
Rsmp::OutputFrame d_output = resampler_.Process(d_input, | |||||
[&](Rsmp::OutputFrame* output, const Rsmp::InputFrame* input) | |||||
{ | |||||
d_frame.ch1.shape_knob = frame.ch1.shape_knob; | |||||
d_frame.ch1.mod_knob = frame.ch1.mod_knob; | |||||
d_frame.ch1.level_mod_knob = frame.ch1.level_mod_knob; | |||||
d_frame.ch1.response_knob = frame.ch1.response_knob; | |||||
d_frame.ch2.shape_knob = frame.ch2.shape_knob; | |||||
d_frame.ch2.mod_knob = frame.ch2.mod_knob; | |||||
d_frame.ch2.level_mod_knob = frame.ch2.level_mod_knob; | |||||
d_frame.ch2.response_knob = frame.ch2.response_knob; | |||||
d_frame.ch1.function_button = frame.ch1.function_button; | |||||
d_frame.ch2.function_button = frame.ch2.function_button; | |||||
d_frame.metering_button = frame.metering_button; | |||||
for (int i = 0; i < kBlockSize; i++) | |||||
{ | |||||
d_frame.ch1.signal_in[i] = input[i].samples[0]; | |||||
d_frame.ch2.signal_in[i] = input[i].samples[1]; | |||||
d_frame.ch1.excite_in[i] = input[i].samples[2]; | |||||
d_frame.ch2.excite_in[i] = input[i].samples[3]; | |||||
d_frame.ch1.level_adc_in[i] = input[i].samples[4]; | |||||
d_frame.ch2.level_adc_in[i] = input[i].samples[5]; | |||||
} | |||||
digital_engine_.Process(d_frame); | |||||
for (int i = 0; i < kBlockSize; i++) | |||||
{ | |||||
output[i].samples[0] = d_frame.ch1.dac_out[i]; | |||||
output[i].samples[1] = d_frame.ch1.pwm_out[i]; | |||||
output[i].samples[2] = d_frame.ch2.dac_out[i]; | |||||
output[i].samples[3] = d_frame.ch2.pwm_out[i]; | |||||
} | |||||
frame.ch1.led_green[0] = d_frame.ch1.led_green[0]; | |||||
frame.ch1.led_green[1] = d_frame.ch1.led_green[1]; | |||||
frame.ch1.led_green[2] = d_frame.ch1.led_green[2]; | |||||
frame.ch1.led_green[3] = d_frame.ch1.led_green[3]; | |||||
frame.ch1.led_red[0] = d_frame.ch1.led_red[0]; | |||||
frame.ch1.led_red[1] = d_frame.ch1.led_red[1]; | |||||
frame.ch1.led_red[2] = d_frame.ch1.led_red[2]; | |||||
frame.ch1.led_red[3] = d_frame.ch1.led_red[3]; | |||||
frame.ch2.led_green[0] = d_frame.ch2.led_green[0]; | |||||
frame.ch2.led_green[1] = d_frame.ch2.led_green[1]; | |||||
frame.ch2.led_green[2] = d_frame.ch2.led_green[2]; | |||||
frame.ch2.led_green[3] = d_frame.ch2.led_green[3]; | |||||
frame.ch2.led_red[0] = d_frame.ch2.led_red[0]; | |||||
frame.ch2.led_red[1] = d_frame.ch2.led_red[1]; | |||||
frame.ch2.led_red[2] = d_frame.ch2.led_red[2]; | |||||
frame.ch2.led_red[3] = d_frame.ch2.led_red[3]; | |||||
frame.lights_updated = true; | |||||
}); | |||||
AnalogEngine::Frame a_frame; | |||||
a_frame.ch1.level_mod_knob = frame.ch1.level_mod_knob; | |||||
a_frame.ch1.response_knob = frame.ch1.response_knob; | |||||
a_frame.ch2.level_mod_knob = frame.ch2.level_mod_knob; | |||||
a_frame.ch2.response_knob = frame.ch2.response_knob; | |||||
a_frame.ch1.signal_in = ch1_signal_in; | |||||
a_frame.ch1.level_cv = ch1_level_cv; | |||||
a_frame.ch2.signal_in = ch2_signal_in; | |||||
a_frame.ch2.level_cv = ch2_level_cv; | |||||
a_frame.ch1.dac_cv = d_output.samples[0]; | |||||
a_frame.ch1.pwm_cv = d_output.samples[1]; | |||||
a_frame.ch2.dac_cv = d_output.samples[2]; | |||||
a_frame.ch2.pwm_cv = d_output.samples[3]; | |||||
analog_engine_.Process(a_frame); | |||||
frame.ch1.signal_out = a_frame.ch1.signal_out; | |||||
frame.ch2.signal_out = a_frame.ch2.signal_out; | |||||
adc_feedback_[0] = a_frame.ch1.adc_out; | |||||
adc_feedback_[1] = a_frame.ch2.adc_out; | |||||
} | |||||
protected: | |||||
static constexpr int kBlockSize = 16; | |||||
static constexpr float kAdcFilterCutoff = 1.f / (2 * M_PI * 20e3f * 1e-9f); | |||||
static constexpr float kAdcFilterGain = -20e3f / 100e3f; | |||||
static constexpr float kAdcFilterOffset = -10.f * -20e3f / 120e3f; | |||||
static constexpr float kSignalInNormalV = 5.f; | |||||
static constexpr float kLevelNormalV = 8.f; | |||||
dsp::TRCFilter<simd::float_4> adc_lpf_; | |||||
using Rsmp = InterpolatingResampler<6, 4, kBlockSize, 256>; | |||||
Rsmp resampler_; | |||||
AnalogEngine analog_engine_; | |||||
DigitalEngine digital_engine_; | |||||
float adc_feedback_[2]; | |||||
}; | |||||
} |
@@ -0,0 +1,85 @@ | |||||
// Copyright 2013 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
// See http://creativecommons.org/licenses/MIT/ for more information. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// Driver for the Mode and Range switches. | |||||
#pragma once | |||||
#include <algorithm> | |||||
#include "stmlib/stmlib.h" | |||||
namespace streams | |||||
{ | |||||
const uint8_t kNumSwitches = 3; | |||||
class SwitchesEmulator | |||||
{ | |||||
public: | |||||
SwitchesEmulator() { } | |||||
~SwitchesEmulator() { } | |||||
void Init() | |||||
{ | |||||
std::fill(&switch_state_[0], &switch_state_[kNumSwitches], 0xff); | |||||
std::fill(&pins_[0], &pins_[kNumSwitches], true); | |||||
} | |||||
void SetPin(uint8_t index, bool state) | |||||
{ | |||||
pins_[index] = !state; | |||||
} | |||||
void Debounce() | |||||
{ | |||||
for (uint8_t i = 0; i < 3; ++i) | |||||
{ | |||||
switch_state_[i] = (switch_state_[i] << 1) | pins_[i]; | |||||
} | |||||
} | |||||
inline bool released(uint8_t index) const | |||||
{ | |||||
return switch_state_[index] == 0x7f; | |||||
} | |||||
inline bool just_pressed(uint8_t index) const | |||||
{ | |||||
return switch_state_[index] == 0x80; | |||||
} | |||||
inline bool pressed(uint8_t index) const | |||||
{ | |||||
return switch_state_[index] == 0x00; | |||||
} | |||||
private: | |||||
uint8_t switch_state_[kNumSwitches]; | |||||
bool pins_[kNumSwitches]; | |||||
}; | |||||
} // namespace streams |
@@ -0,0 +1,708 @@ | |||||
// Copyright 2013 Emilie Gillet. | |||||
// | |||||
// Author: Emilie Gillet (emilie.o.gillet@gmail.com) | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
// See http://creativecommons.org/licenses/MIT/ for more information. | |||||
// | |||||
// ----------------------------------------------------------------------------- | |||||
// | |||||
// User interface. | |||||
#pragma once | |||||
#include <algorithm> | |||||
#include "stmlib/stmlib.h" | |||||
#include "src/Streams/event_queue.hpp" | |||||
#include "src/Streams/audio_cv_meter.hpp" | |||||
#include "src/Streams/cv_scaler.hpp" | |||||
#include "streams/processor.h" | |||||
#include "src/Streams/adc.hpp" | |||||
#include "src/Streams/leds.hpp" | |||||
#include "src/Streams/switches.hpp" | |||||
namespace streams | |||||
{ | |||||
enum DisplayMode | |||||
{ | |||||
DISPLAY_MODE_FUNCTION, | |||||
DISPLAY_MODE_MONITOR, | |||||
DISPLAY_MODE_MONITOR_FUNCTION | |||||
}; | |||||
enum MonitorMode | |||||
{ | |||||
MONITOR_MODE_EXCITE_IN, | |||||
MONITOR_MODE_VCA_CV, | |||||
MONITOR_MODE_AUDIO_IN, | |||||
MONITOR_MODE_OUTPUT, | |||||
MONITOR_MODE_LAST | |||||
}; | |||||
enum Switch | |||||
{ | |||||
SWITCH_MODE_1, | |||||
SWITCH_MODE_2, | |||||
SWITCH_MONITOR, | |||||
SWITCH_LAST | |||||
}; | |||||
struct UiSettings | |||||
{ | |||||
uint8_t function[kNumChannels]; | |||||
uint8_t alternate[kNumChannels]; | |||||
uint8_t monitor_mode; | |||||
uint8_t linked; | |||||
uint8_t padding[2]; | |||||
}; | |||||
class Processor; | |||||
class Ui | |||||
{ | |||||
public: | |||||
Ui() { } | |||||
~Ui() { } | |||||
void Init(AdcEmulator* adc, CvScaler* cv_scaler, Processor* processor, | |||||
UiSettings* settings = nullptr) | |||||
{ | |||||
queue_.Init(); | |||||
time_us_ = 0; | |||||
leds_.Init(); | |||||
switches_.Init(); | |||||
adc_ = adc; | |||||
cv_scaler_ = cv_scaler; | |||||
processor_ = processor; | |||||
for (int i = 0; i < kNumPots; i++) | |||||
{ | |||||
pot_value_[i] = 0; | |||||
pot_threshold_[i] = 0; | |||||
} | |||||
for (int i = 0; i < kNumSwitches; i++) | |||||
{ | |||||
press_time_[i] = 0; | |||||
} | |||||
if (settings) | |||||
{ | |||||
ApplySettings(*settings); | |||||
} | |||||
else | |||||
{ | |||||
// Flash is not formatted. Initialize. | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
ui_settings_.function[i] = PROCESSOR_FUNCTION_ENVELOPE; | |||||
ui_settings_.alternate[i] = false; | |||||
} | |||||
ui_settings_.monitor_mode = MONITOR_MODE_OUTPUT; | |||||
ui_settings_.linked = false; | |||||
ApplySettings(ui_settings_); | |||||
} | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
meter_[i].Init(); | |||||
display_mode_[i] = DISPLAY_MODE_MONITOR; | |||||
} | |||||
secret_handshake_counter_ = 0; | |||||
} | |||||
void Poll(uint32_t timestep_us) | |||||
{ | |||||
queue_.StepTime(timestep_us); | |||||
time_us_ += timestep_us; | |||||
switches_.Debounce(); | |||||
for (uint8_t i = 0; i < kNumSwitches; ++i) | |||||
{ | |||||
if (switches_.just_pressed(i)) | |||||
{ | |||||
queue_.AddEvent(CONTROL_SWITCH, i, 0); | |||||
press_time_[i] = (time_us_ / 1000); | |||||
} | |||||
if (switches_.pressed(i) && press_time_[i] != 0) | |||||
{ | |||||
int32_t pressed_time = (time_us_ / 1000) - press_time_[i]; | |||||
if (pressed_time > kLongPressDuration) | |||||
{ | |||||
queue_.AddEvent(CONTROL_SWITCH, i, pressed_time); | |||||
press_time_[i] = 0; | |||||
} | |||||
} | |||||
if (switches_.released(i) && press_time_[i] != 0) | |||||
{ | |||||
queue_.AddEvent( | |||||
CONTROL_SWITCH, | |||||
i, | |||||
(time_us_ / 1000) - press_time_[i] + 1); | |||||
press_time_[i] = 0; | |||||
} | |||||
} | |||||
for (uint8_t i = 0; i < kNumPots; ++i) | |||||
{ | |||||
int32_t value = adc_->pot(i); | |||||
int32_t current_value = pot_value_[i]; | |||||
if (value >= current_value + pot_threshold_[i] || | |||||
value <= current_value - pot_threshold_[i] || | |||||
!pot_threshold_[i]) | |||||
{ | |||||
Event e; | |||||
e.control_id = i; | |||||
e.data = value; | |||||
queue_.AddEvent(CONTROL_POT, i, e.data); | |||||
pot_value_[i] = value; | |||||
pot_threshold_[i] = 256; | |||||
} | |||||
} | |||||
PaintLeds(timestep_us); | |||||
} | |||||
void DoEvents() | |||||
{ | |||||
bool refresh = false; | |||||
while (queue_.available()) | |||||
{ | |||||
Event e = queue_.PullEvent(); | |||||
if (e.control_type == CONTROL_SWITCH) | |||||
{ | |||||
if (e.data == 0) | |||||
{ | |||||
OnSwitchPressed(e); | |||||
} | |||||
else | |||||
{ | |||||
OnSwitchReleased(e); | |||||
} | |||||
} | |||||
else if (e.control_type == CONTROL_POT) | |||||
{ | |||||
OnPotMoved(e); | |||||
} | |||||
refresh = true; | |||||
} | |||||
if (queue_.idle_time() / 1000 > 1000) | |||||
{ | |||||
queue_.Touch(); | |||||
if (display_mode_[0] == DISPLAY_MODE_MONITOR_FUNCTION && | |||||
display_mode_[1] == DISPLAY_MODE_MONITOR_FUNCTION) | |||||
{ | |||||
display_mode_[0] = display_mode_[1] = DISPLAY_MODE_MONITOR; | |||||
} | |||||
refresh = true; | |||||
} | |||||
// Recompute processor parameters if necessary. | |||||
if (refresh) | |||||
{ | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
processor_[i].Configure(); | |||||
} | |||||
} | |||||
} | |||||
void FlushEvents() | |||||
{ | |||||
queue_.Flush(); | |||||
} | |||||
LedsEmulator& leds() | |||||
{ | |||||
return leds_; | |||||
} | |||||
SwitchesEmulator& switches() | |||||
{ | |||||
return switches_; | |||||
} | |||||
void ToggleLink(uint8_t follower_channel) | |||||
{ | |||||
bool state = !linked(); | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
display_mode_[i] = DISPLAY_MODE_FUNCTION; | |||||
processor_[i].set_linked(state); | |||||
} | |||||
Link(1 - follower_channel); | |||||
SaveState(); | |||||
} | |||||
void SetLinked(bool state) | |||||
{ | |||||
if (state != linked()) | |||||
{ | |||||
ToggleLink(1); | |||||
} | |||||
} | |||||
bool linked() | |||||
{ | |||||
return processor_[0].linked(); | |||||
} | |||||
const UiSettings& settings() | |||||
{ | |||||
return ui_settings_; | |||||
} | |||||
void ApplySettings(const UiSettings& settings) | |||||
{ | |||||
bool ch2_changed = settings.function[1] != ui_settings_.function[1] || | |||||
settings.alternate[1] != ui_settings_.alternate[1]; | |||||
bool channels_eq = settings.function[0] == settings.function[1] && | |||||
settings.alternate[0] == settings.alternate[1]; | |||||
bool unlink = ch2_changed && !channels_eq; | |||||
ui_settings_ = settings; | |||||
if (unlink) | |||||
{ | |||||
ui_settings_.linked = false; | |||||
} | |||||
else if (ui_settings_.linked) | |||||
{ | |||||
ui_settings_.function[1] = ui_settings_.function[0]; | |||||
ui_settings_.alternate[1] = ui_settings_.alternate[0]; | |||||
} | |||||
monitor_mode_ = static_cast<MonitorMode>(ui_settings_.monitor_mode); | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
processor_[i].set_alternate(ui_settings_.alternate[i]); | |||||
processor_[i].set_linked(ui_settings_.linked); | |||||
processor_[i].set_function( | |||||
static_cast<ProcessorFunction>(ui_settings_.function[i])); | |||||
} | |||||
} | |||||
DisplayMode display_mode(int channel) | |||||
{ | |||||
return display_mode_[channel]; | |||||
} | |||||
MonitorMode monitor_mode(void) | |||||
{ | |||||
return monitor_mode_; | |||||
} | |||||
void SyncUI(const Ui& other) | |||||
{ | |||||
ApplySettings(other.ui_settings_); | |||||
for (int i = 0; i < kNumChannels; i++) | |||||
{ | |||||
press_time_[i] = other.press_time_[i]; | |||||
display_mode_[i] = other.display_mode_[i]; | |||||
pot_value_[i] = other.pot_value_[i]; | |||||
pot_threshold_[i] = other.pot_threshold_[i]; | |||||
meter_[i] = other.meter_[i]; | |||||
} | |||||
time_us_ = other.time_us_; | |||||
switches_ = other.switches_; | |||||
monitor_mode_ = other.monitor_mode_; | |||||
ui_settings_ = other.ui_settings_; | |||||
secret_handshake_counter_ = other.secret_handshake_counter_; | |||||
} | |||||
private: | |||||
// Synchronize UI state of channels 1 & 2. | |||||
void Link(uint8_t index) | |||||
{ | |||||
if (processor_[0].linked()) | |||||
{ | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
if (i != index) | |||||
{ | |||||
display_mode_[i] = display_mode_[index]; | |||||
processor_[i].set_function(processor_[index].function()); | |||||
processor_[i].set_alternate(processor_[index].alternate()); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
void OnSwitchPressed(const Event& e) | |||||
{ | |||||
switch (e.control_id) | |||||
{ | |||||
case SWITCH_MONITOR: | |||||
{ | |||||
if (display_mode_[0] == DISPLAY_MODE_MONITOR && | |||||
display_mode_[1] == DISPLAY_MODE_MONITOR) | |||||
{ | |||||
display_mode_[0] = display_mode_[1] = DISPLAY_MODE_MONITOR_FUNCTION; | |||||
} | |||||
else if (display_mode_[0] == DISPLAY_MODE_MONITOR_FUNCTION && | |||||
display_mode_[1] == DISPLAY_MODE_MONITOR_FUNCTION) | |||||
{ | |||||
monitor_mode_ = static_cast<MonitorMode>(monitor_mode_ + 1); | |||||
if (monitor_mode_ == MONITOR_MODE_LAST) | |||||
{ | |||||
monitor_mode_ = static_cast<MonitorMode>(0); | |||||
} | |||||
SaveState(); | |||||
} | |||||
else | |||||
{ | |||||
display_mode_[0] = display_mode_[1] = DISPLAY_MODE_MONITOR; | |||||
} | |||||
} | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
} | |||||
void OnSwitchReleased(const Event& e) | |||||
{ | |||||
// Detect secret handshake for easter egg... | |||||
uint8_t secret_handshake_code = e.control_id; | |||||
secret_handshake_code |= e.data >= kLongPressDuration ? 2 : 0; | |||||
if ((secret_handshake_counter_ & 3) == secret_handshake_code) | |||||
{ | |||||
++secret_handshake_counter_; | |||||
if (secret_handshake_counter_ == 16) | |||||
{ | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
processor_[i].set_alternate(false); | |||||
processor_[i].set_function(PROCESSOR_FUNCTION_LORENZ_GENERATOR); | |||||
} | |||||
SaveState(); | |||||
secret_handshake_counter_ = 0; | |||||
return; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
secret_handshake_counter_ = 0; | |||||
} | |||||
if (e.data >= kLongPressDuration) | |||||
{ | |||||
// Handle long presses. | |||||
switch (e.control_id) | |||||
{ | |||||
case SWITCH_MONITOR: | |||||
break; | |||||
case SWITCH_MODE_1: | |||||
case SWITCH_MODE_2: | |||||
{ | |||||
processor_[e.control_id].set_alternate( | |||||
!processor_[e.control_id].alternate()); | |||||
if (processor_[e.control_id].function() > | |||||
PROCESSOR_FUNCTION_COMPRESSOR) | |||||
{ | |||||
processor_[e.control_id].set_function(PROCESSOR_FUNCTION_ENVELOPE); | |||||
} | |||||
display_mode_[e.control_id] = DISPLAY_MODE_FUNCTION; | |||||
int32_t other = 1 - e.control_id; | |||||
if (display_mode_[other] == DISPLAY_MODE_MONITOR_FUNCTION) | |||||
{ | |||||
display_mode_[other] = DISPLAY_MODE_MONITOR; | |||||
} | |||||
Link(e.control_id); | |||||
SaveState(); | |||||
} | |||||
break; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
switch (e.control_id) | |||||
{ | |||||
case SWITCH_MODE_1: | |||||
case SWITCH_MODE_2: | |||||
{ | |||||
if (display_mode_[e.control_id] == DISPLAY_MODE_FUNCTION) | |||||
{ | |||||
ProcessorFunction index = processor_[e.control_id].function(); | |||||
index = static_cast<ProcessorFunction>(index + 1); | |||||
ProcessorFunction limit = processor_[e.control_id].alternate() | |||||
? PROCESSOR_FUNCTION_FILTER_CONTROLLER | |||||
: PROCESSOR_FUNCTION_COMPRESSOR; | |||||
if (index > limit) | |||||
{ | |||||
index = static_cast<ProcessorFunction>(0); | |||||
} | |||||
processor_[e.control_id].set_function(index); | |||||
} | |||||
else | |||||
{ | |||||
display_mode_[e.control_id] = DISPLAY_MODE_FUNCTION; | |||||
int32_t other = 1 - e.control_id; | |||||
if (display_mode_[other] == DISPLAY_MODE_MONITOR_FUNCTION) | |||||
{ | |||||
display_mode_[other] = DISPLAY_MODE_MONITOR; | |||||
} | |||||
} | |||||
Link(e.control_id); | |||||
SaveState(); | |||||
} | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
void OnPotMoved(const Event& e) | |||||
{ | |||||
processor_[0].set_global(e.control_id, e.data); | |||||
processor_[1].set_global(e.control_id, e.data); | |||||
processor_[e.control_id >> 1].set_parameter(e.control_id & 1, e.data); | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
processor_[i].Configure(); | |||||
} | |||||
} | |||||
void SaveState() | |||||
{ | |||||
ui_settings_.monitor_mode = monitor_mode_; | |||||
ui_settings_.linked = processor_[0].linked(); | |||||
ui_settings_.function[0] = processor_[0].function(); | |||||
ui_settings_.function[1] = processor_[1].function(); | |||||
ui_settings_.alternate[0] = processor_[0].alternate(); | |||||
ui_settings_.alternate[1] = processor_[1].alternate(); | |||||
} | |||||
void PaintLeds(uint32_t timestep_us) | |||||
{ | |||||
leds_.Clear(); | |||||
for (uint8_t i = 0; i < kNumChannels; ++i) | |||||
{ | |||||
uint8_t bank = i * 4; | |||||
switch (display_mode_[i]) | |||||
{ | |||||
case DISPLAY_MODE_FUNCTION: | |||||
{ | |||||
bool alternate = processor_[i].alternate(); | |||||
uint8_t intensity = 255; | |||||
if (processor_[i].linked()) | |||||
{ | |||||
uint8_t phase = (time_us_ / 1000) >> 1; | |||||
phase += i * 128; | |||||
phase = phase < 128 ? phase : (255 - phase); | |||||
intensity = (phase * 224 >> 7) + 32; | |||||
intensity = intensity * intensity >> 8; | |||||
} | |||||
uint8_t function = processor_[i].function(); | |||||
if (function == PROCESSOR_FUNCTION_FILTER_CONTROLLER) | |||||
{ | |||||
for (uint8_t j = 0; j < 4; ++j) | |||||
{ | |||||
leds_.set(bank + j, | |||||
alternate ? intensity : 0, | |||||
alternate ? 0 : intensity); | |||||
} | |||||
} | |||||
else if (function < PROCESSOR_FUNCTION_LORENZ_GENERATOR) | |||||
{ | |||||
leds_.set( | |||||
bank + function, | |||||
alternate ? intensity : 0, | |||||
alternate ? 0 : intensity); | |||||
} | |||||
else | |||||
{ | |||||
uint8_t index = (processor_[i].last_gain() >> 4) * 5 >> 4; | |||||
if (index > 3) | |||||
{ | |||||
index = 3; | |||||
} | |||||
int16_t color = processor_[i].last_frequency(); | |||||
color = color - 128; | |||||
color *= 2; | |||||
if (color < 0) | |||||
{ | |||||
if (color < -127) | |||||
{ | |||||
color = -127; | |||||
} | |||||
leds_.set(bank + index, 255 + (color * 2), 255); | |||||
} | |||||
else | |||||
{ | |||||
if (color > 127) | |||||
{ | |||||
color = 127; | |||||
} | |||||
leds_.set(bank + index, 255, 255 - (color * 2)); | |||||
} | |||||
} | |||||
} | |||||
break; | |||||
case DISPLAY_MODE_MONITOR_FUNCTION: | |||||
{ | |||||
uint8_t position = static_cast<uint8_t>(monitor_mode_); | |||||
leds_.set(position * 2, 255, 0); | |||||
leds_.set(position * 2 + 1, 255, 0); | |||||
} | |||||
break; | |||||
case DISPLAY_MODE_MONITOR: | |||||
PaintMonitor(i, timestep_us); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
void PaintMonitor(uint8_t channel, uint32_t timestep_us) | |||||
{ | |||||
switch (monitor_mode_) | |||||
{ | |||||
case MONITOR_MODE_EXCITE_IN: | |||||
PaintAdaptive(channel, cv_scaler_->excite_sample(channel), 0, timestep_us); | |||||
break; | |||||
case MONITOR_MODE_AUDIO_IN: | |||||
PaintAdaptive(channel, cv_scaler_->audio_sample(channel), 0, timestep_us); | |||||
break; | |||||
case MONITOR_MODE_VCA_CV: | |||||
leds_.PaintPositiveBar(channel, 32768 + cv_scaler_->gain_sample(channel)); | |||||
break; | |||||
case MONITOR_MODE_OUTPUT: | |||||
if (processor_[channel].function() == PROCESSOR_FUNCTION_COMPRESSOR) | |||||
{ | |||||
leds_.PaintNegativeBar(channel, processor_[channel].gain_reduction()); | |||||
} | |||||
else | |||||
{ | |||||
PaintAdaptive( | |||||
channel, | |||||
cv_scaler_->audio_sample(channel), | |||||
cv_scaler_->gain_sample(channel), | |||||
timestep_us); | |||||
} | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
} | |||||
void PaintAdaptive(uint8_t channel, int32_t sample, int32_t gain, | |||||
uint32_t timestep_us) | |||||
{ | |||||
meter_[channel].Process(sample, timestep_us); | |||||
if (meter_[channel].cv()) | |||||
{ | |||||
sample = sample * lut_2164_gain[-gain >> 9] >> 15; | |||||
leds_.PaintCv(channel, sample * 5 >> 2); | |||||
} | |||||
else | |||||
{ | |||||
leds_.PaintPositiveBar(channel, wav_db[meter_[channel].peak() >> 7] + gain); | |||||
} | |||||
} | |||||
EventQueue<16> queue_; | |||||
uint32_t time_us_; | |||||
AdcEmulator* adc_; | |||||
CvScaler* cv_scaler_; | |||||
Processor* processor_; | |||||
LedsEmulator leds_; | |||||
SwitchesEmulator switches_; | |||||
uint32_t press_time_[kNumSwitches]; | |||||
DisplayMode display_mode_[kNumChannels]; | |||||
MonitorMode monitor_mode_; | |||||
UiSettings ui_settings_; | |||||
uint8_t secret_handshake_counter_; | |||||
int32_t pot_value_[kNumPots]; | |||||
int32_t pot_threshold_[kNumPots]; | |||||
AudioCvMeter meter_[kNumChannels]; | |||||
const int32_t kLongPressDuration = 1000; | |||||
DISALLOW_COPY_AND_ASSIGN(Ui); | |||||
}; | |||||
} // namespace streams | |||||