preview fixes, InstrumentCore and 2x03 beginnings

portability/boost
zetaPRIME 2019-01-17 04:05:41 -05:00
parent 897397acca
commit 2b42b7066a
17 changed files with 371 additions and 20 deletions

2
notes
View File

@ -55,6 +55,8 @@ TODO {
bugs to fix {
graph connections sometimes spawn in duplicated :|
- shifted <>? keys still count as different keys for note previewing
-? buffer underruns are being caused by some sync wonkiness between multiple workers
}

View File

@ -148,8 +148,9 @@ void AudioEngine::stop() {
}, Qt::QueuedConnection);
}
void AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t note, bool state) {
QMetaObject::invokeMethod(this, [this, p, port, note, state] {
uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t note, uint16_t nId) {
if (note > -1) nId = previewNote_++;
QMetaObject::invokeMethod(this, [this, p, port, note, nId] {
if (!p) return;
if (mode == Playing || mode == Rendering || mode == PlaybackMode::Paused) return;
if (project != p || mode != Previewing) {
@ -172,17 +173,18 @@ void AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t note
mode = Previewing;
emit this->playbackModeChanged();
}
if (port >= 0 && port <= 255 && state) previewPort_ = static_cast<uint8_t>(port); // assign port if valid (and note on)
if (note < 0) return; // invalid note (port is set before it so that setting the port can be a separate action)
if (port >= 0 && port <= 255 && (note > -1 || note < -3)) previewPort_ = static_cast<uint8_t>(port); // assign port if valid (and note on)
if (note < -3) return; // invalid note (port is set before it so that setting the port can be a separate action)
// assemble message
size_t bi = buf.size();
buf.resize(bi+5);
reinterpret_cast<uint16_t&>(buf[bi]) = static_cast<uint16_t>(note);
reinterpret_cast<int16_t&>(buf[bi+2]) = state ? note : -2;
reinterpret_cast<uint16_t&>(buf[bi]) = nId;
reinterpret_cast<int16_t&>(buf[bi+2]) = note;
}, Qt::QueuedConnection);
return nId;
}
void AudioEngine::buildQueue() {

View File

@ -80,6 +80,7 @@ namespace Xybrid::Audio {
std::vector<uint8_t> buf; /// preallocated buffer for building commands
uint8_t previewPort_ = 0;
uint16_t previewNote_ = 0;
// playback timing and position
float tempo = 140.0;
@ -99,7 +100,7 @@ namespace Xybrid::Audio {
inline constexpr const std::shared_ptr<Data::Project>& playingProject() const { return project; }
void play(std::shared_ptr<Data::Project>);
void stop();
void preview(std::shared_ptr<Data::Project>, int16_t port, int16_t note, bool state);
uint16_t preview(std::shared_ptr<Data::Project>, int16_t port, int16_t note, uint16_t nId = 0);
inline uint8_t previewPort() const { return previewPort_; }
inline void invalidateQueue(Data::Project* p) { if (p == project.get()) queueValid = false; }

View File

@ -13,6 +13,7 @@ void AudioPort::pull() {
if (tickUpdatedOn == t) { lock.unlock(); return; } // someone else got here before us
size_t ts = audioEngine->curTickSize();
size = ts;
size_t s = sizeof(float) * ts;
if (type == Input) {

View File

@ -8,6 +8,7 @@ namespace Xybrid::Data {
public:
float* bufL;
float* bufR;
size_t size;
AudioPort() = default;
~AudioPort() override = default;

12
xybrid/nodelib/basics.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
namespace Xybrid::NodeLib {
// more precision than probably fits in a double, but it certainly shouldn't hurt
constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
constexpr double shortAttack = 0.001;
struct ADSR {
double a = 0, d = 0, s = 1, r = 0;
};
}

View File

@ -0,0 +1,133 @@
#include "instrumentcore.h"
using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
#include "data/porttypes.h"
using namespace Xybrid::Data;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include <QDebug>
Note::Note(uint16_t id) {
this->id = id;
}
void InstrumentCore::reset() {
activeNotes.clear();
activeNotes.reserve(16+1);
deletedNotes.clear();
deletedNotes.reserve(16+1);
smpTime = 1.0 / audioEngine->curSampleRate();
}
void InstrumentCore::process(Node* n) {
if (!n) return;
auto i = std::static_pointer_cast<CommandPort>(n->port(Port::Input, Port::Command, 0));
auto o = std::static_pointer_cast<AudioPort>(n->port(Port::Output, Port::Audio, 0));
if (!i) return;
process(i.get(), o.get());
}
namespace {
double adsrVol(const ADSR& adsr, uint8_t phase, double time) {
switch(phase) {
case 0: {
if (adsr.a == 0) return 1.0;
return std::clamp(time / adsr.a, 0.0, 1.0);
}
case 1: {
if (adsr.d == 0) return adsr.s;
double sp = (1.0 - std::clamp(time / adsr.s, 0.0, 1.0));
sp *= sp;
return adsr.s + sp * (1.0 - adsr.s);
}
case 2: {
if (adsr.r == 0) return 0.0;
return (1.0 - std::clamp(time / adsr.r, 0.0, 1.0)) * adsr.s;
}
default:
return 0.0;
}
}
ADSR defAdsr{0.01, 0.3, 0.75, 0.2};
struct NoteIntern {
};
}
void InstrumentCore::process(CommandPort* i, AudioPort* o) {
// first, parse through commands
i->pull();
size_t mi = 0;
while (i->dataSize >= mi+5) {
uint16_t id = reinterpret_cast<uint16_t&>(i->data[mi]);
int16_t n = reinterpret_cast<int16_t&>(i->data[mi+2]);
if (n > -1) {
//qDebug() << "note on" << id << n;
auto sc = activeNotes.emplace(id, id);
auto& note = sc.first->second;
if (note.adsrPhase == 2) note = Note(id); // reinstantiate on replace
note.note = n;
note.time = note.adsrTime = -smpTime; // compensate for first-advance
note.adsr = defAdsr;
// if (sc.second)
} else if (n < -1) { // note off
//qDebug() << "note off" << id;
//activeNotes.erase(id); // temp; later we'll let it do its own thing
if (auto ni = activeNotes.find(id); ni != activeNotes.end()) {
auto& note = ni->second;
note.adsr.s = adsrVol(note.adsr, note.adsrPhase, note.adsrTime);
note.adsrPhase = 2;
note.adsrTime = -smpTime;
if (n == -3) note.adsr.r = shortAttack;
}
}
mi += 5 + i->data[mi+4]*2;
}
for (auto id : deletedNotes) activeNotes.erase(id);
deletedNotes.clear();
// then do the thing
if (o) o->pull();
if (processNote) {
for (auto& p : activeNotes) {
NoteIntern n;
p.second.intern = &n;
processNote(p.second, o);
p.second.intern = nullptr;
}
}
}
void InstrumentCore::advanceNote(Note& n) {
//auto& ni = *reinterpret_cast<NoteIntern*>(n.intern);
n.time += smpTime;
n.adsrTime += smpTime;
if (n.adsrPhase == 0) {
if (n.adsrTime > n.adsr.a) {
n.adsrPhase++;
n.adsrTime -= n.adsr.a;
}
}
if (n.adsrPhase == 2) {
if (n.adsrTime >= n.adsr.r) deleteNote(n);
}
}
void InstrumentCore::deleteNote(Note& n) { deletedNotes.insert(n.id); }
double Note::ampMult() const {
double a = adsrVol(adsr, adsrPhase, adsrTime);
a *= a;
a *= a;
return a;
}

View File

@ -0,0 +1,62 @@
#pragma once
#include <memory>
#include <functional>
#include <unordered_map>
#include "nodelib/basics.h"
#include "data/node.h"
namespace Xybrid::Data {
class CommandPort;
class AudioPort;
}
namespace Xybrid::NodeLib {
/*!
* \class InstrumentCore
*
* Helper class to form the core of an instrument node.
*
* Not mandatory by any means, but handles all the "standard" commands for you.
*/
class InstrumentCore {
double smpTime;
public:
class Note {
friend class InstrumentCore;
void* intern = nullptr;
public:
uint16_t id;
double note; // floating point to allow smooth pitch bends
double time = 0;
ADSR adsr;
double adsrTime = 0;
uint8_t adsrPhase = 0;
Note() = default;
Note(uint16_t id);
double ampMult() const;
};
std::unordered_map<uint16_t, Note> activeNotes;
std::unordered_set<uint16_t> deletedNotes;
std::function<void(Note&, Data::AudioPort*)> processNote;
InstrumentCore() = default;
inline double sampleTime() const { return smpTime; }
void reset();
void process(Data::Node*);
void process(Data::CommandPort*, Data::AudioPort* = nullptr);
void advanceNote(Note&);
void deleteNote(Note&);
};
}

View File

@ -124,7 +124,7 @@ void IOPort::onParent(std::shared_ptr<Graph>) { add(); }
void IOPort::onDoubleClick() {
// if it's a command input on the root graph...
if (type == Port::Input && dataType == Port::Command && !parent.lock()->parent.lock()) {
audioEngine->preview(project->shared_from_this(), index, -1, false); // set preview port
audioEngine->preview(project->shared_from_this(), index, -127); // set preview port
}
}

View File

@ -0,0 +1,88 @@
#include "2x03.h"
using Xybrid::Instruments::I2x03;
using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
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:2x03";
i->displayName = "2x03";
i->category = "Instrument";
//i->hidden = true;
i->createInstance = []{ return std::make_shared<I2x03>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
// polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
double polyblep(double t, double dt) {
// 0 <= t < 1
if (t < dt) {
t /= dt;
return t+t - t*t - 1.0;
}
// -1 < t < 0
else if (t > 1.0 - dt) {
t = (t - 1.0) / dt;
return t*t + t+t + 1.0;
}
// 0 otherwise
return 0.0;
}
double osc(double time, double freq, double delta) {
//constexpr double duty = 0.125;
double duty = 0.5 + std::cos(time * 2.5) * (1 - 0.125*2) * 0.5;
time *= freq;
double d = 1.0;
if (std::fmod(time, 1.0) >= duty) d = -1.0;
delta *= freq;
d += polyblep(std::fmod(time, 1.0), delta);
d -= polyblep(std::fmod(time + (1.0 - duty), 1.0), delta);
return d;
}
}
I2x03::I2x03() {
}
void I2x03::init() {
addPort(Port::Input, Port::Command, 0);
addPort(Port::Output, Port::Audio, 0);
core.processNote = [this](Note& note, AudioPort* p) {
double enote = note.note;
double freq = 440.0 * std::pow(SEMI, enote - (45+12*2));
size_t ts = p->size;
for (size_t i = 0; i < ts; i++) {
core.advanceNote(note);
//double d = std::clamp((std::sin(note.time * freq * PI*2) + 0.75) * 512.0, -1.0, 1.0) * note.ampMult();
double d = osc(note.time, freq, core.sampleTime()) * note.ampMult();
p->bufL[i] += static_cast<float>(d);
p->bufR[i] += static_cast<float>(d);
//note.time += tt;
}
};
}
void I2x03::reset() {
core.reset();
}
void I2x03::process() {
core.process(this);
}

View File

@ -0,0 +1,31 @@
#pragma once
#include "nodelib/instrumentcore.h"
namespace Xybrid::Instruments {
class I2x03: public Data::Node {
//
NodeLib::InstrumentCore core;
public:
I2x03();
~I2x03() 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 onDoubleClick() override;
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
};
}

View File

@ -79,28 +79,28 @@ void PatchboardScene::keyPressEvent(QKeyEvent* e) {
auto note = Util::keyToNote(e->key());
if (note >= 0) {
if (e->modifiers() & Qt::Modifier::SHIFT) note += 24;
startPreview(e->key(), note);
startPreview(Util::unshiftedKey(e->key()), note);
}
}
}
void PatchboardScene::keyReleaseEvent(QKeyEvent* e) {
QGraphicsScene::keyReleaseEvent(e);
if (!e->isAccepted() && !e->isAutoRepeat()) stopPreview(e->key());
if (!e->isAccepted() && !e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
}
void PatchboardScene::startPreview(int key, int16_t note) {
stopPreview(key); // end current preview first, if applicable
auto p = graph->project->shared_from_this();
audioEngine->preview(p, -1, note, true);
previewKey[key] = {audioEngine->previewPort(), note};
previewKey[key] = {audioEngine->previewPort(), audioEngine->preview(p, -1, note)};
}
void PatchboardScene::stopPreview(int key) {
if (auto k = previewKey.find(key); k != previewKey.end()) {
auto p = graph->project->shared_from_this();
audioEngine->preview(p, k->second[0], k->second[1], false);
//audioEngine->preview(p, k->second[0], k->second[1], false);
audioEngine->preview(p, k->second.first, -2, k->second.second);
previewKey.erase(k);
}
}

View File

@ -13,7 +13,7 @@ namespace Xybrid::UI {
std::shared_ptr<Data::Graph> graph;
QGraphicsView* view;
std::unordered_map<int, std::array<int16_t, 2>> previewKey;
std::unordered_map<int, std::pair<int16_t, uint16_t>> previewKey;
bool resizeQueued = false;
void queueResize();

View File

@ -241,14 +241,14 @@ void PatternEditorView::keyPressEvent(QKeyEvent* e) {
QAbstractItemView::keyPressEvent(e);
if (!e->isAutoRepeat()) {
if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
startPreview(e->key());
startPreview(Util::unshiftedKey(e->key()));
}
}
}
void PatternEditorView::keyReleaseEvent(QKeyEvent* e) {
QAbstractItemView::keyReleaseEvent(e);
if (!e->isAutoRepeat()) stopPreview(e->key());
if (!e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
}
void PatternEditorView::startPreview(int key) {
@ -259,15 +259,14 @@ void PatternEditorView::startPreview(int key) {
stopPreview(key); // end current preview first, if applicable
auto& r = mdl->getPattern()->rowAt(ch, ind.row());
auto p = mdl->getPattern()->project->shared_from_this();
previewKey[key] = {r.port, r.note};
audioEngine->preview(p, r.port, r.note, true);
previewKey[key] = {r.port, audioEngine->preview(p, r.port, r.note)};
}
}
void PatternEditorView::stopPreview(int key) {
if (auto k = previewKey.find(key); k != previewKey.end()) {
auto p = mdl->getPattern()->project->shared_from_this();
audioEngine->preview(p, k->second[0], k->second[1], false);
audioEngine->preview(p, k->second.first, -2, k->second.second);
previewKey.erase(k);
}
}

View File

@ -23,7 +23,7 @@ namespace Xybrid::UI {
std::unique_ptr<QWidget> cornerBoxBox;
std::unique_ptr<QCheckBox> cornerBox;
std::unordered_map<int, std::array<int16_t, 2>> previewKey;
std::unordered_map<int, std::pair<int16_t, uint16_t>> previewKey;
bool colUpdateNeeded = false;

View File

@ -5,6 +5,19 @@
#include <QKeyEvent>
namespace {
std::unordered_map<int, int> shiftMap = [] {
std::unordered_map<int, int> m;
m[Qt::Key_BraceLeft] = Qt::Key_BracketLeft;
m[Qt::Key_BraceRight] = Qt::Key_BracketRight;
m[Qt::Key_Bar] = Qt::Key_Backslash;
m[Qt::Key_Colon] = Qt::Key_Semicolon;
m[Qt::Key_QuoteDbl] = Qt::Key_Apostrophe;
m[Qt::Key_Less] = Qt::Key_Comma;
m[Qt::Key_Greater] = Qt::Key_Period;
m[Qt::Key_Question] = Qt::Key_Slash;
return m;
}();
std::unordered_map<int, int16_t> keyMap = [] {
std::unordered_map<int, int16_t> m;
@ -34,3 +47,8 @@ int16_t Xybrid::Util::keyToNote(int key) {
if (auto f = keyMap.find(key); f != keyMap.end()) return f->second;
return -1; // default to none
}
int Xybrid::Util::unshiftedKey(int key) {
if (auto f = shiftMap.find(key); f != shiftMap.end()) return f->second;
return key;
}

View File

@ -4,4 +4,5 @@
namespace Xybrid::Util {
int16_t keyToNote(int key);
int unshiftedKey(int key);
}