instrument preview

portability/boost
zetaPRIME 2018-12-31 22:10:25 -05:00
parent e5f1615724
commit 50111e75c5
12 changed files with 201 additions and 49 deletions

22
notes
View File

@ -45,22 +45,10 @@ project data {
TODO {
immediate frontburner {
neeeeext {
- hook the graph up to the audio engine! {
- (not so-)recursive dependency queue resolution
- done/readiness test (process() wrapper?)
}
- hook up commands to the graph {
- figure out how to do note numbers
- ^ vector sized by channel count rounded up to next 16
- ^^ on switching to new graph... map of (hashes of) named channels??
- assemble wire command queues (probaby some minor trickiness) and push into ports
- NOTE! note number and port have to be combined in tracking (have to know what port to send the note-offs to)
}
then implement multithreading! :D
}
...
audio engine invokes workers, then QThread::wait()s on them
multithreaded audio
^ audio engine invokes workers, then QThread::wait()s on them
# fix how qt5.12 broke header text (removed elide for now)
@ -79,6 +67,7 @@ TODO {
add metadata and pattern properties (artist, song title, project bpm; pattern name, length etc.)
pattern cut+copy+paste
transpose selection
different context menu for multiple selected nodes
pack/unpack selection to/from subgraph
@ -86,7 +75,7 @@ TODO {
proper playback controls and indicators
play from current pattern
instrument previewing
- instrument previewing
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
? de-hardcode the "» " (probably just make it a static const variable somewhere?)
@ -101,6 +90,7 @@ TODO {
gadgets and bundled things {
(the simple things:)
gain and panning gadget
note transpose
volume meter
Polyplexer (splits a single command input into several monophonic outputs and keeps track of individual notes between them)

View File

@ -91,7 +91,7 @@ void AudioEngine::deinitAudio() {
}
void AudioEngine::play(std::shared_ptr<Project> p) {
QMetaObject::invokeMethod(this, [this, p]() {
QMetaObject::invokeMethod(this, [this, p] {
if (!p) return; // nope
project = p;
@ -121,7 +121,7 @@ void AudioEngine::play(std::shared_ptr<Project> p) {
}
void AudioEngine::stop() {
QMetaObject::invokeMethod(this, [this]() {
QMetaObject::invokeMethod(this, [this] {
project = nullptr;
queueValid = false;
queue.clear();
@ -131,6 +131,43 @@ void AudioEngine::stop() {
}, Qt::QueuedConnection);
}
void AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t note, bool state) {
QMetaObject::invokeMethod(this, [this, p, port, note, state] {
if (!p) return;
if (mode == Playing || mode == Rendering || mode == PlaybackMode::Paused) return;
if (project != p || mode != Previewing) {
deinitAudio();
project = p;
queueValid = false;
queue.clear();
buf.clear();
project->rootGraph->reset();
initAudio();
for (auto& b : buffer) {
b.clear();
b.reserve(static_cast<size_t>(sampleRate/4));
}
tempo = project->tempo;
output->start(this);
mode = Previewing;
emit this->playbackModeChanged();
}
if (port >= 0 && port <= 255) previewPort = static_cast<uint8_t>(port); // assign port if valid
if (note < 0) return; // invalid note (port is set before it so that setting the port can be a separate action)
// assemble message
size_t bi = buf.size();
buf.resize(bi+5);
reinterpret_cast<uint16_t&>(buf[bi]) = static_cast<uint16_t>(note);
reinterpret_cast<int16_t&>(buf[bi+2]) = state ? note : -2;
}, Qt::QueuedConnection);
}
void AudioEngine::buildQueue() {
queue.clear();
// stuff
@ -209,10 +246,35 @@ void AudioEngine::nextTick() {
buffer[0].resize(static_cast<size_t>(sampleRate/10));
buffer[1].resize(static_cast<size_t>(sampleRate/10));
} else if (mode == Previewing) {
// NYI
// WIP
// reset raw buffer
tickBufPtr = tickBuf.get();
tickId++;
// (sample rate / seconds per beat) / ticks per beat
double tickSize = (1.0 * sampleRate / (static_cast<double>(tempo)/60.0)) / (4*6);
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);
if (!queueValid) buildQueue();
// TODO: send previewing commands
if (auto p = std::static_pointer_cast<CommandPort>(project->rootGraph->port(Port::Input, Port::Command, previewPort)); p) {
p->push(buf);
}
buf.clear();
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);
}
} else if (mode == Playing) {
// reset raw buffer
tickBufPtr = tickBuf.get();
@ -384,30 +446,15 @@ void AudioEngine::nextTick() {
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;
}*/
//
}
// ...

View File

@ -31,7 +31,7 @@ namespace Xybrid::Audio {
Q_OBJECT
explicit AudioEngine(QObject *parent = nullptr);
public:
enum PlaybackMode {
enum PlaybackMode : uint8_t {
Stopped, // stopped
Playing, // playing track
Paused, // paused during playback
@ -65,6 +65,8 @@ namespace Xybrid::Audio {
std::unordered_map<std::string*, NoteInfo, PointerCompare<std::string>, PointerCompare<std::string>> nameTrack;
std::vector<uint8_t> buf; /// preallocated buffer for building commands
uint8_t previewPort = 0;
// playback timing and position
float tempo = 140.0;
int seqPos;
@ -82,6 +84,7 @@ namespace Xybrid::Audio {
inline constexpr const std::shared_ptr<Data::Project>& playingProject() const { return project; }
void play(std::shared_ptr<Data::Project>);
void stop();
void preview(std::shared_ptr<Data::Project>, int16_t port, int16_t note, bool state);
inline void invalidateQueue(Data::Project* p) { if (p == project.get()) queueValid = false; }

View File

@ -17,7 +17,7 @@ namespace Xybrid {
namespace Xybrid::Data {
class Graph;
class Project {
class Project : public std::enable_shared_from_this<Project> {
public:
bool editingLocked();

View File

@ -7,16 +7,23 @@ using Xybrid::UI::PatchboardScene;
#include <QScrollBar>
#include <QGraphicsItem>
#include <QMainWindow>
#include <QKeyEvent>
#include <QMenu>
#include <QTimer>
#include <QGraphicsSceneContextMenuEvent>
#include "data/graph.h"
#include "data/project.h"
using namespace Xybrid::Data;
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "util/keys.h"
#include "ui/patchboard/nodeobject.h"
#include "config/colorscheme.h"
@ -66,6 +73,30 @@ void PatchboardScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
m->popup(e->screenPos());
}
void PatchboardScene::keyPressEvent(QKeyEvent* e) {
QGraphicsScene::keyPressEvent(e);
if (!e->isAccepted() && !e->isAutoRepeat()) {
auto note = Util::keyToNote(e->key());
if (note >= 0) {
auto p = graph->project->shared_from_this();
if (e->modifiers() & Qt::Modifier::SHIFT) note += 24;
audioEngine->preview(p, -1, note, true);
}
}
}
void PatchboardScene::keyReleaseEvent(QKeyEvent* e) {
QGraphicsScene::keyReleaseEvent(e);
if (!e->isAccepted() && !e->isAutoRepeat()) {
auto note = Util::keyToNote(e->key());
if (note >= 0) {
auto p = graph->project->shared_from_this();
audioEngine->preview(p, -1, note, false);
audioEngine->preview(p, -1, note+24, false);
}
}
}
void PatchboardScene::queueResize() {
if (!resizeQueued) {
resizeQueued = true;

View File

@ -23,6 +23,8 @@ namespace Xybrid::UI {
void drawBackground(QPainter*, const QRectF&) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
void keyPressEvent(QKeyEvent*) override;
void keyReleaseEvent(QKeyEvent*) override;
void refresh();
};

View File

@ -35,7 +35,7 @@ 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 = []() {
const std::unordered_map<int, int> keyConv = [] {
std::unordered_map<int, int> m;
m[Qt::Key_BraceLeft] = Qt::Key_BracketLeft;
@ -167,7 +167,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
} else if (mod & Qt::Modifier::ALT) {
} else {
if (k == Qt::Key_Space) {
if (k == Qt::Key_Space) { // TODO make this not "modify" if nothing was affected
if (mod & Qt::Modifier::SHIFT) return dc->cancel(); // TODO: once playback is a thing, shift+space to preview row?
dc->cancel();

View File

@ -6,6 +6,7 @@ using Xybrid::UI::PatternEditorModel;
using Xybrid::UI::PatternEditorItemDelegate;
#include "util/strings.h"
#include "util/keys.h"
#include "ui/channelheaderview.h"
using Xybrid::UI::ChannelHeaderView;
@ -17,6 +18,9 @@ using Xybrid::Data::Pattern;
#include "editing/patterncommands.h"
using namespace Xybrid::Editing;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include <QKeyEvent>
#include <QDebug>
@ -92,19 +96,45 @@ PatternEditorView::~PatternEditorView() {
//horizontalHeader()->deleteLater();
}
void PatternEditorView::keyPressEvent(QKeyEvent *event) {
if (/*event->modifiers() & Qt::Modifier::CTRL &&*/ (event->key() == Qt::Key_Tab || event->key() == Qt::Key_Backtab)) { // don't block ctrl+tab
event->ignore();
void PatternEditorView::keyPressEvent(QKeyEvent* e) {
if (/*event->modifiers() & Qt::Modifier::CTRL &&*/ (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab)) { // don't block ctrl+tab
e->ignore();
return;
//QKeyEvent()
}
if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Insert) {
if (!edit(currentIndex(), AnyKeyPressed, event)) {
event->ignore();
if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Insert) {
if (!edit(currentIndex(), AnyKeyPressed, e)) {
e->ignore();
return;
}
}
QAbstractItemView::keyPressEvent(event);
QAbstractItemView::keyPressEvent(e);
if (!e->isAutoRepeat()) {
if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
auto ind = currentIndex();
int cc = ind.column() % PatternEditorModel::colsPerChannel;
int ch = (ind.column() - cc) / PatternEditorModel::colsPerChannel;
if (cc == 1) { // note column
auto& r = mdl->getPattern()->rowAt(ch, ind.row());
auto p = mdl->getPattern()->project->shared_from_this();
previewKey[e->key()] = {r.port, r.note};
audioEngine->preview(p, r.port, r.note, true);
}
}
}
}
void PatternEditorView::keyReleaseEvent(QKeyEvent* e) {
QAbstractItemView::keyReleaseEvent(e);
if (!e->isAutoRepeat()) {
if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
if (auto k = previewKey.find(e->key()); k != previewKey.end()) {
auto p = mdl->getPattern()->project->shared_from_this();
audioEngine->preview(p, k->second[0], k->second[1], false);
previewKey.erase(k);
}
}
}
}
void PatternEditorView::setPattern(const std::shared_ptr<Pattern>& pattern) {

View File

@ -1,6 +1,7 @@
#pragma once
#include <memory>
#include <unordered_map>
#include <QCheckBox>
#include <QTableView>
@ -22,6 +23,8 @@ namespace Xybrid::UI {
std::unique_ptr<QWidget> cornerBoxBox;
std::unique_ptr<QCheckBox> cornerBox;
std::unordered_map<int, std::array<int16_t, 2>> previewKey;
bool colUpdateNeeded = false;
public:
@ -29,7 +32,8 @@ namespace Xybrid::UI {
~PatternEditorView() override;
void updateGeometries() override;
void keyPressEvent(QKeyEvent* event) override;
void keyPressEvent(QKeyEvent*) override;
void keyReleaseEvent(QKeyEvent*) override;
void keyboardSearch(const QString&) override {} // disable accidental search
void setPattern(const std::shared_ptr<Data::Pattern>& pattern);

36
xybrid/util/keys.cpp Normal file
View File

@ -0,0 +1,36 @@
#include "keys.h"
#include <unordered_map>
#include <QKeyEvent>
namespace {
std::unordered_map<int, int16_t> keyMap = [] {
std::unordered_map<int, int16_t> m;
int16_t n = 12*4;
int pianoKeys[] = {
Qt::Key_Q, Qt::Key_W, Qt::Key_E, Qt::Key_R, Qt::Key_T, Qt::Key_Y, Qt::Key_U, Qt::Key_I, Qt::Key_O, Qt::Key_P, Qt::Key_BracketLeft, Qt::Key_BracketRight,
Qt::Key_A, Qt::Key_S, Qt::Key_D, Qt::Key_F, Qt::Key_G, Qt::Key_H, Qt::Key_J, Qt::Key_K, Qt::Key_L, Qt::Key_Semicolon, Qt::Key_Apostrophe, Qt::Key_Backslash,
Qt::Key_Z, Qt::Key_X, Qt::Key_C, Qt::Key_V, Qt::Key_B, Qt::Key_N, Qt::Key_M, Qt::Key_Comma, Qt::Key_Period, Qt::Key_Slash,
};
for (auto i : pianoKeys) m[i] = n++;
// shift fix
m[Qt::Key_BraceLeft] = m[Qt::Key_BracketLeft];
m[Qt::Key_BraceRight] = m[Qt::Key_BracketRight];
m[Qt::Key_Bar] = m[Qt::Key_Backslash];
m[Qt::Key_Colon] = m[Qt::Key_Semicolon];
m[Qt::Key_QuoteDbl] = m[Qt::Key_Apostrophe];
m[Qt::Key_Less] = m[Qt::Key_Comma];
m[Qt::Key_Greater] = m[Qt::Key_Period];
m[Qt::Key_Question] = m[Qt::Key_Slash];
return m;
}();
}
int16_t Xybrid::Util::keyToNote(int key) {
if (auto f = keyMap.find(key); f != keyMap.end()) return f->second;
return -1; // default to none
}

7
xybrid/util/keys.h Normal file
View File

@ -0,0 +1,7 @@
#pragma once
#include <cstdint>
namespace Xybrid::Util {
int16_t keyToNote(int key);
}

View File

@ -54,7 +54,8 @@ SOURCES += \
data/porttypes.cpp \
ui/breadcrumbview.cpp \
gadgets/ioport.cpp \
gadgets/testsynth.cpp
gadgets/testsynth.cpp \
util/keys.cpp
HEADERS += \
mainwindow.h \
@ -83,7 +84,8 @@ HEADERS += \
config/pluginregistry.h \
ui/breadcrumbview.h \
gadgets/ioport.h \
gadgets/testsynth.h
gadgets/testsynth.h \
util/keys.h
FORMS += \
mainwindow.ui