instrument preview
parent
e5f1615724
commit
50111e75c5
22
notes
22
notes
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}*/
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
// ...
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Xybrid::Util {
|
||||
int16_t keyToNote(int key);
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue