252 lines
7.8 KiB
C++
252 lines
7.8 KiB
C++
#include "audioengine.h"
|
|
#include "data/project.h"
|
|
using namespace Xybrid::Audio;
|
|
using namespace Xybrid::Data;
|
|
|
|
#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<int[]>(tickBufSize/sizeof(int)); // aligned to int, which we assume is the native word size
|
|
tickBufPtr = tickBuf.get();
|
|
tickBufEnd = tickBufPtr+tickBufSize;
|
|
|
|
}
|
|
|
|
void* AudioEngine::tickAlloc(size_t size) {
|
|
if (auto r = size % sizeof(int); r != 0) size += sizeof(int) - r; // pad to word
|
|
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
|
|
|
|
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;
|
|
deinitAudio();
|
|
mode = Stopped;
|
|
emit this->playbackModeChanged();
|
|
}, Qt::QueuedConnection);
|
|
}
|
|
|
|
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();
|
|
|
|
Pattern* p = 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();
|
|
|
|
auto advanceSeq = [&] {
|
|
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;
|
|
};
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO then assemble command buffers
|
|
};
|
|
|
|
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";
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|