looots of 2x03 stuff

portability/boost
zetaPRIME 2019-01-19 22:57:18 -05:00
parent 2b42b7066a
commit 307bc54140
10 changed files with 406 additions and 47 deletions

9
notes
View File

@ -53,10 +53,19 @@ TODO {
-? return the latency/buffer size to 100ms once multithreading is streamlined
# fix how qt5.12 broke header text (removed elide for now)
pseudoport (L) for legato (note-on for already-playing note)
make knob notches more even (currently "previous value" is twice as big as any other step at px>1)
add standardized step values for knobs (int enum?)
bugs to fix {
graph connections sometimes spawn in duplicated :|
- shifted <>? keys still count as different keys for note previewing
reset is never called when hooking up a node after starting preview; breaks instrumentcore
^ on rebuilding queue, call reset on anything that wasn't there before (unordered_set)
on starting playback, sometimes a "thunk" sneaks into the waveform?
-? buffer underruns are being caused by some sync wonkiness between multiple workers
}

34
xybrid/nodelib/basics.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "basics.h"
using namespace Xybrid::NodeLib;
#include <cmath>
#include <QCborMap>
#include <QCborValue>
#include <QCborArray>
ADSR ADSR::normalized() {
ADSR adsr = *this;
adsr.a = std::max(adsr.a, shortStep);
adsr.r = std::max(adsr.r, shortStep);
if (adsr.s != 1.0) adsr.d = std::max(adsr.d, shortStep);
return adsr;
}
ADSR::ADSR(const QCborMap& m) {
a = m.value("a").toDouble(a);
d = m.value("d").toDouble(d);
s = m.value("s").toDouble(s);
r = m.value("r").toDouble(r);
}
ADSR::ADSR(const QCborValue& v) : ADSR(v.toMap()) { }
ADSR::operator QCborMap() const {
QCborMap m;
m.insert(QString("a"), a);
m.insert(QString("d"), d);
m.insert(QString("s"), s);
m.insert(QString("r"), r);
return m;
}
ADSR::operator QCborValue() const { return QCborMap(*this).toCborValue(); }

View File

@ -1,12 +1,24 @@
#pragma once
class QCborMap;
class QCborValue;
namespace Xybrid::NodeLib {
// more precision than probably fits in a double, but it certainly shouldn't hurt
constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
constexpr double shortAttack = 0.001;
constexpr double shortStep = 0.0025;
struct ADSR {
double a = 0, d = 0, s = 1, r = 0;
double a = 0.0, d = 0.0, s = 1.0, r = 0.0;
ADSR normalized();
ADSR() = default;
ADSR(const QCborMap&);
ADSR(const QCborValue&);
operator QCborMap() const;
operator QCborValue() const;
};
}

View File

@ -17,10 +17,9 @@ Note::Note(uint16_t id) {
void InstrumentCore::reset() {
activeNotes.clear();
activeNotes.reserve(16+1);
deletedNotes.clear();
deletedNotes.reserve(16+1);
smpTime = 1.0 / audioEngine->curSampleRate();
time = 0;
}
void InstrumentCore::process(Node* n) {
@ -36,11 +35,14 @@ namespace {
switch(phase) {
case 0: {
if (adsr.a == 0) return 1.0;
return std::clamp(time / adsr.a, 0.0, 1.0);
double a = 1.0 - std::clamp(time / adsr.a, 0.0, 1.0);
a *= a;
a *= a;
return 1.0 - a;
}
case 1: {
if (adsr.d == 0) return adsr.s;
double sp = (1.0 - std::clamp(time / adsr.s, 0.0, 1.0));
double sp = (1.0 - std::clamp(time / adsr.d, 0.0, 1.0));
sp *= sp;
return adsr.s + sp * (1.0 - adsr.s);
}
@ -53,10 +55,8 @@ namespace {
}
}
ADSR defAdsr{0.01, 0.3, 0.75, 0.2};
struct NoteIntern {
bool markedForDeletion = false;
};
}
@ -74,36 +74,44 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
if (note.adsrPhase == 2) note = Note(id); // reinstantiate on replace
note.note = n;
note.time = note.adsrTime = -smpTime; // compensate for first-advance
note.adsr = defAdsr;
// if (sc.second)
if (!sc.second) {
// TODO: legato
} else if (onNoteOn) onNoteOn(note);
} else if (n < -1) { // note off
//qDebug() << "note off" << id;
//activeNotes.erase(id); // temp; later we'll let it do its own thing
if (auto ni = activeNotes.find(id); ni != activeNotes.end()) {
auto& note = ni->second;
note.adsr.s = adsrVol(note.adsr, note.adsrPhase, note.adsrTime);
note.adsrPhase = 2;
note.adsrTime = -smpTime;
if (n == -3) note.adsr.r = shortAttack;
if (n == -3) note.adsr.r = shortStep;
if (onNoteOff) onNoteOff(note, n == -3);
}
}
mi += 5 + i->data[mi+4]*2;
}
for (auto id : deletedNotes) activeNotes.erase(id);
deletedNotes.clear();
// then do the thing
if (o) o->pull();
if (processNote) {
for (auto& p : activeNotes) {
for (auto p = activeNotes.begin(); p != activeNotes.end(); ) {
auto& note = p->second;
NoteIntern n;
p.second.intern = &n;
processNote(p.second, o);
p.second.intern = nullptr;
note.intern = &n;
processNote(note, o);
if (n.markedForDeletion) {
if (onDeleteNote) onDeleteNote(note);
auto pr = p;
p++;
activeNotes.erase(pr);
continue;
}
note.intern = nullptr;
p++;
}
}
time += smpTime * audioEngine->curTickSize();
}
void InstrumentCore::advanceNote(Note& n) {
@ -123,7 +131,15 @@ void InstrumentCore::advanceNote(Note& n) {
}
void InstrumentCore::deleteNote(Note& n) { deletedNotes.insert(n.id); }
void InstrumentCore::deleteNote(Note& n) {
if (n.intern != nullptr) {
auto& ni = *reinterpret_cast<NoteIntern*>(n.intern);
ni.markedForDeletion = true;
} else {
if (onDeleteNote) onDeleteNote(n);
activeNotes.erase(n.id);
}
}
double Note::ampMult() const {
double a = adsrVol(adsr, adsrPhase, adsrTime);

View File

@ -22,6 +22,7 @@ namespace Xybrid::NodeLib {
* Not mandatory by any means, but handles all the "standard" commands for you.
*/
class InstrumentCore {
double time;
double smpTime;
public:
@ -38,18 +39,23 @@ namespace Xybrid::NodeLib {
double adsrTime = 0;
uint8_t adsrPhase = 0;
std::array<double, 5> scratch{0.0};
Note() = default;
Note(uint16_t id);
double ampMult() const;
};
std::unordered_map<uint16_t, Note> activeNotes;
std::unordered_set<uint16_t> deletedNotes;
std::function<void(Note&, Data::AudioPort*)> processNote;
std::function<void(Note&)> onNoteOn;
std::function<void(Note&, bool)> onNoteOff;
std::function<void(Note&)> onDeleteNote;
InstrumentCore() = default;
inline double globalTime() const { return time; }
inline double sampleTime() const { return smpTime; }
void reset();

View File

@ -87,6 +87,7 @@ void Transpose::onGadgetCreated() {
k->min = -24;
k->max = 24;
k->step = 1;
k->stepPx = 3;
k->bind(amount);
k->setLabel("Transpose");

View File

@ -10,9 +10,16 @@ using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/knobgadget.h"
using namespace Xybrid::UI;
#include <cmath>
#include <QDebug>
#include <QCborMap>
#include <QCborValue>
#include <QCborArray>
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
@ -26,8 +33,27 @@ namespace {
//inf = i;
});
std::unordered_map<int8_t, QString> waveNames = [] {
std::unordered_map<int8_t, QString> 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;
}();
// silence qtcreator warnings about gcc optimize attributes
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattributes"
// polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
double polyblep(double t, double dt) {
[[gnu::optimize("O3")]] double polyblep(double t, double dt) {
// 0 <= t < 1
if (t < dt) {
t /= dt;
@ -42,17 +68,30 @@ namespace {
return 0.0;
}
double osc(double time, double freq, double delta) {
//constexpr double duty = 0.125;
double duty = 0.5 + std::cos(time * 2.5) * (1 - 0.125*2) * 0.5;
time *= freq;
[[gnu::optimize("O3")]] 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(time, 1.0) >= duty) d = -1.0;
delta *= freq;
d += polyblep(std::fmod(time, 1.0), delta);
d -= polyblep(std::fmod(time + (1.0 - duty), 1.0), delta);
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;
}
[[gnu::optimize("O3")]] 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;
}
[[gnu::optimize("O3")]] 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;
}
#pragma GCC diagnostic pop
}
I2x03::I2x03() {
@ -63,26 +102,238 @@ void I2x03::init() {
addPort(Port::Input, Port::Command, 0);
addPort(Port::Output, Port::Audio, 0);
core.processNote = [this](Note& note, AudioPort* p) {
double enote = note.note;
double freq = 440.0 * std::pow(SEMI, enote - (45+12*2));
core.onNoteOn = [this](Note& note) {
//qDebug() << "note on";
note.adsr = adsr.normalized();
};
core.processNote = [this](Note& note, AudioPort* p) {
//double enote = note.note;
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 d = std::clamp((std::sin(note.time * freq * PI*2) + 0.75) * 512.0, -1.0, 1.0) * note.ampMult();
double d = osc(note.time, freq, core.sampleTime()) * note.ampMult();
p->bufL[i] += static_cast<float>(d);
p->bufR[i] += static_cast<float>(d);
//note.time += tt;
double smp = 0.0;
double n = note.note;
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;
p->bufL[i] += static_cast<float>(smp);
p->bufR[i] += static_cast<float>(smp);
}
};
}
void I2x03::reset() {
core.reset();
void I2x03::reset() { core.reset(); }
void I2x03::process() { core.process(this); }
void I2x03::saveData(QCborMap& m) {
m.insert(QString("wave"), wave);
m.insert(QString("adsr"), adsr);
m.insert(QString("blipTime"), blipTime);
m.insert(QString("blipWave"), blipWave);
m.insert(QString("blipNote"), blipNote);
m.insert(QString("pwmDepth"), pwmDepth);
m.insert(QString("pwmTime"), pwmTime);
m.insert(QString("pwmPhase"), pwmPhase);
}
void I2x03::process() {
core.process(this);
void I2x03::loadData(QCborMap& m) {
wave = static_cast<int8_t>(m.value("wave").toInteger(wave));
adsr = m.value("adsr");
blipTime = m.value("blipTime").toDouble(blipTime);
blipWave = static_cast<int8_t>(m.value("blipWave").toInteger(blipWave));
blipNote = static_cast<int>(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<int8_t>(inp)); f != waveNames.end()) return f->second;
return QString("?");
};
constexpr double w = 248;
constexpr double r1 = 16;
constexpr double r2 = r1+64;
constexpr double spc = 38;
constexpr double l = spc-32;
auto off = QPointF(spc, 0);
constexpr int stepSmall = 3;
constexpr int stepBig = 15;
obj->setGadgetSize(w, 128);
{
KnobGadget* k;
k = new KnobGadget(obj->contents);
k->setPos(l, r1);
k->min = 0;
k->max = 6;
k->step = 1;
k->stepPx = stepBig;
k->bind(wave);
k->fText = wavetxt;
k->setLabel("Wave");
}
{
// adsr group
auto g = QPointF(w-(spc*4), r1);
KnobGadget* k;
k = new KnobGadget(obj->contents);
k->setPos(g + off*0);
k->min = 0.0;
k->max = 5.0;
k->step = .01;
k->stepPx = stepSmall;
k->bind(adsr.a);
k->setLabel("Attack");
k = new KnobGadget(obj->contents);
k->setPos(g + off*1);
k->min = 0.0;
k->max = 5.0;
k->step = .01;
k->stepPx = stepSmall;
k->bind(adsr.d);
k->setLabel("Decay");
k = new KnobGadget(obj->contents);
k->setPos(g + off*2);
k->min = 0.0;
k->max = 1.0;
k->defaultVal = 1.0;
k->step = .01;
k->stepPx = stepSmall;
k->bind(adsr.s);
k->setLabel("Sustain");
k = new KnobGadget(obj->contents);
k->setPos(g + off*3);
k->min = 0.0;
k->max = 5.0;
k->step = .01;
k->stepPx = stepSmall;
k->bind(adsr.r);
k->setLabel("Release");
}
{
// blip group
auto g = QPointF(l, r2);
KnobGadget* k;
k = new KnobGadget(obj->contents);
k->setPos(g + off*0);
k->min = 0.0;
k->max = 0.1;
k->step = .01;
k->stepPx = stepSmall;
k->bind(blipTime);
k->setLabel("Blip");
k = new KnobGadget(obj->contents);
k->setPos(g + off*1);
k->min = -1;
k->max = 4;
k->defaultVal = -1;
k->step = 1;
k->stepPx = stepBig;
k->bind(blipWave);
k->fText = wavetxt;
k->setLabel("Wave");
k = new KnobGadget(obj->contents);
k->setPos(g + off*2);
k->min = -12;
k->max = 12;
k->step = 1;
k->stepPx = stepSmall;
k->bind(blipNote);
k->setLabel("Note");
}
{
// pwm group
auto g = QPointF(w-(spc*3), r2);
KnobGadget* k;
k = new KnobGadget(obj->contents);
k->setPos(g + off*0);
k->min = 0.0;
k->max = 1.0;
k->defaultVal = 0.75;
k->step = .01;
k->stepPx = stepSmall;
k->bind(pwmDepth);
k->fText = [](double d) {
return QString("%1%").arg(d*100, 0);
};
k->setLabel("PWM");
k = new KnobGadget(obj->contents);
k->setPos(g + off*1);
k->min = 0.01;
k->max = 5.0;
k->defaultVal = 3.0;
k->step = .01;
k->stepPx = stepSmall;
k->bind(pwmTime);
k->setLabel("Time");
k = new KnobGadget(obj->contents);
k->setPos(g + off*2);
k->min = 0.0;
k->max = 1.0;
k->step = .01;
k->stepPx = stepSmall;
k->bind(pwmPhase);
k->setLabel("Phase");
}
}

View File

@ -4,8 +4,18 @@
namespace Xybrid::Instruments {
class I2x03: public Data::Node {
//
NodeLib::InstrumentCore core;
NodeLib::ADSR adsr;
int8_t wave = 0;
int8_t blipWave = -1;
int blipNote = 0;
double blipTime = 0.0;
double pwmTime = 3.0;
double pwmDepth = 0.75;
double pwmPhase = 0.0;
public:
I2x03();
~I2x03() override = default;
@ -16,13 +26,13 @@ namespace Xybrid::Instruments {
//void onRename() override;
//void saveData(QCborMap&) override;
//void loadData(QCborMap&) override;
void saveData(QCborMap&) override;
void loadData(QCborMap&) override;
//void onUnparent(std::shared_ptr<Data::Graph>) override;
//void onParent(std::shared_ptr<Data::Graph>) override;
//void onGadgetCreated() override;
void onGadgetCreated() override;
//void onDoubleClick() override;
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;

View File

@ -8,6 +8,24 @@ using namespace Xybrid::UI;
#include <QRadialGradient>
#include <QGraphicsSceneMouseEvent>
template void KnobGadget::bind(double&);
template void KnobGadget::bind(float&);
template void KnobGadget::bind(int8_t&);
template void KnobGadget::bind(uint8_t&);
template void KnobGadget::bind(int16_t&);
template void KnobGadget::bind(uint16_t&);
template void KnobGadget::bind(int32_t&);
template void KnobGadget::bind(uint32_t&);
template void KnobGadget::bind(std::atomic<double>&);
template void KnobGadget::bind(std::atomic<float>&);
template void KnobGadget::bind(std::atomic<int8_t>&);
template void KnobGadget::bind(std::atomic<uint8_t>&);
template void KnobGadget::bind(std::atomic<int16_t>&);
template void KnobGadget::bind(std::atomic<uint16_t>&);
template void KnobGadget::bind(std::atomic<int32_t>&);
template void KnobGadget::bind(std::atomic<uint32_t>&);
double KnobGadget::get() {
double v = fGet();
if (v != lastVal) {
@ -107,6 +125,7 @@ void KnobGadget::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
void KnobGadget::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
if (highlighted) {
auto tdelta = -(e->screenPos().y() - e->buttonDownScreenPos(Qt::LeftButton).y());
tdelta /= stepPx;
fSet(std::clamp(startVal + tdelta * step, min, max));
}
update();

View File

@ -27,6 +27,7 @@ namespace Xybrid::UI {
double min = 0.0;
double max = 1.0;
double step = 0.01;
int stepPx = 1;
double defaultVal = 0.0;
qreal size = 32;