@@ -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 "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 include |