bunch of audio engine and graph skeleton work

portability/boost
zetaPRIME 2018-12-17 14:09:44 -05:00
parent 12a563afc4
commit c1e73b922a
21 changed files with 381 additions and 60 deletions

View File

@ -1,3 +1,4 @@
# attached braces
style=attach
indent-namespaces
keep-one-line-blocks

10
notes
View File

@ -50,9 +50,9 @@ TODO {
- strut command in pattern editor (mostly selection agnostic)
}
group 2 {
skeleton graph and node - data namespace I guess
skeleton plugin registry - stuff into config namespace?
skeleton audio engine
skeleton plugin registry
skeleton graph and node
}
# fix how qt5.12 broke header text (removed elide for now)
@ -71,9 +71,15 @@ TODO {
make everything relevant check if editing is locked
make the save routine displace the old file and write a new one
multi-document, single-instance (QLocalServer etc.)
}
}
dumb per-cycle atomic memory allocator from fixed pool for port buffer allocations
can also set up a tlsf pool per worker; prefix allocations with single byte identifier indicating which one they came from,
and defer freeing operations via message queues
resampler object {
one used internally for each note
reference to sample

View File

@ -19,6 +19,7 @@ project: [
"meta": { "artist": ... "title": ... "comment": ... etc. }
"patterns": [ array of pattern structs ]
"sequence": [ array of pattern numbers, int, separator is anything negative ]
"graph": { root graph (contents) }
}
]

View File

@ -0,0 +1,115 @@
#include "audioengine.h"
#include "data/project.h"
using namespace Xybrid::Audio;
using namespace Xybrid::Data;
#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();
QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit);
}
void AudioEngine::postInit() {
open(QIODevice::ReadOnly);
// set up QAudioOutput and buffer here
}
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
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->setCategory("something?");
output->setBufferSize(sampleRate*4*(10/1000)); // 10ms
output->start(this);
mode = Playing;
}, Qt::QueuedConnection);
}
void AudioEngine::stop() {
QMetaObject::invokeMethod(this, [this]() {
project = nullptr;
// stop and reset
mode = Stopped;
}, Qt::QueuedConnection);
}
qint64 AudioEngine::readData(char *data, qint64 maxlen) {
static double time = 0;
qint64 sr = maxlen;
const double PI = std::atan(1)*4;
const double SEMI = std::pow(2.0, 1.0/12.0);
double vol = std::pow(.5, 4);
while (sr >= 4) {
sr -= 4;
int16_t* l = reinterpret_cast<int16_t*>(data);
int16_t* r = reinterpret_cast<int16_t*>(data+2);
vol = std::pow(.5 + std::sin(time * PI*2) * .15, 4);
/**l = static_cast<int16_t>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 4)) * 32767 * vol);
*r = static_cast<int16_t>(std::sin(time * PI*2 * 440) * 32767 * vol);
*l += static_cast<int16_t>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 11.9)) * 32767 * vol);
*r += static_cast<int16_t>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 7)) * 32767 * vol);*/
*l = 0;
//*l += static_cast<int16_t>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 1)) * 32767 * vol);
//*l += static_cast<int16_t>(std::sin(time * PI*2 * 440 * std::pow(SEMI, 1.1)) * 32767 * vol);
*l += static_cast<int16_t>(std::clamp(std::sin(time * PI*2 * 440 * std::pow(SEMI, time - 48)) * 3, -1.0, 1.0) * 32767 * vol);
*r = *l;
time += 1.0/sampleRate;
data += 4;
}
while (sr > 0) {
sr--;
*data = 0;
++data;
}
//qDebug() << "audio engine requested:" << maxlen;
return maxlen;
}

View File

@ -0,0 +1,52 @@
#pragma once
#include <memory>
#include <list>
#include <vector>
#include <QIODevice>
#include <QAudioOutput>
class QThread;
namespace Xybrid::Data {
class Project;
}
namespace Xybrid::Audio {
class AudioEngine : public QIODevice {
Q_OBJECT
explicit AudioEngine(QObject *parent = nullptr);
public:
enum PlaybackMode {
Stopped, // stopped
Playing, // playing track
Previewing, // instrument live preview
Rendering, // rendering to file
};
private:
QThread* thread;
std::unique_ptr<QAudioOutput> output;
int sampleRate = 48000;
PlaybackMode mode = Stopped;
std::shared_ptr<Data::Project> project;
void postInit();
public:
static void init();
inline constexpr PlaybackMode playbackMode() const { return mode; }
inline constexpr const std::shared_ptr<Data::Project>& playingProject() const { return project; }
void play(std::shared_ptr<Data::Project>);
void stop();
// QIODevice functions
qint64 readData(char* data, qint64 maxlen) override;// {return 0;}
qint64 writeData(const char*, qint64) override { return 0; }
qint64 bytesAvailable() const override { return 1166; }
signals:
void playbackModeChanged(PlaybackMode);
public slots:
};
extern AudioEngine* audioEngine;
}

10
xybrid/data/graph.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include "data/node.h"
namespace Xybrid::Data {
class Graph : public Node {
public:
std::vector<std::shared_ptr<Node>> children;
};
}

42
xybrid/data/node.cpp Normal file
View File

@ -0,0 +1,42 @@
#include "node.h"
using Xybrid::Data::Node;
using Xybrid::Data::Port;
#include "data/graph.h"
#include <algorithm>
bool Port::canConnectTo(DataType d) {
return d == dataType();
}
bool Port::connect(std::shared_ptr<Port> p) {
if (!p) return false; // no blank pointers pls
// actual processing is always done on the input port, since that's where any limits are
if (type == Port::Output) return p->type == Port::Input && p->connect(shared_from_this());
if (!canConnectTo(p->dataType())) return false; // can't hook up to an incompatible data type
for (auto c : connections) if (c.lock() == p) return true; // I guess report success if already connected?
if (singleInput() && connections.size() > 0) return false; // reject multiple connections on single-input ports
// actually hook up
connections.emplace_back(p);
p->connections.emplace_back(shared_from_this());
return true;
}
void Port::disconnect(std::shared_ptr<Port> p) {
if (!p) return;
auto t = shared_from_this();
connections.erase(std::remove_if(connections.begin(), connections.end(), [p](auto w) { return w.lock() == p; }), connections.end());
p->connections.erase(std::remove_if(p->connections.begin(), p->connections.end(), [t](auto w) { return w.lock() == t; }), p->connections.end());
}
void Node::parentTo(std::shared_ptr<Graph> graph) {
auto t = shared_from_this(); // keep alive during reparenting
if (auto p = parent.lock(); p) {
p->children.erase(std::remove(p->children.begin(), p->children.end(), t), p->children.end());
}
parent = graph;
if (graph) {
graph->children.push_back(t);
}
}

44
xybrid/data/node.h Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <memory>
#include <vector>
#include <string>
namespace Xybrid::Data {
class Graph;
class Node;
class Port : public std::enable_shared_from_this<Port> {
public:
enum Type : char {
Input, Output
};
enum DataType : char {
Command, MIDI, Audio, Parameter
};
std::weak_ptr<Node> owner;
std::vector<std::weak_ptr<Port>> connections;
Type type; // TODO: figure out passthrough?
virtual ~Port() = default;
virtual DataType dataType();
virtual bool singleInput() { return false; }
virtual bool canConnectTo(DataType);
/*virtual*/ bool connect(std::shared_ptr<Port>);
/*virtual*/ void disconnect(std::shared_ptr<Port>);
};
class Node : public std::enable_shared_from_this<Node> {
public:
std::weak_ptr<Graph> parent;
int x{}, y{};
std::string name;
std::vector<std::shared_ptr<Port>> inputs, outputs;
virtual ~Node() = default;
void parentTo(std::shared_ptr<Graph>);
};
}

View File

@ -17,9 +17,7 @@ namespace Xybrid::Data {
TimeSignature() = default;
TimeSignature(int b, int r, int t) : beatsPerMeasure(b), rowsPerBeat(r), ticksPerRow(t) {}
constexpr int rowsPerMeasure() const {
return beatsPerMeasure * rowsPerBeat;
}
constexpr int rowsPerMeasure() const { return beatsPerMeasure * rowsPerBeat; }
};
class Pattern {
public:
@ -41,9 +39,7 @@ namespace Xybrid::Data {
Row& operator=(const Row&) noexcept;
bool isEmpty() const {
return numParams() == 0 && port == -1 && note == -1;
}
bool isEmpty() const { return numParams() == 0 && port == -1 && note == -1; }
size_t numParams() const {
if (!this->params) return 0;
@ -117,9 +113,7 @@ namespace Xybrid::Data {
void addChannel(int at = -1);
void deleteChannel(int at);
size_t numChannels() const {
return channels.size();
}
size_t numChannels() const { return channels.size(); }
bool valid() const;
bool validFor(const Project*) const;

View File

@ -2,6 +2,13 @@
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
#include "data/graph.h"
using Xybrid::Data::Graph;
Project::Project() {
rootGraph = std::make_shared<Graph>();
}
Project::~Project() {
// orphan patterns so they're not pointing at a non-project
for (auto& pat : patterns) pat->project = nullptr;

View File

@ -16,6 +16,7 @@ namespace Xybrid {
}
namespace Xybrid::Data {
class Graph;
class Project {
public:
bool editingLocked = false;
@ -27,7 +28,6 @@ namespace Xybrid::Data {
QString fileName;
size_t sampleRate = 48000; // global sr for rendering
float tempo = 140.0;
TimeSignature time;
// default time signature
@ -36,10 +36,10 @@ namespace Xybrid::Data {
std::vector<std::shared_ptr<Pattern>> patterns;
std::vector<Pattern*> sequence; // nullptr as separator
//std::shared_ptr<Graph> mainGraph;
// list of input nodes is just part of mainGraph
std::shared_ptr<Graph> rootGraph;
// list of input nodes is just part of rootGraph
Project() = default;
Project();
~Project();
void updatePatternIndices();

View File

@ -27,9 +27,7 @@ namespace Xybrid::Editing {
PatternDeltaCommand(const std::shared_ptr<Data::Pattern>& pattern, int channel, int row);
~PatternDeltaCommand() override = default;
int id() const override {
return 2000;
}
int id() const override { return 2000; }
bool mergeWith(const QUndoCommand*) override;
void redo() override;
@ -43,9 +41,7 @@ namespace Xybrid::Editing {
PatternRenameCommand(const std::shared_ptr<Data::Pattern>& pattern, const std::string& to);
~PatternRenameCommand() override = default;
int id() const override {
return 2070;
}
int id() const override { return 2070; }
bool mergeWith(const QUndoCommand*) override;
void redo() override;
@ -59,9 +55,7 @@ namespace Xybrid::Editing {
PatternChannelMoveCommand(const std::shared_ptr<Data::Pattern>& pattern, int from, int to);
~PatternChannelMoveCommand() override = default;
int id() const override {
return 2100;
}
int id() const override { return 2100; }
bool mergeWith(const QUndoCommand*) override;
void redo() override;
@ -76,9 +70,7 @@ namespace Xybrid::Editing {
PatternChannelRenameCommand(const std::shared_ptr<Data::Pattern>& pattern, int channel, const std::string& to);
~PatternChannelRenameCommand() override = default;
int id() const override {
return 2101;
}
int id() const override { return 2101; }
bool mergeWith(const QUndoCommand*) override;
void redo() override;

View File

@ -28,9 +28,7 @@ namespace Xybrid::Editing {
ProjectSequencerDeltaCommand(const std::shared_ptr<Data::Project>& project);
~ProjectSequencerDeltaCommand() override = default;
int id() const override {
return 1000;
}
int id() const override { return 1000; }
bool mergeWith(const QUndoCommand*) override;
void redo() override;
@ -43,9 +41,7 @@ namespace Xybrid::Editing {
ProjectPatternMoveCommand(const std::shared_ptr<Data::Project>& project, int from, int to);
~ProjectPatternMoveCommand() override = default;
int id() const override {
return 1001;
}
int id() const override { return 1001; }
bool mergeWith(const QUndoCommand*) override;
void redo() override;

View File

@ -1,10 +1,11 @@
#include "mainwindow.h"
#include <QApplication>
#include "audio/audioengine.h"
#include <vector>
#include <QDebug>
#include <QFontDatabase>
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
@ -12,8 +13,10 @@ int main(int argc, char *argv[]) {
// make sure bundled fonts are loaded
QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf");
Xybrid::MainWindow w;
w.show();
Xybrid::Audio::AudioEngine::init();
auto* w = new Xybrid::MainWindow();
w->show();
return a.exec();
}

View File

@ -9,6 +9,7 @@ using Xybrid::MainWindow;
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QWindow>
#include <QUndoStack>
#include "util/strings.h"
@ -20,6 +21,8 @@ using Xybrid::MainWindow;
#include "editing/projectcommands.h"
#include "audio/audioengine.h"
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
@ -39,6 +42,11 @@ MainWindow::MainWindow(QWidget *parent) :
ui(new Ui::MainWindow) {
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
// remove tab containing system widgets
ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_));
undoStack = new QUndoStack(this);
//undoStack->setUndoLimit(256);
connect(undoStack, &QUndoStack::cleanChanged, [this](bool) {
@ -53,11 +61,10 @@ MainWindow::MainWindow(QWidget *parent) :
redoAction->setShortcuts(QKeySequence::Redo);
ui->menuEdit->addAction(redoAction);
auto t = ui->tabWidget;
auto* t = ui->tabWidget;
t->setCornerWidget(ui->menuBar);
t->setCornerWidget(ui->label, Qt::TopLeftCorner);
auto mb = ui->menuBar;
mb->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
// prevent right pane of pattern view from being collapsed
ui->patternViewSplitter->setCollapsible(1, false);
@ -104,7 +111,7 @@ MainWindow::MainWindow(QWidget *parent) :
});
}
menu->popup(ui->patternList->mapToGlobal(pt));
});
});//*/
}
{ /* Set up sequencer */ } {
@ -163,7 +170,7 @@ MainWindow::MainWindow(QWidget *parent) :
}
menu->popup(ui->patternSequencer->mapToGlobal(pt));
});
});//*/
}
{ /* Set up keyboard shortcuts for pattern view */ } {
@ -186,18 +193,31 @@ MainWindow::MainWindow(QWidget *parent) :
auto count = ui->patternSequencer->horizontalHeader()->count();
ui->patternSequencer->setCurrentIndex(i.siblingAtColumn((count + i.column() + 1) % count));
});
/* tmp test
connect(new QShortcut(QKeySequence("Ctrl+F1"), ui->patchboard), &QShortcut::activated, [this]() {
auto inp = QInputDialog::getText(this, "yes", "yes");
WId id = inp.toULongLong();
auto* w = QWindow::fromWinId(id);
auto* w2 = new QWindow();
auto* wc = QWidget::createWindowContainer(w2);
w->setParent(w2);
ui->patchboard->layout()->addWidget(wc);
});
*/
}
// Set up signaling from project to UI
socket.reset(new UISocket());
socket = new UISocket();
socket->setParent(this);
socket->window = this;
socket->undoStack = undoStack;
connect(socket.get(), &UISocket::updatePatternLists, this, &MainWindow::updatePatternLists);
connect(socket.get(), &UISocket::patternUpdated, [this](Pattern* p) {
connect(socket, &UISocket::updatePatternLists, this, &MainWindow::updatePatternLists);
connect(socket, &UISocket::patternUpdated, [this](Pattern* p) {
if (editingPattern.get() != p) return;
ui->patternEditor->refresh();
});
connect(socket.get(), &UISocket::rowUpdated, [this](Pattern* p, int ch, int r) {
connect(socket, &UISocket::rowUpdated, [this](Pattern* p, int ch, int r) {
if (editingPattern.get() != p) return;
const auto cpc = PatternEditorModel::colsPerChannel;
auto ind = ui->patternEditor->model()->index(r, ch * cpc);
@ -212,6 +232,8 @@ MainWindow::MainWindow(QWidget *parent) :
/*project->sequence.push_back(nullptr);
project->patterns[0]->name = "waffle iron";
project->sequence.push_back(project->newPattern().get());*/
Audio::audioEngine->play(project);
}
MainWindow::~MainWindow() {
@ -269,7 +291,7 @@ void MainWindow::menuFileSaveAs() {
void MainWindow::onNewProjectLoaded() {
undoStack->clear();
project->socket = socket.get();
project->socket = socket;
updatePatternLists();
patternSelection(0);
sequenceSelection(-1);

View File

@ -22,9 +22,9 @@ namespace Xybrid {
private:
Ui::MainWindow* ui;
std::unique_ptr<UISocket> socket;
UISocket* socket;
std::shared_ptr<Data::Project> project;
std::shared_ptr<Data::Pattern> editingPattern; // temporary pattern for testing the editor
std::shared_ptr<Data::Pattern> editingPattern;
QUndoStack* undoStack;
@ -35,9 +35,7 @@ namespace Xybrid {
void updateTitle();
public:
const std::shared_ptr<Data::Project>& getProject() const {
return project;
}
const std::shared_ptr<Data::Project>& getProject() const { return project; }
int patternSelection(int = -100);
int sequenceSelection(int = -100);

View File

@ -79,6 +79,9 @@
<height>0</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
@ -229,12 +232,37 @@
<attribute name="title">
<string>Patchboard</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGraphicsView" name="patchboardView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="extra_">
<attribute name="title">
<string>nonexistent</string>
</attribute>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>310</x>
<y>80</y>
<width>61</width>
<x>10</x>
<y>10</y>
<width>63</width>
<height>16</height>
</rect>
</property>

View File

@ -61,7 +61,7 @@ QVariant PatternEditorHeaderProxyModel::headerData(int section, Qt::Orientation
PatternEditorModel::PatternEditorModel(QObject *parent)
:QAbstractTableModel(parent) {
hprox.reset(new PatternEditorHeaderProxyModel(parent, this));
hprox = new PatternEditorHeaderProxyModel(parent, this);
}
int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {

View File

@ -16,7 +16,7 @@ namespace Xybrid::UI {
static constexpr int colsPerChannel = 2 + (2 * paramSoftCap);
static constexpr int cellPadding = 2;
std::unique_ptr<PatternEditorHeaderProxyModel> hprox;
PatternEditorHeaderProxyModel* hprox;
bool fitHeaderToName = false;

View File

@ -84,7 +84,12 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
PatternEditorView::~PatternEditorView() {
//
horizontalHeader()->deleteLater();
/*mdl.release();
del.release();
hdr.release();
cornerBoxBox.release();
cornerBox.release();*/
//horizontalHeader()->deleteLater();
}
void PatternEditorView::keyPressEvent(QKeyEvent *event) {

View File

@ -4,7 +4,7 @@
#
#-------------------------------------------------
QT += core gui
QT += core gui multimedia
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
@ -41,7 +41,9 @@ SOURCES += \
fileops.cpp \
editing/patterncommands.cpp \
editing/projectcommands.cpp \
editing/compositecommand.cpp
editing/compositecommand.cpp \
data/node.cpp \
audio/audioengine.cpp
HEADERS += \
mainwindow.h \
@ -59,7 +61,10 @@ HEADERS += \
fileops.h \
editing/patterncommands.h \
editing/projectcommands.h \
editing/compositecommand.h
editing/compositecommand.h \
data/node.h \
data/graph.h \
audio/audioengine.h
FORMS += \
mainwindow.ui