272 lines
8.4 KiB
C++
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 = ¬e;
|
|
//auto& note = sc.first->second;
|
|
if (!sc.second) {
|
|
removeTweens(note, ¬e.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, ¬e.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 = ¬e;
|
|
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, ¬e.volume);
|
|
note.volume = vol;
|
|
} else startTween(note, ¬e.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, ¬e.pan);
|
|
note.pan = pan;
|
|
} else startTween(note, ¬e.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, ¬e.note);
|
|
note.note += nd;
|
|
} else startTween(note, ¬e.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
|
|
}
|