#include "2x03.h" using Xybrid::Instruments::I2x03; using namespace Xybrid::NodeLib; using Note = InstrumentCore::Note; using namespace Xybrid::Data; #include "nodelib/commandreader.h" #include "data/porttypes.h" #include "config/pluginregistry.h" using namespace Xybrid::Config; #include "audio/audioengine.h" using namespace Xybrid::Audio; #include "ui/patchboard/nodeobject.h" #include "ui/gadgets/layoutgadget.h" #include "ui/gadgets/knobgadget.h" using namespace Xybrid::UI; #include "util/strings.h" #include "util/ext.h" #include #include #include #include #include // clazy:excludeall=non-pod-global-static RegisterPlugin(I2x03, { i->id = "plug:2x03"; i->displayName = "2x03"; i->category = "Instrument"; }) namespace { std::unordered_map waveNames = [] { std::unordered_map m; m[-1] = "keep"; m[0] = "p50"; m[1] = "p25"; m[2] = "p12.5"; m[3] = "tri"; m[4] = "saw"; m[5] = "pwm.l"; m[6] = "pwm.g"; return m; }(); // polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517 force_opt double polyblep(double t, double dt) { // 0 <= t < 1 if (t < dt) { t /= dt; return t+t - t*t - 1.0; } // -1 < t < 0 else if (t > 1.0 - dt) { t = (t - 1.0) / dt; return t*t + t+t + 1.0; } // 0 otherwise return 0.0; } force_opt double oscPulse(double phase, double delta, double duty = 0.5) { //double duty = 0.5 + std::cos(time * 2.5) * (1 - 0.125*2) * 0.5; double d = 1.0; if (std::fmod(phase, 1.0) >= duty) d = -1.0; d += polyblep(std::fmod(phase, 1.0), delta); d -= polyblep(std::fmod(phase + (1.0 - duty), 1.0), delta); return d - (duty-0.5)*2; // DC offset compensation } force_opt double oscTri(double phase) { phase = std::fmod(phase + 0.75, 1.0); phase = phase * 0.2 + (std::floor(phase*32.0) / 32.0) * 0.8; return std::abs(phase*2.0 - 1.0)*2.0 - 1.0; } force_opt double oscSaw(double phase, double delta) { phase = std::fmod(phase + 0.5, 1.0); double d = phase * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8; d = d * 2.0 - 1.0; d -= polyblep(phase, delta); return d; } } I2x03::I2x03() { } void I2x03::init() { addPort(Port::Input, Port::Command, 0); addPort(Port::Output, Port::Audio, 0); core.onNoteOn = [this](Note& note) { //qDebug() << "note on"; note.adsr = adsr.normalized(); }; core.globalParam['Q'] = [](const ParamReader& pr) { qDebug() << "global recieved" << pr.param() << pr.val(); return true; }; core.processNote = [this](Note& note, AudioPort* p) { double freq;// = 440.0 * std::pow(SEMI, enote - (45+12*2)); double smpTime = core.sampleTime(); size_t ts = p->size; for (size_t i = 0; i < ts; i++) { core.advanceNote(note); double smp = 0.0; double n = note.effectiveNote() + detune; int8_t wave = this->wave; if (note.time < blipTime) { if (blipWave != -1) wave = blipWave; n += blipNote; } freq = 440.0 * std::pow(SEMI, n - (45+12*2)); switch(wave) { case 0: smp = oscPulse(note.scratch[0], smpTime*freq, 0.5); break; case 1: smp = oscPulse(note.scratch[0], smpTime*freq, 0.25); break; case 2: smp = oscPulse(note.scratch[0], smpTime*freq, 0.125); break; case 3: smp = oscTri(note.scratch[0]); break; case 4: smp = oscSaw(note.scratch[0], smpTime*freq); break; case 5: smp = oscPulse(note.scratch[0], smpTime*freq, 0.5 + std::sin(PI*2 * (note.time / pwmTime + pwmPhase)) * pwmDepth * 0.5); break; case 6: smp = oscPulse(note.scratch[0], smpTime*freq, 0.5 + std::sin(PI*2 * ((core.globalTime() + smpTime * i) / pwmTime + pwmPhase)) * pwmDepth * 0.5); break; default: break; } smp *= note.ampMult(); note.scratch[0] += smpTime * freq; auto pn = panSignal(smp, note.pan); p->bufL[i] += pn.first; p->bufR[i] += pn.second; //p->bufL[i] += static_cast(smp); //p->bufR[i] += static_cast(smp); } }; } void I2x03::reset() { core.reset(); } void I2x03::release() { core.release(); } void I2x03::process() { core.process(this); } void I2x03::saveData(QCborMap& m) const { m[qs("wave")] = wave; m[qs("detune")] = detune; m[qs("adsr")] = adsr; m[qs("blipTime")] = blipTime; m[qs("blipWave")] = blipWave; m[qs("blipNote")] = blipNote; m[qs("pwmDepth")] = pwmDepth; m[qs("pwmTime")] = pwmTime; m[qs("pwmPhase")] = pwmPhase; } void I2x03::loadData(const QCborMap& m) { wave = static_cast(m.value("wave").toInteger(wave)); detune = m.value("detune").toDouble(detune); adsr = m.value("adsr"); blipTime = m.value("blipTime").toDouble(blipTime); blipWave = static_cast(m.value("blipWave").toInteger(blipWave)); blipNote = static_cast(m.value("blipNote").toInteger(blipNote)); pwmDepth = m.value("pwmDepth").toDouble(pwmDepth); pwmTime = m.value("pwmTime").toDouble(pwmTime); pwmPhase = m.value("pwmPhase").toDouble(pwmPhase); } void I2x03::onGadgetCreated() { if (!obj) return; auto wavetxt = [](double inp) { if (auto f = waveNames.find(static_cast(inp)); f != waveNames.end()) return f->second; return qs("?"); }; auto ol = new LayoutGadget(obj, true); auto l1 = new LayoutGadget(ol); auto l2 = new LayoutGadget(ol); // first line (new KnobGadget(l1))->bind(wave)->setLabel(qs("Wave"))->setTextFunc(wavetxt)->setRange(0, 6, 1, KnobGadget::BigStep); (new KnobGadget(l1))->bind(detune)->setLabel(qs("Detune"))->setRange(-1.0, 1.0, 0.01); l1->addSpacer(); KnobGadget::autoCreate(l1, adsr); // blip group (new KnobGadget(l2))->bind(blipTime)->setLabel(qs("Blip"))->setRange(0.0, 0.1, 0.001); (new KnobGadget(l2))->bind(blipWave)->setLabel(qs("Wave"))->setTextFunc(wavetxt)->setRange(-1, 4, 1, KnobGadget::BigStep)->setDefault(-1); (new KnobGadget(l2))->bind(blipNote)->setLabel(qs("Note"))->setTextFunc(KnobGadget::textOffset)->setRange(-12, 12, 1); l2->addSpacer(); // pwm group KnobGadget::autoPercent(l2, pwmDepth)->setLabel(qs("PWM"))->setDefault(0.75); (new KnobGadget(l2))->bind(pwmTime)->setLabel(qs("Time"))->setRange(0.01, 5.0, 0.01)->setDefault(3.0); KnobGadget::autoPercent(l2, pwmPhase)->setLabel(qs("Phase")); // }