#include "plugin.hpp" #pragma GCC diagnostic push #ifndef __clang__ #pragma GCC diagnostic ignored "-Wsuggest-override" #endif #include "plaits/dsp/voice.h" #pragma GCC diagnostic pop 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, LPG_COLOR_PARAM, LPG_DECAY_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[16]; plaits::Patch patch = {}; char shared_buffer[16][16384] = {}; float triPhase = 0.f; dsp::SampleRateConverter<16 * 2> outputSrc; dsp::DoubleRingBuffer, 256> outputBuffer; bool lowCpu = false; dsp::BooleanTrigger model1Trigger; dsp::BooleanTrigger model2Trigger; Plaits() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configButton(MODEL1_PARAM, "Pitched models"); configButton(MODEL2_PARAM, "Noise/percussive models"); configParam(FREQ_PARAM, -4.0, 4.0, 0.0, "Frequency", " semitones", 0.f, 12.f); configParam(HARMONICS_PARAM, 0.0, 1.0, 0.5, "Harmonics", "%", 0.f, 100.f); configParam(TIMBRE_PARAM, 0.0, 1.0, 0.5, "Timbre", "%", 0.f, 100.f); configParam(LPG_COLOR_PARAM, 0.0, 1.0, 0.5, "Lowpass gate response", "%", 0.f, 100.f); configParam(MORPH_PARAM, 0.0, 1.0, 0.5, "Morph", "%", 0.f, 100.f); configParam(LPG_DECAY_PARAM, 0.0, 1.0, 0.5, "Lowpass gate decay", "%", 0.f, 100.f); configParam(TIMBRE_CV_PARAM, -1.0, 1.0, 0.0, "Timbre CV"); configParam(FREQ_CV_PARAM, -1.0, 1.0, 0.0, "Frequency CV"); configParam(MORPH_CV_PARAM, -1.0, 1.0, 0.0, "Morph CV"); configInput(ENGINE_INPUT, "Model"); configInput(TIMBRE_INPUT, "Timbre"); configInput(FREQ_INPUT, "FM"); configInput(MORPH_INPUT, "Morph"); configInput(HARMONICS_INPUT, "Harmonics"); configInput(TRIGGER_INPUT, "Trigger"); configInput(LEVEL_INPUT, "Level"); configInput(NOTE_INPUT, "Pitch (1V/oct)"); configOutput(OUT_OUTPUT, "Main"); configOutput(AUX_OUTPUT, "Auxiliary"); for (int i = 0; i < 16; i++) { stmlib::BufferAllocator allocator(shared_buffer[i], sizeof(shared_buffer[i])); voice[i].Init(&allocator); } onReset(); } void onReset() override { patch.engine = 0; patch.lpg_colour = 0.5f; patch.decay = 0.5f; } void onRandomize() override { patch.engine = random::u32() % 16; } json_t* dataToJson() 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)); return rootJ; } void dataFromJson(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); // Legacy <=1.0.2 json_t* lpgColorJ = json_object_get(rootJ, "lpgColor"); if (lpgColorJ) params[LPG_COLOR_PARAM].setValue(json_number_value(lpgColorJ)); // Legacy <=1.0.2 json_t* decayJ = json_object_get(rootJ, "decay"); if (decayJ) params[LPG_DECAY_PARAM].setValue(json_number_value(decayJ)); } void process(const ProcessArgs& args) override { int channels = std::max(inputs[NOTE_INPUT].getChannels(), 1); if (outputBuffer.empty()) { const int blockSize = 12; // Model buttons if (model1Trigger.process(params[MODEL1_PARAM].getValue())) { if (patch.engine >= 8) { patch.engine -= 8; } else { patch.engine = (patch.engine + 1) % 8; } } if (model2Trigger.process(params[MODEL2_PARAM].getValue())) { if (patch.engine < 8) { patch.engine += 8; } else { patch.engine = (patch.engine + 1) % 8 + 8; } } // Model lights // Pulse light at 2 Hz triPhase += 2.f * args.sampleTime * blockSize; if (triPhase >= 1.f) triPhase -= 1.f; float tri = (triPhase < 0.5f) ? triPhase * 2.f : (1.f - triPhase) * 2.f; // Get active engines of all voice channels bool activeEngines[16] = {}; bool pulse = false; for (int c = 0; c < channels; c++) { int activeEngine = voice[c].active_engine(); activeEngines[activeEngine] = true; // Pulse the light if at least one voice is using a different engine. if (activeEngine != patch.engine) pulse = true; } // Set model lights for (int i = 0; i < 16; i++) { // Transpose the [light][color] table int lightId = (i % 8) * 2 + (i / 8); float brightness = activeEngines[i]; if (patch.engine == i && pulse) brightness = tri; lights[MODEL_LIGHT + lightId].setBrightness(brightness); } // Calculate pitch for lowCpu mode if needed float pitch = params[FREQ_PARAM].getValue(); if (lowCpu) pitch += std::log2(48000.f * args.sampleTime); // Update patch patch.note = 60.f + pitch * 12.f; patch.harmonics = params[HARMONICS_PARAM].getValue(); patch.timbre = params[TIMBRE_PARAM].getValue(); patch.morph = params[MORPH_PARAM].getValue(); patch.lpg_colour = params[LPG_COLOR_PARAM].getValue(); patch.decay = params[LPG_DECAY_PARAM].getValue(); patch.frequency_modulation_amount = params[FREQ_CV_PARAM].getValue(); patch.timbre_modulation_amount = params[TIMBRE_CV_PARAM].getValue(); patch.morph_modulation_amount = params[MORPH_CV_PARAM].getValue(); // Render output buffer for each voice dsp::Frame<16 * 2> outputFrames[blockSize]; for (int c = 0; c < channels; c++) { // Construct modulations plaits::Modulations modulations; modulations.engine = inputs[ENGINE_INPUT].getPolyVoltage(c) / 5.f; modulations.note = inputs[NOTE_INPUT].getVoltage(c) * 12.f; modulations.frequency = inputs[FREQ_INPUT].getPolyVoltage(c) * 6.f; modulations.harmonics = inputs[HARMONICS_INPUT].getPolyVoltage(c) / 5.f; modulations.timbre = inputs[TIMBRE_INPUT].getPolyVoltage(c) / 8.f; modulations.morph = inputs[MORPH_INPUT].getPolyVoltage(c) / 8.f; // Triggers at around 0.7 V modulations.trigger = inputs[TRIGGER_INPUT].getPolyVoltage(c) / 3.f; modulations.level = inputs[LEVEL_INPUT].getPolyVoltage(c) / 8.f; modulations.frequency_patched = inputs[FREQ_INPUT].isConnected(); modulations.timbre_patched = inputs[TIMBRE_INPUT].isConnected(); modulations.morph_patched = inputs[MORPH_INPUT].isConnected(); modulations.trigger_patched = inputs[TRIGGER_INPUT].isConnected(); modulations.level_patched = inputs[LEVEL_INPUT].isConnected(); // Render frames plaits::Voice::Frame output[blockSize]; voice[c].Render(patch, modulations, output, blockSize); // Convert output to frames for (int i = 0; i < blockSize; i++) { outputFrames[i].samples[c * 2 + 0] = output[i].out / 32768.f; outputFrames[i].samples[c * 2 + 1] = output[i].aux / 32768.f; } } // Convert output if (lowCpu) { int len = std::min((int) outputBuffer.capacity(), blockSize); std::memcpy(outputBuffer.endData(), outputFrames, len * sizeof(outputFrames[0])); outputBuffer.endIncr(len); } else { outputSrc.setRates(48000, (int) args.sampleRate); int inLen = blockSize; int outLen = outputBuffer.capacity(); outputSrc.setChannels(channels * 2); outputSrc.process(outputFrames, &inLen, outputBuffer.endData(), &outLen); outputBuffer.endIncr(outLen); } } // Set output if (!outputBuffer.empty()) { dsp::Frame<16 * 2> outputFrame = outputBuffer.shift(); for (int c = 0; c < channels; c++) { // Inverting op-amp on outputs outputs[OUT_OUTPUT].setVoltage(-outputFrame.samples[c * 2 + 0] * 5.f, c); outputs[AUX_OUTPUT].setVoltage(-outputFrame.samples[c * 2 + 1] * 5.f, c); } } outputs[OUT_OUTPUT].setChannels(channels); outputs[AUX_OUTPUT].setChannels(channels); } }; 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 { bool lpgMode = false; PlaitsWidget(Plaits* module) { setModule(module); setPanel(Svg::load(asset::plugin(pluginInstance, "res/Plaits.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParam(mm2px(Vec(23.32685, 14.6539)), module, Plaits::MODEL1_PARAM)); addParam(createParam(mm2px(Vec(32.22764, 14.6539)), module, Plaits::MODEL2_PARAM)); addParam(createParam(mm2px(Vec(3.1577, 20.21088)), module, Plaits::FREQ_PARAM)); addParam(createParam(mm2px(Vec(39.3327, 20.21088)), module, Plaits::HARMONICS_PARAM)); addParam(createParam(mm2px(Vec(4.04171, 49.6562)), module, Plaits::TIMBRE_PARAM)); addParam(createParam(mm2px(Vec(42.71716, 49.6562)), module, Plaits::MORPH_PARAM)); addParam(createParam(mm2px(Vec(7.88712, 77.60705)), module, Plaits::TIMBRE_CV_PARAM)); addParam(createParam(mm2px(Vec(27.2245, 77.60705)), module, Plaits::FREQ_CV_PARAM)); addParam(createParam(mm2px(Vec(46.56189, 77.60705)), module, Plaits::MORPH_CV_PARAM)); ParamWidget* lpgColorParam = createParam(mm2px(Vec(4.04171, 49.6562)), module, Plaits::LPG_COLOR_PARAM); lpgColorParam->hide(); addParam(lpgColorParam); ParamWidget* decayParam = createParam(mm2px(Vec(42.71716, 49.6562)), module, Plaits::LPG_DECAY_PARAM); decayParam->hide(); addParam(decayParam); addInput(createInput(mm2px(Vec(3.31381, 92.48067)), module, Plaits::ENGINE_INPUT)); addInput(createInput(mm2px(Vec(14.75983, 92.48067)), module, Plaits::TIMBRE_INPUT)); addInput(createInput(mm2px(Vec(26.20655, 92.48067)), module, Plaits::FREQ_INPUT)); addInput(createInput(mm2px(Vec(37.65257, 92.48067)), module, Plaits::MORPH_INPUT)); addInput(createInput(mm2px(Vec(49.0986, 92.48067)), module, Plaits::HARMONICS_INPUT)); addInput(createInput(mm2px(Vec(3.31381, 107.08103)), module, Plaits::TRIGGER_INPUT)); addInput(createInput(mm2px(Vec(14.75983, 107.08103)), module, Plaits::LEVEL_INPUT)); addInput(createInput(mm2px(Vec(26.20655, 107.08103)), module, Plaits::NOTE_INPUT)); addOutput(createOutput(mm2px(Vec(37.65257, 107.08103)), module, Plaits::OUT_OUTPUT)); addOutput(createOutput(mm2px(Vec(49.0986, 107.08103)), module, Plaits::AUX_OUTPUT)); addChild(createLight>(mm2px(Vec(28.79498, 23.31649)), module, Plaits::MODEL_LIGHT + 0 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 28.71704)), module, Plaits::MODEL_LIGHT + 1 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 34.1162)), module, Plaits::MODEL_LIGHT + 2 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 39.51675)), module, Plaits::MODEL_LIGHT + 3 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 44.91731)), module, Plaits::MODEL_LIGHT + 4 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 50.31785)), module, Plaits::MODEL_LIGHT + 5 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 55.71771)), module, Plaits::MODEL_LIGHT + 6 * 2)); addChild(createLight>(mm2px(Vec(28.79498, 61.11827)), module, Plaits::MODEL_LIGHT + 7 * 2)); } void appendContextMenu(Menu* menu) override { Plaits* module = dynamic_cast(this->module); menu->addChild(new MenuSeparator); menu->addChild(createBoolPtrMenuItem("Low CPU (disable resampling)", &module->lowCpu)); menu->addChild(createBoolMenuItem("Edit LPG response/decay", [=]() {return this->getLpgMode();}, [=](bool val) {this->setLpgMode(val);} )); menu->addChild(new MenuSeparator); menu->addChild(createMenuLabel("Pitched models")); for (int i = 0; i < 8; i++) { menu->addChild(createCheckMenuItem(modelLabels[i], [=]() {return module->patch.engine == i;}, [=]() {module->patch.engine = i;} )); } menu->addChild(new MenuSeparator); menu->addChild(createMenuLabel("Noise/percussive models")); for (int i = 8; i < 16; i++) { menu->addChild(createCheckMenuItem(modelLabels[i], [=]() {return module->patch.engine == i;}, [=]() {module->patch.engine = i;} )); } } void setLpgMode(bool lpgMode) { // ModuleWidget::getParam() doesn't work if the ModuleWidget doesn't have a module. if (!module) return; if (lpgMode) { getParam(Plaits::MORPH_PARAM)->hide(); getParam(Plaits::TIMBRE_PARAM)->hide(); getParam(Plaits::LPG_DECAY_PARAM)->show(); getParam(Plaits::LPG_COLOR_PARAM)->show(); } else { getParam(Plaits::MORPH_PARAM)->show(); getParam(Plaits::TIMBRE_PARAM)->show(); getParam(Plaits::LPG_DECAY_PARAM)->hide(); getParam(Plaits::LPG_COLOR_PARAM)->hide(); } this->lpgMode = lpgMode; } bool getLpgMode() { return this->lpgMode; } }; Model* modelPlaits = createModel("Plaits");