it actually makes music now!
parent
333a06cac7
commit
e5f1615724
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
29
notes
29
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<int[]>(tickBufSize/sizeof(int)); // aligned to int, which we assume is the native word size
|
||||
tickBuf = std::make_unique<size_t[]>(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<ptrdiff_t>(size));
|
||||
if (n + size > tickBufEnd) qWarning() << "Tick buffer overrun!";
|
||||
return n;
|
||||
|
@ -88,7 +94,12 @@ void AudioEngine::play(std::shared_ptr<Project> 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<Project> 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<std::shared_ptr<Node>> 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<int>(project->sequence.size())) p = project->sequence[static_cast<size_t>(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<int>(p->channels.size());
|
||||
for (int c = 0; c < chs; c++) {
|
||||
auto& ct = chTrack[static_cast<size_t>(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<CommandPort*>(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<uint16_t&>(buf[bi]) = ne.noteId; // trigger on note id...
|
||||
reinterpret_cast<int16_t&>(buf[bi+2]) = -2; // note off
|
||||
}
|
||||
}
|
||||
|
||||
for (int c = 0; c < chs; c++) {
|
||||
auto& r = p->rowAt(c, curRow);
|
||||
auto& ct = chTrack[static_cast<size_t>(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<uint16_t&>(buf[bi]) = ct.noteId; // either new note, or note-off on old one
|
||||
reinterpret_cast<int16_t&>(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<uint16_t&>(buf[bi]) = rpl.noteId; // trigger on note id...
|
||||
reinterpret_cast<int16_t&>(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"<<ts<<"samples";
|
||||
|
||||
//qDebug() << "<tick start>";
|
||||
for (auto n : queue) if (!n->try_process()) qWarning() << "Dependency check failed in single threaded mode!";
|
||||
if (auto p = std::static_pointer_cast<AudioPort>(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<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, -6 + note * 5)) * .25);
|
||||
buffer[1][i] = buffer[0][i];
|
||||
time += 1.0/sampleRate;
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include <memory>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
#include <atomic>
|
||||
|
||||
#include <QIODevice>
|
||||
|
@ -11,8 +13,20 @@
|
|||
class QThread;
|
||||
namespace Xybrid::Data {
|
||||
class Project;
|
||||
class Node;
|
||||
}
|
||||
namespace Xybrid::Audio {
|
||||
template <typename T> struct PointerCompare {
|
||||
bool operator()(T* a, T* b) const { return *a == *b; }
|
||||
size_t operator()(T* a) const { return std::hash<T>()(*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<int[]> tickBuf;
|
||||
std::atomic<int*> tickBufPtr;
|
||||
int* tickBufEnd;
|
||||
std::unique_ptr<size_t[]> tickBuf;
|
||||
std::atomic<size_t*> tickBufPtr;
|
||||
size_t* tickBufEnd;
|
||||
|
||||
PlaybackMode mode = Stopped;
|
||||
size_t tickId = 0;
|
||||
std::shared_ptr<Data::Project> project;
|
||||
|
||||
std::deque<std::shared_ptr<Data::Node>> queue;
|
||||
bool queueValid;
|
||||
void buildQueue();
|
||||
|
||||
std::array<uint16_t, 256> portLastNoteId;
|
||||
std::vector<NoteInfo> chTrack;
|
||||
std::vector<NoteInfo> noteEndQueue;
|
||||
std::unordered_map<std::string*, NoteInfo, PointerCompare<std::string>, PointerCompare<std::string>> nameTrack;
|
||||
std::vector<uint8_t> 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<Data::Project>);
|
||||
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;
|
||||
|
|
|
@ -48,6 +48,8 @@ void PluginRegistry::registerPlugin(std::shared_ptr<PluginInfo> 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<Node> PluginRegistry::createInstance(const std::string& id) {
|
||||
|
@ -55,6 +57,7 @@ std::shared_ptr<Node> 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::function<void (std::share
|
|||
ccm->addAction(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::function<void (std::share
|
|||
ccm->addAction(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::function<void (std::share
|
|||
for (auto& i : cm[""]) m->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
|
||||
auto n = pi->createInstance();
|
||||
n->plugin = pi;
|
||||
n->init();
|
||||
f(n);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
class QMenu;
|
||||
|
@ -15,6 +16,7 @@ namespace Xybrid::Config {
|
|||
class PluginInfo {
|
||||
public:
|
||||
std::string id;
|
||||
std::vector<std::string> oldIds;
|
||||
std::string displayName;
|
||||
std::string category;
|
||||
std::function<std::shared_ptr<Data::Node>()> createInstance;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ using namespace Xybrid::Data;
|
|||
|
||||
#include "config/pluginregistry.h"
|
||||
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QDebug>
|
||||
|
@ -33,6 +36,7 @@ bool Port::connect(std::shared_ptr<Port> 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<Port> 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) {
|
|||
graph->children.push_back(t);
|
||||
onParent(graph);
|
||||
}
|
||||
audioEngine->invalidateQueue(project); // just to be safe
|
||||
}
|
||||
|
||||
std::shared_ptr<Port> Node::port(Port::Type t, Port::DataType dt, uint8_t idx, bool addIfNeeded) {
|
||||
|
@ -120,4 +126,35 @@ bool Node::dependsOn(std::shared_ptr<Node> 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; }
|
||||
|
|
|
@ -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<Node> owner;
|
||||
std::vector<std::weak_ptr<Port>> connections;
|
||||
std::weak_ptr<Port> passthroughTo;
|
||||
Type type; // TODO: figure out passthrough?
|
||||
Type type;
|
||||
uint8_t index;
|
||||
size_t tickUpdatedOn = static_cast<size_t>(-1);
|
||||
|
||||
|
@ -64,6 +68,9 @@ namespace Xybrid::Data {
|
|||
};
|
||||
|
||||
class Node : public std::enable_shared_from_this<Node> {
|
||||
friend class Audio::AudioEngine;
|
||||
size_t tick_last = 0;
|
||||
bool try_process(bool checkDependencies = true);
|
||||
public:
|
||||
Project* project;
|
||||
std::weak_ptr<Graph> parent;
|
||||
|
@ -87,6 +94,8 @@ namespace Xybrid::Data {
|
|||
std::unordered_set<std::shared_ptr<Node>> dependencies() const;
|
||||
bool dependsOn(std::shared_ptr<Node>);
|
||||
|
||||
virtual void init() { }
|
||||
virtual void reset() { }
|
||||
virtual void saveData(QCborMap&) { }
|
||||
virtual void loadData(QCborMap&) { }
|
||||
|
||||
|
|
|
@ -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<AudioPort>(connections[0].lock()); p && p->dataType() == Audio) {
|
||||
p->pull();
|
||||
bufL = p->bufL;
|
||||
bufR = p->bufR;
|
||||
return;
|
||||
}
|
||||
}
|
||||
bufL = static_cast<float*>(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<float*>(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<CommandPort>(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<CommandPort>(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<uint8_t> v) {
|
||||
tickUpdatedOn = audioEngine->curTickId();
|
||||
dataSize = v.size();
|
||||
data = static_cast<uint8_t*>(audioEngine->tickAlloc(dataSize));
|
||||
memcpy(data, v.data(), dataSize);
|
||||
}
|
||||
|
|
|
@ -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<uint8_t>);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 <cmath>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "plug:testsynth";
|
||||
i->displayName = "The Testron";
|
||||
i->category = "Instrument";
|
||||
//i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<TestSynth>(); };
|
||||
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<CommandPort>(port(Port::Input, Port::Command, 0));
|
||||
cp->pull();
|
||||
auto p = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
|
||||
p->pull();
|
||||
|
||||
size_t mi = 0;
|
||||
while (cp->dataSize >= mi+5) {
|
||||
uint16_t id = reinterpret_cast<uint16_t&>(cp->data[mi]);
|
||||
int16_t n = reinterpret_cast<int16_t&>(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<float>((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()
|
||||
}
|
|
@ -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<Data::Graph>) override;
|
||||
//void onParent(std::shared_ptr<Data::Graph>) override;
|
||||
|
||||
//void onGadgetCreated() override;
|
||||
|
||||
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
|
||||
};
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue