246 lines
7.9 KiB
C++
246 lines
7.9 KiB
C++
#include "beatpad.h"
|
|
using Xybrid::Instruments::BeatPad;
|
|
using namespace Xybrid::NodeLib;
|
|
using Note = InstrumentCore::Note;
|
|
using namespace Xybrid::Data;
|
|
|
|
#include "nodelib/commandreader.h"
|
|
#include "nodelib/resampler.h"
|
|
|
|
#include "data/project.h"
|
|
#include "data/sample.h"
|
|
|
|
#include "data/porttypes.h"
|
|
#include "config/pluginregistry.h"
|
|
using namespace Xybrid::Config;
|
|
#include "audio/audioengine.h"
|
|
using namespace Xybrid::Audio;
|
|
|
|
#include "ui/waveformpreviewwidget.h"
|
|
|
|
#include "ui/patchboard/nodeobject.h"
|
|
#include "ui/gadgets/knobgadget.h"
|
|
#include "ui/gadgets/selectorgadget.h"
|
|
#include "ui/gadgets/sampleselectorgadget.h"
|
|
using namespace Xybrid::UI;
|
|
|
|
#include "ui/patchboard/nodeuiscene.h"
|
|
#include "uisocket.h"
|
|
|
|
#include "util/strings.h"
|
|
|
|
#include <cmath>
|
|
|
|
#include <QDebug>
|
|
#include <QCborMap>
|
|
#include <QCborValue>
|
|
#include <QCborArray>
|
|
|
|
#include <QMenu>
|
|
#include <QGraphicsProxyWidget>
|
|
|
|
#define qs QStringLiteral
|
|
|
|
namespace {
|
|
bool _ = PluginRegistry::enqueueRegistration([] {
|
|
auto i = std::make_shared<PluginInfo>();
|
|
i->id = "plug:beatpad";
|
|
i->displayName = "BeatPad";
|
|
i->category = "Sampler";
|
|
//i->hidden = true;
|
|
i->createInstance = []{ return std::make_shared<BeatPad>(); };
|
|
PluginRegistry::registerPlugin(i);
|
|
//inf = i;
|
|
});
|
|
|
|
}
|
|
|
|
BeatPad::BeatPad() {
|
|
|
|
}
|
|
|
|
void BeatPad::init() {
|
|
addPort(Port::Input, Port::Command, 0);
|
|
addPort(Port::Output, Port::Audio, 0);
|
|
|
|
core.onNoteOn = [this](Note& note) {
|
|
auto& data = *reinterpret_cast<NoteData*>(¬e.scratch);
|
|
new (&data) NoteData(); // construct in-place
|
|
|
|
// look up config for note
|
|
if (auto f = cfg.find(static_cast<int16_t>(note.note)); f != cfg.end()) {
|
|
data.config = f->second;
|
|
|
|
note.adsr = {0.0, 0.0, 1.0, shortStep}; // minimal tweening
|
|
//
|
|
} else core.deleteNote(note); // cancel note if no config found
|
|
};
|
|
|
|
core.onDeleteNote = [](Note& note) {
|
|
auto& data = *reinterpret_cast<NoteData*>(¬e.scratch);
|
|
data.~NoteData(); // destroy
|
|
};
|
|
|
|
/*core.globalParam['Q'] = [](const ParamReader& pr) {
|
|
qDebug() << "global recieved" << pr.param() << pr.val();
|
|
return true;
|
|
};*/
|
|
|
|
core.processNote = [this](Note& note, AudioPort* p) {
|
|
auto& data = *reinterpret_cast<NoteData*>(¬e.scratch);
|
|
if (!data.config) return core.deleteNote(note);
|
|
auto smp = data.config->smp.lock();
|
|
if (!smp) return core.deleteNote(note);
|
|
|
|
double rate = static_cast<double>(smp->sampleRate) / static_cast<double>(audioEngine->curSampleRate());
|
|
auto start = data.config->start;
|
|
if (start < 0) start = 0;
|
|
auto end = data.config->end;
|
|
if (end < 0) end = smp->length();
|
|
else end = std::min(end, static_cast<decltype(end)>(smp->length()));
|
|
|
|
size_t ts = p->size;
|
|
for (size_t i = 0; i < ts; i++) {
|
|
core.advanceNote(note);
|
|
|
|
// actual sample pos
|
|
double sp = static_cast<double>(start) + data.sampleTime * rate;
|
|
|
|
double ip = std::floor(sp);
|
|
double fp = sp - ip;
|
|
size_t lutIndex = static_cast<size_t>(fp*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS;
|
|
auto& pt = NodeLib::resamplerLUT[lutIndex];
|
|
AudioFrame out(0.0);
|
|
auto ii = static_cast<ptrdiff_t>(ip) - 3;
|
|
for (size_t i = 0; i < 8; i++) {
|
|
auto si = ii+static_cast<ptrdiff_t>(i);
|
|
if (si >= start && si < static_cast<ptrdiff_t>(end)) out += (*smp)[static_cast<size_t>(si)] * pt[i];
|
|
}
|
|
|
|
// stuff
|
|
|
|
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
|
|
data.sampleTime += 1;
|
|
}
|
|
};
|
|
}
|
|
|
|
void BeatPad::reset() { core.reset(); }
|
|
void BeatPad::release() { core.release(); }
|
|
void BeatPad::process() { core.process(this); }
|
|
|
|
void BeatPad::saveData(QCborMap& m) const {
|
|
QCborMap cm;
|
|
for (auto c : cfg) {
|
|
if (auto smp = c.second->smp.lock(); smp) {
|
|
QCborMap e;
|
|
e[qs("sample")] = QCborValue(smp->uuid);
|
|
e[qs("start")] = static_cast<qint64>(c.second->start);
|
|
e[qs("end")] = static_cast<qint64>(c.second->end);
|
|
cm[c.first] = e;
|
|
smp->markForExport();
|
|
}
|
|
}
|
|
m[qs("notecfg")] = cm;
|
|
}
|
|
|
|
void BeatPad::loadData(const QCborMap& m) {
|
|
for (auto ce : m.value("notecfg").toMap()) {
|
|
auto cm = ce.second.toMap();
|
|
auto id = cm.value("sample").toUuid();
|
|
if (auto f = project->samples.find(id); f != project->samples.end()) {
|
|
auto c = std::make_shared<NoteConfig>();
|
|
c->smp = f.value();
|
|
c->start = cm.value("start").toInteger(-1);
|
|
c->end = cm.value("end").toInteger(-1);
|
|
cfg[static_cast<int16_t>(ce.first.toInteger())] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
void BeatPad::onGadgetCreated() {
|
|
if (!obj) return;
|
|
|
|
//
|
|
}
|
|
|
|
void BeatPad::onDoubleClick() { emit project->socket->openNodeUI(this); }
|
|
void BeatPad::initUI(NodeUIScene* scene) {
|
|
// define and create state management struct
|
|
// (lambda captures will end up managing the lifetime of this thing)
|
|
struct UIState {
|
|
std::shared_ptr<NoteConfig> cfg;
|
|
int16_t note = -1;
|
|
|
|
std::function<void(int16_t)> selectNote;
|
|
std::function<void(std::shared_ptr<Sample>)> setSample;
|
|
};
|
|
auto state = std::make_shared<UIState>();
|
|
|
|
// set up gadgets
|
|
auto noteSelector = new SelectorGadget();
|
|
scene->addItem(noteSelector);
|
|
noteSelector->setPos(0, 0);
|
|
noteSelector->setWidth(320);
|
|
|
|
noteSelector->fGetList = [=] {
|
|
std::vector<SelectorGadget::Entry> v;
|
|
v.reserve(cfg.size());
|
|
for (int16_t i = 0; i < 12*8; i++) {
|
|
if (auto f = cfg.find(i); f != cfg.end()) {
|
|
auto c = f->second;
|
|
QString n;
|
|
if (auto smp = c->smp.lock(); smp) n = smp->name;
|
|
v.push_back({ f->first, qs("%1 %2").arg(Util::noteName(i)).arg(n) });
|
|
}
|
|
}
|
|
return v;
|
|
};
|
|
noteSelector->fEditMenu = [=](QMenu* m) {
|
|
auto first = m->actions().at(0);
|
|
auto mAdd = new QMenu("Add...", m);
|
|
m->insertMenu(first, mAdd);
|
|
m->insertSeparator(first);
|
|
for (int16_t oct = 0; oct < 8; oct++) {
|
|
auto mo = mAdd->addMenu(qs("Octave %1").arg(oct));
|
|
for (int16_t i = 0; i < 12; i++) {
|
|
int16_t n = oct*12+i;
|
|
if (auto f = cfg.find(n); f != cfg.end()) mo->addAction(Util::noteName(n))->setDisabled(true);
|
|
else mo->addAction(Util::noteName(n), [=] { state->selectNote(n); });
|
|
}
|
|
}
|
|
};
|
|
|
|
auto sampleSelector = new SampleSelectorGadget(project);
|
|
scene->addItem(sampleSelector);
|
|
sampleSelector->setPos(0, 28);
|
|
sampleSelector->setSize(320, 96);
|
|
|
|
// create functions now that all UI elements exist to be referenced
|
|
state->selectNote = [=](int16_t n) {
|
|
if (auto f = cfg.find(n); f != cfg.end()) state->cfg = f->second;
|
|
else state->cfg = std::make_shared<NoteConfig>();
|
|
state->note = n;
|
|
auto smp = state->cfg->smp.lock();
|
|
sampleSelector->setSample(smp);
|
|
|
|
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name : "")}, false);
|
|
};
|
|
state->setSample = [=](std::shared_ptr<Sample> smp) {
|
|
state->cfg->smp = smp;
|
|
auto n = state->note;
|
|
if (smp) cfg[n] = state->cfg;
|
|
else if (auto f = cfg.find(n); f != cfg.end()) cfg.erase(f);
|
|
|
|
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name : "")}, false);
|
|
};
|
|
|
|
// hook up relevantsignals
|
|
QObject::connect(scene, &NodeUIScene::notePreview, [=](int16_t note) { state->selectNote(note); });
|
|
QObject::connect(noteSelector, &SelectorGadget::onSelect, [=](auto e) { state->selectNote(e.first); });
|
|
QObject::connect(sampleSelector, &SampleSelectorGadget::sampleSelected, [=](auto smp) { state->setSample(smp); });
|
|
|
|
// and lastly, select C-5 by default
|
|
state->selectNote(60);
|
|
}
|