xybrid/xybrid/audio/audioengine.cpp

429 lines
16 KiB
C++

#include "audioengine.h"
#include "data/project.h"
using namespace Xybrid::Audio;
using namespace Xybrid::Data;
#include "data/graph.h"
#include "data/porttypes.h"
#include "mainwindow.h"
#include "uisocket.h"
#include <algorithm>
#include <cmath>
#include <QDebug>
#include <QThread>
// zero-initialize
AudioEngine* Xybrid::Audio::audioEngine = nullptr;
void AudioEngine::init() {
if (audioEngine) return; // already set up
// instantiate singleton
QThread* thread = new QThread;
audioEngine = new AudioEngine(nullptr);
audioEngine->moveToThread(thread);
audioEngine->thread = thread;
// hook up signals
// ...
// and off to the races
thread->start();
//thread->setPriority(QThread::TimeCriticalPriority);
QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit, Qt::QueuedConnection);
}
void AudioEngine::postInit() {
open(QIODevice::ReadOnly);
// set up buffer for per-tick allocation
tickBuf = std::make_unique<size_t[]>(tickBufSize/sizeof(size_t)); // aligned to size_t
tickBufPtr = tickBuf.get();
tickBufEnd = tickBufPtr+tickBufSize;
buf.reserve(1024); // 1kb isn't much to make sure it's super unlikely to have to reallocate
chTrack.reserve(256);
noteEndQueue.reserve(256);
nameTrack.reserve(64+1); // +1 to make extra sure it doesn't rehash later
}
void* AudioEngine::tickAlloc(size_t size) {
if (auto r = size % sizeof(size_t); r != 0) size += sizeof(size_t) - r; // pad
auto n = tickBufPtr.fetch_add(static_cast<ptrdiff_t>(size));
if (n + size > tickBufEnd) qWarning() << "Tick buffer overrun!";
return n;
}
AudioEngine::AudioEngine(QObject *parent) : QIODevice(parent) { }
void AudioEngine::initAudio(bool startNow) {
if (!output) {
const QAudioDeviceInfo& deviceInfo = QAudioDeviceInfo::defaultOutputDevice();
QAudioFormat format;
format.setSampleRate(48000);
format.setChannelCount(2);
format.setSampleSize(16);
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
if (!deviceInfo.isFormatSupported(format)) {
qWarning() << "Default format not supported - trying to use nearest";
format = deviceInfo.nearestFormat(format);
}
sampleRate = format.sampleRate();
output.reset(new QAudioOutput(deviceInfo, format));
output->setObjectName("Xybrid"); // if Qt ever implements naming the stream this way, WE'LL BE READY
output->setBufferSize(static_cast<int>(sampleRate*4*100.0/1000.0)); // 100ms
}
if (startNow) output->start();
}
void AudioEngine::deinitAudio() {
if (output) {
output->stop();
output.reset();
}
}
void AudioEngine::play(std::shared_ptr<Project> p) {
QMetaObject::invokeMethod(this, [this, p]() {
if (!p) return; // nope
project = p;
// stop and reset, then init playback
queueValid = false;
queue.clear();
portLastNoteId.fill(0);
project->rootGraph->reset();
initAudio();
for (auto& b : buffer) {
b.clear();
b.reserve(static_cast<size_t>(sampleRate/4));
}
seqPos = -1;
tempo = project->tempo;
tickAcc = 0;
output->start(this);
//tickId = 0; // actually, no reason to reset this
mode = Playing;
emit this->playbackModeChanged();
}, Qt::QueuedConnection);
}
void AudioEngine::stop() {
QMetaObject::invokeMethod(this, [this]() {
project = nullptr;
queueValid = false;
queue.clear();
deinitAudio();
mode = Stopped;
emit this->playbackModeChanged();
}, Qt::QueuedConnection);
}
void AudioEngine::buildQueue() {
queue.clear();
// stuff
std::deque<std::shared_ptr<Node>> q1, q2;
auto* qCurrent = &q1;
auto* qNext = &q2;
if (auto p = project->rootGraph->port(Port::Output, Port::Audio, 0); p)
if (auto pt = p->passthroughTo.lock(); pt)
if (auto ptn = pt->owner.lock(); ptn)
qCurrent->push_back(ptn);
// T_ODO: make this not process things the weird way around
// oh, it's working properly... it just processing subgraph before its internally-connected *inputs*
while (!qCurrent->empty()) {
// ... this could be made more efficient with some redundancy checking, but whatever
for (auto n : *qCurrent) {
queue.push_front(n); // add to actual queue
for (auto p1 : n->inputs) { // data types...
for (auto p2 : p1.second) { // ports...
for (auto p3 : p2.second->connections) { // connected ports!
auto pc = p3.lock();
if (!pc) continue;
auto pcn = pc->owner.lock();
if (!pcn) continue;
qNext->push_back(pcn);
if (auto pp = pc->passthroughTo.lock(); pp) {
// if it has a passthrough, also place passthrough's owner after (before)
if (auto ppp = pp->owner.lock(); ppp) qNext->push_back(ppp);
}
}
}
}
}
qCurrent->clear();
std::swap(qCurrent, qNext);
}
queueValid = true;
}
qint64 AudioEngine::readData(char *data, qint64 maxlen) {
const constexpr qint64 smp = 2;
const constexpr qint64 stride = smp*2;
qint64 sr = maxlen;
while (sr >= stride) {
if (bufPos >= buffer[0].size()) {
nextTick(); // process next tick when end of buffer reached
if (sr < maxlen) break; // if not the start of the buffer, yield so previewing works
}
//if (bufPos >= buffer[0].size()) break; // if held up still, let the event loop run another cycle
// convert non-interleaved floating point into interleaved int16
int16_t* l = reinterpret_cast<int16_t*>(data);
int16_t* r = reinterpret_cast<int16_t*>(data+smp);
*l = static_cast<int16_t>(buffer[0][bufPos] * 32767);
*r = static_cast<int16_t>(buffer[1][bufPos] * 32767);
bufPos++;
data += stride;
sr -= stride;
}
return maxlen - sr;
}
void AudioEngine::nextTick() {
bufPos = 0;
if (mode == Paused) { // simplest case, just give a 100ms empty buffer
buffer[0].clear();
buffer[1].clear();
buffer[0].resize(static_cast<size_t>(sampleRate/10));
buffer[1].resize(static_cast<size_t>(sampleRate/10));
} else if (mode == Previewing) {
// NYI
// reset raw buffer
tickBufPtr = tickBuf.get();
tickId++;
} else if (mode == Playing) {
// reset raw buffer
tickBufPtr = tickBuf.get();
tickId++;
// empty out last tick
buffer[0].clear();
buffer[1].clear();
if (!queueValid) buildQueue();
Pattern* p = nullptr;
Pattern* pOld = nullptr;
auto setP = [&] {
if (seqPos >= 0 && seqPos < static_cast<int>(project->sequence.size())) p = project->sequence[static_cast<size_t>(seqPos)];
else p = nullptr;
};
setP();
bool newRow = false;
bool newPattern = false;
auto advanceSeq = [&] {
pOld = p;
p = nullptr;
int tries = 0;
while (!p) {
seqPos = (seqPos+1) % static_cast<int>(project->sequence.size());
setP();
if (++tries > 25) return; // either you have 25 separators in a row, or you have no patterns
}
curRow = 0;
// set pattern things
if (p->tempo > 0) tempo = p->tempo;
newPattern = true;
};
auto advanceRow = [&] {
curTick = 0;
curRow++;
if (!p || curRow >= p->rows) advanceSeq();
MainWindow* w = project->socket->window;
QMetaObject::invokeMethod(w, [this, w]{ w->playbackPosition(seqPos, curRow); }, Qt::QueuedConnection);
// process global commands first
for (int c = 0; c < static_cast<int>(p->numChannels()); c++) {
if (auto& row = p->rowAt(c, curRow); row.port == -2 && row.params) {
for (auto p : *row.params) {
if (p[0] == 't' && p[1] > 0) tempo = p[1];
}
}
}
newRow = true;
// assemble command buffers
noteEndQueue.clear();
if (newPattern) { // notes on named channels carry over to their matching channel on the new pattern (if present); everything else is note-offed
if (pOld) {
size_t cs = pOld->channels.size();
for (size_t c = 0; c < cs; c++) {
auto& ch = pOld->channels[c];
if (!chTrack[c].valid) continue; // skip notes that aren't actually playing
if (ch.name.empty()) noteEndQueue.push_back(chTrack[c]); // end notes in unnamed channels right away
else nameTrack[&ch.name] = chTrack[c]; // otherwise keep track for later
}
}
chTrack.clear(); // clear and prepare channel note tracking
chTrack.resize(p->channels.size());
if (nameTrack.size() > 0) { // if there were any
size_t cs = p->channels.size();
for (size_t c = 0; c < cs; c++) {
auto& ch = p->channels[c];
if (ch.name.empty()) continue;
if (auto nt = nameTrack.find(&ch.name); nt != nameTrack.end() && nt->second.valid) {
chTrack[c] = nt->second; // carry over
nt->second.valid = false; // and invalidate
}
}
// dump remainder into note end
for (auto nt : nameTrack) if (nt.second.valid) noteEndQueue.push_back(nt.second);
}
nameTrack.clear();
}
int chs = static_cast<int>(p->channels.size());
for (int c = 0; c < chs; c++) {
auto& ct = chTrack[static_cast<size_t>(c)];
if (!ct.valid) continue; // no saved note
auto& r = p->rowAt(c, curRow);
if (r.note != -1 && r.port >= 0 && r.port != ct.port) { // if explicitly specified for a different port...
noteEndQueue.push_back(ct); // old note overwritten
ct.valid = false;
}
}
auto& cpm = project->rootGraph->inputs[Port::Command];
for (auto p_ : cpm) {
auto* pt = static_cast<CommandPort*>(p_.second.get());
//if (pt->passthroughTo.lock()->connections.empty()) continue; // port isn't hooked up to anything
uint8_t idx = pt->index;
buf.clear();
for (auto& ne : noteEndQueue) {
if (ne.valid && ne.port == idx) {
size_t bi = buf.size();
buf.resize(bi+5, 0);
reinterpret_cast<uint16_t&>(buf[bi]) = ne.noteId; // trigger on note id...
reinterpret_cast<int16_t&>(buf[bi+2]) = -2; // note off
}
}
for (int c = 0; c < chs; c++) {
auto& r = p->rowAt(c, curRow);
auto& ct = chTrack[static_cast<size_t>(c)];
int16_t port = r.port;
if (port < 0 && ct.valid) port = ct.port; // assume last port used on channel if not specified
if (port != idx) continue;
NoteInfo rpl; // default initialization, invalid
if (r.note >= 0) {
if (ct.valid) rpl = ct; // replace
ct = NoteInfo(idx, portLastNoteId[idx]++);
} else if (r.note <= -2 && ct.valid) {
ct.valid = false; // invalidate it here but leave note id intact
// this condition will allow you to note-off the same note id multiple times but anything
// that takes offense to that is a bug anyway
}
size_t bi = buf.size();
buf.resize(bi+5, 0);
reinterpret_cast<uint16_t&>(buf[bi]) = ct.noteId; // either new note, or note-off on old one
reinterpret_cast<int16_t&>(buf[bi+2]) = r.note; // shove note into vector
auto& np = buf[bi+4]; // number of params
if (r.params) {
for (auto& p : *r.params) {
if (p[0] == ' ') continue; // ignore struts
buf.push_back(p[0]);
buf.push_back(p[1]);
np++;
}
}
if (rpl.valid) { // replacing old note on the same port and channel
bi = buf.size();
buf.resize(bi+5, 0);
reinterpret_cast<uint16_t&>(buf[bi]) = rpl.noteId; // trigger on note id...
reinterpret_cast<int16_t&>(buf[bi+2]) = -2; // note off
}
}
//qDebug() << "port" << idx << "data of size" << buf.size();
pt->push(buf);
}
};
curTick++;
if (!p || curTick >= p->time.ticksPerRow) advanceRow();
if (!p) return; // no patterns to be found, abort
// (sample rate / seconds per beat) / ticks per beat
double tickSize = (1.0 * sampleRate / (static_cast<double>(tempo)/60.0)) / (p->time.rowsPerBeat * p->time.ticksPerRow);
tickSize += tickAcc; // add sample remainder from last tick
double tickSf = std::floor(tickSize);
tickAcc = tickSize - tickSf;
size_t ts = static_cast<size_t>(tickSf);
buffer[0].resize(ts);
buffer[1].resize(ts);
//qDebug() << "tick" << tickId << "contains"<<ts<<"samples";
//qDebug() << "<tick start>";
for (auto n : queue) if (!n->try_process()) qWarning() << "Dependency check failed in single threaded mode!";
if (auto p = std::static_pointer_cast<AudioPort>(project->rootGraph->port(Port::Output, Port::Audio, 0)); p) {
p->pull();
size_t bufs = ts * sizeof(float);
memcpy(buffer[0].data(), p->bufL, bufs);
memcpy(buffer[1].data(), p->bufR, bufs);
//p->bufL
}
//buffer[0].data()
// test
/*const double PI = std::atan(1)*4;
const double SEMI = std::pow(2.0, 1.0/12.0);
double time = 0;
int note = curRow % 4;
for (size_t i = 0; i < ts; i++) {
buffer[0][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, -6 + note * 5)) * .25);
buffer[1][i] = buffer[0][i];
time += 1.0/sampleRate;
}*/
}
// ...
else { // old test code
static double time = 0;
const double PI = std::atan(1)*4;
const double SEMI = std::pow(2.0, 1.0/12.0);
for (size_t i = 0; i < buffer[0].size(); i++) {
buffer[0][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, note - (45+12))) * .25);
buffer[1][i] = buffer[0][i];
//buffer[1][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 3)) * .25);
time += 1.0/sampleRate;
}
}
}