diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4b896e5
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+
+### 0.6.1 (2018-09-12)
+
+- Added Macro Oscillator 2 from Audible Instruments Preview
diff --git a/Makefile b/Makefile
index c998d77..cc7d1a3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
SLUG = AudibleInstruments
-VERSION = 0.6.0
+VERSION = 0.6.1
FLAGS += \
-DTEST \
@@ -7,14 +7,23 @@ FLAGS += \
-Wno-unused-local-typedefs
SOURCES += $(wildcard src/*.cpp)
+
SOURCES += eurorack/stmlib/utils/random.cc
SOURCES += eurorack/stmlib/dsp/atan.cc
SOURCES += eurorack/stmlib/dsp/units.cc
+
SOURCES += eurorack/braids/macro_oscillator.cc
SOURCES += eurorack/braids/analog_oscillator.cc
SOURCES += eurorack/braids/digital_oscillator.cc
SOURCES += eurorack/braids/quantizer.cc
SOURCES += eurorack/braids/resources.cc
+
+SOURCES += $(wildcard eurorack/plaits/dsp/*.cc)
+SOURCES += $(wildcard eurorack/plaits/dsp/engine/*.cc)
+SOURCES += $(wildcard eurorack/plaits/dsp/speech/*.cc)
+SOURCES += $(wildcard eurorack/plaits/dsp/physical_modelling/*.cc)
+SOURCES += eurorack/plaits/resources.cc
+
SOURCES += eurorack/clouds/dsp/correlator.cc
SOURCES += eurorack/clouds/dsp/granular_processor.cc
SOURCES += eurorack/clouds/dsp/mu_law.cc
@@ -22,6 +31,7 @@ SOURCES += eurorack/clouds/dsp/pvoc/frame_transformation.cc
SOURCES += eurorack/clouds/dsp/pvoc/phase_vocoder.cc
SOURCES += eurorack/clouds/dsp/pvoc/stft.cc
SOURCES += eurorack/clouds/resources.cc
+
SOURCES += eurorack/elements/dsp/exciter.cc
SOURCES += eurorack/elements/dsp/ominous_voice.cc
SOURCES += eurorack/elements/dsp/resonator.cc
@@ -31,22 +41,27 @@ SOURCES += eurorack/elements/dsp/part.cc
SOURCES += eurorack/elements/dsp/string.cc
SOURCES += eurorack/elements/dsp/voice.cc
SOURCES += eurorack/elements/resources.cc
+
SOURCES += eurorack/rings/dsp/fm_voice.cc
SOURCES += eurorack/rings/dsp/part.cc
SOURCES += eurorack/rings/dsp/string_synth_part.cc
SOURCES += eurorack/rings/dsp/string.cc
SOURCES += eurorack/rings/dsp/resonator.cc
SOURCES += eurorack/rings/resources.cc
+
SOURCES += eurorack/tides/generator.cc
SOURCES += eurorack/tides/resources.cc
+
SOURCES += eurorack/warps/dsp/modulator.cc
SOURCES += eurorack/warps/dsp/oscillator.cc
SOURCES += eurorack/warps/dsp/vocoder.cc
SOURCES += eurorack/warps/dsp/filter_bank.cc
SOURCES += eurorack/warps/resources.cc
+
SOURCES += eurorack/frames/keyframer.cc
SOURCES += eurorack/frames/resources.cc
SOURCES += eurorack/frames/poly_lfo.cc
+
SOURCES += eurorack/peaks/processors.cc
SOURCES += eurorack/peaks/resources.cc
SOURCES += eurorack/peaks/drums/bass_drum.cc
diff --git a/eurorack b/eurorack
index 916d962..0bcabd2 160000
--- a/eurorack
+++ b/eurorack
@@ -1 +1 @@
-Subproject commit 916d9620b538e004c8d1480ce378152805979eba
+Subproject commit 0bcabd2baa131576c38780972aee0ef1b30163f8
diff --git a/res/Plaits.svg b/res/Plaits.svg
new file mode 100644
index 0000000..dabcb9d
--- /dev/null
+++ b/res/Plaits.svg
@@ -0,0 +1,2968 @@
+
+
+
+
diff --git a/src/AudibleInstruments.cpp b/src/AudibleInstruments.cpp
index 57ba4e7..fe068f4 100644
--- a/src/AudibleInstruments.cpp
+++ b/src/AudibleInstruments.cpp
@@ -9,6 +9,7 @@ void init(rack::Plugin *p) {
p->version = TOSTRING(VERSION);
p->addModel(modelBraids);
+ p->addModel(modelPlaits);
p->addModel(modelElements);
p->addModel(modelTides);
p->addModel(modelClouds);
diff --git a/src/AudibleInstruments.hpp b/src/AudibleInstruments.hpp
index 2105786..3568ad2 100644
--- a/src/AudibleInstruments.hpp
+++ b/src/AudibleInstruments.hpp
@@ -7,6 +7,7 @@ using namespace rack;
extern Plugin *plugin;
extern Model *modelBraids;
+extern Model *modelPlaits;
extern Model *modelElements;
extern Model *modelTides;
extern Model *modelClouds;
diff --git a/src/Braids.cpp b/src/Braids.cpp
index ab79e24..e45bbc6 100644
--- a/src/Braids.cpp
+++ b/src/Braids.cpp
@@ -129,7 +129,7 @@ void Braids::step() {
if (!settings.meta_modulation)
pitchV += fm;
if (lowCpu)
- pitchV += log2f(96000.0 / engineGetSampleRate());
+ pitchV += log2f(96000.f * engineGetSampleTime());
int32_t pitch = (pitchV * 12.0 + 60) * 128;
pitch += jitter_source.Render(settings.vco_drift);
pitch = clamp(pitch, 0, 16383);
diff --git a/src/Plaits.cpp b/src/Plaits.cpp
new file mode 100644
index 0000000..39c584b
--- /dev/null
+++ b/src/Plaits.cpp
@@ -0,0 +1,319 @@
+#include "AudibleInstruments.hpp"
+#include "dsp/samplerate.hpp"
+#include "dsp/ringbuffer.hpp"
+#include "dsp/functions.hpp"
+#include "dsp/digital.hpp"
+#include "plaits/dsp/voice.h"
+
+
+struct Plaits : Module {
+ enum ParamIds {
+ MODEL1_PARAM,
+ MODEL2_PARAM,
+ FREQ_PARAM,
+ HARMONICS_PARAM,
+ TIMBRE_PARAM,
+ MORPH_PARAM,
+ TIMBRE_CV_PARAM,
+ FREQ_CV_PARAM,
+ MORPH_CV_PARAM,
+ NUM_PARAMS
+ };
+ enum InputIds {
+ ENGINE_INPUT,
+ TIMBRE_INPUT,
+ FREQ_INPUT,
+ MORPH_INPUT,
+ HARMONICS_INPUT,
+ TRIGGER_INPUT,
+ LEVEL_INPUT,
+ NOTE_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ OUT_OUTPUT,
+ AUX_OUTPUT,
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ ENUMS(MODEL_LIGHT, 8 * 2),
+ NUM_LIGHTS
+ };
+
+ plaits::Voice voice;
+ plaits::Patch patch;
+ plaits::Modulations modulations;
+ char shared_buffer[16384];
+ float triPhase = 0.f;
+
+ SampleRateConverter<2> outputSrc;
+ DoubleRingBuffer, 256> outputBuffer;
+ bool lowCpu = false;
+ bool lpg = false;
+
+ SchmittTrigger model1Trigger;
+ SchmittTrigger model2Trigger;
+
+ Plaits() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
+ memset(shared_buffer, 0, sizeof(shared_buffer));
+ stmlib::BufferAllocator allocator(shared_buffer, sizeof(shared_buffer));
+ voice.Init(&allocator);
+
+ memset(&patch, 0, sizeof(patch));
+ memset(&modulations, 0, sizeof(modulations));
+ onReset();
+ }
+
+ void onReset() override {
+ patch.engine = 0;
+ patch.lpg_colour = 0.5f;
+ patch.decay = 0.5f;
+ }
+
+ void onRandomize() override {
+ patch.engine = randomu32() % 16;
+ }
+
+ json_t *toJson() override {
+ json_t *rootJ = json_object();
+
+ json_object_set_new(rootJ, "lowCpu", json_boolean(lowCpu));
+ json_object_set_new(rootJ, "model", json_integer(patch.engine));
+ json_object_set_new(rootJ, "lpgColor", json_real(patch.lpg_colour));
+ json_object_set_new(rootJ, "decay", json_real(patch.decay));
+
+ return rootJ;
+ }
+
+ void fromJson(json_t *rootJ) override {
+ json_t *lowCpuJ = json_object_get(rootJ, "lowCpu");
+ if (lowCpuJ)
+ lowCpu = json_boolean_value(lowCpuJ);
+
+ json_t *modelJ = json_object_get(rootJ, "model");
+ if (modelJ)
+ patch.engine = json_integer_value(modelJ);
+
+ json_t *lpgColorJ = json_object_get(rootJ, "lpgColor");
+ if (lpgColorJ)
+ patch.lpg_colour = json_number_value(lpgColorJ);
+
+ json_t *decayJ = json_object_get(rootJ, "decay");
+ if (decayJ)
+ patch.decay = json_number_value(decayJ);
+ }
+
+ void step() override {
+ if (outputBuffer.empty()) {
+ const int blockSize = 12;
+
+ // Model buttons
+ if (model1Trigger.process(params[MODEL1_PARAM].value)) {
+ if (patch.engine >= 8) {
+ patch.engine -= 8;
+ }
+ else {
+ patch.engine = (patch.engine + 1) % 8;
+ }
+ }
+ if (model2Trigger.process(params[MODEL2_PARAM].value)) {
+ if (patch.engine < 8) {
+ patch.engine += 8;
+ }
+ else {
+ patch.engine = (patch.engine + 1) % 8 + 8;
+ }
+ }
+
+ // Model lights
+ int activeEngine = voice.active_engine();
+ triPhase += 2.f * engineGetSampleTime() * blockSize;
+ if (triPhase >= 1.f)
+ triPhase -= 1.f;
+ float tri = (triPhase < 0.5f) ? triPhase * 2.f : (1.f - triPhase) * 2.f;
+
+ for (int i = 0; i < 8; i++) {
+ lights[MODEL_LIGHT + 2*i + 0].setBrightness((activeEngine == i) ? 1.f : (patch.engine == i) ? tri : 0.f);
+ lights[MODEL_LIGHT + 2*i + 1].setBrightness((activeEngine == i + 8) ? 1.f : (patch.engine == i + 8) ? tri : 0.f);
+ }
+
+ // Calculate pitch for lowCpu mode if needed
+ float pitch = params[FREQ_PARAM].value;
+ if (lowCpu)
+ pitch += log2f(48000.f * engineGetSampleTime());
+ // Update patch
+ patch.note = 60.f + pitch * 12.f;
+ patch.harmonics = params[HARMONICS_PARAM].value;
+ if (!lpg) {
+ patch.timbre = params[TIMBRE_PARAM].value;
+ patch.morph = params[MORPH_PARAM].value;
+ }
+ else {
+ patch.lpg_colour = params[TIMBRE_PARAM].value;
+ patch.decay = params[MORPH_PARAM].value;
+ }
+ patch.frequency_modulation_amount = params[FREQ_CV_PARAM].value;
+ patch.timbre_modulation_amount = params[TIMBRE_CV_PARAM].value;
+ patch.morph_modulation_amount = params[MORPH_CV_PARAM].value;
+
+ // Update modulations
+ modulations.engine = inputs[ENGINE_INPUT].value / 5.f;
+ modulations.note = inputs[NOTE_INPUT].value * 12.f;
+ modulations.frequency = inputs[FREQ_INPUT].value * 6.f;
+ modulations.harmonics = inputs[HARMONICS_INPUT].value / 5.f;
+ modulations.timbre = inputs[TIMBRE_INPUT].value / 8.f;
+ modulations.morph = inputs[MORPH_INPUT].value / 8.f;
+ // Triggers at around 0.7 V
+ modulations.trigger = inputs[TRIGGER_INPUT].value / 3.f;
+ modulations.level = inputs[LEVEL_INPUT].value / 8.f;
+
+ modulations.frequency_patched = inputs[FREQ_INPUT].active;
+ modulations.timbre_patched = inputs[TIMBRE_INPUT].active;
+ modulations.morph_patched = inputs[MORPH_INPUT].active;
+ modulations.trigger_patched = inputs[TRIGGER_INPUT].active;
+ modulations.level_patched = inputs[LEVEL_INPUT].active;
+
+ // Render frames
+ plaits::Voice::Frame output[blockSize];
+ voice.Render(patch, modulations, output, blockSize);
+
+ // Convert output to frames
+ Frame<2> outputFrames[blockSize];
+ for (int i = 0; i < blockSize; i++) {
+ outputFrames[i].samples[0] = output[i].out / 32768.f;
+ outputFrames[i].samples[1] = output[i].aux / 32768.f;
+ }
+
+ // Convert output
+ if (lowCpu) {
+ int len = min(outputBuffer.capacity(), blockSize);
+ memcpy(outputBuffer.endData(), outputFrames, len * sizeof(Frame<2>));
+ outputBuffer.endIncr(len);
+ }
+ else {
+ outputSrc.setRates(48000, engineGetSampleRate());
+ int inLen = blockSize;
+ int outLen = outputBuffer.capacity();
+ outputSrc.process(outputFrames, &inLen, outputBuffer.endData(), &outLen);
+ outputBuffer.endIncr(outLen);
+ }
+ }
+
+ // Set output
+ if (!outputBuffer.empty()) {
+ Frame<2> outputFrame = outputBuffer.shift();
+ outputs[OUT_OUTPUT].value = outputFrame.samples[0] * 5.f;
+ outputs[AUX_OUTPUT].value = outputFrame.samples[1] * 5.f;
+ }
+ }
+};
+
+
+static const std::string modelLabels[16] = {
+ "Pair of classic waveforms",
+ "Waveshaping oscillator",
+ "Two operator FM",
+ "Granular formant oscillator",
+ "Harmonic oscillator",
+ "Wavetable oscillator",
+ "Chords",
+ "Vowel and speech synthesis",
+ "Granular cloud",
+ "Filtered noise",
+ "Particle noise",
+ "Inharmonic string modeling",
+ "Modal resonator",
+ "Analog bass drum",
+ "Analog snare drum",
+ "Analog hi-hat",
+};
+
+
+struct PlaitsWidget : ModuleWidget {
+ PlaitsWidget(Plaits *module) : ModuleWidget(module) {
+ setPanel(SVG::load(assetPlugin(plugin, "res/Plaits.svg")));
+
+ addChild(Widget::create(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
+ addChild(Widget::create(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+ addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(ParamWidget::create(mm2px(Vec(23.32685, 14.6539)), module, Plaits::MODEL1_PARAM, 0.0, 1.0, 0.0));
+ addParam(ParamWidget::create(mm2px(Vec(32.22764, 14.6539)), module, Plaits::MODEL2_PARAM, 0.0, 1.0, 0.0));
+ addParam(ParamWidget::create(mm2px(Vec(3.1577, 20.21088)), module, Plaits::FREQ_PARAM, -4.0, 4.0, 0.0));
+ addParam(ParamWidget::create(mm2px(Vec(39.3327, 20.21088)), module, Plaits::HARMONICS_PARAM, 0.0, 1.0, 0.5));
+ addParam(ParamWidget::create(mm2px(Vec(4.04171, 49.6562)), module, Plaits::TIMBRE_PARAM, 0.0, 1.0, 0.5));
+ addParam(ParamWidget::create(mm2px(Vec(42.71716, 49.6562)), module, Plaits::MORPH_PARAM, 0.0, 1.0, 0.5));
+ addParam(ParamWidget::create(mm2px(Vec(7.88712, 77.60705)), module, Plaits::TIMBRE_CV_PARAM, -1.0, 1.0, 0.0));
+ addParam(ParamWidget::create(mm2px(Vec(27.2245, 77.60705)), module, Plaits::FREQ_CV_PARAM, -1.0, 1.0, 0.0));
+ addParam(ParamWidget::create(mm2px(Vec(46.56189, 77.60705)), module, Plaits::MORPH_CV_PARAM, -1.0, 1.0, 0.0));
+
+ addInput(Port::create(mm2px(Vec(3.31381, 92.48067)), Port::INPUT, module, Plaits::ENGINE_INPUT));
+ addInput(Port::create(mm2px(Vec(14.75983, 92.48067)), Port::INPUT, module, Plaits::TIMBRE_INPUT));
+ addInput(Port::create(mm2px(Vec(26.20655, 92.48067)), Port::INPUT, module, Plaits::FREQ_INPUT));
+ addInput(Port::create(mm2px(Vec(37.65257, 92.48067)), Port::INPUT, module, Plaits::MORPH_INPUT));
+ addInput(Port::create(mm2px(Vec(49.0986, 92.48067)), Port::INPUT, module, Plaits::HARMONICS_INPUT));
+ addInput(Port::create(mm2px(Vec(3.31381, 107.08103)), Port::INPUT, module, Plaits::TRIGGER_INPUT));
+ addInput(Port::create(mm2px(Vec(14.75983, 107.08103)), Port::INPUT, module, Plaits::LEVEL_INPUT));
+ addInput(Port::create(mm2px(Vec(26.20655, 107.08103)), Port::INPUT, module, Plaits::NOTE_INPUT));
+
+ addOutput(Port::create(mm2px(Vec(37.65257, 107.08103)), Port::OUTPUT, module, Plaits::OUT_OUTPUT));
+ addOutput(Port::create(mm2px(Vec(49.0986, 107.08103)), Port::OUTPUT, module, Plaits::AUX_OUTPUT));
+
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 23.31649)), module, Plaits::MODEL_LIGHT + 0 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 28.71704)), module, Plaits::MODEL_LIGHT + 1 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 34.1162)), module, Plaits::MODEL_LIGHT + 2 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 39.51675)), module, Plaits::MODEL_LIGHT + 3 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 44.91731)), module, Plaits::MODEL_LIGHT + 4 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 50.31785)), module, Plaits::MODEL_LIGHT + 5 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 55.71771)), module, Plaits::MODEL_LIGHT + 6 * 2));
+ addChild(ModuleLightWidget::create>(mm2px(Vec(28.79498, 61.11827)), module, Plaits::MODEL_LIGHT + 7 * 2));
+ }
+
+ void appendContextMenu(Menu *menu) override {
+ Plaits *module = dynamic_cast(this->module);
+
+ struct PlaitsLowCpuItem : MenuItem {
+ Plaits *module;
+ void onAction(EventAction &e) override {
+ module->lowCpu ^= true;
+ }
+ };
+
+ struct PlaitsLPGItem : MenuItem {
+ Plaits *module;
+ void onAction(EventAction &e) override {
+ module->lpg ^= true;
+ }
+ };
+
+ struct PlaitsModelItem : MenuItem {
+ Plaits *module;
+ int model;
+ void onAction(EventAction &e) override {
+ module->patch.engine = model;
+ }
+ };
+
+ menu->addChild(MenuEntry::create());
+ PlaitsLowCpuItem *lowCpuItem = MenuItem::create("Low CPU", CHECKMARK(module->lowCpu));
+ lowCpuItem->module = module;
+ menu->addChild(lowCpuItem);
+ PlaitsLPGItem *lpgItem = MenuItem::create("Edit LPG response/decay", CHECKMARK(module->lpg));
+ lpgItem->module = module;
+ menu->addChild(lpgItem);
+
+ menu->addChild(new MenuEntry());
+ menu->addChild(MenuLabel::create("Models"));
+ for (int i = 0; i < 16; i++) {
+ PlaitsModelItem *modelItem = MenuItem::create(modelLabels[i], CHECKMARK(module->patch.engine == i));
+ modelItem->module = module;
+ modelItem->model = i;
+ menu->addChild(modelItem);
+ }
+ }
+};
+
+
+Model *modelPlaits = Model::create("Audible Instruments", "Plaits", "Macro Oscillator 2", OSCILLATOR_TAG, WAVESHAPER_TAG);
+
diff --git a/src/Tides.cpp b/src/Tides.cpp
index 20b9610..d3dab7d 100644
--- a/src/Tides.cpp
+++ b/src/Tides.cpp
@@ -145,7 +145,7 @@ void Tides::step() {
// Sync
// Slight deviation from spec here.
// Instead of toggling sync by holding the range button, just enable it if the clock port is plugged in.
- generator.set_sync(inputs[CLOCK_INPUT].active);
+ generator.set_sync(inputs[CLOCK_INPUT].active && !sheep);
// Generator
generator.Process(sheep);