UI fixes, more skeletoning, playback actually follows pattern timing

portability/boost
zetaPRIME 2018-12-18 19:33:41 -05:00
parent 96c3c3bd19
commit 6a091c5ea6
5 changed files with 195 additions and 35 deletions

View File

@ -26,22 +26,30 @@ void AudioEngine::init() {
// and off to the races
thread->start();
QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit);
//thread->setPriority(QThread::TimeCriticalPriority);
QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit, Qt::QueuedConnection);
}
void AudioEngine::postInit() {
open(QIODevice::ReadOnly);
// set up QAudioOutput and buffer here
// 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::play(std::shared_ptr<Project> p) {
QMetaObject::invokeMethod(this, [this, p]() {
if (!p) return; // nope
project = p;
// stop and reset, then init playback
void AudioEngine::initAudio(bool startNow) {
if (!output) {
const QAudioDeviceInfo& deviceInfo = QAudioDeviceInfo::defaultOutputDevice();
QAudioFormat format;
@ -60,11 +68,35 @@ void AudioEngine::play(std::shared_ptr<Project> p) {
output.reset(new QAudioOutput(deviceInfo, format));
output->setObjectName("Xybrid"); // if Qt ever implements naming the stream this way, WE'LL BE READY
output->setCategory("something?");
output->setBufferSize(sampleRate*4*(10/1000)); // 10ms
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.reserve(static_cast<size_t>(sampleRate/4));
seqPos = -1;
tempo = project->tempo;
tickAcc = 0;
output->start(this);
for (auto& b : buffer) b.reserve(static_cast<size_t>(sampleRate/4));
//tickId = 0; // actually, no reason to reset this
mode = Playing;
}, Qt::QueuedConnection);
@ -73,7 +105,7 @@ void AudioEngine::play(std::shared_ptr<Project> p) {
void AudioEngine::stop() {
QMetaObject::invokeMethod(this, [this]() {
project = nullptr;
// stop and reset
deinitAudio();
mode = Stopped;
}, Qt::QueuedConnection);
}
@ -84,17 +116,20 @@ qint64 AudioEngine::readData(char *data, qint64 maxlen) {
qint64 sr = maxlen;
while (sr >= stride) {
if (bufPos >= buffer[0].size()) nextTick(); // process next tick when end of buffer reached
if (bufPos >= buffer[0].size()) break; // if held up still, let the event loop run another cycle
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);
data += stride;
*l = static_cast<int16_t>(buffer[0][bufPos] * 32767);
*r = static_cast<int16_t>(buffer[1][bufPos] * 32767);
bufPos++;
data += stride;
sr -= stride;
}
@ -104,19 +139,94 @@ qint64 AudioEngine::readData(char *data, qint64 maxlen) {
void AudioEngine::nextTick() {
bufPos = 0;
buffer[0].clear();
buffer[1].clear();
buffer[0].resize(480);
buffer[1].resize(480);
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++;
static double time = 0;
const double PI = std::atan(1)*4;
const double SEMI = std::pow(2.0, 1.0/12.0);
// empty out last tick
buffer[0].clear();
buffer[1].clear();
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, 0)) * .25);
buffer[1][i] = static_cast<float>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 3)) * .25);
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();
time += 1.0/sampleRate;
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();
};
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 = static_cast<int>(random() % 5);
static int note = -1;
note = (note+1) % 6;
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;
}
}
}

View File

@ -3,6 +3,7 @@
#include <memory>
#include <list>
#include <vector>
#include <atomic>
#include <QIODevice>
#include <QAudioOutput>
@ -19,6 +20,7 @@ namespace Xybrid::Audio {
enum PlaybackMode {
Stopped, // stopped
Playing, // playing track
Paused, // paused during playback
Previewing, // instrument live preview
Rendering, // rendering to file
};
@ -30,10 +32,25 @@ namespace Xybrid::Audio {
std::vector<float> buffer[2];
size_t bufPos = 0;
static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough
std::unique_ptr<int[]> tickBuf;
std::atomic<int*> tickBufPtr;
int* tickBufEnd;
PlaybackMode mode = Stopped;
size_t tickId = 0;
std::shared_ptr<Data::Project> project;
// playback timing and position
float tempo = 140.0;
int seqPos;
int curRow;
int curTick;
double tickAcc; // accumulator for tick remainder
void postInit();
void initAudio(bool startNow = false);
void deinitAudio();
void nextTick();
public:
static void init();
@ -42,11 +59,16 @@ namespace Xybrid::Audio {
void play(std::shared_ptr<Data::Project>);
void stop();
void* tickAlloc(size_t size);
inline size_t curTickSize() { return buffer[0].size(); }
// QIODevice functions
qint64 readData(char* data, qint64 maxlen) override;
qint64 writeData(const char*, qint64) override { return 0; }
qint64 bytesAvailable() const override { return 0; } // not actually used by QAudioOutput
volatile int note = 12*5;
signals:
void playbackModeChanged(PlaybackMode);

View File

@ -18,7 +18,9 @@ namespace Xybrid::Data {
};
std::weak_ptr<Node> owner;
std::vector<std::weak_ptr<Port>> connections;
std::weak_ptr<Port> passthroughFor;
Type type; // TODO: figure out passthrough?
size_t tickUpdatedOn = static_cast<size_t>(-1);
virtual ~Port() = default;
@ -27,6 +29,8 @@ namespace Xybrid::Data {
virtual bool canConnectTo(DataType);
/*virtual*/ bool connect(std::shared_ptr<Port>);
/*virtual*/ void disconnect(std::shared_ptr<Port>);
virtual void pull(); // make sure data for this tick is available
};
class Node : public std::enable_shared_from_this<Node> {
@ -40,5 +44,7 @@ namespace Xybrid::Data {
virtual ~Node() = default;
void parentTo(std::shared_ptr<Graph>);
virtual void process() { }
};
}

View File

@ -32,6 +32,7 @@ using Xybrid::UI::PatternEditorModel;
using Xybrid::UI::PatternEditorItemDelegate;
using namespace Xybrid::Editing;
using namespace Xybrid::Audio;
namespace {
constexpr const auto projectFilter = u8"Xybrid project (*.xyp)\nAll files (*)";
@ -194,6 +195,12 @@ MainWindow::MainWindow(QWidget *parent) :
ui->patternSequencer->setCurrentIndex(i.siblingAtColumn((count + i.column() + 1) % count));
});
// TEMP - play/stop
connect(new QShortcut(QKeySequence("Ctrl+P"), ui->pattern), &QShortcut::activated, [this]() {
if (audioEngine->playbackMode() == AudioEngine::Playing) audioEngine->stop();
else audioEngine->play(project);
});
/* tmp test
connect(new QShortcut(QKeySequence("Ctrl+F1"), ui->patchboard), &QShortcut::activated, [this]() {
auto inp = QInputDialog::getText(this, "yes", "yes");
@ -227,13 +234,6 @@ MainWindow::MainWindow(QWidget *parent) :
// and start with a new project
menuFileNew();
// TEMP: fill out some initial data
/*project->sequence.push_back(nullptr);
project->patterns[0]->name = "waffle iron";
project->sequence.push_back(project->newPattern().get());*/
Audio::audioEngine->play(project);
}
MainWindow::~MainWindow() {

View File

@ -13,6 +13,9 @@ using Xybrid::UI::PatternEditorModel;
#include "editing/patterncommands.h"
using namespace Xybrid::Editing;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include <limits>
#include <QDebug>
@ -32,6 +35,21 @@ namespace {
Qt::Key_A, Qt::Key_B, Qt::Key_C, Qt::Key_D, Qt::Key_E, Qt::Key_F,
};
const std::unordered_map<int, int> keyConv = []() {
std::unordered_map<int, int> m;
m[Qt::Key_BraceLeft] = Qt::Key_BracketLeft;
m[Qt::Key_BraceRight] = Qt::Key_BracketRight;
m[Qt::Key_Bar] = Qt::Key_Backslash;
m[Qt::Key_Colon] = Qt::Key_Semicolon;
m[Qt::Key_QuoteDbl] = Qt::Key_Apostrophe;
m[Qt::Key_Less] = Qt::Key_Comma;
m[Qt::Key_Greater] = Qt::Key_Period;
m[Qt::Key_Question] = Qt::Key_Slash;
return m;
}();
template <typename T>
[[maybe_unused]] void insertDigit(T& val, size_t hex) { // insert hex digit into a particular value
if (static_cast<int>(val) == -1) val = 0;
@ -126,10 +144,12 @@ bool PatternEditorItemDelegate::eventFilter(QObject *obj, QEvent *event) {
bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) {
if (index.data().isNull()) return false; // no channels?
auto type = event->type();
if (type == QEvent::KeyRelease) qDebug() << "key release";
if (type == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
auto k = static_cast<QKeyEvent*>(event)->key(); // grab key
if (auto i = keyConv.find(k); i != keyConv.end()) k = i->second;
auto mod = static_cast<QKeyEvent*>(event)->modifiers();
auto m = static_cast<PatternEditorModel*>(model); // we know this will always be pattern editor
auto p = m->getPattern();
@ -223,6 +243,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
if (k == pianoKeys[i]) { // piano input
row.note = static_cast<int16_t>(i + (12*4)); // C-4
if (mod & Qt::Modifier::SHIFT) row.note += 24; // shift for +2 octave
audioEngine->note = row.note; // TEMP - testing audio engine
if (row.port == -1) { // if no port specified, default to last port used (for a note event) in channel, then (TODO) last port value applied
for (int i = index.row() - 1; i >= 0; i--) {
auto& r = p->rowAt(ch, i);
@ -235,7 +256,8 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
return dc->commit();
} else if (i < 10 && k == numberKeys[i]) { // set octave
if (row.note >= 0) row.note = static_cast<int16_t>((row.note % 12) + 12*i);
static_cast<PatternEditorModel*>(model)->updateColumnDisplay();
audioEngine->note = row.note; // TEMP - testing audio engine
//static_cast<PatternEditorModel*>(model)->updateColumnDisplay();
return dc->commit();
}
}