it actually makes music now!

portability/boost
zetaPRIME 2018-12-28 12:19:32 -05:00
parent 333a06cac7
commit e5f1615724
18 changed files with 474 additions and 36 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

29
notes
View File

@ -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
}
}

View File

@ -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

View File

@ -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;
}
}*/
}

View File

@ -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;

View File

@ -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);
});

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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; }

View File

@ -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&) { }

View File

@ -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);
}

View File

@ -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>);
};
}

View File

@ -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()
}

View File

@ -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;
};
}

View File

@ -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() {

View File

@ -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;

View File

@ -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