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 {
|
TODO {
|
||||||
immediate frontburner {
|
immediate frontburner {
|
||||||
neeeeext {
|
neeeeext {
|
||||||
hook the graph up to the audio engine! {
|
- hook the graph up to the audio engine! {
|
||||||
recursive dependency queue resolution
|
- (not so-)recursive dependency queue resolution
|
||||||
done/readiness test (process() wrapper?)
|
- done/readiness test (process() wrapper?)
|
||||||
}
|
}
|
||||||
hook up commands to the graph {
|
- hook up commands to the graph {
|
||||||
figure out how to do note numbers
|
- figure out how to do note numbers
|
||||||
^ vector sized by channel count rounded up to next 16
|
- ^ vector sized by channel count rounded up to next 16
|
||||||
^^ on switching to new graph... map of (hashes of) named channels??
|
- ^^ on switching to new graph... map of (hashes of) named channels??
|
||||||
assemble wire command queues (probaby some minor trickiness) and push into ports
|
- 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)
|
- 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
|
then implement multithreading! :D
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ TODO {
|
||||||
import/export subgraph as file (*.xyg)
|
import/export subgraph as file (*.xyg)
|
||||||
|
|
||||||
proper playback controls and indicators
|
proper playback controls and indicators
|
||||||
|
play from current pattern
|
||||||
instrument previewing
|
instrument previewing
|
||||||
|
|
||||||
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
|
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 {
|
on-the-wire command format {
|
||||||
ushort noteId // for sending commands to the same note
|
uint16_t noteId // for sending commands to the same note
|
||||||
short note // note number >= 0, -1 for none, -2 note off, -3 hard cut
|
int16_t note // note number >= 0, -1 for none, -2 note off, -3 hard cut
|
||||||
unsigned char numParams * {
|
uint8_t numParams x {
|
||||||
unsigned char cmd
|
uint8_t cmd
|
||||||
unsigned char amount
|
uint8_t amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# Xybrid
|
![Xybrid logo](asset-work/xybrid-logo-banner480.png)
|
||||||
|
Xybrid: deeply modular tracker
|
||||||
something something, actual readme coming later
|
|
||||||
|
|
||||||
## Build dependencies:
|
## Build dependencies:
|
||||||
- Qt 5.12 or later
|
- Qt 5.12 or later
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
#include "data/project.h"
|
#include "data/project.h"
|
||||||
using namespace Xybrid::Audio;
|
using namespace Xybrid::Audio;
|
||||||
using namespace Xybrid::Data;
|
using namespace Xybrid::Data;
|
||||||
|
#include "data/graph.h"
|
||||||
|
#include "data/porttypes.h"
|
||||||
|
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "uisocket.h"
|
#include "uisocket.h"
|
||||||
|
@ -36,14 +38,18 @@ void AudioEngine::postInit() {
|
||||||
open(QIODevice::ReadOnly);
|
open(QIODevice::ReadOnly);
|
||||||
|
|
||||||
// set up buffer for per-tick allocation
|
// 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();
|
tickBufPtr = tickBuf.get();
|
||||||
tickBufEnd = tickBufPtr+tickBufSize;
|
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) {
|
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));
|
auto n = tickBufPtr.fetch_add(static_cast<ptrdiff_t>(size));
|
||||||
if (n + size > tickBufEnd) qWarning() << "Tick buffer overrun!";
|
if (n + size > tickBufEnd) qWarning() << "Tick buffer overrun!";
|
||||||
return n;
|
return n;
|
||||||
|
@ -88,7 +94,12 @@ void AudioEngine::play(std::shared_ptr<Project> p) {
|
||||||
QMetaObject::invokeMethod(this, [this, p]() {
|
QMetaObject::invokeMethod(this, [this, p]() {
|
||||||
if (!p) return; // nope
|
if (!p) return; // nope
|
||||||
project = p;
|
project = p;
|
||||||
|
|
||||||
// stop and reset, then init playback
|
// stop and reset, then init playback
|
||||||
|
queueValid = false;
|
||||||
|
queue.clear();
|
||||||
|
portLastNoteId.fill(0);
|
||||||
|
project->rootGraph->reset();
|
||||||
|
|
||||||
initAudio();
|
initAudio();
|
||||||
for (auto& b : buffer) {
|
for (auto& b : buffer) {
|
||||||
|
@ -112,12 +123,57 @@ void AudioEngine::play(std::shared_ptr<Project> p) {
|
||||||
void AudioEngine::stop() {
|
void AudioEngine::stop() {
|
||||||
QMetaObject::invokeMethod(this, [this]() {
|
QMetaObject::invokeMethod(this, [this]() {
|
||||||
project = nullptr;
|
project = nullptr;
|
||||||
|
queueValid = false;
|
||||||
|
queue.clear();
|
||||||
deinitAudio();
|
deinitAudio();
|
||||||
mode = Stopped;
|
mode = Stopped;
|
||||||
emit this->playbackModeChanged();
|
emit this->playbackModeChanged();
|
||||||
}, Qt::QueuedConnection);
|
}, 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) {
|
qint64 AudioEngine::readData(char *data, qint64 maxlen) {
|
||||||
const constexpr qint64 smp = 2;
|
const constexpr qint64 smp = 2;
|
||||||
const constexpr qint64 stride = smp*2;
|
const constexpr qint64 stride = smp*2;
|
||||||
|
@ -166,14 +222,20 @@ void AudioEngine::nextTick() {
|
||||||
buffer[0].clear();
|
buffer[0].clear();
|
||||||
buffer[1].clear();
|
buffer[1].clear();
|
||||||
|
|
||||||
|
if (!queueValid) buildQueue();
|
||||||
|
|
||||||
Pattern* p = nullptr;
|
Pattern* p = nullptr;
|
||||||
|
Pattern* pOld = nullptr;
|
||||||
auto setP = [&] {
|
auto setP = [&] {
|
||||||
if (seqPos >= 0 && seqPos < static_cast<int>(project->sequence.size())) p = project->sequence[static_cast<size_t>(seqPos)];
|
if (seqPos >= 0 && seqPos < static_cast<int>(project->sequence.size())) p = project->sequence[static_cast<size_t>(seqPos)];
|
||||||
else p = nullptr;
|
else p = nullptr;
|
||||||
};
|
};
|
||||||
setP();
|
setP();
|
||||||
|
|
||||||
|
bool newRow = false;
|
||||||
|
bool newPattern = false;
|
||||||
auto advanceSeq = [&] {
|
auto advanceSeq = [&] {
|
||||||
|
pOld = p;
|
||||||
p = nullptr;
|
p = nullptr;
|
||||||
int tries = 0;
|
int tries = 0;
|
||||||
while (!p) {
|
while (!p) {
|
||||||
|
@ -185,6 +247,8 @@ void AudioEngine::nextTick() {
|
||||||
|
|
||||||
// set pattern things
|
// set pattern things
|
||||||
if (p->tempo > 0) tempo = p->tempo;
|
if (p->tempo > 0) tempo = p->tempo;
|
||||||
|
|
||||||
|
newPattern = true;
|
||||||
};
|
};
|
||||||
auto advanceRow = [&] {
|
auto advanceRow = [&] {
|
||||||
curTick = 0;
|
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++;
|
curTick++;
|
||||||
|
@ -219,8 +386,18 @@ void AudioEngine::nextTick() {
|
||||||
buffer[1].resize(ts);
|
buffer[1].resize(ts);
|
||||||
//qDebug() << "tick" << tickId << "contains"<<ts<<"samples";
|
//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
|
// 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);
|
const double SEMI = std::pow(2.0, 1.0/12.0);
|
||||||
double time = 0;
|
double time = 0;
|
||||||
int note = curRow % 4;
|
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[0][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, -6 + note * 5)) * .25);
|
||||||
buffer[1][i] = buffer[0][i];
|
buffer[1][i] = buffer[0][i];
|
||||||
time += 1.0/sampleRate;
|
time += 1.0/sampleRate;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
#include <unordered_map>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
|
@ -11,8 +13,20 @@
|
||||||
class QThread;
|
class QThread;
|
||||||
namespace Xybrid::Data {
|
namespace Xybrid::Data {
|
||||||
class Project;
|
class Project;
|
||||||
|
class Node;
|
||||||
}
|
}
|
||||||
namespace Xybrid::Audio {
|
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 {
|
class AudioEngine : public QIODevice {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
explicit AudioEngine(QObject *parent = nullptr);
|
explicit AudioEngine(QObject *parent = nullptr);
|
||||||
|
@ -33,14 +47,24 @@ namespace Xybrid::Audio {
|
||||||
size_t bufPos = 0;
|
size_t bufPos = 0;
|
||||||
|
|
||||||
static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough
|
static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough
|
||||||
std::unique_ptr<int[]> tickBuf;
|
std::unique_ptr<size_t[]> tickBuf;
|
||||||
std::atomic<int*> tickBufPtr;
|
std::atomic<size_t*> tickBufPtr;
|
||||||
int* tickBufEnd;
|
size_t* tickBufEnd;
|
||||||
|
|
||||||
PlaybackMode mode = Stopped;
|
PlaybackMode mode = Stopped;
|
||||||
size_t tickId = 0;
|
size_t tickId = 0;
|
||||||
std::shared_ptr<Data::Project> project;
|
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
|
// playback timing and position
|
||||||
float tempo = 140.0;
|
float tempo = 140.0;
|
||||||
int seqPos;
|
int seqPos;
|
||||||
|
@ -59,9 +83,12 @@ namespace Xybrid::Audio {
|
||||||
void play(std::shared_ptr<Data::Project>);
|
void play(std::shared_ptr<Data::Project>);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
|
inline void invalidateQueue(Data::Project* p) { if (p == project.get()) queueValid = false; }
|
||||||
|
|
||||||
void* tickAlloc(size_t size);
|
void* tickAlloc(size_t size);
|
||||||
inline size_t curTickId() const { return tickId; }
|
inline size_t curTickId() const { return tickId; }
|
||||||
inline size_t curTickSize() const { return buffer[0].size(); }
|
inline size_t curTickSize() const { return buffer[0].size(); }
|
||||||
|
inline int curSampleRate() const { return sampleRate; }
|
||||||
|
|
||||||
// QIODevice functions
|
// QIODevice functions
|
||||||
qint64 readData(char* data, qint64 maxlen) override;
|
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 (pi->id.empty()) return;
|
||||||
if (plugins.find(pi->id) != plugins.end()) return;
|
if (plugins.find(pi->id) != plugins.end()) return;
|
||||||
plugins[pi->id] = pi;
|
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) {
|
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;
|
if (f == plugins.end()) return nullptr;
|
||||||
auto n = f->second->createInstance();
|
auto n = f->second->createInstance();
|
||||||
n->plugin = f->second;
|
n->plugin = f->second;
|
||||||
|
n->init();
|
||||||
return n;
|
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] {
|
ccm->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
|
||||||
auto n = pi->createInstance();
|
auto n = pi->createInstance();
|
||||||
n->plugin = pi;
|
n->plugin = pi;
|
||||||
|
n->init();
|
||||||
f(n);
|
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] {
|
ccm->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
|
||||||
auto n = pi->createInstance();
|
auto n = pi->createInstance();
|
||||||
n->plugin = pi;
|
n->plugin = pi;
|
||||||
|
n->init();
|
||||||
f(n);
|
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] {
|
for (auto& i : cm[""]) m->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] {
|
||||||
auto n = pi->createInstance();
|
auto n = pi->createInstance();
|
||||||
n->plugin = pi;
|
n->plugin = pi;
|
||||||
|
n->init();
|
||||||
f(n);
|
f(n);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
class QMenu;
|
class QMenu;
|
||||||
|
@ -15,6 +16,7 @@ namespace Xybrid::Config {
|
||||||
class PluginInfo {
|
class PluginInfo {
|
||||||
public:
|
public:
|
||||||
std::string id;
|
std::string id;
|
||||||
|
std::vector<std::string> oldIds;
|
||||||
std::string displayName;
|
std::string displayName;
|
||||||
std::string category;
|
std::string category;
|
||||||
std::function<std::shared_ptr<Data::Node>()> createInstance;
|
std::function<std::shared_ptr<Data::Node>()> createInstance;
|
||||||
|
|
|
@ -33,6 +33,9 @@ Graph::Graph() {
|
||||||
plugin = inf; // harder bind
|
plugin = inf; // harder bind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// propagate
|
||||||
|
void Graph::reset() { for (auto c : children) c->reset(); }
|
||||||
|
|
||||||
void Graph::saveData(QCborMap& m) {
|
void Graph::saveData(QCborMap& m) {
|
||||||
// graph properties
|
// graph properties
|
||||||
// ... maybe there will be some at some point
|
// ... maybe there will be some at some point
|
||||||
|
|
|
@ -13,6 +13,7 @@ namespace Xybrid::Data {
|
||||||
// position of viewport within graph (not serialized)
|
// position of viewport within graph (not serialized)
|
||||||
int viewX{}, viewY{};
|
int viewX{}, viewY{};
|
||||||
|
|
||||||
|
void reset() override;
|
||||||
void saveData(QCborMap&) override;
|
void saveData(QCborMap&) override;
|
||||||
void loadData(QCborMap&) override;
|
void loadData(QCborMap&) override;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ using namespace Xybrid::Data;
|
||||||
|
|
||||||
#include "config/pluginregistry.h"
|
#include "config/pluginregistry.h"
|
||||||
|
|
||||||
|
#include "audio/audioengine.h"
|
||||||
|
using namespace Xybrid::Audio;
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
@ -33,6 +36,7 @@ bool Port::connect(std::shared_ptr<Port> p) {
|
||||||
// actually hook up
|
// actually hook up
|
||||||
connections.emplace_back(p);
|
connections.emplace_back(p);
|
||||||
p->connections.emplace_back(shared_from_this());
|
p->connections.emplace_back(shared_from_this());
|
||||||
|
if (auto o = owner.lock(); o) audioEngine->invalidateQueue(o->project);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +45,7 @@ void Port::disconnect(std::shared_ptr<Port> p) {
|
||||||
auto t = shared_from_this();
|
auto t = shared_from_this();
|
||||||
connections.erase(std::remove_if(connections.begin(), connections.end(), [p](auto w) { return w.lock() == p; }), connections.end());
|
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());
|
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() {
|
void Port::cleanConnections() {
|
||||||
|
@ -66,6 +71,7 @@ void Node::parentTo(std::shared_ptr<Graph> graph) {
|
||||||
graph->children.push_back(t);
|
graph->children.push_back(t);
|
||||||
onParent(graph);
|
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) {
|
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;
|
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; }
|
std::string Node::pluginName() const { if (!plugin) return "(unknown plugin)"; return plugin->displayName; }
|
||||||
|
|
|
@ -21,6 +21,10 @@ namespace Xybrid::Config {
|
||||||
class PluginInfo;
|
class PluginInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace Xybrid::Audio {
|
||||||
|
class AudioEngine;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Xybrid::Data {
|
namespace Xybrid::Data {
|
||||||
class Project;
|
class Project;
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ namespace Xybrid::Data {
|
||||||
std::weak_ptr<Node> owner;
|
std::weak_ptr<Node> owner;
|
||||||
std::vector<std::weak_ptr<Port>> connections;
|
std::vector<std::weak_ptr<Port>> connections;
|
||||||
std::weak_ptr<Port> passthroughTo;
|
std::weak_ptr<Port> passthroughTo;
|
||||||
Type type; // TODO: figure out passthrough?
|
Type type;
|
||||||
uint8_t index;
|
uint8_t index;
|
||||||
size_t tickUpdatedOn = static_cast<size_t>(-1);
|
size_t tickUpdatedOn = static_cast<size_t>(-1);
|
||||||
|
|
||||||
|
@ -64,6 +68,9 @@ namespace Xybrid::Data {
|
||||||
};
|
};
|
||||||
|
|
||||||
class Node : public std::enable_shared_from_this<Node> {
|
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:
|
public:
|
||||||
Project* project;
|
Project* project;
|
||||||
std::weak_ptr<Graph> parent;
|
std::weak_ptr<Graph> parent;
|
||||||
|
@ -87,6 +94,8 @@ namespace Xybrid::Data {
|
||||||
std::unordered_set<std::shared_ptr<Node>> dependencies() const;
|
std::unordered_set<std::shared_ptr<Node>> dependencies() const;
|
||||||
bool dependsOn(std::shared_ptr<Node>);
|
bool dependsOn(std::shared_ptr<Node>);
|
||||||
|
|
||||||
|
virtual void init() { }
|
||||||
|
virtual void reset() { }
|
||||||
virtual void saveData(QCborMap&) { }
|
virtual void saveData(QCborMap&) { }
|
||||||
virtual void loadData(QCborMap&) { }
|
virtual void loadData(QCborMap&) { }
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,17 @@ void AudioPort::pull() {
|
||||||
size_t s = sizeof(float) * ts;
|
size_t s = sizeof(float) * ts;
|
||||||
|
|
||||||
if (type == Input) {
|
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));
|
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
|
memset(bufL, 0, s*2); // clear buffers
|
||||||
|
|
||||||
for (auto c : connections) { // mix
|
for (auto c : connections) { // mix
|
||||||
|
@ -34,7 +43,37 @@ void AudioPort::pull() {
|
||||||
bufR = pt->bufR;
|
bufR = pt->bufR;
|
||||||
} else { // output without valid passthrough, just clear and prepare a blank buffer
|
} else { // output without valid passthrough, just clear and prepare a blank buffer
|
||||||
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
|
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
|
||||||
bufR = bufL + s;
|
bufR = &bufL[ts];
|
||||||
memset(bufL, 0, s*2); // clear buffers
|
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; }
|
Port::DataType dataType() const override { return Port::Command; }
|
||||||
bool singleInput() const override { return true; }
|
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
|
// and start with a new project
|
||||||
menuFileNew();
|
menuFileNew();
|
||||||
|
|
||||||
//auto q = QJsonObject();
|
|
||||||
//q.insert(QMetaObject::, "frenk");
|
|
||||||
qDebug() << QVariant::fromValue(Data::Port::Audio).toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow() {
|
MainWindow::~MainWindow() {
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void PortObject::connectTo(Xybrid::UI::PortObject* o) {
|
void PortObject::connectTo(PortObject* o) {
|
||||||
if (!o) return;
|
if (!o) return;
|
||||||
if (connections.find(o) != connections.end()) return;
|
if (connections.find(o) != connections.end()) return;
|
||||||
if (port->type == o->port->type) return;
|
if (port->type == o->port->type) return;
|
||||||
|
@ -331,6 +331,10 @@ PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) {
|
||||||
this->in = in;
|
this->in = in;
|
||||||
this->out = out;
|
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;
|
in->connections[out] = this;
|
||||||
out->connections[in] = this;
|
out->connections[in] = this;
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,8 @@ SOURCES += \
|
||||||
config/pluginregistry.cpp \
|
config/pluginregistry.cpp \
|
||||||
data/porttypes.cpp \
|
data/porttypes.cpp \
|
||||||
ui/breadcrumbview.cpp \
|
ui/breadcrumbview.cpp \
|
||||||
gadgets/ioport.cpp
|
gadgets/ioport.cpp \
|
||||||
|
gadgets/testsynth.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
mainwindow.h \
|
mainwindow.h \
|
||||||
|
@ -81,7 +82,8 @@ HEADERS += \
|
||||||
data/porttypes.h \
|
data/porttypes.h \
|
||||||
config/pluginregistry.h \
|
config/pluginregistry.h \
|
||||||
ui/breadcrumbview.h \
|
ui/breadcrumbview.h \
|
||||||
gadgets/ioport.h
|
gadgets/ioport.h \
|
||||||
|
gadgets/testsynth.h
|
||||||
|
|
||||||
FORMS += \
|
FORMS += \
|
||||||
mainwindow.ui
|
mainwindow.ui
|
||||||
|
|
Loading…
Reference in New Issue