UI fixes, more skeletoning, playback actually follows pattern timing
parent
96c3c3bd19
commit
6a091c5ea6
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() { }
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue