#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 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(n->port(Port::Input, Port::Command, 0)); auto o = std::static_pointer_cast(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(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(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(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 }