Browse Source

Add Tyler's Streams code.

v1
Andrew Belt 1 year ago
parent
commit
7b326b51f6
15 changed files with 3522 additions and 108 deletions
  1. +487
    -108
      src/Streams.cpp
  2. +540
    -0
      src/Streams/aafilter.hpp
  3. +89
    -0
      src/Streams/aafilter.py
  4. +66
    -0
      src/Streams/adc.hpp
  5. +218
    -0
      src/Streams/analog_engine.hpp
  6. +118
    -0
      src/Streams/audio_cv_meter.hpp
  7. +138
    -0
      src/Streams/cv_scaler.hpp
  8. +236
    -0
      src/Streams/digital_engine.hpp
  9. +105
    -0
      src/Streams/event_queue.hpp
  10. +170
    -0
      src/Streams/leds.hpp
  11. +207
    -0
      src/Streams/resampler.hpp
  12. +118
    -0
      src/Streams/sos.hpp
  13. +237
    -0
      src/Streams/streams.hpp
  14. +85
    -0
      src/Streams/switches.hpp
  15. +708
    -0
      src/Streams/ui.hpp

+ 487
- 108
src/Streams.cpp View File

@@ -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");

+ 540
- 0
src/Streams/aafilter.hpp View File

@@ -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]]]
}
}
};

}

+ 89
- 0
src/Streams/aafilter.py View File

@@ -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('}')

+ 66
- 0
src/Streams/adc.hpp View File

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

+ 218
- 0
src/Streams/analog_engine.hpp View File

@@ -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;
};

}

+ 118
- 0
src/Streams/audio_cv_meter.hpp View File

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

+ 138
- 0
src/Streams/cv_scaler.hpp View File

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

+ 236
- 0
src/Streams/digital_engine.hpp View File

@@ -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;
};

}

+ 105
- 0
src/Streams/event_queue.hpp View File

@@ -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_;
};

}


+ 170
- 0
src/Streams/leds.hpp View File

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