xybrid/xybrid/nodes/sampler/beatpad.cpp

241 lines
7.8 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/layoutgadget.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/ext.h"
#include "util/strings.h"
#include <cmath>
#include <iostream>
#include <QDebug>
#include <QCborMap>
#include <QCborValue>
#include <QCborArray>
#include <QMenu>
#include <QGraphicsProxyWidget>
// clazy:excludeall=non-pod-global-static
RegisterPlugin(BeatPad, {
i->id = "plug:beatpad";
i->displayName = "BeatPad";
i->category = "Sampler";
})
BeatPad::BeatPad() {
}
void BeatPad::init() {
addPort(Port::Input, Port::Command, 0);
addPort(Port::Output, Port::Audio, 0);
core.onNoteOn = [this](Note& note) {
auto& data = *hard_cast<NoteData*>(&note.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 = *hard_cast<NoteData*>(&note.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 = *hard_cast<NoteData*>(&note.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());
//std::cout << "rate: " << rate << std::endl;
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;
if (sp >= static_cast<double>(end)) return core.deleteNote(note);
auto out = NodeLib::resamp(smp.get(), sp, rate);
(*p)[i] += out.gainBalance(data.config->gain, 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);
e[qs("gain")] = c.second->gain;
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);
c->gain = cm.value("gain").toDouble(0.0);
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;
//UIState() { qDebug() << "constructing ui"; }
//~UIState() { qDebug() << "deconstructing ui"; }
};
auto state = scene->makeStateObject<UIState>();
// init layout
auto ol = (new LayoutGadget(scene, true))->setPanel(true);
// set up gadgets
auto noteSelector = new SelectorGadget(ol);
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.section('/', -1, -1);
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, ol);
sampleSelector->setSize(320, 96);
auto r1 = (new LayoutGadget(ol))->setMetrics(0, -1, 0.0);
auto gain = KnobGadget::autoGain(r1);
// 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);
gain->bind(state->cfg->gain);
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name.section('/', -1, -1) : "")}, 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);
};
//ol->updateGeometry();
// 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);
}