diff --git a/asset-work/xybrid-logo-banner480.png b/asset-work/xybrid-logo-banner480.png new file mode 100644 index 0000000..4d2e469 Binary files /dev/null and b/asset-work/xybrid-logo-banner480.png differ diff --git a/notes b/notes index bda3856..c496718 100644 --- a/notes +++ b/notes @@ -46,16 +46,16 @@ project data { TODO { immediate frontburner { neeeeext { - hook the graph up to the audio engine! { - recursive dependency queue resolution - done/readiness test (process() wrapper?) + - hook the graph up to the audio engine! { + - (not so-)recursive dependency queue resolution + - done/readiness test (process() wrapper?) } - hook up commands to the graph { - figure out how to do note numbers - ^ vector sized by channel count rounded up to next 16 - ^^ on switching to new graph... map of (hashes of) named channels?? - assemble wire command queues (probaby some minor trickiness) and push into ports - NOTE! note number and port have to be combined in tracking (have to know what port to send the note-offs to) + - hook up commands to the graph { + - figure out how to do note numbers + - ^ vector sized by channel count rounded up to next 16 + - ^^ on switching to new graph... map of (hashes of) named channels?? + - assemble wire command queues (probaby some minor trickiness) and push into ports + - NOTE! note number and port have to be combined in tracking (have to know what port to send the note-offs to) } then implement multithreading! :D } @@ -85,6 +85,7 @@ TODO { import/export subgraph as file (*.xyg) proper playback controls and indicators + play from current pattern instrument previewing pattern editor cells can have (dynamic) tool tips; set this up with port names, etc. @@ -167,11 +168,11 @@ graph+node+port system { } on-the-wire command format { - ushort noteId // for sending commands to the same note - short note // note number >= 0, -1 for none, -2 note off, -3 hard cut - unsigned char numParams * { - unsigned char cmd - unsigned char amount + uint16_t noteId // for sending commands to the same note + int16_t note // note number >= 0, -1 for none, -2 note off, -3 hard cut + uint8_t numParams x { + uint8_t cmd + uint8_t amount } } diff --git a/readme.md b/readme.md index 2c57252..f6bcf02 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,5 @@ -# Xybrid - -something something, actual readme coming later +![Xybrid logo](asset-work/xybrid-logo-banner480.png) +Xybrid: deeply modular tracker ## Build dependencies: - Qt 5.12 or later diff --git a/xybrid/audio/audioengine.cpp b/xybrid/audio/audioengine.cpp index 6b12daa..2b835d1 100644 --- a/xybrid/audio/audioengine.cpp +++ b/xybrid/audio/audioengine.cpp @@ -2,6 +2,8 @@ #include "data/project.h" using namespace Xybrid::Audio; using namespace Xybrid::Data; +#include "data/graph.h" +#include "data/porttypes.h" #include "mainwindow.h" #include "uisocket.h" @@ -36,14 +38,18 @@ void AudioEngine::postInit() { open(QIODevice::ReadOnly); // set up buffer for per-tick allocation - tickBuf = std::make_unique(tickBufSize/sizeof(int)); // aligned to int, which we assume is the native word size + tickBuf = std::make_unique(tickBufSize/sizeof(size_t)); // aligned to size_t tickBufPtr = tickBuf.get(); tickBufEnd = tickBufPtr+tickBufSize; + buf.reserve(1024); // 1kb isn't much to make sure it's super unlikely to have to reallocate + chTrack.reserve(256); + noteEndQueue.reserve(256); + nameTrack.reserve(64+1); // +1 to make extra sure it doesn't rehash later } void* AudioEngine::tickAlloc(size_t size) { - if (auto r = size % sizeof(int); r != 0) size += sizeof(int) - r; // pad to word + if (auto r = size % sizeof(size_t); r != 0) size += sizeof(size_t) - r; // pad auto n = tickBufPtr.fetch_add(static_cast(size)); if (n + size > tickBufEnd) qWarning() << "Tick buffer overrun!"; return n; @@ -88,7 +94,12 @@ void AudioEngine::play(std::shared_ptr p) { QMetaObject::invokeMethod(this, [this, p]() { if (!p) return; // nope project = p; + // stop and reset, then init playback + queueValid = false; + queue.clear(); + portLastNoteId.fill(0); + project->rootGraph->reset(); initAudio(); for (auto& b : buffer) { @@ -112,12 +123,57 @@ void AudioEngine::play(std::shared_ptr p) { void AudioEngine::stop() { QMetaObject::invokeMethod(this, [this]() { project = nullptr; + queueValid = false; + queue.clear(); deinitAudio(); mode = Stopped; emit this->playbackModeChanged(); }, Qt::QueuedConnection); } +void AudioEngine::buildQueue() { + queue.clear(); + // stuff + std::deque> q1, q2; + auto* qCurrent = &q1; + auto* qNext = &q2; + + if (auto p = project->rootGraph->port(Port::Output, Port::Audio, 0); p) + if (auto pt = p->passthroughTo.lock(); pt) + if (auto ptn = pt->owner.lock(); ptn) + qCurrent->push_back(ptn); + + // T_ODO: make this not process things the weird way around + // oh, it's working properly... it just processing subgraph before its internally-connected *inputs* + while (!qCurrent->empty()) { + // ... this could be made more efficient with some redundancy checking, but whatever + for (auto n : *qCurrent) { + queue.push_front(n); // add to actual queue + for (auto p1 : n->inputs) { // data types... + for (auto p2 : p1.second) { // ports... + for (auto p3 : p2.second->connections) { // connected ports! + auto pc = p3.lock(); + if (!pc) continue; + auto pcn = pc->owner.lock(); + if (!pcn) continue; + qNext->push_back(pcn); + if (auto pp = pc->passthroughTo.lock(); pp) { + // if it has a passthrough, also place passthrough's owner after (before) + if (auto ppp = pp->owner.lock(); ppp) qNext->push_back(ppp); + } + + } + } + } + } + + qCurrent->clear(); + std::swap(qCurrent, qNext); + } + + queueValid = true; +} + qint64 AudioEngine::readData(char *data, qint64 maxlen) { const constexpr qint64 smp = 2; const constexpr qint64 stride = smp*2; @@ -166,14 +222,20 @@ void AudioEngine::nextTick() { buffer[0].clear(); buffer[1].clear(); + if (!queueValid) buildQueue(); + Pattern* p = nullptr; + Pattern* pOld = nullptr; auto setP = [&] { if (seqPos >= 0 && seqPos < static_cast(project->sequence.size())) p = project->sequence[static_cast(seqPos)]; else p = nullptr; }; setP(); + bool newRow = false; + bool newPattern = false; auto advanceSeq = [&] { + pOld = p; p = nullptr; int tries = 0; while (!p) { @@ -185,6 +247,8 @@ void AudioEngine::nextTick() { // set pattern things if (p->tempo > 0) tempo = p->tempo; + + newPattern = true; }; auto advanceRow = [&] { curTick = 0; @@ -202,7 +266,110 @@ void AudioEngine::nextTick() { } } - // TODO then assemble command buffers + newRow = true; + + // assemble command buffers + + noteEndQueue.clear(); + if (newPattern) { // notes on named channels carry over to their matching channel on the new pattern (if present); everything else is note-offed + if (pOld) { + size_t cs = pOld->channels.size(); + for (size_t c = 0; c < cs; c++) { + auto& ch = pOld->channels[c]; + if (!chTrack[c].valid) continue; // skip notes that aren't actually playing + if (ch.name.empty()) noteEndQueue.push_back(chTrack[c]); // end notes in unnamed channels right away + else nameTrack[&ch.name] = chTrack[c]; // otherwise keep track for later + } + } + chTrack.clear(); // clear and prepare channel note tracking + chTrack.resize(p->channels.size()); + if (nameTrack.size() > 0) { // if there were any + size_t cs = p->channels.size(); + for (size_t c = 0; c < cs; c++) { + auto& ch = p->channels[c]; + if (ch.name.empty()) continue; + if (auto nt = nameTrack.find(&ch.name); nt != nameTrack.end() && nt->second.valid) { + chTrack[c] = nt->second; // carry over + nt->second.valid = false; // and invalidate + } + } + // dump remainder into note end + for (auto nt : nameTrack) if (nt.second.valid) noteEndQueue.push_back(nt.second); + } + nameTrack.clear(); + } + + int chs = static_cast(p->channels.size()); + for (int c = 0; c < chs; c++) { + auto& ct = chTrack[static_cast(c)]; + if (!ct.valid) continue; // no saved note + auto& r = p->rowAt(c, curRow); + if (r.note != -1 && r.port >= 0 && r.port != ct.port) { // if explicitly specified for a different port... + noteEndQueue.push_back(ct); // old note overwritten + ct.valid = false; + } + } + + auto& cpm = project->rootGraph->inputs[Port::Command]; + for (auto p_ : cpm) { + auto* pt = static_cast(p_.second.get()); + //if (pt->passthroughTo.lock()->connections.empty()) continue; // port isn't hooked up to anything + uint8_t idx = pt->index; + buf.clear(); + + for (auto& ne : noteEndQueue) { + if (ne.valid && ne.port == idx) { + size_t bi = buf.size(); + buf.resize(bi+5, 0); + reinterpret_cast(buf[bi]) = ne.noteId; // trigger on note id... + reinterpret_cast(buf[bi+2]) = -2; // note off + } + } + + for (int c = 0; c < chs; c++) { + auto& r = p->rowAt(c, curRow); + auto& ct = chTrack[static_cast(c)]; + int16_t port = r.port; + if (port < 0 && ct.valid) port = ct.port; // assume last port used on channel if not specified + if (port != idx) continue; + + NoteInfo rpl; // default initialization, invalid + + if (r.note >= 0) { + if (ct.valid) rpl = ct; // replace + ct = NoteInfo(idx, portLastNoteId[idx]++); + } else if (r.note <= -2 && ct.valid) { + ct.valid = false; // invalidate it here but leave note id intact + // this condition will allow you to note-off the same note id multiple times but anything + // that takes offense to that is a bug anyway + } + + size_t bi = buf.size(); + buf.resize(bi+5, 0); + reinterpret_cast(buf[bi]) = ct.noteId; // either new note, or note-off on old one + reinterpret_cast(buf[bi+2]) = r.note; // shove note into vector + auto& np = buf[bi+4]; // number of params + + if (r.params) { + for (auto& p : *r.params) { + if (p[0] == ' ') continue; // ignore struts + buf.push_back(p[0]); + buf.push_back(p[1]); + np++; + } + } + + if (rpl.valid) { // replacing old note on the same port and channel + bi = buf.size(); + buf.resize(bi+5, 0); + reinterpret_cast(buf[bi]) = rpl.noteId; // trigger on note id... + reinterpret_cast(buf[bi+2]) = -2; // note off + } + } + + //qDebug() << "port" << idx << "data of size" << buf.size(); + pt->push(buf); + } }; curTick++; @@ -219,8 +386,18 @@ void AudioEngine::nextTick() { buffer[1].resize(ts); //qDebug() << "tick" << tickId << "contains"<"; + for (auto n : queue) if (!n->try_process()) qWarning() << "Dependency check failed in single threaded mode!"; + if (auto p = std::static_pointer_cast(project->rootGraph->port(Port::Output, Port::Audio, 0)); p) { + p->pull(); + size_t bufs = ts * sizeof(float); + memcpy(buffer[0].data(), p->bufL, bufs); + memcpy(buffer[1].data(), p->bufR, bufs); + //p->bufL + } + //buffer[0].data() // test - const double PI = std::atan(1)*4; + /*const double PI = std::atan(1)*4; const double SEMI = std::pow(2.0, 1.0/12.0); double time = 0; int note = curRow % 4; @@ -229,7 +406,7 @@ void AudioEngine::nextTick() { buffer[0][i] = static_cast(std::sin(time * PI*2 * 440 * std::pow(SEMI, -6 + note * 5)) * .25); buffer[1][i] = buffer[0][i]; time += 1.0/sampleRate; - } + }*/ } diff --git a/xybrid/audio/audioengine.h b/xybrid/audio/audioengine.h index ced110c..d2b7629 100644 --- a/xybrid/audio/audioengine.h +++ b/xybrid/audio/audioengine.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -11,8 +13,20 @@ class QThread; namespace Xybrid::Data { class Project; + class Node; } namespace Xybrid::Audio { + template struct PointerCompare { + bool operator()(T* a, T* b) const { return *a == *b; } + size_t operator()(T* a) const { return std::hash()(*a); } + }; + struct NoteInfo { + bool valid = false; + uint8_t port = 0; + uint16_t noteId = 0; + NoteInfo() = default; + NoteInfo(uint8_t p, uint16_t nId) { valid = true; port = p; noteId = nId; } + }; class AudioEngine : public QIODevice { Q_OBJECT explicit AudioEngine(QObject *parent = nullptr); @@ -33,14 +47,24 @@ namespace Xybrid::Audio { size_t bufPos = 0; static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough - std::unique_ptr tickBuf; - std::atomic tickBufPtr; - int* tickBufEnd; + std::unique_ptr tickBuf; + std::atomic tickBufPtr; + size_t* tickBufEnd; PlaybackMode mode = Stopped; size_t tickId = 0; std::shared_ptr project; + std::deque> queue; + bool queueValid; + void buildQueue(); + + std::array portLastNoteId; + std::vector chTrack; + std::vector noteEndQueue; + std::unordered_map, PointerCompare> nameTrack; + std::vector buf; /// preallocated buffer for building commands + // playback timing and position float tempo = 140.0; int seqPos; @@ -59,9 +83,12 @@ namespace Xybrid::Audio { void play(std::shared_ptr); void stop(); + inline void invalidateQueue(Data::Project* p) { if (p == project.get()) queueValid = false; } + void* tickAlloc(size_t size); inline size_t curTickId() const { return tickId; } inline size_t curTickSize() const { return buffer[0].size(); } + inline int curSampleRate() const { return sampleRate; } // QIODevice functions qint64 readData(char* data, qint64 maxlen) override; diff --git a/xybrid/config/pluginregistry.cpp b/xybrid/config/pluginregistry.cpp index 222c70c..8ba5ee1 100644 --- a/xybrid/config/pluginregistry.cpp +++ b/xybrid/config/pluginregistry.cpp @@ -48,6 +48,8 @@ void PluginRegistry::registerPlugin(std::shared_ptr pi) { if (pi->id.empty()) return; if (plugins.find(pi->id) != plugins.end()) return; plugins[pi->id] = pi; + // there might be a better way to do this? + for (auto& id : pi->oldIds) plugins[id] = pi; } std::shared_ptr PluginRegistry::createInstance(const std::string& id) { @@ -55,6 +57,7 @@ std::shared_ptr PluginRegistry::createInstance(const std::string& id) { if (f == plugins.end()) return nullptr; auto n = f->second->createInstance(); n->plugin = f->second; + n->init(); return n; } @@ -114,6 +117,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::functionaddAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] { auto n = pi->createInstance(); n->plugin = pi; + n->init(); f(n); }); } @@ -129,6 +133,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::functionaddAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] { auto n = pi->createInstance(); n->plugin = pi; + n->init(); f(n); }); } @@ -139,6 +144,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::functionaddAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] { auto n = pi->createInstance(); n->plugin = pi; + n->init(); f(n); }); diff --git a/xybrid/config/pluginregistry.h b/xybrid/config/pluginregistry.h index cce6c8f..196f358 100644 --- a/xybrid/config/pluginregistry.h +++ b/xybrid/config/pluginregistry.h @@ -1,7 +1,8 @@ #pragma once -#include #include +#include +#include #include class QMenu; @@ -15,6 +16,7 @@ namespace Xybrid::Config { class PluginInfo { public: std::string id; + std::vector oldIds; std::string displayName; std::string category; std::function()> createInstance; diff --git a/xybrid/data/graph.cpp b/xybrid/data/graph.cpp index b145677..2242a66 100644 --- a/xybrid/data/graph.cpp +++ b/xybrid/data/graph.cpp @@ -33,6 +33,9 @@ Graph::Graph() { plugin = inf; // harder bind } +// propagate +void Graph::reset() { for (auto c : children) c->reset(); } + void Graph::saveData(QCborMap& m) { // graph properties // ... maybe there will be some at some point diff --git a/xybrid/data/graph.h b/xybrid/data/graph.h index 87971f6..9b1939c 100644 --- a/xybrid/data/graph.h +++ b/xybrid/data/graph.h @@ -13,6 +13,7 @@ namespace Xybrid::Data { // position of viewport within graph (not serialized) int viewX{}, viewY{}; + void reset() override; void saveData(QCborMap&) override; void loadData(QCborMap&) override; diff --git a/xybrid/data/node.cpp b/xybrid/data/node.cpp index 73198ea..2dd55de 100644 --- a/xybrid/data/node.cpp +++ b/xybrid/data/node.cpp @@ -6,6 +6,9 @@ using namespace Xybrid::Data; #include "config/pluginregistry.h" +#include "audio/audioengine.h" +using namespace Xybrid::Audio; + #include #include @@ -33,6 +36,7 @@ bool Port::connect(std::shared_ptr p) { // actually hook up connections.emplace_back(p); p->connections.emplace_back(shared_from_this()); + if (auto o = owner.lock(); o) audioEngine->invalidateQueue(o->project); return true; } @@ -41,6 +45,7 @@ void Port::disconnect(std::shared_ptr p) { auto t = shared_from_this(); connections.erase(std::remove_if(connections.begin(), connections.end(), [p](auto w) { return w.lock() == p; }), connections.end()); p->connections.erase(std::remove_if(p->connections.begin(), p->connections.end(), [t](auto w) { return w.lock() == t; }), p->connections.end()); + if (auto o = owner.lock(); o) audioEngine->invalidateQueue(o->project); } void Port::cleanConnections() { @@ -66,6 +71,7 @@ void Node::parentTo(std::shared_ptr graph) { graph->children.push_back(t); onParent(graph); } + audioEngine->invalidateQueue(project); // just to be safe } std::shared_ptr Node::port(Port::Type t, Port::DataType dt, uint8_t idx, bool addIfNeeded) { @@ -120,4 +126,35 @@ bool Node::dependsOn(std::shared_ptr o) { return false; } +bool Node::try_process(bool checkDependencies) { + size_t tick_this = audioEngine->curTickId(); + if (tick_last == tick_this) return true; // already processed + + if (checkDependencies) { // check if dependencies are done + for (auto& t : inputs) { + for (auto& p : t.second) { + for (auto& c : p.second->connections) { + // if connection still exists, *and its owner* still exists... + if (auto cp = c.lock(); cp) { + if (auto n = cp->owner.lock(); n) { + if (auto cpp = cp->passthroughTo.lock(); cpp) { // passthrough... + if (auto np = cpp->owner.lock(); np && np->tick_last != tick_this) return false; + } + if (n->tick_last != tick_this) return false; + } + } + } + } + } + } + + /*auto qd = qDebug() << "processing" << QString::fromStdString(pluginName()); + if (!name.empty()) qd << "named" << QString::fromStdString(name); + if (auto p = parent.lock(); p && !p->name.empty()) qd << "within" << QString::fromStdString(p->name);*/ + process(); + + tick_last = tick_this; + return true; +} + std::string Node::pluginName() const { if (!plugin) return "(unknown plugin)"; return plugin->displayName; } diff --git a/xybrid/data/node.h b/xybrid/data/node.h index 3db9ab0..ee991b0 100644 --- a/xybrid/data/node.h +++ b/xybrid/data/node.h @@ -21,6 +21,10 @@ namespace Xybrid::Config { class PluginInfo; } +namespace Xybrid::Audio { + class AudioEngine; +} + namespace Xybrid::Data { class Project; @@ -41,7 +45,7 @@ namespace Xybrid::Data { std::weak_ptr owner; std::vector> connections; std::weak_ptr passthroughTo; - Type type; // TODO: figure out passthrough? + Type type; uint8_t index; size_t tickUpdatedOn = static_cast(-1); @@ -64,6 +68,9 @@ namespace Xybrid::Data { }; class Node : public std::enable_shared_from_this { + friend class Audio::AudioEngine; + size_t tick_last = 0; + bool try_process(bool checkDependencies = true); public: Project* project; std::weak_ptr parent; @@ -87,6 +94,8 @@ namespace Xybrid::Data { std::unordered_set> dependencies() const; bool dependsOn(std::shared_ptr); + virtual void init() { } + virtual void reset() { } virtual void saveData(QCborMap&) { } virtual void loadData(QCborMap&) { } diff --git a/xybrid/data/porttypes.cpp b/xybrid/data/porttypes.cpp index 4f43a6d..129084a 100644 --- a/xybrid/data/porttypes.cpp +++ b/xybrid/data/porttypes.cpp @@ -14,8 +14,17 @@ void AudioPort::pull() { size_t s = sizeof(float) * ts; if (type == Input) { + if (connections.size() == 1) { + // if this is a single connection, just repoint to source audio + if (auto p = std::static_pointer_cast(connections[0].lock()); p && p->dataType() == Audio) { + p->pull(); + bufL = p->bufL; + bufR = p->bufR; + return; + } + } bufL = static_cast(audioEngine->tickAlloc(s*2)); - bufR = bufL + s; + bufR = &bufL[ts]; // for some reason just adding the size wonks out memset(bufL, 0, s*2); // clear buffers for (auto c : connections) { // mix @@ -34,7 +43,37 @@ void AudioPort::pull() { bufR = pt->bufR; } else { // output without valid passthrough, just clear and prepare a blank buffer bufL = static_cast(audioEngine->tickAlloc(s*2)); - bufR = bufL + s; + bufR = &bufL[ts]; memset(bufL, 0, s*2); // clear buffers } } + +void CommandPort::pull() { + auto t = audioEngine->curTickId(); + if (tickUpdatedOn == t) return; + tickUpdatedOn = t; + + dataSize = 0; + if (type == Input) { + for (auto c : connections) { + if (auto p = std::static_pointer_cast(c.lock()); p && p->dataType() == Command) { + p->pull(); + data = p->data; // just repoint to input's buffer + dataSize = p->dataSize; + break; + } + } + } else if (auto pt = std::static_pointer_cast(passthroughTo.lock()); pt && pt->dataType() == Command) { + // valid passthrough + pt->pull(); + data = pt->data; // again, just repoint + dataSize = pt->dataSize; + } // don't need an else case, size is already zero +} + +void CommandPort::push(std::vector v) { + tickUpdatedOn = audioEngine->curTickId(); + dataSize = v.size(); + data = static_cast(audioEngine->tickAlloc(dataSize)); + memcpy(data, v.data(), dataSize); +} diff --git a/xybrid/data/porttypes.h b/xybrid/data/porttypes.h index 3a18bb4..ddb5262 100644 --- a/xybrid/data/porttypes.h +++ b/xybrid/data/porttypes.h @@ -28,5 +28,10 @@ namespace Xybrid::Data { Port::DataType dataType() const override { return Port::Command; } bool singleInput() const override { return true; } + + void pull() override; + + /// Push a data buffer + void push(std::vector); }; } diff --git a/xybrid/gadgets/testsynth.cpp b/xybrid/gadgets/testsynth.cpp new file mode 100644 index 0000000..b16c296 --- /dev/null +++ b/xybrid/gadgets/testsynth.cpp @@ -0,0 +1,93 @@ +#include "testsynth.h" +using Xybrid::Gadgets::TestSynth; +using namespace Xybrid::Data; + +#include "data/porttypes.h" + +#include "config/pluginregistry.h" +using namespace Xybrid::Config; + +#include "audio/audioengine.h" +using namespace Xybrid::Audio; + +#include + +#include + +namespace { + bool _ = PluginRegistry::enqueueRegistration([] { + auto i = std::make_shared(); + i->id = "plug:testsynth"; + i->displayName = "The Testron"; + i->category = "Instrument"; + //i->hidden = true; + i->createInstance = []{ return std::make_shared(); }; + PluginRegistry::registerPlugin(i); + //inf = i; + }); +} + +TestSynth::TestSynth() { + // +} + +void TestSynth::init() { + addPort(Port::Input, Port::Command, 0); + addPort(Port::Output, Port::Audio, 0); +} + +void TestSynth::reset() { + osc = 0.0; + osc2 = 0.0; + cvol = 0.0; + tvol = 0.0; + noteId = 0; +} + +void TestSynth::process() { + auto cp = std::static_pointer_cast(port(Port::Input, Port::Command, 0)); + cp->pull(); + auto p = std::static_pointer_cast(port(Port::Output, Port::Audio, 0)); + p->pull(); + + size_t mi = 0; + while (cp->dataSize >= mi+5) { + uint16_t id = reinterpret_cast(cp->data[mi]); + int16_t n = reinterpret_cast(cp->data[mi+2]); + if (n > -1) { + noteId = id; + note = n; + tvol = 1.0; + } else if (n < -1 && id == noteId) { // note off + tvol = 0.0; + } + mi += 5 + cp->data[mi+4]; + } + + const double PI = std::atan(1)*4; + const double SEMI = std::pow(2.0, 1.0/12.0); + + size_t ts = audioEngine->curTickSize(); + + for (size_t s = 0; s < ts; s++) { + if (tvol > cvol) cvol += 64.0 / audioEngine->curSampleRate(); + else if (tvol < cvol) cvol -= 16.0 / audioEngine->curSampleRate(); + cvol = std::clamp(cvol, 0.0, 1.0); + if (cvol == 0.0) { osc = osc2 = 0.0; } + float oscV = static_cast((std::sin(osc * PI*2) + std::sin(osc2 * PI*2) * std::pow(.75, 4)) * std::pow(cvol*.5, 4)); + + double enote = note + std::sin(lfo * PI*2) * 0.1; + double freq = 440.0 * std::pow(SEMI, enote - (45+12)); + osc += freq / audioEngine->curSampleRate(); + osc = std::fmod(osc, 1.0); + osc2 += (freq * .5) / audioEngine->curSampleRate(); + osc2 = std::fmod(osc2, 1.0); + + lfo += 3.0 / audioEngine->curSampleRate(); + lfo = std::fmod(lfo, 1.0); + + p->bufL[s] = oscV; + p->bufR[s] = oscV; + } + //audioEngine->curSampleRate() +} diff --git a/xybrid/gadgets/testsynth.h b/xybrid/gadgets/testsynth.h new file mode 100644 index 0000000..8c67a72 --- /dev/null +++ b/xybrid/gadgets/testsynth.h @@ -0,0 +1,37 @@ +#pragma once + +#include "data/node.h" + +namespace Xybrid::Gadgets { + class TestSynth : public Data::Node { + // + double osc = 0; + double osc2 = 0; + double note = 45+12; + double lfo = 0; + + uint16_t noteId = 0; + double cvol = 0; + double tvol = 0; + public: + TestSynth(); + ~TestSynth() override = default; + + void init() override; + void reset() override; + void process() override; + + //void onRename() override; + + //void saveData(QCborMap&) override; + //void loadData(QCborMap&) override; + + //void onUnparent(std::shared_ptr) override; + //void onParent(std::shared_ptr) override; + + //void onGadgetCreated() override; + + //void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override; + }; +} + diff --git a/xybrid/mainwindow.cpp b/xybrid/mainwindow.cpp index b1cd99c..6cad668 100644 --- a/xybrid/mainwindow.cpp +++ b/xybrid/mainwindow.cpp @@ -320,10 +320,6 @@ MainWindow::MainWindow(QWidget *parent) : // and start with a new project menuFileNew(); - - //auto q = QJsonObject(); - //q.insert(QMetaObject::, "frenk"); - qDebug() << QVariant::fromValue(Data::Port::Audio).toString(); } MainWindow::~MainWindow() { diff --git a/xybrid/ui/patchboard/nodeobject.cpp b/xybrid/ui/patchboard/nodeobject.cpp index 97e8580..72e7600 100644 --- a/xybrid/ui/patchboard/nodeobject.cpp +++ b/xybrid/ui/patchboard/nodeobject.cpp @@ -31,7 +31,7 @@ namespace { }; } -void PortObject::connectTo(Xybrid::UI::PortObject* o) { +void PortObject::connectTo(PortObject* o) { if (!o) return; if (connections.find(o) != connections.end()) return; if (port->type == o->port->type) return; @@ -331,6 +331,10 @@ PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) { this->in = in; this->out = out; + // remove dupes + if (in->connections[out]) delete in->connections[out]; + if (out->connections[in]) delete out->connections[in]; + // and hook up in->connections[out] = this; out->connections[in] = this; diff --git a/xybrid/xybrid.pro b/xybrid/xybrid.pro index a7f377d..f43495b 100644 --- a/xybrid/xybrid.pro +++ b/xybrid/xybrid.pro @@ -53,7 +53,8 @@ SOURCES += \ config/pluginregistry.cpp \ data/porttypes.cpp \ ui/breadcrumbview.cpp \ - gadgets/ioport.cpp + gadgets/ioport.cpp \ + gadgets/testsynth.cpp HEADERS += \ mainwindow.h \ @@ -81,7 +82,8 @@ HEADERS += \ data/porttypes.h \ config/pluginregistry.h \ ui/breadcrumbview.h \ - gadgets/ioport.h + gadgets/ioport.h \ + gadgets/testsynth.h FORMS += \ mainwindow.ui