xybrid/xybrid/nodelib/instrumentcore.cpp

272 lines
8.4 KiB
C++

#include "instrumentcore.h"
using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
using Tween = InstrumentCore::Tween;
#include "nodelib/commandreader.h"
#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);
activeTweens.clear();
activeTweens.reserve(16*2+1);
smpTime = 1.0 / audioEngine->curSampleRate();
time = 0;
}
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;
double a = 1.0 - std::clamp(time / adsr.a, 0.0, 1.0);
a *= a;
a *= a;
return 1.0 - a;
}
case 1: {
if (adsr.d == 0) return adsr.s;
double sp = (1.0 - std::clamp(time / adsr.d, 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;
}
}
struct NoteIntern {
bool markedForDeletion = false;
decltype(InstrumentCore::activeTweens.equal_range(0)) tweenSet;
};
}
void InstrumentCore::process(CommandPort* i, AudioPort* o) {
// first, parse through commands
auto cr = CommandReader(i);
while (++cr) {
uint16_t id = cr.noteId();
int16_t n = cr.note();
Note* notePtr = nullptr;
if (n > -1) {
auto sc = activeNotes.try_emplace(id, id);
auto& note = sc.first->second;
notePtr = &note;
//auto& note = sc.first->second;
if (!sc.second) {
removeTweens(note, &note.note); // stop any note-value tweens
if (note.adsrPhase == 2) { note = Note(id); goto forceRetrigger; } // reinstantiate on replace
if (cr.numParams() > 0 && cr.param(0) == 't') {
startTween(note, &note.note, n, 0, cr.val(0));
} else note.note = n;
if (onNoteLegato) onNoteLegato(note);
} else {
forceRetrigger:
note.note = n;
note.time = note.adsrTime = -smpTime; // compensate for first-advance
if (onNoteOn) onNoteOn(note);
}
} else { // existing note
if (auto ni = activeNotes.find(id); ni != activeNotes.end()) {
auto& note = ni->second;
notePtr = &note;
if (n < -1) { // note off
note.adsr.s = adsrVol(note.adsr, note.adsrPhase, note.adsrTime);
note.adsrPhase = 2;
note.adsrTime = -smpTime;
if (n == -3) note.adsr.r = shortStep;
if (onNoteOff) onNoteOff(note, n == -3);
}
}
}
if (notePtr) { // params
auto& note = *notePtr;
auto pr = ParamReader(cr);
while (++pr) {
auto p = pr.param();
auto v = pr.val();
if (p == 't' || p == ',') continue;
// TODO: custom param stuff...
switch(p) {
case 'v': {
double vol = (1.0*v) / 255.0;
auto t = pr.tween();
if (t <= 0) {
removeTweens(note, &note.volume);
note.volume = vol;
} else startTween(note, &note.volume, vol, shortStep, t);
break;
}
case 'p': {
double pan = std::clamp((1.0*static_cast<int8_t>(v)) / 127.0, -1.0, 1.0);
auto t = pr.tween();
if (t <= 0) {
removeTweens(note, &note.pan);
note.pan = pan;
} else startTween(note, &note.pan, pan, shortStep, t);
break;
}
case 'g': // g/G - glissando
[[fallthrough]];
case 'G': {
double nd = v;
if (p == 'g') nd *= -1.0;
auto t = pr.tween();
if (t <= 0) {
removeTweens(note, &note.note);
note.note += nd;
} else startTween(note, &note.note, note.note + nd, shortStep, t);
break;
}
default:
break;
}
}
}
}
// then do the thing
if (o) o->pull();
double tickTime = smpTime * audioEngine->curTickSize();
if (processNote) {
for (auto p = activeNotes.begin(); p != activeNotes.end(); ) {
auto& note = p->second;
NoteIntern n;
note.intern = &n;
n.tweenSet = activeTweens.equal_range(note.id);
for (auto it = n.tweenSet.first; it != n.tweenSet.second; ++it) it->second.startTick(note, tickTime);
processNote(note, o);
if (n.markedForDeletion) {
if (onDeleteNote) onDeleteNote(note);
activeTweens.erase(note.id);
p = activeNotes.erase(p);
continue;
}
// stuff
for (auto it = n.tweenSet.first; it != n.tweenSet.second; ) {
if (it->second.flags & 1) it = activeTweens.erase(it);
else ++it;
}
note.intern = nullptr;
++p;
}
}
time += tickTime;
}
void InstrumentCore::advanceNote(Note& n) {
auto& ni = *reinterpret_cast<NoteIntern*>(n.intern);
n.time += smpTime;
n.adsrTime += smpTime;
for (auto it = ni.tweenSet.first; it != ni.tweenSet.second; ++it) it->second.process(n);
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) {
if (n.intern != nullptr) {
auto& ni = *reinterpret_cast<NoteIntern*>(n.intern);
ni.markedForDeletion = true;
} else {
if (onDeleteNote) onDeleteNote(n);
activeTweens.erase(n.id);
activeNotes.erase(n.id);
}
}
void InstrumentCore::removeTweens(InstrumentCore::Note& n, double* op) {
auto r = activeTweens.equal_range(n.id);
for (auto it = r.first; it != r.second; ) {
if (it->second.op == op) it = activeTweens.erase(it);
else ++it;
}
}
void InstrumentCore::removeTweens(InstrumentCore::Note& n) { activeTweens.erase(n.id); }
Tween& InstrumentCore::startTween(InstrumentCore::Note& n, double* op, double val, double time, int16_t ticks) {
// remove anything already operating on the same note
removeTweens(n, op);
auto it = activeTweens.emplace(std::make_pair(n.id, Tween(n, op, val, time, ticks)));
return it->second;
}
double Note::ampMult() const {
double a = adsrVol(adsr, adsrPhase, adsrTime) * volume;
return a*a; // most synthesizers use a curve of 40log(vol) dB... which simplifies to vol^2
}
Tween::Tween(InstrumentCore::Note& n, double* op, double val, double time, int16_t ticks) {
this->noteId = n.id;
this->op = op;
valStart = *op;
valEnd = val;
if (ticks >= 0) ticksLeft = ticks;
else {
timeStart = n.time;
timeEnd = timeStart + time;
}
}
void Tween::startTick(Note& n, double tickTime) {
if (ticksLeft >= 0) {
timeStart = n.time;
timeEnd = timeStart + tickTime * ticksLeft;
valStart = *op;
ticksLeft--;
}
}
void Tween::process(Note& n) {
if (flags & 1) return; // already done
if (timeEnd == timeStart) {
*op = valEnd; // instant
flags |= 1;
return;
}
double p = std::clamp((n.time - timeStart) / (timeEnd - timeStart), 0.0, 1.0);
*op = valStart * (1.0-p) + valEnd * p;
if (n.time >= timeEnd) flags |= 1; // mark finished
}