#include "thicc.h" using Xybrid::Instruments::Thicc; 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 #include #include // clazy:excludeall=non-pod-global-static RegisterPlugin(Thicc, { i->id = "plug:thicc"; i->displayName = "THiCC"; i->category = "Instrument"; }) namespace { [[maybe_unused]] inline double wrap(double d) { while (true) { if (d > 1.0) d = (d - 2.0) * -1; //d-=2.0; else if (d < -1.0) d = (d + 2.0) * -1; else return d; } } [[maybe_unused]] inline double lerp(double a, double b, double p) { return b * p + a * (1.0 - p); } // 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 inline double push(double in, double mod, double factor) { double s = in < 0 ? -1 : 1; in *= s; //if (mod < 0) mod = 1.0/-mod; //else mod += 1.0; mod = mod < 0 ? lerp(1.0, 1.0/factor, -mod) : lerp(1.0, factor, mod); return std::pow(in, mod)*s; } force_opt double oscSaw(double phase, double delta, double mod) { 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 = push(d, -mod, 5); d -= polyblep(phase, delta); return d; } force_opt double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); } force_opt double oscPulse(double phase, double delta, double mod) { double duty = (mod+1.0)/2.0; 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; } // for clang on freebsd (and possibly other non-apple llvm sources) it seems we need to specify more. // wave function list(s) const constexpr std::array waveFunc = { &oscSaw, &oscSine, &oscPulse, }; const std::array waveName = { qs("saw"), qs("sine"), qs("pulse"), }; } Thicc::Thicc() { } void Thicc::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(); note.scratch[0] = static_cast(QRandomGenerator::global()->generate() % 573000); }; /*core.globalParam['Q'] = [](const ParamReader& pr) { qDebug() << "global recieved" << pr.param() << pr.val(); return true; };*/ core.processNote = [this](Note& note, AudioPort* p) { double freq; auto osc = waveFunc[static_cast(wave)]; int vc = voices; double vf = static_cast(vc); double spr = std::pow(SEMI, detune/(vf/2.0)); double smpTime = core.sampleTime(); size_t ts = p->size; for (size_t i = 0; i < ts; i++) { core.advanceNote(note); double n = note.effectiveNote(); freq = 440.0 * std::pow(SEMI, n - (45+12*2)); auto si = note.scratch[0]; double delta = smpTime * freq; note.scratch[0] += delta; double o = 0; for (int i = 0; i < vc; i++) { auto ii = static_cast(i); double pc = ii / vf; // phase coefficient double dc = ii - ((vf-1.0)/2.0); // detune coefficient double dm = std::pow(spr, dc); double sg = i % 2 == 0 ? 1.0 : -1.0; //o += (std::fmod(pc + si * dm, 1.0) * 2.0 - 1.0) * sg; o += osc(pc + si * dm, delta * dm, mod) * sg; } o /= 1.0 + ((vf - 1.0)/5.0); AudioFrame out = o; (*p)[i] += out.gainBalance(0, note.pan) * note.ampMult(); } }; } void Thicc::reset() { core.reset(); } void Thicc::release() { core.release(); } void Thicc::process() { core.process(this); } void Thicc::saveData(QCborMap& m) const { m[qs("adsr")] = adsr; m[qs("wave")] = wave; m[qs("voices")] = voices; m[qs("mod")] = mod; m[qs("detune")] = detune; } void Thicc::loadData(const QCborMap& m) { adsr = m.value("adsr"); wave = static_cast(m.value("wave").toInteger(wave)); voices = static_cast(m.value("voices").toInteger(voices)); mod = m.value("mod").toDouble(mod); detune = m.value("detune").toDouble(detune); } void Thicc::onGadgetCreated() { auto wn = [](size_t i) { if (i >= waveName.size()) return QString::number(i); return waveName[i]; }; auto l = new LayoutGadget(obj); (new KnobGadget(l))->bind(wave)->setLabel(qs("Wave"))->setTextFunc(wn)->setRange(0, waveFunc.size()-1, 1, KnobGadget::BigStep); (new KnobGadget(l))->bind(mod)->setLabel(qs("W. Mod"))->setRange(-1.0, 1.0, 0.01); l->addSpacer(); (new KnobGadget(l))->bind(voices)->setLabel(qs("Voices"))->setRange(1, 16, 1, KnobGadget::BigStep)->setDefault(1); (new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setTextFunc(KnobGadget::textPercent)->setRange(0.0, 1.0, 0.001); l->addSpacer(); KnobGadget::autoCreate(l, adsr); }