nodelib things, InstrumentCore param support!

portability/boost
zetaPRIME 2019-01-27 14:42:47 -05:00
parent 205be0edde
commit d8ec92a9c6
9 changed files with 228 additions and 61 deletions

View File

@ -2,3 +2,4 @@
style=attach
indent-namespaces
keep-one-line-blocks
indent-labels

48
notes
View File

@ -11,42 +11,32 @@ project data {
sample rate, channel count (1 or 2), length
probably QSharedPointer to raw float array (size=length*channels, non-interleaved)
}
}
parameters {
standard extensions {
tXX - set tween time (in ticks) for previous parameter
,XX - extra param bytes
}
pattern {
name, id
length in rows (duh)
time signature: beats/measure, rows/beat, ticks/row (can default to project global? except that doesn't make complete sense)
tempo change on enter (float, defaults to 0; only applied if >0)
when creating new pattern {
time signature set to project defaults (either static or fallback)
rows = 4 measures
}
InstrumentCore (stock instrument behavior) {
legato accepts tweens! must be first parameter entry
per-pattern channels; note continuity is defined by name
^ by default, send note-off on entering a pattern without a channel of that name
also send note-off on old note when triggering a new one regardless of what instrument it is,
*AFTER* sending the new note-on (to not break or make things harder for legato instruments)
command format
01 C-5 v7F ... ... ...
instrument (port) number first; note-sharp-octave notation same as most trackers, but arbitrary number of a single type of parameter
[plugins receive commands more or less exactly as written; meaning is by convention more than anything, but there is a "standard" way of handling notes, handled by a library on the lua side]
- leave pitch bends to automation? or build them as per-tick messages from host? also, stepped by tick or smoothed per sample?
x note-on events send the actual note as a float value
- nope, separate event for cents (bcd? that would futz with interpolation though... signed byte, -100..100)
unique port for globals (-2 internally, styled as (G), and placed by, get this, pressing g) {
what to do with the notes?
tXX - tempo (second tXX as high byte, .XX for fine tempo (0..100))
> anything else?
}
vXX - volume; 00 .. FF -> 0.0 .. 1.0 (accepts tweens)
pXX - panning; signed byte? 00 as center (accepts tweens)
gXX/GXX - glissando (pitch bend); g=down, G=up; relative semitones (accepts tweens)
}
global port (G) {
tXX - tempo (how to implement >255? fine tempo?)
}
}
TODO {
immediate frontburner {
- instrumentcore tweens (unordered_multimap...)
actual support for commands in InstrumentCore
- iterator/reader abstraction for commands
- actual support for commands in InstrumentCore
node function to release unneeded old data when stopping playback?
}
@ -58,7 +48,7 @@ TODO {
bugs to fix {
-? graph connections sometimes spawn in duplicated :|
on starting playback, sometimes a "thunk" sneaks into the waveform?
- on starting playback, sometimes a "thunk" sneaks into the waveform?
-? buffer underruns are being caused by some sync wonkiness between multiple workers
}

View File

@ -1,14 +1,21 @@
#pragma once
#include <utility>
#include <cmath>
class QCborMap;
class QCborValue;
namespace Xybrid::NodeLib {
// more precision than probably fits in a double, but it certainly shouldn't hurt
constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const constexpr double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
/// Multiplier to compensate for the balance equation
// (1.0 / cos(PI*0.25))
const constexpr double PAN_MULT = 1.414213562373095048801688724209698078569671875376948073176;
constexpr double shortStep = 0.0025;
/// Sane mimimum transition time to avoid clip artifacts
const constexpr double shortStep = 0.0025;
struct ADSR {
double a = 0.0, d = 0.0, s = 1.0, r = 0.0;
@ -21,4 +28,10 @@ namespace Xybrid::NodeLib {
operator QCborMap() const;
operator QCborValue() const;
};
inline std::pair<float, float> panSignal(double in, double pan) {
if (pan == 0.0) return { in, in };
double s = (pan+1.0) * PI * 0.25;
return { in * (std::cos(s) * PAN_MULT), in * (std::sin(s) * PAN_MULT) };
}
}

View File

@ -0,0 +1,54 @@
#include "commandreader.h"
using namespace Xybrid::NodeLib;
using namespace Xybrid::Data;
#include "data/porttypes.h"
CommandReader::CommandReader(CommandPort* p) {
p->pull();
data = p->data;
dataSize = p->dataSize;
}
CommandReader::operator bool() const { return dataSize >= cur+5; }
CommandReader& CommandReader::operator++() {
if (cur == static_cast<size_t>(-1)) cur = 0;
else if (dataSize >= cur+5) cur += 5 + data[cur+4]*2;
return *this;
}
ParamReader::ParamReader(const CommandReader& cr) : cr(cr) {
if (cr) {
pmax = cr.data[cr.cur+4];
}
}
ParamReader::operator bool() const { return pn >= 0 && pn < pmax; }
ParamReader &ParamReader::operator++() {
++pn;
return *this;
}
int16_t ParamReader::next(bool acceptsTweens, uint8_t num) const {
auto n = static_cast<uint8_t>(pn);
while (++n < pmax) {
auto p = cr.param(n);
if (p != ',' && (acceptsTweens && p != 't')) break;
if (p == ',' && --num == 0) return cr.val(n);
}
return -1;
}
int16_t ParamReader::tween() const {
auto n = static_cast<uint8_t>(pn);
while (++n < pmax) {
auto p = cr.param(n);
if (p == 't') return cr.val(n);
if (p == ',') continue;
break;
}
return -1;
}

View File

@ -0,0 +1,47 @@
#pragma once
#include <memory>
namespace Xybrid::Data {
class CommandPort;
}
namespace Xybrid::NodeLib {
class ParamReader;
class CommandReader {
friend class ParamReader;
uint8_t* data;
size_t dataSize;
size_t cur = static_cast<size_t>(-1);
public:
CommandReader(Data::CommandPort*);
CommandReader(std::shared_ptr<Data::CommandPort> p) : CommandReader(p.get()) { }
operator bool() const;
CommandReader& operator++();
// data access
inline uint16_t noteId() const { return reinterpret_cast<uint16_t&>(data[cur]); }
inline int16_t note() const { return reinterpret_cast<int16_t&>(data[cur+2]); }
inline uint8_t numParams() const { return data[cur+4]; }
inline uint8_t param(uint8_t num) const { return data[cur+5+num*2]; }
inline uint8_t val(uint8_t num) const { return data[cur+6+num*2]; }
};
class ParamReader {
CommandReader cr;
int16_t pn = -1;
int16_t pmax = 0;
public:
ParamReader(const CommandReader&);
operator bool() const;
ParamReader& operator++();
// data access
inline uint8_t param() const { return cr.param(static_cast<uint8_t>(pn)); }
inline uint8_t val() const { return cr.val(static_cast<uint8_t>(pn)); }
int16_t next(bool acceptsTweens, uint8_t = 1) const;
int16_t tween() const;
};
}

View File

@ -3,6 +3,8 @@ using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
using Tween = InstrumentCore::Tween;
#include "nodelib/commandreader.h"
#include "data/porttypes.h"
using namespace Xybrid::Data;
@ -19,7 +21,7 @@ void InstrumentCore::reset() {
activeNotes.clear();
activeNotes.reserve(16+1);
activeTweens.clear();
activeTweens.reserve(16*3+1);
activeTweens.reserve(16*2+1);
smpTime = 1.0 / audioEngine->curSampleRate();
time = 0;
@ -67,34 +69,87 @@ namespace {
void InstrumentCore::process(CommandPort* i, AudioPort* o) {
// first, parse through commands
i->pull();
size_t mi = 0;
while (i->dataSize >= mi+5) {
uint16_t id = reinterpret_cast<uint16_t&>(i->data[mi]);
int16_t n = reinterpret_cast<int16_t&>(i->data[mi+2]);
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;
note.note = n;
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); // reinstantiate on replace
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 if (n < -1) { // note off
} else { // existing note
if (auto ni = activeNotes.find(id); ni != activeNotes.end()) {
auto& note = ni->second;
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);
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;
}
}
}
mi += 5 + i->data[mi+4]*2;
}
// then do the thing
@ -170,17 +225,16 @@ void InstrumentCore::removeTweens(InstrumentCore::Note& n, double* op) {
}
void InstrumentCore::removeTweens(InstrumentCore::Note& n) { activeTweens.erase(n.id); }
void InstrumentCore::startTween(InstrumentCore::Note& n, double* op, double val, double time, int16_t ticks) {
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);
activeTweens.emplace(std::make_pair(n.id, Tween(n, op, val, time, ticks)));
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);
a *= a;
a *= a;
return a;
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) {
@ -205,7 +259,9 @@ void Tween::startTick(Note& n, double tickTime) {
}
void Tween::process(Note& n) {
if (flags & 1) return; // already done
if (timeEnd == timeStart) {
*op = valEnd; // instant
flags |= 1;
return;
}

View File

@ -34,6 +34,9 @@ namespace Xybrid::NodeLib {
double note; // floating point to allow smooth pitch bends
double time = 0;
double volume = 1.0;
double pan = 0.0;
ADSR adsr;
double adsrTime = 0;
@ -93,7 +96,7 @@ namespace Xybrid::NodeLib {
void removeTweens(Note&, double*);
/// Removes all tweens matching the specified note.
void removeTweens(Note&);
void startTween(Note&, double*, double val, double time, int16_t ticks = -1);
Tween& startTween(Note&, double*, double val, double time, int16_t ticks = -1);
};
}

View File

@ -2,6 +2,9 @@
using Xybrid::Gadgets::GainBalance;
using namespace Xybrid::Data;
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
#include "data/porttypes.h"
#include "config/pluginregistry.h"
@ -41,17 +44,14 @@ void GainBalance::init() {
}
void GainBalance::process() { // TODO: lerp from tick to tick?
const double PI = std::atan(1)*4;
const double M = 1.0 / std::cos(PI * 0.25);
double g = gain.load();
double b = balance.load();
// calculate multipliers
double gm = std::pow(10.0, g / 20.0); // dBFS
double s = (b+1.0) * PI * 0.25;
double lm = std::cos(s) * M;
double rm = std::sin(s) * M;
double lm = std::cos(s) * PAN_MULT;
double rm = std::sin(s) * PAN_MULT;
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));

View File

@ -152,8 +152,11 @@ void I2x03::init() {
}
smp *= note.ampMult();
note.scratch[0] += smpTime * freq;
p->bufL[i] += static_cast<float>(smp);
p->bufR[i] += static_cast<float>(smp);
auto pn = panSignal(smp, note.pan);
p->bufL[i] += pn.first;
p->bufR[i] += pn.second;
//p->bufL[i] += static_cast<float>(smp);
//p->bufR[i] += static_cast<float>(smp);
}
};
}