preview fixes, InstrumentCore and 2x03 beginnings
parent
897397acca
commit
2b42b7066a
2
notes
2
notes
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace Xybrid::Data {
|
|||
public:
|
||||
float* bufL;
|
||||
float* bufR;
|
||||
size_t size;
|
||||
|
||||
AudioPort() = default;
|
||||
~AudioPort() override = default;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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&);
|
||||
|
||||
};
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -4,4 +4,5 @@
|
|||
|
||||
namespace Xybrid::Util {
|
||||
int16_t keyToNote(int key);
|
||||
int unshiftedKey(int key);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue