|
@@ -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<Frame<2>, 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<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0))); |
|
|
|
|
|
addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); |
|
|
|
|
|
addChild(Widget::create<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); |
|
|
|
|
|
addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); |
|
|
|
|
|
|
|
|
|
|
|
addParam(ParamWidget::create<TL1105>(mm2px(Vec(23.32685, 14.6539)), module, Plaits::MODEL1_PARAM, 0.0, 1.0, 0.0)); |
|
|
|
|
|
addParam(ParamWidget::create<TL1105>(mm2px(Vec(32.22764, 14.6539)), module, Plaits::MODEL2_PARAM, 0.0, 1.0, 0.0)); |
|
|
|
|
|
addParam(ParamWidget::create<Rogan3PSWhite>(mm2px(Vec(3.1577, 20.21088)), module, Plaits::FREQ_PARAM, -4.0, 4.0, 0.0)); |
|
|
|
|
|
addParam(ParamWidget::create<Rogan3PSWhite>(mm2px(Vec(39.3327, 20.21088)), module, Plaits::HARMONICS_PARAM, 0.0, 1.0, 0.5)); |
|
|
|
|
|
addParam(ParamWidget::create<Rogan1PSWhite>(mm2px(Vec(4.04171, 49.6562)), module, Plaits::TIMBRE_PARAM, 0.0, 1.0, 0.5)); |
|
|
|
|
|
addParam(ParamWidget::create<Rogan1PSWhite>(mm2px(Vec(42.71716, 49.6562)), module, Plaits::MORPH_PARAM, 0.0, 1.0, 0.5)); |
|
|
|
|
|
addParam(ParamWidget::create<Trimpot>(mm2px(Vec(7.88712, 77.60705)), module, Plaits::TIMBRE_CV_PARAM, -1.0, 1.0, 0.0)); |
|
|
|
|
|
addParam(ParamWidget::create<Trimpot>(mm2px(Vec(27.2245, 77.60705)), module, Plaits::FREQ_CV_PARAM, -1.0, 1.0, 0.0)); |
|
|
|
|
|
addParam(ParamWidget::create<Trimpot>(mm2px(Vec(46.56189, 77.60705)), module, Plaits::MORPH_CV_PARAM, -1.0, 1.0, 0.0)); |
|
|
|
|
|
|
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(3.31381, 92.48067)), Port::INPUT, module, Plaits::ENGINE_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(14.75983, 92.48067)), Port::INPUT, module, Plaits::TIMBRE_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(26.20655, 92.48067)), Port::INPUT, module, Plaits::FREQ_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(37.65257, 92.48067)), Port::INPUT, module, Plaits::MORPH_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(49.0986, 92.48067)), Port::INPUT, module, Plaits::HARMONICS_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(3.31381, 107.08103)), Port::INPUT, module, Plaits::TRIGGER_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(14.75983, 107.08103)), Port::INPUT, module, Plaits::LEVEL_INPUT)); |
|
|
|
|
|
addInput(Port::create<PJ301MPort>(mm2px(Vec(26.20655, 107.08103)), Port::INPUT, module, Plaits::NOTE_INPUT)); |
|
|
|
|
|
|
|
|
|
|
|
addOutput(Port::create<PJ301MPort>(mm2px(Vec(37.65257, 107.08103)), Port::OUTPUT, module, Plaits::OUT_OUTPUT)); |
|
|
|
|
|
addOutput(Port::create<PJ301MPort>(mm2px(Vec(49.0986, 107.08103)), Port::OUTPUT, module, Plaits::AUX_OUTPUT)); |
|
|
|
|
|
|
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 23.31649)), module, Plaits::MODEL_LIGHT + 0 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 28.71704)), module, Plaits::MODEL_LIGHT + 1 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 34.1162)), module, Plaits::MODEL_LIGHT + 2 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 39.51675)), module, Plaits::MODEL_LIGHT + 3 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 44.91731)), module, Plaits::MODEL_LIGHT + 4 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 50.31785)), module, Plaits::MODEL_LIGHT + 5 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 55.71771)), module, Plaits::MODEL_LIGHT + 6 * 2)); |
|
|
|
|
|
addChild(ModuleLightWidget::create<MediumLight<GreenRedLight>>(mm2px(Vec(28.79498, 61.11827)), module, Plaits::MODEL_LIGHT + 7 * 2)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void appendContextMenu(Menu *menu) override { |
|
|
|
|
|
Plaits *module = dynamic_cast<Plaits*>(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<PlaitsLowCpuItem>("Low CPU", CHECKMARK(module->lowCpu)); |
|
|
|
|
|
lowCpuItem->module = module; |
|
|
|
|
|
menu->addChild(lowCpuItem); |
|
|
|
|
|
PlaitsLPGItem *lpgItem = MenuItem::create<PlaitsLPGItem>("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<PlaitsModelItem>(modelLabels[i], CHECKMARK(module->patch.engine == i)); |
|
|
|
|
|
modelItem->module = module; |
|
|
|
|
|
modelItem->model = i; |
|
|
|
|
|
menu->addChild(modelItem); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Model *modelPlaits = Model::create<Plaits, PlaitsWidget>("Audible Instruments", "Plaits", "Macro Oscillator 2", OSCILLATOR_TAG, WAVESHAPER_TAG); |
|
|
|
|
|
|