Compare commits
168 Commits
0390f473ac
...
7aa71c5cc2
Author | SHA1 | Date |
---|---|---|
Rachel Fae Fox (foxiepaws) | 7aa71c5cc2 | |
Zithia Satazaki | b92dd4f3c5 | |
Zithia Satazaki | a7ece838f1 | |
Zithia Satazaki | ab811c363c | |
Zithia Satazaki | 29b0367dfb | |
Zithia Satazaki | 00cfb9df3a | |
Zithia Satazaki | 9bed15f564 | |
Zithia Satazaki | e6fa6d33fa | |
Zithia Satazaki | e845325178 | |
Zithia Satazaki | 25b12be6ea | |
Zithia Satazaki | f969aa0d2e | |
Zithia Satazaki | b0b754a37d | |
Zithia Satazaki | 3ff986d90c | |
Zithia Satazaki | 5385740516 | |
Zithia Satazaki | 5f1b9d03b4 | |
Zithia Satazaki | 042e13eefb | |
Zithia Satazaki | bf84a1dc1f | |
Zithia Satazaki | 7809e2ab58 | |
Zithia Satazaki | ab3cad2c22 | |
Zithia Satazaki | 59de376d05 | |
Zithia Satazaki | 95c0af06be | |
Zithia Satazaki | 22b06c465a | |
Zithia Satazaki | b615b18d45 | |
Zithia Satazaki | 9b131e5322 | |
Zithia Satazaki | acbba0403b | |
Zithia Satazaki | 15c0aaed82 | |
Zithia Satazaki | 7e495b5645 | |
Zithia Satazaki | e007063e92 | |
zetaPRIME | 2bfd06acf2 | |
zetaPRIME | a6032be6a9 | |
zetaPRIME | d75b7878f8 | |
zetaPRIME | aa264a8065 | |
zetaPRIME | 369a7d979d | |
zetaPRIME | a7c26b9722 | |
zetaPRIME | 779af6bdab | |
zetaPRIME | 853ba8a901 | |
zetaPRIME | 8b020975cc | |
zetaPRIME | be67e02004 | |
zetaPRIME | e63c93e146 | |
zetaPRIME | a28d6e48b6 | |
zetaPRIME | acbca4ae0b | |
zetaPRIME | 0a14aec9e5 | |
zetaPRIME | cbce51744c | |
zetaPRIME | 4cbde894c4 | |
zetaPRIME | 4330ab847f | |
zetaPRIME | e962cda5cc | |
zetaPRIME | e87471c39a | |
zetaPRIME | f430ebab00 | |
zetaPRIME | d95e6ce1d5 | |
zetaPRIME | 41a591a957 | |
zetaPRIME | 1ebc8f04c5 | |
zetaPRIME | c6e22d3521 | |
zetaPRIME | cf81f91a0b | |
zetaPRIME | 5d9b39f84b | |
zetaPRIME | 7abccd7a38 | |
zetaPRIME | 5dca500640 | |
zetaPRIME | 3caf08c3db | |
zetaPRIME | df520dfd2d | |
zetaPRIME | 33916acc65 | |
zetaPRIME | dd6c9009a7 | |
zetaPRIME | e4475cbce0 | |
zetaPRIME | 987d22d332 | |
zetaPRIME | 79b1d9239f | |
zetaPRIME | 0008997fe7 | |
zetaPRIME | dc042ae1ac | |
zetaPRIME | 3eb25120ce | |
zetaPRIME | 7f471bf5ce | |
zetaPRIME | b1c8377db5 | |
zetaPRIME | 3137c5e699 | |
zetaPRIME | 4db24fb188 | |
zetaPRIME | 58826188c6 | |
zetaPRIME | 6fcc6db4e6 | |
zetaPRIME | e78cebfd77 | |
zetaPRIME | 3af34095d2 | |
zetaPRIME | 3af05f2cc3 | |
zetaPRIME | 1a6f85e0fa | |
zetaPRIME | a3be1bded0 | |
zetaPRIME | 0f3da1f094 | |
zetaPRIME | b4b9918c7d | |
zetaPRIME | 7b566929a2 | |
zetaPRIME | fd81de5040 | |
zetaPRIME | 930992025d | |
zetaPRIME | bf793d20fd | |
zetaPRIME | 4822ef1bb6 | |
zetaPRIME | 401ee05aa4 | |
zetaPRIME | cf75f22403 | |
zetaPRIME | e5c664a5ad | |
zetaPRIME | b8f9664f7c | |
zetaPRIME | d360074e81 | |
zetaPRIME | 7e1e80eaea | |
zetaPRIME | e39f8603f6 | |
zetaPRIME | a2a643a80c | |
zetaPRIME | a9cbd629dc | |
zetaPRIME | 19a82764cc | |
zetaPRIME | d960da0775 | |
zetaPRIME | f7212a222d | |
zetaPRIME | 31b13ad3f1 | |
zetaPRIME | 6fd5276aaf | |
zetaPRIME | 0551c4f7e7 | |
zetaPRIME | 18557d933b | |
zetaPRIME | 2d077d90c0 | |
zetaPRIME | 6b126db234 | |
zetaPRIME | e1e4089b88 | |
zetaPRIME | 9455834b2a | |
zetaPRIME | c168ed95d3 | |
zetaPRIME | 7858449637 | |
zetaPRIME | ff4ffaac61 | |
zetaPRIME | 9ed8d6039d | |
zetaPRIME | 34b4721f69 | |
zetaPRIME | 9590462891 | |
zetaPRIME | 394b24223f | |
zetaPRIME | 7fa610f4af | |
zetaPRIME | f9f8391bba | |
zetaPRIME | 30b9db6a1a | |
zetaPRIME | 53f2f27285 | |
zetaPRIME | 06230dde55 | |
zetaPRIME | 22f5b0502d | |
zetaPRIME | 8db7adfa74 | |
zetaPRIME | 71ec8dba73 | |
zetaPRIME | 63c09b46aa | |
zetaPRIME | 3b5bdd2e07 | |
zetaPRIME | ba6683cd2d | |
zetaPRIME | 59f45ab382 | |
zetaPRIME | 4782eedf9c | |
zetaPRIME | 70a6edf824 | |
zetaPRIME | 638c7e5c12 | |
zetaPRIME | c41ffbfcce | |
zetaPRIME | 46a73bef74 | |
zetaPRIME | c027c422fd | |
zetaPRIME | a48e9d4583 | |
zetaPRIME | bac05e3f68 | |
zetaPRIME | 10bcacf8c1 | |
zetaPRIME | d3521416f2 | |
zetaPRIME | 8a7cb67bf3 | |
zetaPRIME | c069f5b9f3 | |
zetaPRIME | 93c5fc0611 | |
zetaPRIME | 98f157fb01 | |
zetaPRIME | e36f2c110c | |
zetaPRIME | d28e1837d4 | |
zetaPRIME | 12ba77fc96 | |
zetaPRIME | 4cf9a61a56 | |
zetaPRIME | d8d7fac590 | |
zetaPRIME | 149ab65c08 | |
zetaPRIME | affb86d76a | |
zetaPRIME | d0db5a6b4d | |
zetaPRIME | 6ee9a0db6b | |
zetaPRIME | 80c90451f3 | |
zetaPRIME | d4a12647d2 | |
zetaPRIME | 25408ba776 | |
zetaPRIME | b86452b1af | |
zetaPRIME | ec94dce150 | |
zetaPRIME | eb40b74234 | |
zetaPRIME | 651daf5e4a | |
zetaPRIME | 187b51d524 | |
zetaPRIME | d4aa622fa6 | |
zetaPRIME | 14af2ffeb7 | |
zetaPRIME | 60df49db69 | |
zetaPRIME | a78d41b134 | |
zetaPRIME | 4c6c135617 | |
zetaPRIME | 72b5eb3b53 | |
zetaPRIME | 39f5966c0f | |
zetaPRIME | b57974e066 | |
zetaPRIME | 26a2bf4e82 | |
zetaPRIME | 82bb4e48e1 | |
zetaPRIME | 84a2f2441d | |
zetaPRIME | d101975d5d | |
zetaPRIME | ac2e81ab10 | |
zetaPRIME | 1b8eeffbcf |
61
notes
61
notes
|
@ -31,31 +31,57 @@ parameters {
|
|||
}
|
||||
|
||||
TODO {
|
||||
immediate frontburner {
|
||||
|
||||
distortion effect
|
||||
single-selection sampler
|
||||
|
||||
global (default) pan (PXX) for InstrumentCore
|
||||
|
||||
add ,XX support to global tempo
|
||||
settings dialog {
|
||||
about-license info
|
||||
}
|
||||
|
||||
> add common oscillators to a nodelib header
|
||||
|
||||
revert-to-saved menu action
|
||||
|
||||
automation node {
|
||||
listens for one specific param
|
||||
supports tweening
|
||||
value bounds, scaling exponent
|
||||
passthrough command port with option to consume marked param
|
||||
|
||||
how to UI?
|
||||
I guess some sort of text box/spinner to enter bounds
|
||||
dial for exponent
|
||||
focusable control to set param
|
||||
}
|
||||
|
||||
editing song info should probably be an UndoStack action
|
||||
editing song *tempo* ABSOLUTELY should
|
||||
|
||||
maybe retool rendering to feed f32 (or even f64) to ffmpeg
|
||||
|
||||
figure out what to actually do with directory config
|
||||
|
||||
buffer helper akin to what quicklevel does {
|
||||
keeps a buffer length, running averages, etc.
|
||||
can lerp across tick for speed
|
||||
useful for level reading, waveform output, compression/sidechaining etc.
|
||||
}
|
||||
|
||||
solo gadget {
|
||||
interprets incoming commands as monophonic with portamento
|
||||
probably not super useful for tracked things but good for playing live
|
||||
}
|
||||
|
||||
- actual config file loading/saving
|
||||
color scheme load/save
|
||||
|
||||
- indexer abstraction for audioports (assign/add std::pair<float, float>)
|
||||
maybe a similar abstraction for processing notes to what commandreader does
|
||||
|
||||
maybe interpolate between resampler LUT levels
|
||||
|
||||
bugs to fix {
|
||||
playback after stopping immediately after a note in the first pattern played sometimes skips that note
|
||||
things can apparently be hooked up cyclically, which completely breaks the queue
|
||||
|
||||
pattern switching is slow when changing (especially increasing) number of rows; set fixed page size to avoid reallocation?
|
||||
}
|
||||
|
||||
misc features needed before proper release {
|
||||
ABOUT BOX WITH INCLIB LICENSE NOTICES
|
||||
|
||||
expand/compact pattern 2x/3x, keeping fold interval
|
||||
|
||||
at *least* js plugin support, with lua+lv2 highly preferable
|
||||
|
@ -69,22 +95,21 @@ TODO {
|
|||
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
|
||||
|
||||
make the save routine displace the old file and write a new one
|
||||
|
||||
open file from command line argument
|
||||
^ multi-document, single-instance (QLocalServer etc.)
|
||||
}
|
||||
|
||||
gadgets and bundled things {
|
||||
(the simple things:)
|
||||
- gain and panning gadget
|
||||
- note transpose
|
||||
volume meter
|
||||
- volume meter
|
||||
|
||||
"wrap clipper" (gain up, then wrap around +-1.0, then gain down)
|
||||
|
||||
Polyplexer (splits a single command input into several monophonic outputs and keeps track of individual notes between them)
|
||||
|
||||
probably three sorts of sampler (quick drum sequencer, quick single-sample "wavetable", then the full-on tracker sampler later on)
|
||||
- quick drum sequencer (BeatPad)
|
||||
- quick single-sample "wavetable" (Capaxitor)
|
||||
full-fat tracker sampler at some point
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
namespace Xybrid::Audio {
|
||||
typedef double bufferType;
|
||||
}
|
|
@ -5,6 +5,9 @@ using namespace Xybrid::Data;
|
|||
#include "data/graph.h"
|
||||
#include "data/porttypes.h"
|
||||
|
||||
#include "config/audioconfig.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include "mainwindow.h"
|
||||
#include "uisocket.h"
|
||||
|
||||
|
@ -13,11 +16,16 @@ using namespace Xybrid::Data;
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QThread>
|
||||
#include <QMutex>
|
||||
#include <QTimer>
|
||||
#include <QProcess>
|
||||
#include <QFileInfo>
|
||||
#include <QCoreApplication>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
#define FFMPEG "/usr/local/bin/ffmpeg"
|
||||
|
@ -41,8 +49,7 @@ void AudioEngine::init() {
|
|||
// ...
|
||||
|
||||
// and off to the races
|
||||
thread->start();
|
||||
thread->setPriority(QThread::TimeCriticalPriority);
|
||||
thread->start(QThread::TimeCriticalPriority);
|
||||
QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit, Qt::QueuedConnection);
|
||||
}
|
||||
void AudioEngine::postInit() {
|
||||
|
@ -87,7 +94,7 @@ void AudioEngine::initAudio(bool startNow) {
|
|||
const QAudioDeviceInfo& deviceInfo = QAudioDeviceInfo::defaultOutputDevice();
|
||||
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(48000);
|
||||
format.setSampleRate(sampleRate);
|
||||
format.setChannelCount(2);
|
||||
format.setSampleSize(16);
|
||||
format.setCodec("audio/pcm");
|
||||
|
@ -103,15 +110,17 @@ void AudioEngine::initAudio(bool startNow) {
|
|||
output.reset(new QAudioOutput(deviceInfo, format));
|
||||
output->setCategory("Xybrid");
|
||||
output->setObjectName("Xybrid"); // if Qt ever implements naming the stream this way, WE'LL BE READY
|
||||
output->setBufferSize(static_cast<int>(sampleRate*4*( 64.0 )/1000.0)); // 64ms seems to be a sweet spot now
|
||||
//output->setBufferSize(static_cast<int>(sampleRate*4*( 64.0 )/1000.0)); // 64ms seems to be a sweet spot now
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
if (startNow) output->start();
|
||||
if (startNow) startOutput();
|
||||
}
|
||||
|
||||
void AudioEngine::deinitAudio() {
|
||||
if (output) {
|
||||
QTimer::singleShot(20, [this] { // delay to flush buffers with silence, else we get leftovers on next playback
|
||||
QTimer::singleShot(20, this, [this] { // delay to flush buffers with silence, else we get leftovers on next playback
|
||||
if (output && mode == Stopped) {
|
||||
output->stop();
|
||||
output.reset();
|
||||
|
@ -120,11 +129,27 @@ void AudioEngine::deinitAudio() {
|
|||
}
|
||||
}
|
||||
|
||||
void AudioEngine::startOutput() {
|
||||
if (!output) return;
|
||||
|
||||
//outBuf.resize(0);
|
||||
output->setNotifyInterval(0); // don't need this here
|
||||
output->setBufferSize(sampleRate*4*bufferMs/1000); // set canonical buffer size
|
||||
output->start(this); // set output to take the active role
|
||||
}
|
||||
|
||||
void AudioEngine::play(std::shared_ptr<Project> p, int fromPos) {
|
||||
QMetaObject::invokeMethod(this, [this, p, fromPos] {
|
||||
if (!p) return; // nope
|
||||
project = p;
|
||||
|
||||
if (output) output->stop();
|
||||
output.reset();
|
||||
|
||||
// load audio settings
|
||||
sampleRate = AudioConfig::playbackSampleRate;
|
||||
bufferMs = AudioConfig::playbackBufferMs;
|
||||
|
||||
// stop and reset, then init playback
|
||||
queueValid = false;
|
||||
queue.clear();
|
||||
|
@ -143,7 +168,11 @@ void AudioEngine::play(std::shared_ptr<Project> p, int fromPos) {
|
|||
tempo = project->tempo;
|
||||
tickAcc = 0;
|
||||
|
||||
output->start(this);
|
||||
// properly initialize note tracking to prevent
|
||||
chTrack.clear(); // overwritten starting notes
|
||||
chTrack.resize(findPattern()->channels.size());
|
||||
|
||||
startOutput();
|
||||
|
||||
mode = Playing;
|
||||
emit this->playbackModeChanged();
|
||||
|
@ -176,6 +205,11 @@ uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t
|
|||
deinitAudio();
|
||||
project = p;
|
||||
|
||||
// load audio settings
|
||||
sampleRate = AudioConfig::previewSampleRate;
|
||||
bufferMs = AudioConfig::previewBufferMs;
|
||||
|
||||
// reset state
|
||||
queueValid = false;
|
||||
queue.clear();
|
||||
buf.clear();
|
||||
|
@ -189,7 +223,7 @@ uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t
|
|||
}
|
||||
tempo = project->tempo;
|
||||
|
||||
output->start(this);
|
||||
startOutput();
|
||||
mode = Previewing;
|
||||
emit this->playbackModeChanged();
|
||||
}
|
||||
|
@ -210,7 +244,7 @@ uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t
|
|||
return nId;
|
||||
}
|
||||
|
||||
void AudioEngine::render(std::shared_ptr<Project> p, QString filename) {
|
||||
void AudioEngine::render(std::shared_ptr<Project> p, QString fileName) {
|
||||
if (!p) return; // yeah, no
|
||||
|
||||
if (mode != Stopped) {
|
||||
|
@ -219,48 +253,70 @@ void AudioEngine::render(std::shared_ptr<Project> p, QString filename) {
|
|||
}
|
||||
|
||||
project = p;
|
||||
|
||||
queueValid = false;
|
||||
queue.clear();
|
||||
portLastNoteId.fill(0);
|
||||
project->rootGraph->reset();
|
||||
for (auto& b : buffer) {
|
||||
b.clear();
|
||||
b.reserve(static_cast<size_t>(sampleRate/4));
|
||||
}
|
||||
|
||||
seqPos = -1;
|
||||
curRow = -1;
|
||||
curTick = -2;
|
||||
tempo = project->tempo;
|
||||
tickAcc = 0;
|
||||
|
||||
initAudio(); // we actually need the period size. whoops.
|
||||
|
||||
QProcess enc;
|
||||
QStringList param;
|
||||
param << "-y" << "-f" << "s16le" << "-ac" << "2" << "-ar" << QString::number(sampleRate) << "-i" << "pipe:";
|
||||
if (!project->title.isEmpty()) param << "-metadata" << qs("title=%1").arg(project->title);
|
||||
if (!project->artist.isEmpty()) param << "-metadata" << qs("artist=%1").arg(project->artist);
|
||||
param << "-f" << "mp3" << "-codec:a" << "libmp3lame"<< "-q:a" << "0"; // specify mp3, vbr v0
|
||||
param << filename;
|
||||
|
||||
enc.start(FFMPEG, param);
|
||||
enc.waitForStarted();
|
||||
|
||||
std::vector<char> dat;
|
||||
dat.resize(1024);
|
||||
|
||||
mode = Rendering;
|
||||
while (mode == Rendering) {
|
||||
enc.write(&dat[0], readData(&dat[0], 1024));
|
||||
//enc.write(read(1024));
|
||||
}
|
||||
|
||||
enc.closeWriteChannel();
|
||||
enc.waitForFinished();
|
||||
QMetaObject::invokeMethod(this, [this, fileName] {
|
||||
// load sample rate
|
||||
sampleRate = AudioConfig::renderSampleRate;
|
||||
|
||||
stop();
|
||||
// reset state
|
||||
queueValid = false;
|
||||
queue.clear();
|
||||
portLastNoteId.fill(0);
|
||||
project->rootGraph->reset();
|
||||
for (auto& b : buffer) {
|
||||
b.clear();
|
||||
b.reserve(static_cast<size_t>(sampleRate/4));
|
||||
}
|
||||
|
||||
seqPos = -1;
|
||||
curRow = -1;
|
||||
curTick = -2;
|
||||
tempo = project->tempo;
|
||||
tickAcc = 0;
|
||||
|
||||
// properly initialize note tracking to prevent
|
||||
chTrack.clear(); // overwritten starting notes
|
||||
chTrack.resize(findPattern()->channels.size());
|
||||
|
||||
initAudio(); // we actually need the period size. whoops.
|
||||
|
||||
QFileInfo fi(fileName);
|
||||
auto ext = fi.suffix().toLower();
|
||||
|
||||
QProcess enc;
|
||||
QStringList param;
|
||||
param << "-y" << "-f" << "s16le" << "-ac" << "2" << "-ar" << QString::number(sampleRate) << "-i" << "pipe:";
|
||||
if (!project->title.isEmpty()) param << "-metadata" << qs("title=%1").arg(project->title);
|
||||
if (!project->artist.isEmpty()) param << "-metadata" << qs("artist=%1").arg(project->artist);
|
||||
// flac out is pretty simple, as it turns out
|
||||
if (ext == "flac") param << "-c:a" << "flac" << "-compression_level" << "8";
|
||||
// else specify mp3, vbr v0
|
||||
else param << "-f" << "mp3" << "-codec:a" << "libmp3lame"<< "-q:a" << "0";
|
||||
param << fileName;
|
||||
|
||||
enc.start(FFMPEG, param);
|
||||
enc.waitForStarted();
|
||||
|
||||
std::vector<char> dat;
|
||||
dat.resize(1024);
|
||||
|
||||
//QElapsedTimer timer;
|
||||
//timer.start();
|
||||
//mode = Rendering;
|
||||
while (mode == Rendering) {
|
||||
enc.write(&dat[0], readData(&dat[0], 1024));
|
||||
}
|
||||
//std::cout << "Render finished in " << static_cast<float>(timer.elapsed())/1000 << " seconds." << std::endl;
|
||||
|
||||
enc.closeWriteChannel();
|
||||
enc.waitForFinished();
|
||||
|
||||
stop();
|
||||
|
||||
});
|
||||
|
||||
while (mode == Rendering && project == p) QCoreApplication::processEvents(); // hold modality but allow UI updates
|
||||
|
||||
//qDebug() << enc.readAllStandardOutput();
|
||||
//qDebug() << enc.readAllStandardError();
|
||||
|
@ -270,7 +326,7 @@ void AudioEngine::buildQueue() {
|
|||
// keep track of what was there before
|
||||
std::unordered_set<Node*> prev;
|
||||
prev.reserve(queue.size() + 1);
|
||||
for (auto n : queue) prev.insert(n.get());
|
||||
for (auto& n : queue) prev.insert(n.get());
|
||||
|
||||
queue.clear();
|
||||
// stuff
|
||||
|
@ -285,11 +341,11 @@ void AudioEngine::buildQueue() {
|
|||
|
||||
while (!qCurrent->empty()) {
|
||||
// ... this could be made more efficient with some redundancy checking, but whatever
|
||||
for (auto n : *qCurrent) {
|
||||
for (auto& n : *qCurrent) {
|
||||
qf.push_front(n); // add to actual queue
|
||||
for (auto p1 : n->inputs) { // data types...
|
||||
for (auto p2 : p1.second) { // ports...
|
||||
for (auto p3 : p2.second->connections) { // connected ports!
|
||||
for (auto& p1 : n->inputs) { // data types...
|
||||
for (auto& p2 : p1.second) { // ports...
|
||||
for (auto& p3 : p2.second->connections) { // connected ports!
|
||||
auto pc = p3.lock();
|
||||
if (!pc) continue;
|
||||
auto pcn = pc->owner.lock();
|
||||
|
@ -311,7 +367,7 @@ void AudioEngine::buildQueue() {
|
|||
|
||||
// assemble final deduplicated queue
|
||||
std::unordered_set<Node*> dd;
|
||||
for (auto n : qf) {
|
||||
for (auto& n : qf) {
|
||||
if (dd.find(n.get()) == dd.end()) {
|
||||
queue.push_back(n);
|
||||
dd.insert(n.get());
|
||||
|
@ -350,8 +406,8 @@ qint64 AudioEngine::readData(char *data, qint64 maxlen) {
|
|||
// 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);
|
||||
*l = static_cast<int16_t>(std::clamp(buffer[0][bufPos], -1.0f, 1.0f) * 32767);
|
||||
*r = static_cast<int16_t>(std::clamp(buffer[1][bufPos], -1.0f, 1.0f) * 32767);
|
||||
*l = static_cast<int16_t>(std::clamp(buffer[0][bufPos] * 32768.0, -32767.0, 32767.0));
|
||||
*r = static_cast<int16_t>(std::clamp(buffer[1][bufPos] * 32768.0, -32767.0, 32767.0));
|
||||
|
||||
bufPos++;
|
||||
data += stride;
|
||||
|
@ -414,7 +470,7 @@ void AudioEngine::nextTick() {
|
|||
processNodes();
|
||||
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);
|
||||
size_t bufs = ts * sizeof(bufferType);
|
||||
memcpy(buffer[0].data(), p->bufL, bufs);
|
||||
memcpy(buffer[1].data(), p->bufR, bufs);
|
||||
}
|
||||
|
@ -458,8 +514,20 @@ void AudioEngine::nextTick() {
|
|||
// process global commands first
|
||||
for (int c = 0; c < static_cast<int>(p->numChannels()); c++) {
|
||||
if (auto& row = p->rowAt(c, curRow); row.port == -2 && row.params) {
|
||||
for (auto p : *row.params) {
|
||||
if (p[0] == 't' && p[1] > 0) tempo = p[1];
|
||||
auto& p = *row.params;
|
||||
auto n = p.size();
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (p[i][0] == 't') { // tempo
|
||||
auto ot = tempo;
|
||||
tempo = p[i][1];
|
||||
double m = 1.0;
|
||||
while (i < n-1 && p[i+1][0] == ',') { // param notation: little-endian
|
||||
m *= 256.0;
|
||||
tempo += m*p[i+1][1];
|
||||
i++;
|
||||
}
|
||||
if (tempo <= 1) tempo = ot; // reject tempo changes below 1bpm for safety
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -509,7 +577,7 @@ void AudioEngine::nextTick() {
|
|||
}
|
||||
|
||||
auto& cpm = project->rootGraph->inputs[Port::Command];
|
||||
for (auto p_ : cpm) {
|
||||
for (auto& p_ : cpm) {
|
||||
auto* pt = static_cast<CommandPort*>(p_.second.get());
|
||||
//if (pt->passthroughTo.lock()->connections.empty()) continue; // port isn't hooked up to anything
|
||||
uint8_t idx = pt->index;
|
||||
|
@ -586,7 +654,7 @@ void AudioEngine::nextTick() {
|
|||
processNodes();
|
||||
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);
|
||||
size_t bufs = ts * sizeof(bufferType);
|
||||
memcpy(buffer[0].data(), p->bufL, bufs);
|
||||
memcpy(buffer[1].data(), p->bufR, bufs);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#include <QSemaphore>
|
||||
#include <QWaitCondition>
|
||||
|
||||
#include "audio/audio.h"
|
||||
|
||||
class QThread;
|
||||
namespace Xybrid::Data {
|
||||
class Project;
|
||||
|
@ -53,12 +55,16 @@ namespace Xybrid::Audio {
|
|||
private:
|
||||
QThread* thread;
|
||||
std::unique_ptr<QAudioOutput> output;
|
||||
QIODevice* outStream;
|
||||
int sampleRate = 48000;
|
||||
int bufferMs = 64;
|
||||
|
||||
std::vector<float> buffer[2];
|
||||
std::vector<bufferType> buffer[2];
|
||||
size_t bufPos = 0;
|
||||
//std::vector<char> outBuf;
|
||||
|
||||
static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough
|
||||
// 32MiB really isn't much to take up for being a far higher ceiling than we should ever need
|
||||
static const constexpr size_t tickBufSize = (1024*1024*32);
|
||||
std::unique_ptr<size_t[]> tickBuf;
|
||||
std::atomic<size_t*> tickBufPtr;
|
||||
size_t* tickBufEnd;
|
||||
|
@ -95,9 +101,11 @@ namespace Xybrid::Audio {
|
|||
void postInit();
|
||||
void initAudio(bool startNow = false);
|
||||
void deinitAudio();
|
||||
void startOutput();
|
||||
Data::Pattern* findPattern(int = 0);
|
||||
void nextTick();
|
||||
void processNodes();
|
||||
|
||||
public:
|
||||
static void init();
|
||||
inline constexpr PlaybackMode playbackMode() const { return mode; }
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
namespace Xybrid::Config {
|
||||
namespace AudioConfig {
|
||||
extern int playbackSampleRate;
|
||||
extern int playbackBufferMs;
|
||||
|
||||
extern int previewSampleRate;
|
||||
extern int previewBufferMs;
|
||||
|
||||
extern int renderSampleRate;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
#include "colorscheme.h"
|
||||
using Xybrid::Config::ColorScheme;
|
||||
|
||||
ColorScheme Xybrid::Config::colorScheme;
|
|
@ -25,6 +25,7 @@ namespace Xybrid::Config {
|
|||
QColor waveformBg = {23, 23, 23};
|
||||
QColor waveformBgHighlight = {31, 31, 47};
|
||||
QColor waveformFgPrimary = {191, 163, 255};
|
||||
QColor waveformLoopPoints = {255, 127, 127};
|
||||
};
|
||||
extern ColorScheme colorScheme;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
#include "audioconfig.h"
|
||||
#include "uiconfig.h"
|
||||
#include "colorscheme.h"
|
||||
#include "directories.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
// Audio defaults
|
||||
int AudioConfig::playbackSampleRate = 48000;
|
||||
int AudioConfig::playbackBufferMs = 64;
|
||||
|
||||
int AudioConfig::previewSampleRate = 48000;
|
||||
int AudioConfig::previewBufferMs = 64;
|
||||
|
||||
int AudioConfig::renderSampleRate = 48000;
|
||||
|
||||
// UIConfig defaults
|
||||
bool UIConfig::verticalKnobs = false;
|
||||
bool UIConfig::invertScrollWheel = false;
|
||||
|
||||
// instantiate color scheme
|
||||
ColorScheme Xybrid::Config::colorScheme;
|
||||
|
||||
// Directories
|
||||
const QString Directories::configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/config.dat");
|
||||
const QString Directories::stateFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/state.dat");
|
||||
|
||||
QString Directories::projects = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/projects");
|
||||
QString Directories::presets = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/nodes");
|
||||
QString Directories::userDefaultTemplate = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/default.xyp");
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#include "directories.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include <QStandardPaths>
|
||||
|
||||
const QString Directories::configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/config.json");
|
||||
|
||||
QString Directories::projects = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/projects");
|
||||
QString Directories::presets = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/nodes");
|
|
@ -5,8 +5,10 @@
|
|||
namespace Xybrid::Config {
|
||||
namespace Directories {
|
||||
const extern QString configFile;
|
||||
const extern QString stateFile;
|
||||
|
||||
extern QString projects;
|
||||
extern QString presets;
|
||||
extern QString userDefaultTemplate;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ using Xybrid::Gadgets::IOPort;
|
|||
|
||||
#include "util/strings.h"
|
||||
|
||||
namespace {
|
||||
namespace { // clazy:excludeall=non-pod-global-static
|
||||
typedef std::list<std::function<void()>> fqueue; // typedef so QtCreator's auto indent doesn't completely break :|
|
||||
fqueue& regQueue() {
|
||||
static fqueue q;
|
||||
|
@ -27,15 +27,15 @@ namespace {
|
|||
QHash<QString, std::shared_ptr<PluginInfo>> plugins;
|
||||
|
||||
QString priorityCategories[] {
|
||||
"Gadget", "Instrument", "Sampler", "Effect"
|
||||
"Gadget", "Effect", "Instrument", "Sampler"
|
||||
};
|
||||
}
|
||||
|
||||
bool PluginRegistry::enqueueRegistration(std::function<void ()> f) {
|
||||
std::shared_ptr<PluginInfo> PluginRegistry::enqueueRegistration(std::function<void ()> f) {
|
||||
auto& queue = regQueue();
|
||||
queue.push_back(f);
|
||||
if (initialized()) f();
|
||||
return true;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PluginRegistry::init() {
|
||||
|
@ -65,7 +65,7 @@ std::shared_ptr<Node> PluginRegistry::createInstance(const QString& id) {
|
|||
void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::shared_ptr<Node>)> f, Graph* g) {
|
||||
std::map<QString, std::map<QString, std::shared_ptr<PluginInfo>>> cm; // category map
|
||||
cm.try_emplace(""); // force empty category
|
||||
for (auto i : plugins) {
|
||||
for (auto& i : qAsConst(plugins)) {
|
||||
if (i->hidden) continue;
|
||||
cm.try_emplace(i->category);
|
||||
cm[i->category][i->displayName] = i;
|
||||
|
@ -76,27 +76,27 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
|
|||
auto* mio = m->addMenu("I/O Port");
|
||||
//auto* mi = mio->addMenu("Input");
|
||||
//auto* mo = mio->addMenu("Output");
|
||||
Port::DataType d[] {Port::Audio, Port::Command};
|
||||
Port::DataType d[] {Port::Command, Port::Audio};
|
||||
|
||||
for (auto dt : d) {
|
||||
auto* mi = mio->addMenu(QString("&%1 In").arg(Util::enumName(dt)));
|
||||
auto* mo = mio->addMenu(QString("&%1 Out").arg(Util::enumName(dt)));
|
||||
auto* mi = mio->addMenu(qs("&%1 In").arg(Util::enumName(dt)));
|
||||
auto* mo = mio->addMenu(qs("&%1 Out").arg(Util::enumName(dt)));
|
||||
//mi->setStyleSheet("QMenu { menu-scrollable: 1; }");
|
||||
//mi->setFixedHeight(256);
|
||||
|
||||
for (int ih = 0; ih < 16; ih++) {
|
||||
QString n = QString::number(ih, 16).toUpper();
|
||||
auto* mis = mi->addMenu(QString(u8"%10-%1F").arg(n));
|
||||
auto* mos = mo->addMenu(QString(u8"%10-%1F").arg(n));
|
||||
auto* mis = mi->addMenu(qs("%10-%1F").arg(n));
|
||||
auto* mos = mo->addMenu(qs("%10-%1F").arg(n));
|
||||
for (int il = 0; il < 16; il++) {
|
||||
int i = ih*16+il;
|
||||
QString nn = Util::hex(i);
|
||||
mis->addAction(nn, [f, dt, i] {
|
||||
mis->addAction(nn, m, [f, dt, i] {
|
||||
auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
|
||||
n->setPort(Port::Input, dt, static_cast<uint8_t>(i));
|
||||
f(n);
|
||||
})->setEnabled(!g->port(Port::Input, dt, static_cast<uint8_t>(i)));
|
||||
mos->addAction(nn, [f, dt, i] {
|
||||
mos->addAction(nn, m, [f, dt, i] {
|
||||
auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
|
||||
n->setPort(Port::Output, dt, static_cast<uint8_t>(i));
|
||||
f(n);
|
||||
|
@ -115,7 +115,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
|
|||
if (auto c = cm.find(pc); c != cm.end()) {
|
||||
auto* ccm = m->addMenu(c->first);
|
||||
for (auto& i : c->second) {
|
||||
ccm->addAction(i.second->displayName, [f, pi = i.second] {
|
||||
ccm->addAction(i.second->displayName, m, [f, pi = i.second] {
|
||||
auto n = pi->createInstance();
|
||||
n->plugin = pi;
|
||||
n->init();
|
||||
|
@ -131,7 +131,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
|
|||
if (c.first.isEmpty() || c.second.empty()) continue;
|
||||
auto* ccm = m->addMenu(c.first);
|
||||
for (auto& i : c.second) {
|
||||
ccm->addAction(i.second->displayName, [f, pi = i.second] {
|
||||
ccm->addAction(i.second->displayName, m, [f, pi = i.second] {
|
||||
auto n = pi->createInstance();
|
||||
n->plugin = pi;
|
||||
n->init();
|
||||
|
@ -142,7 +142,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
|
|||
|
||||
m->addSeparator();
|
||||
|
||||
for (auto& i : cm[""]) m->addAction(i.second->displayName, [f, pi = i.second] {
|
||||
for (auto& i : cm[""]) m->addAction(i.second->displayName, m, [f, pi = i.second] {
|
||||
auto n = pi->createInstance();
|
||||
n->plugin = pi;
|
||||
n->init();
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace Xybrid::Config {
|
|||
};
|
||||
|
||||
namespace PluginRegistry {
|
||||
bool enqueueRegistration(std::function<void()>);
|
||||
std::shared_ptr<PluginInfo> enqueueRegistration(std::function<void()>);
|
||||
void registerPlugin(std::shared_ptr<PluginInfo>);
|
||||
void init();
|
||||
|
||||
|
@ -37,3 +37,10 @@ namespace Xybrid::Config {
|
|||
void populatePluginMenu(QMenu*, std::function<void(std::shared_ptr<Data::Node>)>, Data::Graph* = nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
#define RegisterPlugin(NAME, ...) \
|
||||
namespace { std::shared_ptr<Xybrid::Config::PluginInfo> _regInfo_##NAME = Xybrid::Config::PluginRegistry::enqueueRegistration([] { \
|
||||
auto i = std::make_shared<Xybrid::Config::PluginInfo>();\
|
||||
i->createInstance = []{ return std::make_shared<NAME>(); };\
|
||||
__VA_ARGS__ \
|
||||
Xybrid::Config::PluginRegistry::registerPlugin(i); }); }
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace Xybrid::Config {
|
||||
namespace UIConfig {
|
||||
/// Determines if KnobGadgets turn with vertical mouse movement instead of horizontal.
|
||||
extern bool verticalKnobs;
|
||||
|
||||
/// Controls if scroll wheel function is inverted for knobs, etc.
|
||||
extern bool invertScrollWheel;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
#include "uistate.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "fileops.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
std::list<QString> UIState::recentFiles;
|
||||
|
||||
void UIState::save() {
|
||||
FileOps::saveUIState();
|
||||
}
|
||||
|
||||
void UIState::addRecentFile(const QString &f) {
|
||||
if (!recentFiles.empty() && recentFiles.front() == f) return; // if it's already the most recent file, skip
|
||||
recentFiles.remove(f); // remove any existing instance from later in the list
|
||||
recentFiles.push_front(f);
|
||||
while (recentFiles.size() > MAX_RECENTS) recentFiles.pop_back(); // trim to max size
|
||||
save(); // and save changes
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <list>
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace Xybrid::Config {
|
||||
namespace UIState {
|
||||
const constexpr size_t MAX_RECENTS = 10;
|
||||
extern std::list<QString> recentFiles;
|
||||
|
||||
void save();
|
||||
void addRecentFile(const QString& f);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Xybrid::Data {
|
||||
struct AudioFrame {
|
||||
// stored as double here for operational precision
|
||||
double l = 0;
|
||||
double r = 0;
|
||||
double l = 0.0;
|
||||
double r = 0.0;
|
||||
|
||||
inline AudioFrame() = default;
|
||||
inline AudioFrame(double v) : l(v), r(v) { }
|
||||
|
@ -45,6 +47,11 @@ namespace Xybrid::Data {
|
|||
r *= m;
|
||||
}
|
||||
|
||||
inline AudioFrame flip() { return {r, l}; }
|
||||
inline AudioFrame clamp(double m = 1.0) { return { std::clamp(l, -m, m), std::clamp(r, -m, m) }; }
|
||||
|
||||
static inline AudioFrame lerp(AudioFrame a, AudioFrame b, double r) { return b * r + a * (1.0 - r); }
|
||||
|
||||
static AudioFrame gainBalanceMult(double gain, double balance = 0.0);
|
||||
inline AudioFrame gainBalance(double gain, double balance = 0.0) const { return *this*gainBalanceMult(gain, balance); }
|
||||
};
|
||||
|
|
|
@ -15,24 +15,16 @@ using namespace Xybrid::Config;
|
|||
#include <QMetaType>
|
||||
#include <QMetaEnum>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
namespace {
|
||||
std::shared_ptr<PluginInfo> inf;
|
||||
bool c = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "graph";
|
||||
i->displayName = "Subgraph";
|
||||
i->createInstance = []{ return std::make_shared<Graph>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
inf = i;
|
||||
});
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Graph, {
|
||||
i->id = "graph";
|
||||
i->displayName = "Subgraph";
|
||||
})
|
||||
|
||||
//std::string Graph::pluginName() const { return "Subgraph"; }
|
||||
|
||||
Graph::Graph() {
|
||||
plugin = inf; // harder bind
|
||||
plugin = _regInfo_Graph;//inf; // harder bind
|
||||
}
|
||||
|
||||
// propagate
|
||||
|
@ -48,7 +40,7 @@ void Graph::saveData(QCborMap& m) const {
|
|||
QCborArray c;
|
||||
|
||||
int idx = 0;
|
||||
for (auto ch : children) {
|
||||
for (auto& ch : children) {
|
||||
if (!ch->plugin) continue;
|
||||
indices[ch.get()] = idx++;
|
||||
c << ch->toCbor();
|
||||
|
@ -62,14 +54,14 @@ void Graph::saveData(QCborMap& m) const {
|
|||
// array { oIdx, dataType, pIdx, iIdx, dataType, pIdx }
|
||||
QCborArray cn;
|
||||
|
||||
for (auto ch : children) {
|
||||
for (auto& ch : children) {
|
||||
if (!ch->plugin) continue; // already skipped over
|
||||
int idx = indices[ch.get()];
|
||||
for (auto dt : ch->outputs) {
|
||||
for (auto op : dt.second) {
|
||||
for (auto& dt : ch->outputs) {
|
||||
for (auto& op : dt.second) {
|
||||
auto o = op.second;
|
||||
o->cleanConnections(); // let's just do some groundskeeping here
|
||||
for (auto iw : o->connections) {
|
||||
for (auto& iw : o->connections) {
|
||||
auto i = iw.lock();
|
||||
QCborArray c;
|
||||
c << idx;
|
||||
|
@ -123,7 +115,7 @@ void Graph::onParent(std::shared_ptr<Graph>) {
|
|||
for (auto c : children) {
|
||||
c->project = project;
|
||||
// let this handle the recursion for us, since this is all this function does
|
||||
if (c->plugin == inf) c->onParent(c->parent.lock());
|
||||
if (c->plugin == _regInfo_Graph) c->onParent(c->parent.lock());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ bool Port::connect(std::shared_ptr<Port> p) {
|
|||
// actual processing is always done on the input port, since that's where any limits are
|
||||
if (type == Output) return p->type == 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?
|
||||
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
|
||||
if (auto o = owner.lock(), po = p->owner.lock(); !o || !po || po->dependsOn(o)) return false; // no dependency loops!
|
||||
// actually hook up
|
||||
|
@ -81,6 +81,7 @@ void Port::cleanConnections() {
|
|||
std::shared_ptr<Port> Port::makePort(DataType dt) {
|
||||
if (dt == Audio) return std::make_shared<AudioPort>();
|
||||
if (dt == Command) return std::make_shared<CommandPort>();
|
||||
if (dt == Parameter) return std::make_shared<ParameterPort>();
|
||||
// fallback
|
||||
return std::make_shared<Port>();
|
||||
}
|
||||
|
@ -117,7 +118,7 @@ QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
|
|||
{ /* nodes */ } {
|
||||
QCborArray nm;
|
||||
int idx = 0;
|
||||
for (auto n : v) {
|
||||
for (auto& n : v) {
|
||||
if (n->isVolatile()) continue; // skip things with volatile locality (i/o ports etc.)
|
||||
indices[n.get()] = idx++;
|
||||
nm << n->toCbor();
|
||||
|
@ -128,21 +129,21 @@ QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
|
|||
// exported samples
|
||||
if (auto v = Sample::finishExport(); !v.empty()) {
|
||||
QCborMap smp;
|
||||
for (auto s : v) smp[QCborValue(s->uuid)] = s->toCbor();
|
||||
for (auto& s : v) smp[QCborValue(s->uuid)] = s->toCbor();
|
||||
m[qs("samples")] = smp;
|
||||
}
|
||||
|
||||
{ /* connections */ } {
|
||||
QCborArray cm;
|
||||
|
||||
for (auto n : v) {
|
||||
for (auto& n : v) {
|
||||
if (n->isVolatile()) continue; // already skipped
|
||||
int idx = indices[n.get()];
|
||||
for (auto dt : n->outputs) {
|
||||
for (auto op : dt.second) {
|
||||
for (auto& dt : n->outputs) {
|
||||
for (auto& op : dt.second) {
|
||||
auto o = op.second;
|
||||
o->cleanConnections(); // let's just do some groundskeeping here
|
||||
for (auto iw : o->connections) {
|
||||
for (auto& iw : o->connections) {
|
||||
auto i = iw.lock();
|
||||
if (auto in = indices.find(i->owner.lock().get()); in != indices.end()) { // only connections within the collection
|
||||
QCborArray c;
|
||||
|
@ -165,7 +166,7 @@ QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
|
|||
{ /* center */ } {
|
||||
int count = 0;
|
||||
QPoint center;
|
||||
for (auto n : v) {
|
||||
for (auto& n : v) {
|
||||
if (n->isVolatile()) continue;
|
||||
center += QPoint(n->x, n->y);
|
||||
count++;
|
||||
|
@ -226,7 +227,7 @@ std::vector<std::shared_ptr<Node>> Node::multiFromCbor(const QCborMap& m, std::s
|
|||
|
||||
if (!cp.isNull()) { // offset and such
|
||||
QPoint off = cp - center;
|
||||
for (auto n : v) {
|
||||
for (auto& n : v) {
|
||||
n->x += off.x();
|
||||
n->y += off.y();
|
||||
}
|
||||
|
@ -304,7 +305,7 @@ void Node::collapsePorts(Port::Type t, Port::DataType dt) {
|
|||
if (mdt == m.end()) return; // nothing there
|
||||
auto& mm = mdt->second;
|
||||
uint8_t maxIdx = 0;
|
||||
for (auto p : mm) maxIdx = std::max(maxIdx, p.first);
|
||||
for (auto& p : mm) maxIdx = std::max(maxIdx, p.first);
|
||||
uint8_t firstUnused = 255;
|
||||
for (uint8_t i = 0; i <= maxIdx; i++) {
|
||||
if (auto pi = mm.find(i); pi != mm.end()) {
|
||||
|
@ -346,7 +347,7 @@ bool Node::try_process(bool checkDependencies) {
|
|||
|
||||
if (checkDependencies) { // check if dependencies are done
|
||||
auto checkInput = Util::yCombinator([tick_this](auto checkInput, std::shared_ptr<Port> p) -> bool {
|
||||
for (auto c : p->connections) { // check each connection; if node valid...
|
||||
for (auto& c : p->connections) { // check each connection; if node valid...
|
||||
if (auto cp = c.lock(); cp) {
|
||||
if (auto n = cp->owner.lock(); n) {
|
||||
if (n->tick_last != tick_this) return false; // if node itself not yet processed, check failed
|
||||
|
@ -362,7 +363,7 @@ bool Node::try_process(bool checkDependencies) {
|
|||
return true;
|
||||
});
|
||||
|
||||
for (auto t : inputs) for (auto p : t.second) if (!checkInput(p.second)) return false;
|
||||
for (auto& t : inputs) for (auto& p : t.second) if (!checkInput(p.second)) return false;
|
||||
|
||||
/*for (auto& t : inputs) {
|
||||
for (auto& p : t.second) {
|
||||
|
|
|
@ -71,6 +71,7 @@ namespace Xybrid::Data {
|
|||
virtual DataType dataType() const { return static_cast<DataType>(-1); }
|
||||
virtual bool singleInput() const { return false; }
|
||||
virtual bool canConnectTo(DataType) const;
|
||||
inline bool isConnected() const { return connections.size() > 0; }
|
||||
/*virtual*/ bool connect(std::shared_ptr<Port>);
|
||||
/*virtual*/ void disconnect(std::shared_ptr<Port>);
|
||||
void cleanConnections();
|
||||
|
|
|
@ -14,7 +14,7 @@ void AudioPort::pull() {
|
|||
|
||||
size_t ts = audioEngine->curTickSize();
|
||||
size = ts;
|
||||
size_t s = sizeof(float) * ts;
|
||||
size_t s = sizeof(bufferType) * ts;
|
||||
|
||||
if (type == Input) {
|
||||
if (connections.size() == 1) {
|
||||
|
@ -26,11 +26,11 @@ void AudioPort::pull() {
|
|||
goto done;//return;
|
||||
}
|
||||
}
|
||||
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
|
||||
bufL = static_cast<bufferType*>(audioEngine->tickAlloc(s*2));
|
||||
bufR = &bufL[ts]; // for some reason just adding the size wonks out
|
||||
memset(bufL, 0, s*2); // clear buffers
|
||||
|
||||
for (auto c : connections) { // mix
|
||||
for (auto& c : connections) { // mix
|
||||
if (auto p = std::static_pointer_cast<AudioPort>(c.lock()); p && p->dataType() == Audio) {
|
||||
p->pull();
|
||||
for (size_t i = 0; i < ts; i++) {
|
||||
|
@ -45,7 +45,7 @@ void AudioPort::pull() {
|
|||
bufL = pt->bufL;
|
||||
bufR = pt->bufR;
|
||||
} else { // output without valid passthrough, just clear and prepare a blank buffer
|
||||
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
|
||||
bufL = static_cast<bufferType*>(audioEngine->tickAlloc(s*2));
|
||||
bufR = &bufL[ts];
|
||||
memset(bufL, 0, s*2); // clear buffers
|
||||
}
|
||||
|
@ -60,13 +60,13 @@ void CommandPort::pull() {
|
|||
lock.lock();
|
||||
if (tickUpdatedOn == t) { lock.unlock(); return; } // someone else got here before us
|
||||
|
||||
dataSize = 0;
|
||||
size = 0;
|
||||
if (type == Input) {
|
||||
for (auto c : connections) {
|
||||
for (auto& c : connections) {
|
||||
if (auto p = std::static_pointer_cast<CommandPort>(c.lock()); p && p->dataType() == Command) {
|
||||
p->pull();
|
||||
data = p->data; // just repoint to input's buffer
|
||||
dataSize = p->dataSize;
|
||||
size = p->size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ void CommandPort::pull() {
|
|||
// valid passthrough
|
||||
pt->pull();
|
||||
data = pt->data; // again, just repoint
|
||||
dataSize = pt->dataSize;
|
||||
size = pt->size;
|
||||
} // don't need an else case, size is already zero
|
||||
|
||||
tickUpdatedOn = t;
|
||||
|
@ -83,7 +83,37 @@ void CommandPort::pull() {
|
|||
|
||||
void CommandPort::push(std::vector<uint8_t> v) {
|
||||
tickUpdatedOn = audioEngine->curTickId();
|
||||
dataSize = v.size();
|
||||
data = static_cast<uint8_t*>(audioEngine->tickAlloc(dataSize));
|
||||
memcpy(data, v.data(), dataSize);
|
||||
size = v.size();
|
||||
data = static_cast<uint8_t*>(audioEngine->tickAlloc(size));
|
||||
memcpy(data, v.data(), size);
|
||||
}
|
||||
|
||||
void ParameterPort::pull() {
|
||||
auto t = audioEngine->curTickId();
|
||||
if (tickUpdatedOn == t) return;
|
||||
lock.lock();
|
||||
if (tickUpdatedOn == t) { lock.unlock(); return; } // someone else got here before us
|
||||
|
||||
data = nullptr;
|
||||
if (type == Input) {
|
||||
if (isConnected()) {
|
||||
if (auto p = std::static_pointer_cast<ParameterPort>(connections[0].lock()); p) {
|
||||
p->pull();
|
||||
data = p->data;
|
||||
size = p->size;
|
||||
}
|
||||
}
|
||||
} else if (auto pt = std::static_pointer_cast<ParameterPort>(passthroughTo.lock()); pt && pt->dataType() == Parameter) {
|
||||
pt->pull();
|
||||
data = pt->data;
|
||||
size = pt->size;
|
||||
}
|
||||
if (!data) { // no buffer pulled from input or passthrough; create a new one
|
||||
size = audioEngine->curTickSize();
|
||||
data = static_cast<double*>(audioEngine->tickAlloc(size * sizeof(double)));
|
||||
std::fill_n(data, size, std::numeric_limits<double>::quiet_NaN());
|
||||
}
|
||||
|
||||
tickUpdatedOn = t;
|
||||
lock.unlock();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "data/node.h"
|
||||
#include "data/audioframe.h"
|
||||
#include "audio/audio.h"
|
||||
|
||||
namespace Xybrid::Data {
|
||||
class AudioPort : public Port {
|
||||
|
@ -13,21 +14,21 @@ namespace Xybrid::Data {
|
|||
FrameRef(AudioPort* port, size_t at) : port(port), at(at) { }
|
||||
public:
|
||||
FrameRef& operator=(AudioFrame f) {
|
||||
port->bufL[at] = static_cast<float>(f.l);
|
||||
port->bufR[at] = static_cast<float>(f.r);
|
||||
port->bufL[at] = static_cast<Audio::bufferType>(f.l);
|
||||
port->bufR[at] = static_cast<Audio::bufferType>(f.r);
|
||||
return *this;
|
||||
}
|
||||
FrameRef& operator+=(AudioFrame f) {
|
||||
port->bufL[at] += static_cast<float>(f.l);
|
||||
port->bufR[at] += static_cast<float>(f.r);
|
||||
port->bufL[at] += static_cast<Audio::bufferType>(f.l);
|
||||
port->bufR[at] += static_cast<Audio::bufferType>(f.r);
|
||||
return *this;
|
||||
}
|
||||
operator AudioFrame() const { return { port->bufL[at], port->bufR[at] }; }
|
||||
AudioFrame operator*(AudioFrame o) const { return static_cast<AudioFrame>(*this) * o; }
|
||||
};
|
||||
|
||||
float* bufL;
|
||||
float* bufR;
|
||||
Audio::bufferType* bufL;
|
||||
Audio::bufferType* bufR;
|
||||
size_t size;
|
||||
|
||||
AudioPort() = default;
|
||||
|
@ -44,7 +45,7 @@ namespace Xybrid::Data {
|
|||
class CommandPort : public Port {
|
||||
public:
|
||||
uint8_t* data;
|
||||
size_t dataSize;
|
||||
size_t size;
|
||||
|
||||
CommandPort() = default;
|
||||
~CommandPort() override = default;
|
||||
|
@ -57,4 +58,20 @@ namespace Xybrid::Data {
|
|||
/// Push a data buffer
|
||||
void push(std::vector<uint8_t>);
|
||||
};
|
||||
|
||||
class ParameterPort : public Port {
|
||||
public:
|
||||
double* data;
|
||||
size_t size;
|
||||
|
||||
ParameterPort() = default;
|
||||
~ParameterPort() override = default;
|
||||
|
||||
double& operator[](size_t at) { return data[at]; }
|
||||
double& at(size_t at) { return data[at]; }
|
||||
|
||||
Port::DataType dataType() const override { return Port::Parameter; }
|
||||
|
||||
void pull() override;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ using namespace Xybrid::Data;
|
|||
std::array<float, 2> Sample::plotBetween(size_t ch, size_t start, size_t end) const {
|
||||
if (end < start) end = start;
|
||||
if (ch >= 2 || start >= data[ch].size()) return {0, 0};
|
||||
end = std::min(end, data[ch].size());
|
||||
end = std::min(end, data[ch].size()-1);
|
||||
float mx = -100;
|
||||
float mn = 100;
|
||||
|
||||
|
@ -48,22 +48,48 @@ std::array<float, 2> Sample::plotBetween(size_t ch, size_t start, size_t end) co
|
|||
return {mn, mx};
|
||||
}
|
||||
|
||||
// threshold in MiB-stored-as-float for saving long samples as s16 instead
|
||||
const constexpr double PCM_MiB_THRESHOLD = 5;
|
||||
const constexpr int PCM_THRESHOLD = static_cast<int>(PCM_MiB_THRESHOLD * (1024*1024) / sizeof(float));
|
||||
QCborMap Sample::toCbor() const {
|
||||
QCborMap m;
|
||||
|
||||
m[qs("name")] = name;
|
||||
m[qs("rate")] = sampleRate;
|
||||
|
||||
QString fmt = qs("f32");
|
||||
if (numChannels() * length() > PCM_THRESHOLD) fmt = qs("s16");
|
||||
m[qs("fmt")] = fmt;
|
||||
|
||||
{
|
||||
QCborArray ch;
|
||||
|
||||
auto n = static_cast<size_t>(numChannels());
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
ch[static_cast<qsizetype>(i)] = QByteArray(reinterpret_cast<const char*>(data[i].data()), static_cast<int>(data[i].size() * sizeof(data[i][0])));
|
||||
if (fmt == qs("f32")) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
ch[static_cast<qsizetype>(i)] = QByteArray(reinterpret_cast<const char*>(data[i].data()), static_cast<int>(data[i].size() * sizeof(data[i][0])));
|
||||
}
|
||||
} else if (fmt == qs("s16")) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
auto sz = data[i].size();
|
||||
QByteArray dat(static_cast<int>(sz * 2), static_cast<char>(0));
|
||||
|
||||
for (size_t j = 0; j < sz; j++) *reinterpret_cast<int16_t*>(dat.data() + j*2) = static_cast<int16_t>(std::clamp(static_cast<double>(data[i][j]) * 32768.0, -32767.0, 32767.0));
|
||||
ch[static_cast<qsizetype>(i)] = dat;
|
||||
}
|
||||
}
|
||||
|
||||
m[qs("channels")] = ch;
|
||||
}
|
||||
|
||||
if (loopStart >= 0) { // only store if there is a loop point
|
||||
m[qs("loopStart")] = loopStart;
|
||||
m[qs("loopEnd")] = loopEnd;
|
||||
}
|
||||
|
||||
m[qs("note")] = baseNote;
|
||||
m[qs("subNote")] = subNote;
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
|
@ -74,16 +100,37 @@ std::shared_ptr<Sample> Sample::fromCbor(const QCborMap& m, QUuid uuid) {
|
|||
|
||||
smp->sampleRate = static_cast<int>(m.value("rate").toInteger(48000));
|
||||
|
||||
auto fmt = m.value("fmt").toString(qs("f32"));
|
||||
|
||||
auto ch = m.value("channels").toArray();
|
||||
auto s = static_cast<size_t>(ch.size());
|
||||
|
||||
for (size_t i = 0; i < s; i++) {
|
||||
auto c = ch[static_cast<qint64>(i)].toByteArray();
|
||||
auto bs = static_cast<size_t>(c.size());
|
||||
smp->data[i].resize(bs / sizeof(*smp->data[i].begin()));
|
||||
memcpy(smp->data[i].data(), c.constData(), bs);
|
||||
if (fmt == qs("f32")) {
|
||||
for (size_t i = 0; i < s; i++) {
|
||||
auto c = ch[static_cast<qint64>(i)].toByteArray();
|
||||
auto bs = static_cast<size_t>(c.size());
|
||||
smp->data[i].resize(bs / sizeof(*smp->data[i].begin()));
|
||||
memcpy(smp->data[i].data(), c.constData(), bs);
|
||||
}
|
||||
} else if (fmt == qs("s16")) {
|
||||
for (size_t i = 0; i < s; i++) {
|
||||
auto c = ch[static_cast<qint64>(i)].toByteArray();
|
||||
auto bs = static_cast<size_t>(c.size());
|
||||
auto sz = bs / 2;
|
||||
smp->data[i].resize(sz);
|
||||
for (size_t j = 0; j < sz; j++) {
|
||||
smp->data[i][j] = static_cast<float>(static_cast<double>(*reinterpret_cast<int16_t*>(c.data()+j*2))/32768.0);
|
||||
}
|
||||
//memcpy(smp->data[i].data(), c.constData(), bs);
|
||||
}
|
||||
}
|
||||
|
||||
smp->loopStart = static_cast<int>(m.value("loopStart").toInteger(-1));
|
||||
smp->loopEnd = static_cast<int>(m.value("loopEnd").toInteger(-1));
|
||||
|
||||
smp->baseNote = static_cast<int>(m.value("note").toInteger(60));
|
||||
smp->subNote = m.value("subNote").toDouble(0.0);
|
||||
|
||||
return smp;
|
||||
}
|
||||
std::shared_ptr<Sample> Sample::fromCbor(const QCborValue& m, QUuid uuid) { return fromCbor(m.toMap(), uuid); }
|
||||
|
@ -188,7 +235,7 @@ std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
|
|||
qCritical() << (probe.errorString());
|
||||
}
|
||||
auto mystdout = probe.readAllStandardOutput();
|
||||
auto mystderr = probe.readAllStandardError();
|
||||
// auto mystderr = probe.readAllStandardError();
|
||||
auto doc = QJsonDocument::fromJson(mystdout);
|
||||
info = doc.object()["streams"].toArray().first().toObject();
|
||||
}
|
||||
|
@ -236,7 +283,7 @@ std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
|
|||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
namespace { // clazy:excludeall=non-pod-global-static
|
||||
bool exporting = false;
|
||||
std::unordered_map<Sample*, bool> exportMap;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,12 @@ namespace Xybrid::Data {
|
|||
|
||||
std::array<std::vector<float>, 2> data;
|
||||
|
||||
int loopStart = -1;
|
||||
int loopEnd = -1;
|
||||
|
||||
int baseNote = 60;
|
||||
double subNote = 0.0;
|
||||
|
||||
inline AudioFrame operator[] (size_t at) const {
|
||||
if (data[1].empty()) return {data[0][at]};
|
||||
return {data[0][at], data[1][at]};
|
||||
|
@ -40,6 +46,8 @@ namespace Xybrid::Data {
|
|||
inline int length() const { return static_cast<int>(data[0].size()); }
|
||||
std::array<float, 2> plotBetween(size_t ch, size_t start, size_t end) const;
|
||||
|
||||
inline double getNote() const { return static_cast<double>(baseNote) + subNote; }
|
||||
|
||||
QCborMap toCbor() const;
|
||||
static std::shared_ptr<Sample> fromCbor(const QCborMap&, QUuid);
|
||||
static std::shared_ptr<Sample> fromCbor(const QCborValue&, QUuid);
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
#include "fileops.h"
|
||||
|
||||
#include "uisocket.h"
|
||||
|
||||
#include "config/audioconfig.h"
|
||||
#include "config/uistate.h"
|
||||
#include "config/uiconfig.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <QFile>
|
||||
#include <QCborMap>
|
||||
#include <QCborArray>
|
||||
#include <QCborStreamReader>
|
||||
#include <QCborStreamWriter>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include <QUndoStack>
|
||||
#include <QFileDialog>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
using Xybrid::Data::Project;
|
||||
using Xybrid::Data::Pattern;
|
||||
using Xybrid::Data::Graph;
|
||||
using Xybrid::Data::Node;
|
||||
|
||||
namespace FileOps = Xybrid::FileOps;
|
||||
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
namespace { // utilities
|
||||
struct _MapSaver {
|
||||
QCborMap& root;
|
||||
QString name;
|
||||
QCborMap map;
|
||||
|
||||
_MapSaver(QCborMap& root, const QString& name) : root(root), name(name) { }
|
||||
~_MapSaver() {
|
||||
root[name] = map;
|
||||
}
|
||||
inline auto operator[](const QString& s) { return map[s]; }
|
||||
};
|
||||
|
||||
inline void load(QCborValueRef m, QString& v) { v = m.toString(v); }
|
||||
inline void load(QCborValueRef m, bool& v) { v = m.toBool(v); }
|
||||
inline void load(QCborValueRef m, int& v) { v = static_cast<int>(m.toInteger(v)); }
|
||||
}
|
||||
|
||||
#define lsection(NAME) if (auto _sec = root[qs(#NAME)].toMap(); !_sec.isEmpty())
|
||||
#define lvar(NS, NAME) load(_sec[qs(#NAME)], NS::NAME)
|
||||
|
||||
#define ssection(NAME) if (_MapSaver _sec(root, qs(#NAME)) ; true)
|
||||
#define svar(NS, NAME) _sec[qs(#NAME)] = NS::NAME
|
||||
|
||||
void FileOps::loadConfig() {
|
||||
QFile file(Config::Directories::configFile);
|
||||
if (file.open({QFile::ReadOnly})) { // file exists! read in
|
||||
QCborStreamReader read(&file);
|
||||
auto root = QCborValue::fromCbor(read).toMap();
|
||||
file.close();
|
||||
|
||||
lsection(directories) {
|
||||
lvar(Directories, projects);
|
||||
lvar(Directories, presets);
|
||||
}
|
||||
|
||||
lsection(ui) {
|
||||
lvar(UIConfig, verticalKnobs);
|
||||
lvar(UIConfig, invertScrollWheel);
|
||||
}
|
||||
|
||||
lsection(audio) {
|
||||
lvar(AudioConfig, playbackSampleRate);
|
||||
lvar(AudioConfig, playbackBufferMs);
|
||||
lvar(AudioConfig, previewSampleRate);
|
||||
lvar(AudioConfig, previewBufferMs);
|
||||
lvar(AudioConfig, renderSampleRate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// make sure directories exist
|
||||
if (auto d = QDir(Directories::projects); !d.exists()) d.mkpath(".");
|
||||
if (auto d = QDir(Directories::presets); !d.exists()) d.mkpath(".");
|
||||
}
|
||||
|
||||
void FileOps::saveConfig() {
|
||||
QFileInfo fi(Directories::configFile);
|
||||
fi.dir().mkpath("."); // make sure directory exists
|
||||
|
||||
QFile file(fi.filePath());
|
||||
if (!file.open({QFile::WriteOnly})) return;
|
||||
|
||||
QCborMap root;
|
||||
|
||||
ssection(directories) {
|
||||
svar(Directories, projects);
|
||||
svar(Directories, presets);
|
||||
}
|
||||
|
||||
ssection(ui) {
|
||||
svar(UIConfig, verticalKnobs);
|
||||
svar(UIConfig, invertScrollWheel);
|
||||
}
|
||||
|
||||
ssection(audio) {
|
||||
svar(AudioConfig, playbackSampleRate);
|
||||
svar(AudioConfig, playbackBufferMs);
|
||||
svar(AudioConfig, previewSampleRate);
|
||||
svar(AudioConfig, previewBufferMs);
|
||||
svar(AudioConfig, renderSampleRate);
|
||||
}
|
||||
|
||||
// write out
|
||||
QCborStreamWriter w(&file);
|
||||
root.toCborValue().toCbor(w);
|
||||
file.close();
|
||||
}
|
||||
|
||||
void FileOps::loadUIState() {
|
||||
QFile file(Directories::stateFile);
|
||||
if (file.open({QFile::ReadOnly})) { // file exists! read in
|
||||
QCborStreamReader read(&file);
|
||||
auto root = QCborValue::fromCbor(read).toMap();
|
||||
file.close();
|
||||
|
||||
if (auto recent = root[qs("recent")].toArray(); !recent.isEmpty()) {
|
||||
UIState::recentFiles.clear();
|
||||
for (auto r : recent) UIState::recentFiles.push_back(r.toString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void FileOps::saveUIState() {
|
||||
QFileInfo fi(Directories::stateFile);
|
||||
fi.dir().mkpath("."); // make sure directory exists
|
||||
|
||||
QFile file(fi.filePath());
|
||||
if (!file.open({QFile::WriteOnly})) return;
|
||||
|
||||
QCborMap root;
|
||||
|
||||
{
|
||||
QCborArray recent;
|
||||
for (auto& r : UIState::recentFiles) recent.append(r);
|
||||
root[qs("recent")] = recent;
|
||||
}
|
||||
|
||||
// write out
|
||||
QCborStreamWriter w(&file);
|
||||
root.toCborValue().toCbor(w);
|
||||
file.close();
|
||||
}
|
|
@ -35,26 +35,36 @@ namespace {
|
|||
+ (static_cast<uint32_t>(minor)<<16)
|
||||
+ (static_cast<uint32_t>(major)<<24);
|
||||
}
|
||||
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,1);
|
||||
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,2);
|
||||
|
||||
constexpr const QSize dlgSize(700, 500);
|
||||
}
|
||||
|
||||
const QString FileOps::Filter::project = qs("Xybrid project (*.xyp);;All files (*)");
|
||||
const QString FileOps::Filter::node = qs("Xybrid node (*.xyn);;All files (*)");
|
||||
|
||||
const QString FileOps::Filter::audioIn = qs("Audio files (*.mp3, *.ogg, *.flac, *.wav);;MPEG Layer 3 (*.mp3);;All files (*)");
|
||||
const QString FileOps::Filter::audioOut = qs("MPEG Layer 3 (*.mp3)"); // only supported formats
|
||||
const QString FileOps::Filter::audioIn = qs("Audio files (*.mp3 *.ogg *.flac *.wav);;MPEG Layer 3 (*.mp3);;All files (*)");
|
||||
const QString FileOps::Filter::audioOut = qs("Audio files (*.mp3 *.flac);;MPEG Layer 3 (*.mp3);;FLAC (*.flac)"); // only supported formats
|
||||
|
||||
QString FileOps::showOpenDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter) {
|
||||
return QFileDialog::getOpenFileName(parent, caption, directory, filter); // just a wrapper for now
|
||||
QFileDialog dlg(parent, caption, directory, filter);
|
||||
dlg.resize(dlgSize);
|
||||
dlg.setFileMode(QFileDialog::ExistingFile);
|
||||
dlg.setAcceptMode(QFileDialog::AcceptOpen);
|
||||
if (!dlg.exec()) return QString(); // canceled
|
||||
auto sf = dlg.selectedFiles().at(0);
|
||||
return sf;
|
||||
}
|
||||
|
||||
QString FileOps::showSaveAsDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter, const QString& suffix) {
|
||||
QFileDialog dlg(parent, caption, directory, filter);
|
||||
dlg.resize(dlgSize);
|
||||
dlg.setDefaultSuffix(suffix);
|
||||
dlg.setFileMode(QFileDialog::AnyFile);
|
||||
dlg.setAcceptMode(QFileDialog::AcceptSave);
|
||||
if (!dlg.exec()) return QString(); // canceled
|
||||
return dlg.selectedFiles()[0];
|
||||
auto sf = dlg.selectedFiles().at(0);
|
||||
return sf;
|
||||
}
|
||||
|
||||
bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
|
||||
|
@ -63,7 +73,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
|
|||
if (fileName.isEmpty()) return false; // fail
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QFile::WriteOnly)) return false;
|
||||
if (!file.open({QFile::WriteOnly})) return false;
|
||||
|
||||
// header
|
||||
QCborArray root;
|
||||
|
@ -90,7 +100,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
|
|||
|
||||
{ /* Patterns */ } {
|
||||
QCborArray ptns;
|
||||
for (auto p : project->patterns) {
|
||||
for (auto& p : project->patterns) {
|
||||
QCborMap pm;
|
||||
pm[qs("name")] = p->name;
|
||||
pm[qs("fold")] = p->fold;
|
||||
|
@ -135,7 +145,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
|
|||
|
||||
{ /* Samples */ } {
|
||||
QCborMap smp;
|
||||
for (auto s : project->samples) smp[QCborValue(s->uuid)] = s->toCbor();
|
||||
for (auto& s : qAsConst(project->samples)) smp[QCborValue(s->uuid)] = s->toCbor();
|
||||
main[qs("samples")] = smp;
|
||||
}
|
||||
|
||||
|
@ -158,7 +168,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
|
|||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<Project> FileOps::loadProject(QString fileName) {
|
||||
std::shared_ptr<Project> FileOps::loadProject(QString fileName, bool asTemplate) {
|
||||
QCborArray root;
|
||||
{
|
||||
QFile file(fileName);
|
||||
|
@ -171,13 +181,12 @@ std::shared_ptr<Project> FileOps::loadProject(QString fileName) {
|
|||
}
|
||||
|
||||
// header and sanity checks
|
||||
if (root.at(0) != QString("xybrid:project")) return nullptr; // not a project
|
||||
if (root.at(0) != qs("xybrid:project")) return nullptr; // not a project
|
||||
if (auto v = root.at(1); !v.isInteger() || v.toInteger() > XYBRID_VERSION) return nullptr; // invalid version or too new
|
||||
if (!root.at(2).isMap()) return nullptr; // so close, but... nope
|
||||
|
||||
// intentionally allocate project and control block separately
|
||||
std::shared_ptr<Project> project(new Project());
|
||||
project->fileName = fileName;
|
||||
auto project = std::make_shared<Project>();
|
||||
if (!asTemplate) project->fileName = fileName;
|
||||
QCborMap main = root.at(2).toMap();
|
||||
|
||||
{ /* Project metadata */ } {
|
||||
|
@ -259,9 +268,23 @@ std::shared_ptr<Project> FileOps::loadProject(QString fileName) {
|
|||
return project;
|
||||
}
|
||||
|
||||
std::shared_ptr<Project> FileOps::newProject(bool useTemplate) {
|
||||
std::shared_ptr<Project> project;
|
||||
if (useTemplate) {
|
||||
project = loadProject(Config::Directories::userDefaultTemplate, true);
|
||||
if (!project) project = loadProject(":/template/default.xyp", true);
|
||||
}
|
||||
if (!project) {
|
||||
project = std::make_shared<Project>();
|
||||
project->sequence.emplace_back(project->newPattern());
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
bool FileOps::saveNode(std::shared_ptr<Node> node, QString fileName) {
|
||||
QFile file(fileName);
|
||||
if (!file.open(QFile::WriteOnly)) return false;
|
||||
if (!file.open({QFile::WriteOnly})) return false;
|
||||
|
||||
Sample::startExport();
|
||||
|
||||
|
@ -272,7 +295,7 @@ bool FileOps::saveNode(std::shared_ptr<Node> node, QString fileName) {
|
|||
// and write in any exported samples
|
||||
if (auto v = Sample::finishExport(); !v.empty()) {
|
||||
QCborMap smp;
|
||||
for (auto s : v) smp[QCborValue(s->uuid)] = s->toCbor();
|
||||
for (auto& s : v) smp[QCborValue(s->uuid)] = s->toCbor();
|
||||
root << smp;
|
||||
}
|
||||
|
||||
|
@ -317,45 +340,4 @@ std::shared_ptr<Node> FileOps::loadNode(QString fileName, std::shared_ptr<Graph>
|
|||
return Node::fromCbor(root.at(2), parent); // let Node handle the rest
|
||||
}
|
||||
|
||||
void FileOps::loadConfig() {
|
||||
QFile file(Config::Directories::configFile);
|
||||
if (file.open(QFile::ReadOnly)) { // file exists! read in
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
auto root = doc.object();
|
||||
file.close();
|
||||
|
||||
if (auto dirs = root["directories"].toObject(); !dirs.isEmpty()) {
|
||||
if (auto s = dirs["projects"].toString(); !s.isNull()) Config::Directories::projects = s;
|
||||
if (auto s = dirs["presets"].toString(); !s.isNull()) Config::Directories::presets = s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// make sure directories exist
|
||||
if (auto d = QDir(Config::Directories::projects); !d.exists()) d.mkpath(".");
|
||||
if (auto d = QDir(Config::Directories::presets); !d.exists()) d.mkpath(".");
|
||||
}
|
||||
|
||||
void FileOps::saveConfig() {
|
||||
QFileInfo fi(Config::Directories::configFile);
|
||||
fi.dir().mkpath("."); // make sure directory exists
|
||||
|
||||
QFile file(fi.filePath());
|
||||
if (!file.open(QFile::WriteOnly)) return;
|
||||
|
||||
QJsonDocument doc;
|
||||
QJsonObject root;
|
||||
|
||||
{
|
||||
QJsonObject dirs;
|
||||
|
||||
dirs["projects"] = Config::Directories::projects;
|
||||
dirs["presets"] = Config::Directories::presets;
|
||||
|
||||
root["directories"] = dirs;
|
||||
}
|
||||
|
||||
doc.setObject(root);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ namespace Xybrid::FileOps {
|
|||
QString showSaveAsDialog(QWidget* parent = nullptr, const QString& caption = QString(), const QString& directory = QString(), const QString& filter = QString(), const QString& suffix = QString());
|
||||
|
||||
bool saveProject(std::shared_ptr<Data::Project> project, QString fileName = QString());
|
||||
std::shared_ptr<Data::Project> loadProject(QString fileName);
|
||||
std::shared_ptr<Data::Project> loadProject(QString fileName, bool asTemplate = false);
|
||||
std::shared_ptr<Data::Project> newProject(bool useTemplate = true);
|
||||
|
||||
bool saveNode(std::shared_ptr<Data::Node> node, QString fileName);
|
||||
std::shared_ptr<Data::Node> loadNode(QString fileName, std::shared_ptr<Data::Graph> parent = nullptr);
|
||||
|
@ -32,4 +33,7 @@ namespace Xybrid::FileOps {
|
|||
void loadConfig();
|
||||
void saveConfig();
|
||||
|
||||
void loadUIState();
|
||||
void saveUIState();
|
||||
|
||||
}
|
||||
|
|
102
xybrid/main.cpp
102
xybrid/main.cpp
|
@ -4,15 +4,28 @@
|
|||
#include "data/graph.h"
|
||||
#include "fileops.h"
|
||||
|
||||
#include "util/mem.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFontDatabase>
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QFontDatabase>
|
||||
|
||||
#include <QCborMap>
|
||||
#include <QCborArray>
|
||||
#include <QCborStreamReader>
|
||||
#include <QCborStreamWriter>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
qRegisterMetaType<Xybrid::Data::Port>();
|
||||
|
||||
// enable antialiasing on accelerated graphicsview
|
||||
QSurfaceFormat fmt;
|
||||
fmt.setSamples(10);
|
||||
|
@ -20,20 +33,103 @@ int main(int argc, char *argv[]) {
|
|||
QSurfaceFormat::setDefaultFormat(fmt);
|
||||
QApplication a(argc, argv);
|
||||
|
||||
QCommandLineParser cl;
|
||||
cl.addHelpOption();
|
||||
cl.addVersionOption();
|
||||
cl.addPositionalArgument("[project...]", QApplication::translate("main", "Project file(s) to open."));
|
||||
|
||||
cl.process(a);
|
||||
auto args = cl.positionalArguments();
|
||||
|
||||
QString userName = qEnvironmentVariable("USER");
|
||||
if (userName.isEmpty()) userName = qEnvironmentVariable("USERNAME");
|
||||
|
||||
QString socketName = qs("xybrid-ipc-%1").arg(userName);
|
||||
|
||||
QLocalSocket tryc;
|
||||
tryc.connectToServer(socketName);
|
||||
tryc.waitForConnected(1000); // wait for connection attempt (can't hang on local)
|
||||
if (tryc.isOpen()) { // if server already exists, give it the signal and exit
|
||||
QCborArray root;
|
||||
root << "open";
|
||||
|
||||
QCborArray lst;
|
||||
for (auto& fn : args) {
|
||||
QFileInfo fi(fn);
|
||||
if (!fi.exists()) continue;
|
||||
lst << fi.absoluteFilePath();
|
||||
}
|
||||
root << lst;
|
||||
|
||||
QCborStreamWriter csw(&tryc);
|
||||
root.toCborValue().toCbor(csw);
|
||||
|
||||
tryc.waitForBytesWritten();
|
||||
tryc.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
QLocalServer srv;
|
||||
srv.setSocketOptions(QLocalServer::UserAccessOption);
|
||||
srv.removeServer(socketName); // if it exists and we're here, previous instance probably crashed or was killed
|
||||
srv.listen(socketName);
|
||||
QObject::connect(&srv, &QLocalServer::newConnection, &srv, [&]() {
|
||||
auto s = srv.nextPendingConnection();
|
||||
s->waitForDisconnected();
|
||||
|
||||
QCborStreamReader csr(s);
|
||||
auto root = QCborValue::fromCbor(csr).toArray();
|
||||
s->deleteLater();
|
||||
|
||||
auto cmd = root.at(0).toString();
|
||||
|
||||
if (cmd == "open") {
|
||||
auto lst = root.at(1).toArray();
|
||||
if (lst.isEmpty()) {
|
||||
auto w = new Xybrid::MainWindow(nullptr);
|
||||
w->show();
|
||||
}
|
||||
for (auto e : lst) {
|
||||
QFileInfo fi(e.toString());
|
||||
if (!fi.exists()) continue;
|
||||
auto fileName = fi.absoluteFilePath();
|
||||
if (auto w = Xybrid::MainWindow::projectWindow(fileName); w) w->tryFocus();
|
||||
else {
|
||||
w = new Xybrid::MainWindow(nullptr, fileName);
|
||||
if (w->getProject()) w->show();
|
||||
else (w->deleteLater());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// make sure bundled fonts are loaded
|
||||
QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf");
|
||||
QFontDatabase::addApplicationFont(":/fonts/Arcon-Rounded-Regular.otf");
|
||||
|
||||
Xybrid::FileOps::loadConfig();
|
||||
Xybrid::FileOps::loadUIState();
|
||||
|
||||
Xybrid::Config::PluginRegistry::init();
|
||||
Xybrid::Audio::AudioEngine::init();
|
||||
|
||||
auto* w = new Xybrid::MainWindow();
|
||||
w->show();
|
||||
Xybrid::Util::reserveInitialPool(); // reserve arena pool ahead of time
|
||||
|
||||
bool opn = false;
|
||||
|
||||
for (auto& fn : args) {
|
||||
QFileInfo fi(fn);
|
||||
if (!fi.exists()) continue;
|
||||
auto fileName = fi.absoluteFilePath();
|
||||
auto w = new Xybrid::MainWindow(nullptr, fileName);
|
||||
if (w->getProject()) { w->show(); opn = true; }
|
||||
else (w->deleteLater());
|
||||
}
|
||||
|
||||
if (!opn) { // always show one window on launch
|
||||
auto w = new Xybrid::MainWindow(nullptr);
|
||||
w->show();
|
||||
}
|
||||
|
||||
// hook up exit event
|
||||
QObject::connect(&a, &QCoreApplication::aboutToQuit, [] {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include "settingsdialog.h"
|
||||
using Xybrid::MainWindow;
|
||||
|
||||
#include <QDebug>
|
||||
|
@ -15,7 +16,6 @@ using Xybrid::MainWindow;
|
|||
#include <QUndoStack>
|
||||
#include <QTimer>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QGLWidget>
|
||||
|
||||
#include <QScroller>
|
||||
#include <QGraphicsTextItem>
|
||||
|
@ -41,6 +41,7 @@ using Xybrid::MainWindow;
|
|||
#include "editing/projectcommands.h"
|
||||
#include "editing/patterncommands.h"
|
||||
|
||||
#include "config/uistate.h"
|
||||
#include "config/pluginregistry.h"
|
||||
#include "audio/audioengine.h"
|
||||
|
||||
|
@ -69,14 +70,17 @@ namespace {
|
|||
//
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) :
|
||||
std::unordered_set<MainWindow*> MainWindow::openWindows;
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent, const QString& fileName) :
|
||||
QMainWindow(parent),
|
||||
ui(new Ui::MainWindow) {
|
||||
socket = new UISocket(); // create this first
|
||||
ui->setupUi(this);
|
||||
|
||||
// remove tab containing system widgets
|
||||
// remove tabs containing system widgets
|
||||
ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_));
|
||||
ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_2));
|
||||
|
||||
undoStack = new QUndoStack(this);
|
||||
//undoStack->setUndoLimit(256);
|
||||
|
@ -84,13 +88,14 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
updateTitle();
|
||||
});
|
||||
|
||||
auto efa = ui->menuEdit->actions().at(0);
|
||||
auto* undoAction = undoStack->createUndoAction(this, tr("&Undo"));
|
||||
undoAction->setShortcuts(QKeySequence::Undo);
|
||||
ui->menuEdit->addAction(undoAction);
|
||||
ui->menuEdit->insertAction(efa, undoAction);
|
||||
|
||||
auto* redoAction = undoStack->createRedoAction(this, tr("&Redo"));
|
||||
redoAction->setShortcuts(QKeySequence::Redo);
|
||||
ui->menuEdit->addAction(redoAction);
|
||||
ui->menuEdit->insertAction(efa, redoAction);
|
||||
|
||||
// prevent right pane of pattern view from being collapsed
|
||||
ui->patternViewSplitter->setCollapsible(1, false);
|
||||
|
@ -124,6 +129,55 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
|
||||
}
|
||||
|
||||
{ /* Set up floater container */ } {
|
||||
auto fc = ui->floaterContainer;
|
||||
fc->setParent(this);
|
||||
fc->setAttribute(Qt::WA_TransparentForMouseEvents); // click through
|
||||
fc->setFocusPolicy(Qt::NoFocus); // can't be focused
|
||||
|
||||
fc->move(0, 0);
|
||||
fc->setFixedSize(this->size());
|
||||
|
||||
setFloater();
|
||||
}
|
||||
|
||||
{ /* Set up recent file entries */ } {
|
||||
auto fm = ui->menuFile;
|
||||
|
||||
auto bfr = ui->actionNew_Window;
|
||||
|
||||
for (size_t i = 0; i < UIState::MAX_RECENTS; i++) {
|
||||
auto ac = new QAction(fm);
|
||||
ac->setVisible(false);
|
||||
fm->insertAction(bfr, ac);
|
||||
recentFileActions.push_back(ac);
|
||||
|
||||
QObject::connect(ac, &QAction::triggered, ac, [this, i]() { openRecentProject(i); });
|
||||
|
||||
}
|
||||
|
||||
fm->insertSeparator(bfr);
|
||||
|
||||
// update list every time we show this menu
|
||||
QObject::connect(fm, &QMenu::aboutToShow, fm, [this]() {
|
||||
auto ri = UIState::recentFiles.begin();
|
||||
auto sz = UIState::recentFiles.size();
|
||||
for (size_t i = 0; i < UIState::MAX_RECENTS; i++) {
|
||||
auto ac = recentFileActions[i];
|
||||
if (i<sz) {
|
||||
QFileInfo fi(*ri);
|
||||
QString ix = i == 9 ? qs("1&0") : qs("&%1").arg(i+1);
|
||||
ac->setText(qs("%1 %2").arg(ix, fi.fileName()));
|
||||
ac->setVisible(true);
|
||||
ri++;
|
||||
} else {
|
||||
ac->setVisible(false);
|
||||
ac->setText(QString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{ /* Set up pattern list */ } {
|
||||
// model
|
||||
ui->patternList->setModel(new PatternListModel(ui->patternList, this));
|
||||
|
@ -319,12 +373,12 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
auto* vp = new QOpenGLWidget();
|
||||
view->setViewport(vp); // enable hardware acceleration
|
||||
}
|
||||
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
|
||||
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
|
||||
// Under OSX these cause Xybrid to crash.
|
||||
#ifndef __APPLE__
|
||||
#ifndef __APPLE__
|
||||
glEnable(GL_MULTISAMPLE);
|
||||
glEnable(GL_LINE_SMOOTH);
|
||||
#endif
|
||||
#endif
|
||||
//QGL::FormatOption::Rgba
|
||||
|
||||
|
||||
|
@ -352,7 +406,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
view->setDragMode(QGraphicsView::RubberBandDrag);
|
||||
}
|
||||
} else if (e->type() == QEvent::MouseButtonRelease) { // disable drag after end
|
||||
QTimer::singleShot(1, [view] {
|
||||
QTimer::singleShot(1, view, [view] {
|
||||
view->setDragMode(QGraphicsView::NoDrag);
|
||||
});
|
||||
}
|
||||
|
@ -371,8 +425,52 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
connect(ui->sampleList->selectionModel(), &QItemSelectionModel::currentChanged, this, [this, mdl](const QModelIndex& ind, const QModelIndex& old [[maybe_unused]]) {
|
||||
selectSampleForEditing(mdl->itemAt(ind));
|
||||
});
|
||||
|
||||
// edit pane
|
||||
connect(ui->groupSampleLoop, &QGroupBox::toggled, this, [this](bool on) {
|
||||
if (editingSample) {
|
||||
if (on) {
|
||||
editingSample->loopStart = ui->spinSampleLoopStart->value();
|
||||
editingSample->loopEnd = ui->spinSampleLoopEnd->value();
|
||||
} else {
|
||||
editingSample->loopStart = -1;
|
||||
editingSample->loopEnd = -1;
|
||||
}
|
||||
ui->waveformPreview->update();
|
||||
}
|
||||
});
|
||||
connect(ui->spinSampleLoopStart, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
if (editingSample && ui->groupSampleLoop->isChecked()) {
|
||||
editingSample->loopStart = v;
|
||||
ui->waveformPreview->update();
|
||||
}
|
||||
});
|
||||
connect(ui->spinSampleLoopEnd, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
if (editingSample && ui->groupSampleLoop->isChecked()) {
|
||||
editingSample->loopEnd = v;
|
||||
ui->waveformPreview->update();
|
||||
}
|
||||
});
|
||||
|
||||
connect(ui->spinSampleNote, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
auto sp = ui->spinSampleNote;
|
||||
sp->setSuffix(")");
|
||||
sp->setPrefix(qs("%1 (").arg(Util::noteName(static_cast<int16_t>(v))));
|
||||
|
||||
if (editingSample) editingSample->baseNote = v;
|
||||
});
|
||||
connect(ui->spinSampleNoteSub, qOverload<double>(&QDoubleSpinBox::valueChanged), this, [this](double v) {
|
||||
auto sp = ui->spinSampleNoteSub;
|
||||
sp->setPrefix(v >= 0.0 ? qs("+") : QString());
|
||||
|
||||
if (editingSample) editingSample->subNote = v;
|
||||
});
|
||||
emit ui->spinSampleNoteSub->valueChanged(0.0); // force refresh
|
||||
}
|
||||
|
||||
// force fonts to display properly
|
||||
updateFont();
|
||||
|
||||
// Set up signaling from project to UI
|
||||
socket->setParent(this);
|
||||
socket->window = this;
|
||||
|
@ -421,8 +519,19 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
|
||||
selectSampleForEditing(nullptr); // init blank
|
||||
|
||||
// and start with a new project
|
||||
menuFileNew();
|
||||
bool isFirst = false;//openWindows.empty();
|
||||
openWindows.insert(this);
|
||||
|
||||
if (fileName.isEmpty()) {
|
||||
// start with a new project
|
||||
menuFileNew();
|
||||
} else {
|
||||
openProject(fileName, isFirst);
|
||||
if (!project) {
|
||||
if (!isFirst) close();
|
||||
else menuFileNew();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
|
@ -430,9 +539,27 @@ MainWindow::~MainWindow() {
|
|||
delete ui;
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent*) {
|
||||
MainWindow* MainWindow::projectWindow(const QString &fileName) {
|
||||
if (fileName.isEmpty()) return nullptr;
|
||||
for (auto w : openWindows) if (w->project && w->project->fileName == fileName) return w;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MainWindow::resizeEvent(QResizeEvent* e) {
|
||||
this->QMainWindow::resizeEvent(e);
|
||||
|
||||
ui->floaterContainer->setFixedSize(this->size());
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* e) {
|
||||
if (promptSave()) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
undoStack->clear();
|
||||
setAttribute(Qt::WA_DeleteOnClose); // delete when done
|
||||
openWindows.erase(this); // and remove from list now
|
||||
if (openWindows.size() == 0 && SettingsDialog::instance) SettingsDialog::instance->reject();
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
|
||||
|
@ -450,25 +577,66 @@ bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
|
|||
return false;
|
||||
}
|
||||
|
||||
void MainWindow::tryFocus() {
|
||||
raise();
|
||||
activateWindow();
|
||||
}
|
||||
|
||||
void MainWindow::openProject(const QString& fileName, bool failSilent) {
|
||||
auto pw = projectWindow(fileName);
|
||||
if (pw && pw != this) {
|
||||
pw->tryFocus();
|
||||
return;
|
||||
}
|
||||
auto np = FileOps::loadProject(fileName);
|
||||
if (!np) {
|
||||
if (!failSilent) QMessageBox::critical(this, qs("Error"), qs("Error loading project \"%1\".").arg(QFileInfo(fileName).fileName()));
|
||||
return;
|
||||
}
|
||||
if (audioEngine->playingProject() == project) audioEngine->stop();
|
||||
project = np;
|
||||
UIState::addRecentFile(fileName);
|
||||
onNewProjectLoaded();
|
||||
}
|
||||
|
||||
void MainWindow::openRecentProject(size_t idx) {
|
||||
if (promptSave()) return;
|
||||
if (idx > UIState::recentFiles.size()) return;
|
||||
auto it = UIState::recentFiles.begin();
|
||||
for (size_t i = 0; i < idx; i++) it++;
|
||||
|
||||
openProject(QString(*it)); // need to copy string before opening
|
||||
}
|
||||
|
||||
bool MainWindow::promptSave() {
|
||||
if (!project) return false; // window closing on open
|
||||
if (!undoStack->isClean()) {
|
||||
auto ftxt = project->fileName.isEmpty() ? qs("unsaved project") : qs("project \"%1\"").arg(QFileInfo(project->fileName).fileName());
|
||||
auto r = QMessageBox::warning(this, qs("Unsaved changes"), qs("Save changes to %1?").arg(ftxt),
|
||||
static_cast<QMessageBox::StandardButtons>(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel));
|
||||
|
||||
if (r == QMessageBox::Cancel || r == QMessageBox::Escape) return true; // signal abort
|
||||
if (r == QMessageBox::Yes) {
|
||||
menuFileSave();
|
||||
if (project->fileName.isEmpty()) return true; // save-as-new canceled
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MainWindow::menuFileNew() {
|
||||
if (promptSave()) return;
|
||||
auto hold = project; // keep alive until done
|
||||
if (audioEngine->playingProject() == project) audioEngine->stop();
|
||||
project = std::make_shared<Project>();
|
||||
project->sequence.emplace_back(project->newPattern());
|
||||
project = FileOps::newProject();
|
||||
|
||||
onNewProjectLoaded();
|
||||
}
|
||||
|
||||
void MainWindow::menuFileOpen() {
|
||||
if (promptSave()) return;
|
||||
if (auto fileName = FileOps::showOpenDialog(this, "Open project...", Config::Directories::projects, FileOps::Filter::project); !fileName.isEmpty()) {
|
||||
auto np = FileOps::loadProject(fileName);
|
||||
if (!np) {
|
||||
QMessageBox::critical(this, "Error", "Error loading project");
|
||||
return;
|
||||
}
|
||||
if (audioEngine->playingProject() == project) audioEngine->stop();
|
||||
project = np;
|
||||
onNewProjectLoaded();
|
||||
openProject(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,6 +656,7 @@ void MainWindow::menuFileSaveAs() {
|
|||
}
|
||||
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) {
|
||||
FileOps::saveProject(project, fileName);
|
||||
UIState::addRecentFile(fileName);
|
||||
undoStack->setClean();
|
||||
updateTitle();
|
||||
}
|
||||
|
@ -495,28 +664,63 @@ void MainWindow::menuFileSaveAs() {
|
|||
|
||||
void MainWindow::menuFileExport() {
|
||||
if (project->exportFileName.isEmpty()) menuFileExportAs();
|
||||
else {
|
||||
audioEngine->render(project, project->exportFileName);
|
||||
}
|
||||
else render();
|
||||
}
|
||||
|
||||
void MainWindow::menuFileExportAs() {
|
||||
QString saveDir = Config::Directories::projects;
|
||||
if (!project->fileName.isEmpty()) {
|
||||
QFileInfo f(project->fileName);
|
||||
saveDir = f.dir().filePath(f.baseName());
|
||||
}
|
||||
saveDir = f.dir().filePath(f.baseName()).append(".mp3");
|
||||
} else saveDir = saveDir.append("/untitled.mp3");
|
||||
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::audioOut, "mp3"); !fileName.isEmpty()) {
|
||||
project->exportFileName = fileName;
|
||||
audioEngine->render(project, project->exportFileName);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::render() {
|
||||
std::vector<QWidget*> dis {
|
||||
ui->pattern, ui->patchboard, ui->samples,
|
||||
ui->menuBar, ui->playButton
|
||||
};
|
||||
for (auto w : dis) w->setEnabled(false);
|
||||
setFloater(ui->floaterRendering);
|
||||
|
||||
audioEngine->render(project, project->exportFileName);
|
||||
if (openWindows.find(this) == openWindows.end()) return; // don't try to update UI if the window has been disposed
|
||||
|
||||
setFloater();
|
||||
for (auto w : dis) w->setEnabled(true);
|
||||
}
|
||||
|
||||
void MainWindow::menuFileNewWindow() {
|
||||
auto w = new MainWindow();
|
||||
w->show();
|
||||
}
|
||||
|
||||
void MainWindow::menuSettings() {
|
||||
SettingsDialog::tryOpen();
|
||||
}
|
||||
|
||||
void MainWindow::menuQuit() {
|
||||
auto c = openWindows.size();
|
||||
if (c > 1) { // prompt if more than just this window
|
||||
auto r = QMessageBox::warning(this, qs("Quit"), qs("Close %1 open projects?").arg(c),
|
||||
static_cast<QMessageBox::StandardButtons>(QMessageBox::Yes | QMessageBox::No));
|
||||
|
||||
if (r == QMessageBox::No || r == QMessageBox::Escape) return;
|
||||
}
|
||||
|
||||
// assemble list
|
||||
std::vector<MainWindow*> cl;
|
||||
cl.push_back(this);
|
||||
for (auto w : openWindows) if (w != this) cl.push_back(w);
|
||||
|
||||
// and close
|
||||
for (auto w : cl) w->close();
|
||||
}
|
||||
|
||||
void MainWindow::onNewProjectLoaded() {
|
||||
undoStack->clear();
|
||||
|
||||
|
@ -545,6 +749,8 @@ void MainWindow::onNewProjectLoaded() {
|
|||
updateTitle();
|
||||
setSongInfoPaneExpanded(false);
|
||||
|
||||
if (ui->tabWidget->currentWidget() == ui->patchboard) ui->patchboardView->setFocus();
|
||||
|
||||
emit projectLoaded();
|
||||
}
|
||||
|
||||
|
@ -588,7 +794,7 @@ void MainWindow::updateTitle() {
|
|||
if (project->fileName.isEmpty()) songTitle = qs("(new project)");
|
||||
else songTitle = QFileInfo(project->fileName).baseName();
|
||||
} else {
|
||||
if (!project->artist.isEmpty()) songTitle = qs("%1 - %2").arg(project->artist).arg(project->title);
|
||||
if (!project->artist.isEmpty()) songTitle = qs("%1 - %2").arg(project->artist, project->title);
|
||||
else songTitle = project->title;
|
||||
}
|
||||
|
||||
|
@ -597,6 +803,19 @@ void MainWindow::updateTitle() {
|
|||
setWindowTitle(qs("Xybrid - ") % songTitle);
|
||||
}
|
||||
|
||||
void MainWindow::updateFont() {
|
||||
QString font = qs("Iosevka Term Light");
|
||||
double pt = 10.0;
|
||||
|
||||
QString fstr = qs("font: %2pt \"%1\";").arg(font).arg(pt);
|
||||
QString tfstr = qs("QTableView { %1 }").arg(fstr);
|
||||
QString hfstr = qs("QHeaderView { %1 }").arg(fstr);
|
||||
|
||||
ui->patternSequencer->setStyleSheet(tfstr);
|
||||
ui->patternEditor->setStyleSheet(tfstr);
|
||||
ui->patternEditor->verticalHeader()->setStyleSheet(hfstr);
|
||||
}
|
||||
|
||||
void MainWindow::setSongInfoPaneExpanded(bool open) {
|
||||
if (open) {
|
||||
ui->songInfoPane->setCurrentIndex(1);
|
||||
|
@ -609,6 +828,12 @@ void MainWindow::setSongInfoPaneExpanded(bool open) {
|
|||
ui->songInfoPane->setMaximumHeight(s);
|
||||
}
|
||||
|
||||
void MainWindow::setFloater(QWidget* w) {
|
||||
auto idx = ui->floaterContainer->indexOf(w);
|
||||
if (idx < 0) idx = 0;
|
||||
ui->floaterContainer->setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
bool MainWindow::selectPatternForEditing(Pattern* pattern) {
|
||||
if (!pattern || pattern == editingPattern.get()) return false; // no u
|
||||
if (pattern->project != project.get()) return false; // wrong project
|
||||
|
@ -626,20 +851,48 @@ bool MainWindow::selectPatternForEditing(Pattern* pattern) {
|
|||
|
||||
void MainWindow::selectSampleForEditing(std::shared_ptr<Xybrid::Data::Sample> smp) {
|
||||
if (!smp || smp->project != project.get()) { // no valid sample selected
|
||||
editingSample = nullptr;
|
||||
ui->sampleViewPane->setEnabled(false);
|
||||
ui->sampleInfo->setText(qs("(no sample selected)"));
|
||||
|
||||
ui->groupSampleLoop->setChecked(false);
|
||||
ui->spinSampleLoopStart->setValue(0);
|
||||
ui->spinSampleLoopEnd->setValue(0);
|
||||
|
||||
ui->spinSampleNote->setValue(60);
|
||||
ui->spinSampleNoteSub->setValue(0.0);
|
||||
} else {
|
||||
editingSample = nullptr;
|
||||
ui->sampleViewPane->setEnabled(true);
|
||||
ui->sampleInfo->setText(
|
||||
qs("%1 // %2\n%3 %4Hz, %5 frames (%6)")
|
||||
.arg(smp->name.section('/', -1, -1))
|
||||
.arg(smp->uuid.toString())
|
||||
.arg(smp->numChannels() == 2 ? qs("Stereo") : qs("Mono"))
|
||||
.arg(smp->name.section('/', -1, -1),
|
||||
smp->uuid.toString(),
|
||||
smp->numChannels() == 2 ? qs("Stereo") : qs("Mono"))
|
||||
.arg(smp->sampleRate)
|
||||
.arg(smp->length())
|
||||
.arg(Util::sampleLength(smp->sampleRate, smp->length()))
|
||||
);
|
||||
|
||||
ui->spinSampleLoopStart->setRange(0, smp->length());
|
||||
ui->spinSampleLoopEnd->setRange(0, smp->length());
|
||||
|
||||
if (smp->loopStart < 0) { // loop disabled
|
||||
ui->groupSampleLoop->setChecked(false);
|
||||
ui->spinSampleLoopStart->setValue(0);
|
||||
ui->spinSampleLoopEnd->setValue(smp->length());
|
||||
} else {
|
||||
ui->groupSampleLoop->setChecked(true);
|
||||
ui->spinSampleLoopStart->setValue(smp->loopStart);
|
||||
ui->spinSampleLoopEnd->setValue(smp->loopEnd);
|
||||
}
|
||||
|
||||
ui->spinSampleNote->setValue(smp->baseNote);
|
||||
ui->spinSampleNoteSub->setValue(smp->subNote);
|
||||
|
||||
editingSample = smp;
|
||||
}
|
||||
|
||||
ui->waveformPreview->setSample(smp);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
#include "uisocket.h"
|
||||
|
@ -19,16 +21,25 @@ namespace Xybrid {
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
explicit MainWindow(QWidget *parent = nullptr, const QString& fileName = QString());
|
||||
~MainWindow() override;
|
||||
|
||||
static std::unordered_set<MainWindow*> openWindows;
|
||||
static MainWindow* projectWindow(const QString& fileName);
|
||||
|
||||
private:
|
||||
Ui::MainWindow* ui;
|
||||
UISocket* socket;
|
||||
std::shared_ptr<Data::Project> project;
|
||||
std::shared_ptr<Data::Pattern> editingPattern;
|
||||
std::shared_ptr<Data::Sample> editingSample;
|
||||
|
||||
QUndoStack* undoStack;
|
||||
std::vector<QAction*> recentFileActions;
|
||||
|
||||
void openProject(const QString& fileName, bool failSilent = false);
|
||||
void openRecentProject(size_t idx);
|
||||
bool promptSave();
|
||||
|
||||
void onNewProjectLoaded();
|
||||
void updatePatternLists();
|
||||
|
@ -42,8 +53,12 @@ namespace Xybrid {
|
|||
void openPatternProperties(const std::shared_ptr<Data::Pattern>&);
|
||||
|
||||
void updateTitle();
|
||||
void updateFont();
|
||||
|
||||
void setSongInfoPaneExpanded(bool);
|
||||
void setFloater(QWidget* = nullptr);
|
||||
|
||||
void render();
|
||||
|
||||
public:
|
||||
const std::shared_ptr<Data::Project>& getProject() const { return project; }
|
||||
|
@ -56,10 +71,13 @@ namespace Xybrid {
|
|||
inline UISocket* uiSocket() { return socket; }
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent*) override;
|
||||
void closeEvent(QCloseEvent*) override;
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void tryFocus();
|
||||
|
||||
void menuFileNew();
|
||||
void menuFileOpen();
|
||||
void menuFileSave();
|
||||
|
@ -69,6 +87,8 @@ namespace Xybrid {
|
|||
void menuFileExportAs();
|
||||
|
||||
void menuFileNewWindow();
|
||||
void menuSettings();
|
||||
void menuQuit();
|
||||
|
||||
signals:
|
||||
void projectLoaded();
|
||||
|
|
|
@ -452,7 +452,9 @@
|
|||
<property name="font">
|
||||
<font>
|
||||
<family>Iosevka Term Light</family>
|
||||
<pointsize>9</pointsize>
|
||||
<pointsize>10</pointsize>
|
||||
<italic>false</italic>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
|
@ -504,7 +506,9 @@
|
|||
<property name="font">
|
||||
<font>
|
||||
<family>Iosevka Term Light</family>
|
||||
<pointsize>9</pointsize>
|
||||
<pointsize>10</pointsize>
|
||||
<italic>false</italic>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
|
@ -687,6 +691,248 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="sampleEditRow" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<property name="spacing">
|
||||
<number>2</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="QGroupBox" name="groupSampleLoop">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Loop</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<property name="spacing">
|
||||
<number>2</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="QLabel" name="labelSampleLoopStart">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinSampleLoopStart">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_2" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||
<property name="spacing">
|
||||
<number>2</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="QLabel" name="labelSampleLoopEnd">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>End</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinSampleLoopEnd">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupSampleNote">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Base Note</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<property name="spacing">
|
||||
<number>2</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="QSpinBox" name="spinSampleNote">
|
||||
<property name="maximum">
|
||||
<number>119</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinSampleNoteSub">
|
||||
<property name="minimum">
|
||||
<double>-1.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
@ -801,6 +1047,109 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="extra_2">
|
||||
<attribute name="title">
|
||||
<string>floater</string>
|
||||
</attribute>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="floaterContainer">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="floaterNone"/>
|
||||
<widget class="QWidget" name="floaterRendering">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_13">
|
||||
<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>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>388</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Xybrid::UI::FloaterBG" name="floaterRenderingBg" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>120</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_14">
|
||||
<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="QLabel" name="labelRendering">
|
||||
<property name="text">
|
||||
<string>Rendering...</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -840,11 +1189,15 @@
|
|||
<addaction name="separator"/>
|
||||
<addaction name="actionNew_Window"/>
|
||||
<addaction name="actionClose_Window"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionQuit"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuEdit">
|
||||
<property name="title">
|
||||
<string>Edit</string>
|
||||
</property>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionSettings"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<property name="title">
|
||||
|
@ -890,7 +1243,7 @@
|
|||
</action>
|
||||
<action name="actionSave_As">
|
||||
<property name="text">
|
||||
<string>Sa&ve As...</string>
|
||||
<string>Save &As...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+S</string>
|
||||
|
@ -925,12 +1278,28 @@
|
|||
</action>
|
||||
<action name="actionExport_As">
|
||||
<property name="text">
|
||||
<string>E&xport As...</string>
|
||||
<string>Export As...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+E</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionQuit">
|
||||
<property name="text">
|
||||
<string>&Quit</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSettings">
|
||||
<property name="text">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+F1</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
|
@ -950,6 +1319,12 @@
|
|||
<header>ui/waveformpreviewwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Xybrid::UI::FloaterBG</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>ui/floaterbg.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="res/resources.qrc"/>
|
||||
|
@ -1083,6 +1458,38 @@
|
|||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionQuit</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuQuit()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>459</x>
|
||||
<y>305</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionSettings</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuSettings()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>459</x>
|
||||
<y>305</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<slots>
|
||||
<slot>menuFileNew()</slot>
|
||||
|
@ -1092,5 +1499,7 @@
|
|||
<slot>menuFileNewWindow()</slot>
|
||||
<slot>menuFileExport()</slot>
|
||||
<slot>menuFileExportAs()</slot>
|
||||
<slot>menuQuit()</slot>
|
||||
<slot>menuSettings()</slot>
|
||||
</slots>
|
||||
</ui>
|
||||
|
|
|
@ -8,14 +8,14 @@ class QCborValue;
|
|||
|
||||
namespace Xybrid::NodeLib {
|
||||
// more precision than probably fits in a double, but it certainly shouldn't hurt
|
||||
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
|
||||
const constexpr double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
|
||||
const inline constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
|
||||
const inline constexpr double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
|
||||
/// Multiplier to compensate for the balance equation
|
||||
// (1.0 / cos(PI*0.25))
|
||||
const constexpr double PAN_MULT = 1.414213562373095048801688724209698078569671875376948073176;
|
||||
/// (1.0 / cos(PI*0.25))
|
||||
const inline constexpr double PAN_MULT = 1.414213562373095048801688724209698078569671875376948073176;
|
||||
|
||||
/// Sane mimimum transition time to avoid clip artifacts
|
||||
const constexpr double shortStep = 0.0025;
|
||||
const inline constexpr double shortStep = 0.0025;
|
||||
struct ADSR {
|
||||
double a = 0.0, d = 0.0, s = 1.0, r = 0.0;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ using namespace Xybrid::Data;
|
|||
CommandReader::CommandReader(CommandPort* p) {
|
||||
p->pull();
|
||||
data = p->data;
|
||||
dataSize = p->dataSize;
|
||||
dataSize = p->size;
|
||||
}
|
||||
|
||||
CommandReader::operator bool() const { return dataSize >= cur+5; }
|
||||
|
|
|
@ -37,6 +37,7 @@ void InstrumentCore::reset() {
|
|||
time = 0;
|
||||
|
||||
volume = 1.0;
|
||||
pan = 0.0;
|
||||
}
|
||||
|
||||
void InstrumentCore::process(Node* n) {
|
||||
|
@ -102,6 +103,7 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
|
|||
forceRetrigger:
|
||||
note.note = n;
|
||||
note.time = note.adsrTime = -smpTime; // compensate for first-advance
|
||||
note.pan = pan;
|
||||
if (onNoteOn) onNoteOn(note);
|
||||
}
|
||||
} else { // existing note
|
||||
|
@ -194,6 +196,10 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
|
|||
volume = (1.0*v) / 255.0;
|
||||
continue;
|
||||
}
|
||||
case 'P': {
|
||||
pan = std::clamp((1.0*static_cast<int8_t>(v)) / 127.0, -1.0, 1.0);
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -203,7 +209,7 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
|
|||
// then do the thing
|
||||
if (o) o->pull();
|
||||
|
||||
double tickTime = smpTime * audioEngine->curTickSize();
|
||||
double tickTime = smpTime * static_cast<double>(audioEngine->curTickSize());
|
||||
|
||||
if (processNote) {
|
||||
for (auto p = activeNotes.begin(); p != activeNotes.end(); ) {
|
||||
|
|
|
@ -3,10 +3,27 @@
|
|||
#include <memory>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#ifdef WITH_BOOST
|
||||
#include <boost/container/pmr/memory_resource.hpp>
|
||||
#include <boost/container/pmr/polymorphic_allocator.hpp>
|
||||
using boost::container::pmr::polymorphic_allocator;
|
||||
template <class Key,
|
||||
class T,
|
||||
class Hash = std::hash<Key>,
|
||||
class Pred = std::equal_to<Key>>
|
||||
using unordered_map = std::unordered_map<Key, T, Hash, Pred, polymorphic_allocator<std::pair<const Key,T>>>;
|
||||
template <class Key, class T,
|
||||
class Hash = std::hash<Key>,
|
||||
class Pred = std::equal_to<Key>>
|
||||
using unordered_multimap = std::unordered_multimap<Key, T, Hash, Pred, polymorphic_allocator<std::pair<const Key,T>>>;
|
||||
#else
|
||||
using std::pmr::unordered_map;
|
||||
using std::pmr::unordered_multimap;
|
||||
#endif
|
||||
#include <array>
|
||||
#include "nodelib/basics.h"
|
||||
|
||||
#include "data/node.h"
|
||||
#include "util/mem.h"
|
||||
|
||||
namespace Xybrid::Data {
|
||||
class CommandPort;
|
||||
|
@ -23,9 +40,6 @@ namespace Xybrid::NodeLib {
|
|||
* Not mandatory by any means, but handles all the "standard" commands for you.
|
||||
*/
|
||||
class InstrumentCore {
|
||||
double time;
|
||||
double smpTime;
|
||||
|
||||
public:
|
||||
class Note {
|
||||
friend class InstrumentCore;
|
||||
|
@ -33,7 +47,7 @@ namespace Xybrid::NodeLib {
|
|||
void* intern = nullptr;
|
||||
public:
|
||||
uint16_t id;
|
||||
double note; // floating point to allow smooth pitch bends
|
||||
double note = 64.0; // floating point to allow smooth pitch bends
|
||||
double noteAdd = 0.0;
|
||||
double time = 0.0;
|
||||
|
||||
|
@ -45,7 +59,12 @@ namespace Xybrid::NodeLib {
|
|||
double adsrTime = 0;
|
||||
uint8_t adsrPhase = 0;
|
||||
|
||||
std::array<double, 5> scratch{0.0};
|
||||
union {
|
||||
std::array<double, 5> scratch {0.0};
|
||||
std::array<void*, 5> ptr;
|
||||
};
|
||||
|
||||
template<typename T> inline T& scratchAs() { return *(reinterpret_cast<T*>(reinterpret_cast<void*>(&scratch))); }
|
||||
|
||||
Note() = default;
|
||||
Note(InstrumentCore*, uint16_t id);
|
||||
|
@ -76,11 +95,19 @@ namespace Xybrid::NodeLib {
|
|||
void startTick(Note&, double tickTime);
|
||||
void process(Note&, double smpTime);
|
||||
};
|
||||
private:
|
||||
//
|
||||
|
||||
double time;
|
||||
double smpTime;
|
||||
|
||||
public:
|
||||
|
||||
double volume = 1.0;
|
||||
double pan = 0.0;
|
||||
|
||||
std::unordered_map<uint16_t, Note> activeNotes;
|
||||
std::unordered_multimap<uint16_t, Tween> activeTweens;
|
||||
unordered_map<uint16_t, Note> activeNotes = {16, Util::ralloc};
|
||||
unordered_multimap<uint16_t, Tween> activeTweens = {16, Util::ralloc};
|
||||
|
||||
std::function<bool(Note*, const ParamReader&)> paramFilter;
|
||||
std::unordered_map<uint8_t, std::function<bool(const ParamReader&)>> globalParam;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include "util/ext.h"
|
||||
|
||||
namespace Xybrid::NodeLib {
|
||||
namespace Oscillator {
|
||||
//
|
||||
}
|
||||
namespace Osc = Oscillator;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
#include "param.h"
|
||||
|
||||
using Xybrid::NodeLib::Param;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
ParameterPort* Param::makePort(Data::Node* node, const QString& name) {
|
||||
if (port) return port;
|
||||
|
||||
node->inputs.try_emplace(Port::Parameter);
|
||||
auto& inp = node->inputs.find(Port::Parameter)->second;
|
||||
|
||||
uint8_t id = 0;
|
||||
auto it = inp.begin();
|
||||
while (it != inp.end() && it->first == id) { ++id; ++it; } // scan for first unused id
|
||||
|
||||
auto n = name;
|
||||
if (n.isEmpty()) n = this->name.toLower();
|
||||
|
||||
port = static_cast<ParameterPort*>(node->addPort(Port::Input, Port::Parameter, id).get());
|
||||
port->name = n;
|
||||
return port;
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <QCborMap>
|
||||
|
||||
#include "data/node.h"
|
||||
#include "data/porttypes.h"
|
||||
|
||||
//namespace Xybrid::Data { class ParameterPort; }
|
||||
namespace Xybrid::NodeLib {
|
||||
/// Plugin parameter with associated metadata and automation faculties
|
||||
class Param {
|
||||
//
|
||||
Param() = default;
|
||||
public:
|
||||
class Reader {
|
||||
friend class Param;
|
||||
Param* p;
|
||||
size_t pos = 0;
|
||||
bool r;
|
||||
Reader(Param* p, bool r) : p(p), r(r) { }
|
||||
public:
|
||||
double next() {
|
||||
if (r) {
|
||||
auto v = p->port->data[pos++];
|
||||
if (!std::isnan(v)) {
|
||||
if (p->min == -p->max) // "signed" logic if symmetrical around zero
|
||||
v = std::clamp(v, -1.0, 1.0) * p->max;
|
||||
else // extents 0.0 .. 1.0 scaled across parameter range
|
||||
v = std::clamp(p->min + v * (p->max - p->min), p->min, p->max);
|
||||
|
||||
p->vt = v;
|
||||
}
|
||||
}
|
||||
if (!std::isnan(p->vt)) return p->vt;
|
||||
return p->value;
|
||||
}
|
||||
};
|
||||
|
||||
enum Flags : uint8_t {
|
||||
ResetOnTick = 0b00000001,
|
||||
};
|
||||
|
||||
QString name;
|
||||
double min, max, def;
|
||||
|
||||
double value;
|
||||
double vt = std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
Flags flags;
|
||||
|
||||
Data::ParameterPort* port = nullptr;
|
||||
|
||||
Param(const QString& name, double min, double max, double def, Flags flags = {}) : Param() {
|
||||
this->name = name;
|
||||
this->min = min;
|
||||
this->max = max;
|
||||
this->def = def;
|
||||
this->value = def;
|
||||
this->flags = flags;
|
||||
}
|
||||
|
||||
Data::ParameterPort* makePort(Data::Node*, const QString& name = { });
|
||||
|
||||
Reader start() {
|
||||
bool r = port && port->isConnected();
|
||||
if (r) port->pull();
|
||||
if (flags & ResetOnTick) vt = std::numeric_limits<double>::quiet_NaN();
|
||||
return Reader(this, r);
|
||||
}
|
||||
|
||||
inline QString saveName() const {
|
||||
return name.toLower().remove(' ');
|
||||
}
|
||||
|
||||
inline void save(QCborMap& m, QString id = { }) const {
|
||||
if (id.isEmpty()) id = saveName();
|
||||
m[id] = value;
|
||||
}
|
||||
|
||||
inline void load(const QCborMap& m, QString id = { }) {
|
||||
if (id.isEmpty()) id = saveName();
|
||||
value = m.value(id).toDouble(value);
|
||||
vt = std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,49 +1,73 @@
|
|||
#include "resampler.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#ifdef WITH_BOOST
|
||||
#include <boost/math/special_functions/bessel.hpp>
|
||||
#define cyl_bessel_i boost::math::cyl_bessel_i
|
||||
using boost::math::cyl_bessel_i;
|
||||
#else
|
||||
#include <cmath>
|
||||
#define cyl_bessel_i std::cyl_bessel_i
|
||||
using std::cyl_bessel_i;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
|
||||
|
||||
const constexpr double KAISER_ALPHA = 7.5;
|
||||
// 5.5 for downrate; example gave 7.5
|
||||
const constexpr double KAISER_ALPHA = 5.5;
|
||||
const constexpr double KAISER_BETA = PI * KAISER_ALPHA;
|
||||
|
||||
inline constexpr double sinc(double x) {
|
||||
if (x == 0) return 1;
|
||||
double px = x * PI;
|
||||
double px = x * PI/1;
|
||||
return std::sin(px) / px;
|
||||
}
|
||||
|
||||
#if __cplusplus >= 202002L
|
||||
using std::lerp;
|
||||
#else
|
||||
inline constexpr double lerp(double a, double b, double t) { return (1.0 - t) * a + t * b; }
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
// generate
|
||||
const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> Xybrid::NodeLib::resamplerLUT = [] {
|
||||
const std::array<std::array<std::array<double, LUT_TAPS>, LUT_LEVELS>, LUT_STEPS> Xybrid::NodeLib::resamplerLUT = [] {
|
||||
|
||||
double denom = cyl_bessel_i(0, KAISER_BETA);
|
||||
|
||||
std::array<std::array<double, LUT_TAPS>, LUT_STEPS> t;
|
||||
t[0] = {0, 0, 0, 1, 0, 0, 0, 0}; // we already know the ideal integer step
|
||||
for (size_t step = 1; step < LUT_STEPS; step++) {
|
||||
double sv = static_cast<double>(step) / LUT_STEPS;
|
||||
std::array<std::array<std::array<double, LUT_TAPS>, LUT_LEVELS>, LUT_STEPS> t;
|
||||
//t[0] = {0, 0, 0, 1, 0, 0, 0, 0}; // we already know the ideal integer step
|
||||
for (size_t step = 0; step < LUT_STEPS; step++) {
|
||||
double sv = static_cast<double>(step) / LUT_STEPS; // subvalue (offset of tap position)
|
||||
for (size_t tap = 0; tap < LUT_TAPS; tap++) {
|
||||
double x = static_cast<double>(tap) - sv;
|
||||
t[step][tap] = sinc(x-(LUT_TAPS/2-1)) * (cyl_bessel_i(0, KAISER_BETA * std::sqrt(1 - std::pow(((2 * (x+1)) / (LUT_TAPS)) - 1, 2))) / denom);
|
||||
if (t[step][tap] != t[step][tap]) t[step][tap] = 0; // NaN guard
|
||||
//std::cout << "tap " << tap << ": " << t[step][tap] << " ";
|
||||
double x = static_cast<double>(tap) - sv; // x position of tap;
|
||||
double sx = x-LUT_HTAPS;
|
||||
double kaiser = cyl_bessel_i(0, KAISER_BETA * std::sqrt(1.0 - std::pow( ( (2.0*(x+1))/(LUT_TAPS) ) - 1.0, 2 ) ) ) / denom; // original kaiser window generation
|
||||
//double kaiser = cyl_bessel_i(0, KAISER_BETA * std::sqrt(1.0 - std::pow( (2.0*x)/(LUT_TAPS-2) - 1.0, 2 ) ) ) / denom; // by-the-book kaiser window of length LUT_TAPS-1
|
||||
//double idl = (2.0*PI)/(LUT_TAPS-1);
|
||||
//double kaiser = 0.40243 - 0.49804 * std::cos(idl * x) + 0.09831 * std::cos(2.0 * idl * x) - 0.00122 * std::cos(3.0 * idl * x); // approximate
|
||||
//kaiser = std::max(kaiser, 0.0);
|
||||
|
||||
for (size_t i = 0; i < LUT_LEVELS; i++) {
|
||||
double m = 1.0/std::max(static_cast<double>(i), 1.0); // sinc function "expands" the higher we pitch things
|
||||
double om = lerp(0.2, 1.0, m) // we need to compensate slightly for amplitude loss at higher levels
|
||||
/* */ * lerp(1.0, kaiser, std::pow(m, 0.333)); // apply kaiser proportionally; we want it less the higher we go
|
||||
t[step][i][tap] = sinc(sx*m) * om;
|
||||
if (t[step][i][tap] != t[step][i][tap]) t[step][i][tap] = 0; // NaN guard
|
||||
}
|
||||
|
||||
if (t[step][0][tap] != t[step][0][tap]) t[step][0][tap] = 0; // NaN guard
|
||||
}
|
||||
//std::cout << "\n";
|
||||
}
|
||||
/*t[0] = {};
|
||||
t[0][LUT_HTAPS] = 1;*/
|
||||
/*for (auto v : t[0]) std::cout << v << ", ";
|
||||
std::cout << std::endl;*/
|
||||
return t;
|
||||
}();
|
||||
|
|
|
@ -1,9 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
|
||||
#include "data/audioframe.h"
|
||||
#include "data/sample.h"
|
||||
|
||||
namespace Xybrid::NodeLib {
|
||||
const constexpr size_t LUT_LEVELS = 16;
|
||||
const constexpr size_t LUT_TAPS = 8;
|
||||
const constexpr ptrdiff_t LUT_HTAPS = LUT_TAPS/2-1;//static_cast<ptrdiff_t>(LUT_TAPS - (LUT_TAPS+0.5)/2);
|
||||
const constexpr size_t LUT_STEPS = 1024;
|
||||
extern const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> resamplerLUT;
|
||||
extern const std::array<std::array<std::array<double, LUT_TAPS>, LUT_LEVELS>, LUT_STEPS> resamplerLUT;
|
||||
|
||||
inline Data::AudioFrame resamp(Data::Sample* smp, double pos, double rate [[maybe_unused]]) {
|
||||
auto loop = smp->loopStart >= 0;
|
||||
auto len = static_cast<ptrdiff_t>(smp->length());
|
||||
auto ls = static_cast<ptrdiff_t>(smp->loopStart);
|
||||
auto le = static_cast<ptrdiff_t>(smp->loopEnd);
|
||||
auto ll = le - ls;
|
||||
|
||||
double ip = std::floor(pos);
|
||||
auto& pt = NodeLib::resamplerLUT[static_cast<size_t>((pos - ip)*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS][static_cast<size_t>(std::clamp(std::floor(rate - 0.00001), 0.0, (LUT_LEVELS-1)*1.0))];
|
||||
|
||||
Data::AudioFrame out(0.0);
|
||||
|
||||
auto ii = static_cast<ptrdiff_t>(ip) - LUT_HTAPS;
|
||||
for (size_t i = 0; i < 8; i++) {
|
||||
if (loop && ii >= le) ii = ((ii - ls) % ll) + ls;
|
||||
else if (ii >= len) return out; // we can early-out here
|
||||
if (ii >= 0) out += (*smp)[static_cast<size_t>(ii)] * pt[i];
|
||||
ii++;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
#include "svfilter.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
#include "audio/audioengine.h"
|
||||
|
||||
using Xybrid::NodeLib::SVFilter;
|
||||
using Xybrid::NodeLib::GenericSVFilter;
|
||||
using Xybrid::Data::AudioFrame;
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
template class Xybrid::NodeLib::GenericSVFilter<AudioFrame>;
|
||||
template class Xybrid::NodeLib::GenericSVFilter<double>;
|
||||
/*
|
||||
template<typename DT>
|
||||
void GenericSVFilter<DT>::process(DT in, double cutoff, double resonance, int ovs) {
|
||||
if (ovs <= 0) return;
|
||||
cutoff = std::max(cutoff, 1.0);
|
||||
resonance = std::max(resonance, 0.01);
|
||||
|
||||
double f = 2.0 * std::sin(PI * cutoff / (audioEngine->curSampleRate() * ovs));
|
||||
double q = std::sqrt(1.0 - std::atan(std::sqrt(resonance)) * 2.0 / PI);
|
||||
double damp = std::sqrt(q);
|
||||
|
||||
for (int i = 0; i < ovs; i++) {
|
||||
low += band*f;
|
||||
high = in*damp - low - band*q;
|
||||
band += high*f;
|
||||
}
|
||||
notch = high+low;
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include "data/audioframe.h"
|
||||
#include "nodelib/basics.h"
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
namespace Xybrid::NodeLib {
|
||||
/// 12db Chamberlin State Variable Filter
|
||||
template<typename DT> class GenericSVFilter {
|
||||
public:
|
||||
/// Default oversampling level. Enough to mostly eliminate artifacting at high cutoff.
|
||||
static const constexpr int DEFAULT_OVERSAMP = 3;
|
||||
|
||||
DT low = 0.0;
|
||||
DT high = 0.0;
|
||||
DT band = 0.0;
|
||||
DT notch = 0.0;
|
||||
|
||||
GenericSVFilter() = default;
|
||||
~GenericSVFilter() = default;
|
||||
// nothing used here should care about taking the raw approach
|
||||
inline GenericSVFilter<DT>(const GenericSVFilter<DT>& o) { std::memcpy(static_cast<void*>(this), static_cast<const void*>(&o), sizeof(o)); }
|
||||
inline GenericSVFilter<DT>& operator=(const GenericSVFilter<DT>& o) { std::memcpy(static_cast<void*>(this), static_cast<const void*>(&o), sizeof(o)); return *this; }
|
||||
|
||||
void process(DT in, double cutoff, double resonance, int oversamp = DEFAULT_OVERSAMP) {
|
||||
if (oversamp <= 0) return;
|
||||
cutoff = std::max(cutoff, 1.0);
|
||||
resonance = std::max(resonance, 0.01);
|
||||
|
||||
double f = 2.0 * std::sin(PI * cutoff / (audioEngine->curSampleRate() * oversamp));
|
||||
double q = std::sqrt(1.0 - std::atan(std::sqrt(resonance)) * 2.0 / PI);
|
||||
double damp = std::sqrt(q);
|
||||
|
||||
for (int i = 0; i < oversamp; i++) {
|
||||
low += band*f;
|
||||
high = in*damp - low - band*q;
|
||||
band += high*f;
|
||||
}
|
||||
notch = high+low;
|
||||
}
|
||||
inline void reset() { low = 0.0; high = 0.0; band = 0.0; notch = 0.0; }
|
||||
inline void normalize(double m) {
|
||||
if constexpr (std::is_arithmetic_v<DT>) {
|
||||
low = std::clamp(low, -m, m);
|
||||
high = std::clamp(high, -m, m);
|
||||
band = std::clamp(band, -m, m);
|
||||
notch = std::clamp(notch, -m, m);
|
||||
} else {
|
||||
low = low.clamp(m);
|
||||
high = high.clamp(m);
|
||||
band = band.clamp(m);
|
||||
notch = notch.clamp(m);
|
||||
}
|
||||
}
|
||||
|
||||
static inline double scaledResonance(double r) { return std::pow(10, r*5); }
|
||||
|
||||
};
|
||||
|
||||
// explicit instantiation declarations to eliminate warnings
|
||||
extern template void Xybrid::NodeLib::GenericSVFilter<Data::AudioFrame>::process(Data::AudioFrame, double, double, int);
|
||||
extern template void Xybrid::NodeLib::GenericSVFilter<double>::process(double, double, double, int);
|
||||
|
||||
/// 12db Chamberlin State Variable Filter
|
||||
typedef GenericSVFilter<Xybrid::Data::AudioFrame> SVFilter;
|
||||
/// 12db Chamberlin State Variable Filter (mono version)
|
||||
typedef GenericSVFilter<double> SVFilterM;
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
#include "autopan.h"
|
||||
|
||||
using Xybrid::Effects::AutoPan;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
#include "data/audioframe.h"
|
||||
#include "data/porttypes.h"
|
||||
|
||||
#include "config/pluginregistry.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
#include "ui/gadgets/layoutgadget.h"
|
||||
#include "ui/gadgets/togglegadget.h"
|
||||
#include "ui/gadgets/knobgadget.h"
|
||||
using namespace Xybrid::UI;
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QCborMap>
|
||||
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(AutoPan, {
|
||||
i->id = "fx:autopan";
|
||||
i->displayName = "Auto Pan";
|
||||
i->category = "Effect";
|
||||
})
|
||||
|
||||
AutoPan::AutoPan() { }
|
||||
|
||||
void AutoPan::init() {
|
||||
addPort(Port::Input, Port::Audio, 0);
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
}
|
||||
|
||||
void AutoPan::reset() {
|
||||
cyc = 0.0;
|
||||
}
|
||||
|
||||
void AutoPan::process() {
|
||||
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
|
||||
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
|
||||
in->pull();
|
||||
out->pull();
|
||||
|
||||
auto sr = audioEngine->curSampleRate();
|
||||
auto ts = audioEngine->curTickSize();
|
||||
|
||||
auto eRate = rate;
|
||||
if (bpmRelative) eRate *= audioEngine->curTempo() / 60.0;
|
||||
|
||||
auto tl = static_cast<double>(ts) / static_cast<double>(sr); // tick length
|
||||
for (size_t f = 0; f < ts; f++) {
|
||||
AudioFrame fCurrent = (*in)[f];
|
||||
auto cc = cyc+phase + (static_cast<double>(f) / static_cast<double>(sr)) * eRate;
|
||||
|
||||
(*out)[f] = fCurrent.gainBalance(0.0, level * std::sin(cc * 2*PI));
|
||||
}
|
||||
|
||||
cyc = std::fmod(cyc + tl*eRate, 1.0);
|
||||
}
|
||||
|
||||
void AutoPan::saveData(QCborMap& m) const {
|
||||
m[qs("level")] = level;
|
||||
m[qs("rate")] = rate;
|
||||
m[qs("phase")] = phase;
|
||||
m[qs("bpmRelative")] = bpmRelative;
|
||||
}
|
||||
|
||||
void AutoPan::loadData(const QCborMap& m) {
|
||||
level = m.value("level").toDouble(level);
|
||||
rate = m.value("rate").toDouble(rate);
|
||||
phase = m.value("phase").toDouble(phase);
|
||||
bpmRelative = m.value("bpmRelative").toBool(bpmRelative);
|
||||
}
|
||||
|
||||
void AutoPan::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
auto l = new LayoutGadget(obj);
|
||||
l->setMetrics(-1, 2);
|
||||
|
||||
(new KnobGadget(l))->bind(rate)->setLabel(qs("Rate"))->setRange(0.0, 8.0, 0.01, -1, 0.001)->setDefault(1.0);
|
||||
auto l2 = (new LayoutGadget(l, true))->setMetrics(0, 2);
|
||||
(new ToggleGadget(l2))->bind(bpmRelative)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
|
||||
KnobGadget::autoPercent(l2, phase)->setLabel(qs("Phase"))->setSize(22);
|
||||
KnobGadget::autoBalance(l, level)->setLabel(qs("Level"));
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "data/node.h"
|
||||
|
||||
namespace Xybrid::Effects {
|
||||
class AutoPan : public Data::Node {
|
||||
double level;
|
||||
double rate = 1.0;
|
||||
double phase = 0.0;
|
||||
bool bpmRelative = false;
|
||||
|
||||
double cyc; // current cycle tracking
|
||||
|
||||
public:
|
||||
AutoPan();
|
||||
~AutoPan() override = default;
|
||||
|
||||
void init() override;
|
||||
void reset() override;
|
||||
//void release() override;
|
||||
void process() override;
|
||||
|
||||
void saveData(QCborMap&) const override;
|
||||
void loadData(const QCborMap&) override;
|
||||
|
||||
void onGadgetCreated() override;
|
||||
};
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
using Xybrid::Effects::Delay;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
|
@ -24,24 +26,20 @@ using namespace Xybrid::UI;
|
|||
|
||||
#include <QCborMap>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "gadget:delay";
|
||||
i->displayName = "Delay";
|
||||
i->category = "Effect";
|
||||
i->createInstance = []{ return std::make_shared<Delay>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
});
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Delay, {
|
||||
i->id = "fx:delay";
|
||||
i->oldIds = {"gadget:delay"};
|
||||
i->displayName = "Delay";
|
||||
i->category = "Effect";
|
||||
})
|
||||
|
||||
Delay::Delay() { }
|
||||
|
||||
void Delay::init() {
|
||||
addPort(Port::Input, Port::Audio, 0);
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
addPort(Port::Output, Port::Audio, 1)->name = "wet only";
|
||||
}
|
||||
|
||||
void Delay::reset() {
|
||||
|
@ -61,7 +59,7 @@ void Delay::process() {
|
|||
auto fbMult = std::pow(feedback, 2);
|
||||
|
||||
// calculate number of frames ahead to write to the buffer
|
||||
int frames = static_cast<int>(delayTime * static_cast<double>(audioEngine->curSampleRate()) * (timeInBeats ? (60.0 / audioEngine->curTempo()) : 1.0));
|
||||
int frames = static_cast<int>(delayTime * static_cast<double>(audioEngine->curSampleRate()) * (bpmRelative ? (60.0 / audioEngine->curTempo()) : 1.0));
|
||||
|
||||
// enlarge buffer if too small
|
||||
if (auto mc = frames+1; buf.capacity() < mc) buf.setCapacity(mc);
|
||||
|
@ -69,8 +67,12 @@ void Delay::process() {
|
|||
|
||||
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
|
||||
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
|
||||
auto wout = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 1));
|
||||
in->pull();
|
||||
out->pull();
|
||||
bool oc = out->isConnected();
|
||||
bool wc = wout->isConnected();
|
||||
if (oc) out->pull();
|
||||
if (wc) wout->pull();
|
||||
|
||||
size_t ts = audioEngine->curTickSize();
|
||||
for (size_t f = 0; f < ts; f++) {
|
||||
|
@ -79,21 +81,25 @@ void Delay::process() {
|
|||
if (!buf.areIndexesValid()) buf.normalizeIndexes(); // make sure we can actually reach the point we need
|
||||
int i = frames + buf.firstIndex();
|
||||
buf[i] += (fCurrent * delayMult) + (fOut * fbMult);
|
||||
if (pingPong) buf[i] = buf[i].flip();
|
||||
|
||||
(*out)[f] = fCurrent + fOut;
|
||||
if (oc) (*out)[f] = fCurrent + fOut;
|
||||
if (wc) (*wout)[f] = fOut;
|
||||
}
|
||||
}
|
||||
|
||||
void Delay::saveData(QCborMap& m) const {
|
||||
m[qs("time")] = QCborValue(delayTime);
|
||||
m[qs("inBeats")] = QCborValue(timeInBeats);
|
||||
m[qs("amount")] = QCborValue(amount);
|
||||
m[qs("feedback")] = QCborValue(feedback);
|
||||
m[qs("time")] = delayTime;
|
||||
m[qs("bpmRelative")] = bpmRelative;
|
||||
m[qs("pingPong")] = pingPong;
|
||||
m[qs("amount")] = amount;
|
||||
m[qs("feedback")] = feedback;
|
||||
}
|
||||
|
||||
void Delay::loadData(const QCborMap& m) {
|
||||
delayTime = m.value("time").toDouble(delayTime);
|
||||
timeInBeats = m.value("inBeats").toBool(timeInBeats);
|
||||
bpmRelative = m.value("bpmRelative").toBool(m.value("inBeats").toBool(bpmRelative));
|
||||
pingPong = m.value("pingPong").toBool(pingPong);
|
||||
amount = m.value("amount").toDouble(amount);
|
||||
feedback = m.value("feedback").toDouble(feedback);
|
||||
}
|
||||
|
@ -102,10 +108,12 @@ void Delay::onGadgetCreated() {
|
|||
if (!obj) return;
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
(new KnobGadget(l))->bind(delayTime)->setLabel(qs("Time"))->setRange(0.0, 5.0, 0.001)->setDefault(0.5);
|
||||
(new ToggleGadget(l))->bind(timeInBeats)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
|
||||
l->addSpacer();
|
||||
(new KnobGadget(l))->bind(amount)->setLabel(qs("Amount"))->setDefault(0.5);
|
||||
l->addSpacer();
|
||||
(new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setDefault(0.0);
|
||||
(new KnobGadget(l))->bind(delayTime)->setLabel(qs("Time"))->setRange(0.0, 5.0, 0.001, -1, 0.01)->setDefault(0.5);
|
||||
auto l2 = (new LayoutGadget(l, true))->setMetrics(0, 4);
|
||||
(new ToggleGadget(l2))->bind(bpmRelative)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
|
||||
(new ToggleGadget(l2))->bind(pingPong)->setColor({127, 191, 255})->setToolTip(qs("Stereo ping-pong"));
|
||||
//l->addSpacer();
|
||||
(new KnobGadget(l))->bind(amount)->setLabel(qs("Level"))->setTextFunc(KnobGadget::textPercent)->setDefault(0.5);
|
||||
//l->addSpacer();
|
||||
(new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setTextFunc(KnobGadget::textPercent)->setDefault(0.0);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ namespace Xybrid::Effects {
|
|||
QContiguousCache<Data::AudioFrame> buf;
|
||||
|
||||
double delayTime = 0.5;
|
||||
bool timeInBeats = false;
|
||||
bool bpmRelative = false;
|
||||
|
||||
bool pingPong = false;
|
||||
|
||||
double amount = 0.5;
|
||||
double feedback = 0.0;
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
#include "distortion.h"
|
||||
|
||||
using Xybrid::Effects::Distortion;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
#include "data/audioframe.h"
|
||||
#include "data/porttypes.h"
|
||||
|
||||
#include "config/pluginregistry.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
#include "ui/gadgets/layoutgadget.h"
|
||||
#include "ui/gadgets/togglegadget.h"
|
||||
#include "ui/gadgets/knobgadget.h"
|
||||
using namespace Xybrid::UI;
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QCborMap>
|
||||
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Distortion, {
|
||||
i->id = "fx:distortion";
|
||||
i->displayName = "Distortion";
|
||||
i->category = "Effect";
|
||||
})
|
||||
|
||||
Distortion::Distortion() = default;
|
||||
|
||||
void Distortion::init() {
|
||||
addPort(Port::Input, Port::Audio, 0);
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
}
|
||||
|
||||
void Distortion::reset() {
|
||||
//
|
||||
}
|
||||
|
||||
namespace {
|
||||
inline double sxp(double v, double e) {
|
||||
double s = v < 0 ? -1.0 : 1.0;
|
||||
return std::pow(std::abs(v), e) * s;
|
||||
}
|
||||
inline AudioFrame sxp(AudioFrame v, double e) { return {sxp(v.l, e), sxp(v.r, e)}; }
|
||||
}
|
||||
|
||||
void Distortion::process() {
|
||||
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
|
||||
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
|
||||
in->pull();
|
||||
out->pull();
|
||||
|
||||
auto ts = audioEngine->curTickSize();
|
||||
|
||||
auto d = drive.start(), s = shape.start(), m = mix.start(), o = output.start();
|
||||
|
||||
for (size_t f = 0; f < ts; f++) {
|
||||
AudioFrame inp = (*in)[f];
|
||||
auto g = inp.gainBalance(d.next());
|
||||
auto pv = s.next();
|
||||
auto exp = pv > 0.0 ? 1.0 / (1.0 + pv) : -pv + 1.0;
|
||||
|
||||
|
||||
(*out)[f] = AudioFrame::lerp(inp, sxp(g.clamp(), exp), m.next()).gainBalance(o.next());
|
||||
}
|
||||
}
|
||||
|
||||
void Distortion::saveData(QCborMap& m) const {
|
||||
drive.save(m);
|
||||
shape.save(m);
|
||||
mix.save(m);
|
||||
output.save(m);
|
||||
}
|
||||
|
||||
void Distortion::loadData(const QCborMap& m) {
|
||||
drive.load(m);
|
||||
shape.load(m);
|
||||
mix.load(m);
|
||||
output.load(m);
|
||||
}
|
||||
|
||||
void Distortion::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
KnobGadget::autoGain(l, drive);
|
||||
(new KnobGadget(l))->setRange(0, 0, 0.1)->bind(shape)->setTextFunc(KnobGadget::textOffset);
|
||||
KnobGadget::autoPercent(l, mix);
|
||||
KnobGadget::autoGain(l, output);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "data/node.h"
|
||||
#include "nodelib/param.h"
|
||||
|
||||
namespace Xybrid::Effects {
|
||||
class Distortion : public Data::Node {
|
||||
NodeLib::Param drive = {"Drive", 0.0, 24.0, 0.0};
|
||||
NodeLib::Param shape = {"Shape", -10.0, 10.0, 0.0};
|
||||
NodeLib::Param mix = {"Mix", 0.0, 1.0, 1.0};
|
||||
NodeLib::Param output = {"Output", -12.0, 12.0, 0.0};
|
||||
|
||||
public:
|
||||
Distortion();
|
||||
~Distortion() override = default;
|
||||
|
||||
void init() override;
|
||||
void reset() override;
|
||||
//void release() override;
|
||||
void process() override;
|
||||
|
||||
void saveData(QCborMap&) const override;
|
||||
void loadData(const QCborMap&) override;
|
||||
|
||||
void onGadgetCreated() override;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
#include "ringmod.h"
|
||||
|
||||
using Xybrid::Effects::RingMod;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
#include "data/audioframe.h"
|
||||
#include "data/porttypes.h"
|
||||
|
||||
#include "config/pluginregistry.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
#include "ui/gadgets/layoutgadget.h"
|
||||
#include "ui/gadgets/togglegadget.h"
|
||||
#include "ui/gadgets/knobgadget.h"
|
||||
using namespace Xybrid::UI;
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QCborMap>
|
||||
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(RingMod, {
|
||||
i->id = "fx:ringmod";
|
||||
i->displayName = "Ring Mod";
|
||||
i->category = "Effect";
|
||||
})
|
||||
|
||||
RingMod::RingMod() { }
|
||||
|
||||
void RingMod::init() {
|
||||
addPort(Port::Input, Port::Audio, 0)->name = "carrier";
|
||||
addPort(Port::Input, Port::Audio, 1)->name = "modulator";
|
||||
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
}
|
||||
|
||||
void RingMod::reset() {
|
||||
//
|
||||
}
|
||||
|
||||
void RingMod::process() {
|
||||
auto c = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
|
||||
auto m = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 1));
|
||||
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
|
||||
c->pull();
|
||||
m->pull();
|
||||
out->pull();
|
||||
|
||||
auto ts = audioEngine->curTickSize();
|
||||
for (size_t f = 0; f < ts; f++) {
|
||||
AudioFrame fc = (*c)[f];
|
||||
AudioFrame fm = (*m)[f];
|
||||
if (am) fm = {std::abs(fm.l), std::abs(fm.r)};
|
||||
|
||||
(*out)[f] = AudioFrame::lerp(fc, fc*fm, mix);
|
||||
}
|
||||
}
|
||||
|
||||
void RingMod::saveData(QCborMap& m) const {
|
||||
m[qs("mix")] = mix;
|
||||
m[qs("am")] = am;
|
||||
}
|
||||
|
||||
void RingMod::loadData(const QCborMap& m) {
|
||||
mix = m.value("mix").toDouble(mix);
|
||||
am = m.value("am").toBool(am);
|
||||
}
|
||||
|
||||
void RingMod::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
l->setMetrics(3, 4);
|
||||
KnobGadget::autoPercent(l, mix)->setLabel(qs("Mix"))->setDefault(1.0);
|
||||
(new ToggleGadget(l))->bind(am)->setToolTip("AM mode", {1.0, 0.0})->setColor({127, 255, 127});
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "data/node.h"
|
||||
|
||||
namespace Xybrid::Effects {
|
||||
class RingMod : public Data::Node {
|
||||
double mix = 1.0;
|
||||
bool am = false;
|
||||
|
||||
public:
|
||||
RingMod();
|
||||
~RingMod() override = default;
|
||||
|
||||
void init() override;
|
||||
void reset() override;
|
||||
//void release() override;
|
||||
void process() override;
|
||||
|
||||
void saveData(QCborMap&) const override;
|
||||
void loadData(const QCborMap&) override;
|
||||
|
||||
void onGadgetCreated() override;
|
||||
};
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* Description: State Variable Filter
|
||||
*
|
||||
*
|
||||
* Version:
|
||||
* Version:
|
||||
* Created: Fri Nov 1 23:36:50 2019
|
||||
* Revision: None
|
||||
* Author: Rachel Fae Fox (foxiepaws),fox@foxiepa.ws
|
||||
|
@ -16,6 +16,8 @@
|
|||
using Xybrid::Effects::SVF;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
|
@ -38,35 +40,27 @@ using namespace Xybrid::UI;
|
|||
|
||||
#include <QCborMap>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
#define MIN(a,b) (a<b?a:b)
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "gadget:svf";
|
||||
i->displayName = "Filter";
|
||||
i->category = "Effect";
|
||||
i->createInstance = []{ return std::make_shared<SVF>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
});
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(SVF, {
|
||||
i->id = "fx:svf";
|
||||
i->oldIds = {"gadget:svf"};
|
||||
i->displayName = "Filter";
|
||||
i->category = "Effect";
|
||||
})
|
||||
|
||||
SVF::SVF() { }
|
||||
|
||||
void SVF::init() {
|
||||
auto sr = audioEngine->curSampleRate();
|
||||
this->max_freq = ((float)sr / 4.0);
|
||||
this->frequency = 12000;
|
||||
this->resonance = 65;
|
||||
addPort(Port::Input, Port::Audio, 0);
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
|
||||
//addPort(Port::Input, Port::Parameter, 0);
|
||||
//auto p = Param("Cutoff", 0.0, 16000.0, 0.0);
|
||||
cutoff.makePort(this);
|
||||
}
|
||||
|
||||
void SVF::reset() {
|
||||
release();
|
||||
auto sr = audioEngine->curSampleRate();
|
||||
filter.reset();
|
||||
}
|
||||
|
||||
void SVF::release() {
|
||||
|
@ -74,107 +68,76 @@ void SVF::release() {
|
|||
}
|
||||
|
||||
void SVF::process() {
|
||||
|
||||
|
||||
|
||||
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
|
||||
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
|
||||
in->pull();
|
||||
out->pull();
|
||||
if (this->fm != _off)
|
||||
if (this->frequency > 0) { this->frequency-=0.1; }
|
||||
|
||||
auto r = filter.scaledResonance(resonance);
|
||||
auto c = cutoff.start();
|
||||
|
||||
size_t ts = audioEngine->curTickSize();
|
||||
for (size_t f = 0; f < ts; f++) {
|
||||
AudioFrame fCurrent = (*in)[f];
|
||||
AudioFrame fOut;
|
||||
if (this->fm == _off) {
|
||||
(*out)[f] = fCurrent;
|
||||
continue;
|
||||
}
|
||||
|
||||
double res = this->resonance;
|
||||
//double damp = MIN(2.0*(1.0 - pow(res, 0.25)), MIN(2.0, 2.0/freq - freq*0.5));
|
||||
double q = sqrt(1.0 - atan(sqrt(res)) * 2.0 / PI);
|
||||
double damp = sqrt(q);
|
||||
AudioFrame inp = (*in)[f];
|
||||
|
||||
//double freq = 2.0*sin(M_PI * MIN(0.25, this->frequency/audioEngine->curSampleRate()));
|
||||
double freq = this->frequency / (audioEngine->curSampleRate() * 2 );
|
||||
double in_l, in_r;
|
||||
double low_l, low_r;
|
||||
double band_l, band_r;
|
||||
double high_l, high_r;
|
||||
double notch_l, notch_r;
|
||||
filter.process(inp, c.next(), r);
|
||||
|
||||
in_l = fCurrent.l;
|
||||
in_r = fCurrent.r;
|
||||
low_l=low.l;
|
||||
low_r=low.r;
|
||||
band_l=band.l;
|
||||
band_r=band.r;
|
||||
high_l=high.l;
|
||||
high_r=high.r;
|
||||
notch_l=notch.l;
|
||||
notch_r=notch.r;
|
||||
|
||||
|
||||
low_l = low_l + freq * band_l;
|
||||
high_l = damp * in_l - low_l - q * band_l;
|
||||
band_l = freq * high_l + band_l;
|
||||
notch_l = high_l + low_l;
|
||||
low_r = low_r + freq * band_r;
|
||||
high_r = damp * in_r - low_r - q * band_r;
|
||||
band_r = freq * high_r + band_r;
|
||||
notch_r = high_r + low_r;
|
||||
low_l = low_l + freq * band_l;
|
||||
high_l = damp * in_l - low_l - q * band_l;
|
||||
band_l = freq * high_l + band_l;
|
||||
notch_l = high_l + low_l;
|
||||
low_r = low_r + freq * band_r;
|
||||
high_r = damp * in_r - low_r - q * band_r;
|
||||
band_r = freq * high_r + band_r;
|
||||
notch_r = high_r + low_r;
|
||||
|
||||
low = {low_l, low_r};
|
||||
band = {band_l, band_r};
|
||||
high = {high_l, high_r};
|
||||
notch = {notch_l, notch_r};
|
||||
|
||||
|
||||
switch (fm) {
|
||||
case _low:
|
||||
(*out)[f] = this->low;
|
||||
break;
|
||||
case _band:
|
||||
(*out)[f] = this->band;
|
||||
break;
|
||||
case _high:
|
||||
(*out)[f] = this->high;
|
||||
break;
|
||||
case _notch:
|
||||
(*out)[f] = this->notch;
|
||||
break;
|
||||
}
|
||||
switch (mode) {
|
||||
case Low:
|
||||
(*out)[f] = filter.low;
|
||||
break;
|
||||
case High:
|
||||
(*out)[f] = filter.high;
|
||||
break;
|
||||
case Band:
|
||||
(*out)[f] = filter.band;
|
||||
break;
|
||||
case Notch:
|
||||
(*out)[f] = filter.notch;
|
||||
break;
|
||||
default:
|
||||
(*out)[f] = inp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SVF::saveData(QCborMap& m) const {
|
||||
//m[qs("frequency")] = QCborValue(frequency);
|
||||
//m[qs("resonance")] = QCborValue(resonance);
|
||||
//m[qs("cutoff")] = cutoff;
|
||||
cutoff.save(m);
|
||||
m[qs("resonance")] = resonance;
|
||||
m[qs("mode")] = mode;
|
||||
}
|
||||
|
||||
void SVF::loadData(const QCborMap& m) {
|
||||
//frequency = m.value("frequency").toDouble(frequency);
|
||||
//resonance = m.value("resonance").toDouble(resonance);
|
||||
cutoff.load(m, "frequency");
|
||||
cutoff.load(m);
|
||||
//cutoff = m.value("cutoff").toDouble(m.value("frequency").toDouble(cutoff));
|
||||
resonance = m.value("resonance").toDouble(resonance);
|
||||
mode = static_cast<FilterMode>(m.value("mode").toInteger(mode));
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::unordered_map<SVF::FilterMode, QString> modeNames = [] {
|
||||
std::unordered_map<SVF::FilterMode, QString> m;
|
||||
m[SVF::Off] = "off";
|
||||
m[SVF::Low] = "low";
|
||||
m[SVF::High] = "high";
|
||||
m[SVF::Band] = "band";
|
||||
m[SVF::Notch] = "notch";
|
||||
return m;
|
||||
}();
|
||||
}
|
||||
|
||||
void SVF::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
//(new KnobGadget(l))->bind(freq)->setLabel(qs("Frequency Step"))->setRange(0.0, 10, 0.5)->setDefault(1.0);
|
||||
(new KnobGadget(l))->bind(frequency)->setLabel(qs("Frequency"))->setRange(0.0, this->max_freq, 10.0)->setDefault(6440.0);
|
||||
l->addSpacer();
|
||||
(new KnobGadget(l))->bind(resonance)->setLabel(qs("Resonance"))->setRange(0.0, 100.0, 0.1)->setDefault(0.0);
|
||||
l->addSpacer();
|
||||
(new KnobGadget(l))->bind(fm)->setLabel(qs("Filter Mode"))->setRange(0,4,1)->setDefault(0);
|
||||
auto modetxt = [](double inp) {
|
||||
if (auto f = modeNames.find(static_cast<FilterMode>(inp)); f != modeNames.end()) return f->second;
|
||||
return qs("?");
|
||||
};
|
||||
|
||||
KnobGadget::autoCutoff(l, cutoff);
|
||||
(new KnobGadget(l))->bind(resonance)->setLabel(qs("Res"))->setTextFunc(KnobGadget::textPercent)->setRange(0.0, 1.0, 0.01)->setDefault(0.0);
|
||||
(new KnobGadget(l))->bind(mode)->setLabel(qs("Mode"))->setTextFunc(modetxt)->setRange(0, Notch, 1, KnobGadget::BigStep)->setDefault(0);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* Description:
|
||||
*
|
||||
*
|
||||
* Version:
|
||||
* Version:
|
||||
* Created: Fri Nov 1 23:34:34 2019
|
||||
* Revision: None
|
||||
* Author: Rachel Fae Fox (foxiepaws),fox@foxiepa.ws
|
||||
|
@ -14,29 +14,24 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QContiguousCache>
|
||||
|
||||
#include "data/node.h"
|
||||
#include "data/audioframe.h"
|
||||
#include "nodelib/svfilter.h"
|
||||
#include "nodelib/param.h"
|
||||
|
||||
namespace Xybrid::Effects {
|
||||
class SVF : public Data::Node {
|
||||
enum FilterMode {_off, _low, _band, _high, _notch };
|
||||
double frequency= 0.5;
|
||||
NodeLib::SVFilter filter;
|
||||
|
||||
//double cutoff = 6440.0;
|
||||
NodeLib::Param cutoff = {"Cutoff", 0, 16000, 0};
|
||||
|
||||
double resonance = 0.0;
|
||||
|
||||
Xybrid::Data::AudioFrame low = 0.0;
|
||||
Xybrid::Data::AudioFrame band = 0.0;
|
||||
Xybrid::Data::AudioFrame high = 0.0;
|
||||
Xybrid::Data::AudioFrame notch = 0.0;
|
||||
FilterMode fm = _off;
|
||||
|
||||
// solve these in cons.
|
||||
double max_freq;
|
||||
double freq;
|
||||
double q;
|
||||
|
||||
public:
|
||||
enum FilterMode : uchar { Off, Low, High, Band, Notch };
|
||||
FilterMode mode = Low;
|
||||
|
||||
SVF();
|
||||
~SVF() override = default;
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
using Xybrid::Gadgets::GainBalance;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
|
@ -23,20 +25,13 @@ using namespace Xybrid::UI;
|
|||
|
||||
#include <QCborMap>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "gadget:gainbalance";
|
||||
i->displayName = "Gain/Balance";
|
||||
i->category = "Gadget";
|
||||
//i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<GainBalance>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
//inf = i;
|
||||
});
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(GainBalance, {
|
||||
i->id = "gadget:gainbalance";
|
||||
i->displayName = "Gain/Balance";
|
||||
i->category = "Gadget";
|
||||
//i->hidden = true;
|
||||
})
|
||||
|
||||
GainBalance::GainBalance() {
|
||||
|
||||
|
@ -74,6 +69,6 @@ void GainBalance::onGadgetCreated() {
|
|||
obj->showPluginName = false;
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
(new KnobGadget(l))->bind(gain)->setRange(-60, 6, .1)->setLabel("Gain")->setTextFunc([](double d) { return QString("%1dB").arg(d); });
|
||||
(new KnobGadget(l))->bind(balance)->setRange(-1.0, 1.0)->setLabel("Balance");
|
||||
KnobGadget::autoGain(l, gain);
|
||||
KnobGadget::autoBalance(l, balance);
|
||||
}
|
||||
|
|
|
@ -26,17 +26,14 @@ using namespace Xybrid::Audio;
|
|||
|
||||
#include "util/strings.h"
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "ioport";
|
||||
i->displayName = "I/O Port";
|
||||
i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<IOPort>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
//inf = i;
|
||||
});
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(IOPort, {
|
||||
i->id = "ioport";
|
||||
i->displayName = "I/O Port";
|
||||
i->hidden = true;
|
||||
})
|
||||
|
||||
namespace {
|
||||
Port::Type opposite(Port::Type t) {
|
||||
if (t == Port::Input) return Port::Output;
|
||||
return Port::Input;
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
using Xybrid::Gadgets::MixBoard;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "nodelib/basics.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
|
@ -27,18 +29,12 @@ using namespace Xybrid::UI;
|
|||
#include <QCborMap>
|
||||
#include <QCborArray>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "gadget:mixboard";
|
||||
i->displayName = "Mixer Board";
|
||||
i->category = "Gadget";
|
||||
i->createInstance = []{ return std::make_shared<MixBoard>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
});
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(MixBoard, {
|
||||
i->id = "gadget:mixboard";
|
||||
i->displayName = "Mixer Board";
|
||||
i->category = "Gadget";
|
||||
})
|
||||
|
||||
MixBoard::MixBoard() {
|
||||
|
||||
|
@ -137,7 +133,8 @@ void MixBoard::onGadgetCreated() {
|
|||
if (!obj) return;
|
||||
{
|
||||
auto k = new Gadget();
|
||||
for (auto c : obj->contents->childItems()) c->setParentItem(k);
|
||||
auto ch = obj->contents->childItems(); // avoid detach warnings
|
||||
for (auto c : ch) c->setParentItem(k);
|
||||
k->deleteLater();
|
||||
}
|
||||
qDeleteAll(obj->contents->childItems()); // clear out anything already there
|
||||
|
@ -159,13 +156,13 @@ void MixBoard::onGadgetCreated() {
|
|||
/*auto mute = */(new ToggleGadget(tl))->bind(sections[i].mute)->setColor({255, 0, 0})->setToolTip("Mute", {-1, 0});
|
||||
/*auto solo = */(new ToggleGadget(tl))->bind(sections[i].solo)->setColor({191, 191, 0})->setToolTip("Solo", {-1, 0});
|
||||
|
||||
/*auto gain = */(new KnobGadget(ln))->bind(sections[i].gain)->setRange(-60, 6, .1)->setLabel("Gain")->setTextFunc([](double d) { return QString("%1dB").arg(d); });
|
||||
/*auto gain = */KnobGadget::autoGain(ln, sections[i].gain);
|
||||
|
||||
auto end = (new LayoutGadget(ln, true))->setMetrics(-1, spc);
|
||||
auto bIns = (new ButtonGadget(end))->setSize(16, 16)->setText("+");
|
||||
auto bDel = (new ButtonGadget(end))->setSize(16, 16)->setText("-");
|
||||
QObject::connect(bIns, &ButtonGadget::clicked, [this, i] { insertSection(static_cast<uint8_t>(i)); });
|
||||
QObject::connect(bDel, &ButtonGadget::clicked, [this, i] { removeSection(static_cast<uint8_t>(i)); });
|
||||
QObject::connect(bIns, &ButtonGadget::clicked, obj, [this, i] { insertSection(static_cast<uint8_t>(i)); });
|
||||
QObject::connect(bDel, &ButtonGadget::clicked, obj, [this, i] { removeSection(static_cast<uint8_t>(i)); });
|
||||
|
||||
if (count <= 1) delete bDel; // no dropping to zero
|
||||
}
|
||||
|
@ -175,5 +172,5 @@ void MixBoard::onGadgetCreated() {
|
|||
for (auto i = 0; i < static_cast<int>(count); i++) c[i]->setY(lc[i]->y() + lc[i]->boundingRect().center().y());
|
||||
|
||||
auto btn = (new ButtonGadget(l))->setSize(16, 16)->setText("+");
|
||||
QObject::connect(btn, &ButtonGadget::clicked, [this, count] { insertSection(static_cast<uint8_t>(count)); });
|
||||
QObject::connect(btn, &ButtonGadget::clicked, obj, [this, count] { insertSection(static_cast<uint8_t>(count)); });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
#include "quicklevel.h"
|
||||
using Xybrid::Gadgets::QuickLevel;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
#include <QPainter>
|
||||
#include <QGraphicsScene>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "config/pluginregistry.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
using namespace Xybrid::UI;
|
||||
|
||||
#include "data/audioframe.h"
|
||||
#include "data/porttypes.h"
|
||||
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(QuickLevel, {
|
||||
i->id = "gadget:quicklevel";
|
||||
i->displayName = "Quick Level";
|
||||
i->category = "Gadget";
|
||||
})
|
||||
|
||||
QuickLevel::QuickLevel() {
|
||||
|
||||
}
|
||||
|
||||
void QuickLevel::init() {
|
||||
auto in = addPort(Port::Input, Port::Audio, 0);
|
||||
auto out = addPort(Port::Output, Port::Audio, 0);
|
||||
out->passthroughTo = in;
|
||||
|
||||
lv = { };
|
||||
}
|
||||
|
||||
void QuickLevel::reset() {
|
||||
release();
|
||||
auto sr = audioEngine->curSampleRate();
|
||||
buf.setCapacity(static_cast<int>(sr * SPAN_TIME)); // fixed timestep
|
||||
}
|
||||
|
||||
void QuickLevel::release() {
|
||||
buf.clear();
|
||||
buf.setCapacity(0);
|
||||
|
||||
lv = { };
|
||||
|
||||
if (obj) QMetaObject::invokeMethod(obj->scene(), "update", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void QuickLevel::process() {
|
||||
if (!obj) return;
|
||||
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
|
||||
in->pull();
|
||||
size_t ts = audioEngine->curTickSize();
|
||||
|
||||
decltype(lv) clv; // compute on local copy to avoid artifacting on draw thread
|
||||
for (size_t c = 0; c < 2; c++) {
|
||||
clv[c][0] = std::numeric_limits<double>::max();
|
||||
clv[c][1] = std::numeric_limits<double>::min();
|
||||
}
|
||||
|
||||
// push entire tick; we don't need to clear anything as the buffer's maximum size is exactly how much we want
|
||||
for (size_t s = 0; s < ts; s++) buf.append(static_cast<AudioFrame>((*in)[s]));
|
||||
|
||||
if (!buf.areIndexesValid()) buf.normalizeIndexes();
|
||||
auto fst = buf.firstIndex(), lst = buf.lastIndex();
|
||||
for (int i = fst; i <= lst; i++) {
|
||||
auto f = buf.at(i);
|
||||
clv[0][0] = std::min(clv[0][0], f.l);
|
||||
clv[0][1] = std::max(clv[0][1], f.l);
|
||||
clv[1][0] = std::min(clv[1][0], f.r);
|
||||
clv[1][1] = std::max(clv[1][1], f.r);
|
||||
}
|
||||
|
||||
lv = clv;
|
||||
|
||||
if (obj) QMetaObject::invokeMethod(obj, [obj = obj] {
|
||||
obj->scene()->update(obj->sceneBoundingRect());
|
||||
}, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
// clear levels on port disconnect
|
||||
void QuickLevel::onPortDisconnected(Data::Port::Type, Data::Port::DataType, uint8_t, std::weak_ptr<Data::Port>) {
|
||||
lv = { };
|
||||
|
||||
if (obj) QMetaObject::invokeMethod(obj->scene(), "update", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void QuickLevel::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
|
||||
obj->customChrome = true;
|
||||
obj->autoPositionPorts = false;
|
||||
obj->setGadgetSize(QPointF(43, 89));
|
||||
|
||||
auto r = obj->boundingRect();
|
||||
auto pm = PortObject::portSize * .5 + PortObject::portSpacing;
|
||||
auto offs = QPointF(r.width() / 2 + pm, 0);
|
||||
obj->inputPortContainer->setPos(r.center() - offs);
|
||||
obj->outputPortContainer->setPos(r.center() + offs);
|
||||
}
|
||||
|
||||
namespace {
|
||||
inline constexpr double dval(double in) {
|
||||
in = std::clamp(in, -1.0, 1.0);
|
||||
double n = in < 0.0 ? -1.0 : 1.0;
|
||||
return std::pow(std::abs(in), QuickLevel::DISPLAY_EXPONENT) * n;
|
||||
}
|
||||
}
|
||||
|
||||
void QuickLevel::drawCustomChrome(QPainter* painter, const QStyleOptionGraphicsItem* opt) {
|
||||
auto r = obj->boundingRect();
|
||||
NodeObject::drawPanel(painter, opt, r, 4);
|
||||
|
||||
// set up bar geometry
|
||||
QSizeF barSize(16, 81);
|
||||
QRectF barL(QPointF(), barSize);
|
||||
QRectF barR(QPointF(), barSize);
|
||||
barL.moveCenter(r.center() + QPointF(-9.5, 0));
|
||||
barR.moveCenter(r.center() + QPointF(9.5, 0));
|
||||
|
||||
double oh = barSize.height() / 2;
|
||||
|
||||
// draw bars
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
auto b = i == 0 ? barL : barR;
|
||||
|
||||
// bg
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor(0, 0, 0, 127));
|
||||
painter->drawRect(b);
|
||||
|
||||
// level
|
||||
QRectF bc(QPoint(), QSizeF(b.width(), 0));
|
||||
bc.moveCenter(b.center());
|
||||
bc.adjust(0, dval(-lv[i][1]) * oh, 0, dval(-lv[i][0]) * oh);
|
||||
|
||||
if (std::abs(lv[i][0]) > 1.0 || std::abs(lv[i][1]) > 1.0) painter->setBrush(QColor(255, 255, 63));
|
||||
else painter->setBrush(QColor(63, 255, 63));
|
||||
painter->drawRect(bc);
|
||||
|
||||
// center tick
|
||||
QRectF ln(QPoint(), QSizeF(b.width(), 1));
|
||||
ln.moveCenter(b.center());
|
||||
painter->setBrush(QColor(0, 0, 0, 127));
|
||||
painter->drawRect(ln.adjusted(0, -1, 0, 1));
|
||||
painter->setBrush(QColor(255, 255, 255, 191));
|
||||
painter->drawRect(ln);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <QContiguousCache>
|
||||
|
||||
#include "data/node.h"
|
||||
#include "data/audioframe.h"
|
||||
#include <array>
|
||||
namespace Xybrid::Gadgets {
|
||||
class QuickLevel : public Data::Node {
|
||||
QContiguousCache<Data::AudioFrame> buf;
|
||||
|
||||
std::array<std::array<double, 2>, 2> lv;
|
||||
public:
|
||||
// time across which the displayed levels are calculated
|
||||
static const constexpr double SPAN_TIME = 1.0/30;
|
||||
static const constexpr double DISPLAY_EXPONENT = 1;
|
||||
|
||||
QuickLevel();
|
||||
~QuickLevel() override = default;
|
||||
|
||||
void init() override;
|
||||
void reset() override;
|
||||
void release() override;
|
||||
void process() override;
|
||||
|
||||
void onPortDisconnected(Data::Port::Type, Data::Port::DataType, uint8_t, std::weak_ptr<Data::Port>) override;
|
||||
|
||||
void onGadgetCreated() override;
|
||||
void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
|
||||
|
||||
};
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
using Xybrid::Gadgets::Transpose;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include "data/porttypes.h"
|
||||
|
||||
#include "config/pluginregistry.h"
|
||||
|
@ -19,20 +21,12 @@ using namespace Xybrid::UI;
|
|||
|
||||
#include <QCborMap>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "gadget:transpose";
|
||||
i->displayName = "Transpose";
|
||||
i->category = "Gadget";
|
||||
//i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<Transpose>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
//inf = i;
|
||||
});
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Transpose, {
|
||||
i->id = "gadget:transpose";
|
||||
i->displayName = "Transpose";
|
||||
i->category = "Gadget";
|
||||
})
|
||||
|
||||
Transpose::Transpose() {
|
||||
|
||||
|
@ -44,19 +38,24 @@ void Transpose::init() {
|
|||
}
|
||||
|
||||
void Transpose::process() {
|
||||
int off = amount.load();
|
||||
int off = octave.load()*12 + amount.load();
|
||||
|
||||
auto in = std::static_pointer_cast<CommandPort>(port(Port::Input, Port::Command, 0));
|
||||
auto out = std::static_pointer_cast<CommandPort>(port(Port::Output, Port::Command, 0));
|
||||
if (off == 0) { // a
|
||||
if (out->passthroughTo.expired()) out->passthroughTo = in;
|
||||
return;
|
||||
}
|
||||
if (!out->passthroughTo.expired()) out->passthroughTo.reset();
|
||||
in->pull();
|
||||
out->pull();
|
||||
|
||||
out->data = reinterpret_cast<uint8_t*>(audioEngine->tickAlloc(in->dataSize));
|
||||
out->dataSize = in->dataSize;
|
||||
memcpy(out->data, in->data, in->dataSize); // precopy
|
||||
out->data = reinterpret_cast<uint8_t*>(audioEngine->tickAlloc(in->size));
|
||||
out->size = in->size;
|
||||
memcpy(out->data, in->data, in->size); // precopy
|
||||
|
||||
size_t mi = 0;
|
||||
while (out->dataSize >= mi+5) {
|
||||
while (out->size >= mi+5) {
|
||||
int16_t& n = reinterpret_cast<int16_t&>(out->data[mi+2]);
|
||||
if (n > -1) {
|
||||
int nn = n;
|
||||
|
@ -70,16 +69,28 @@ void Transpose::process() {
|
|||
|
||||
void Transpose::saveData(QCborMap& m) const {
|
||||
m[qs("amount")] = QCborValue(amount);
|
||||
m[qs("octave")] = QCborValue(octave);
|
||||
}
|
||||
|
||||
void Transpose::loadData(const QCborMap& m) {
|
||||
auto oct = m.value("octave");
|
||||
if (oct.isUndefined()) { // convert from single value
|
||||
int a = static_cast<int>(m.value("amount").toInteger(0));
|
||||
int s = a < 0 ? -1 : 1;
|
||||
a = std::abs(a);
|
||||
octave = (a-(a%12))/12*s;
|
||||
amount = a%12 * s;
|
||||
return;
|
||||
}
|
||||
octave = static_cast<int>(oct.toInteger(0));
|
||||
amount = static_cast<int>(m.value("amount").toInteger(0));
|
||||
}
|
||||
|
||||
void Transpose::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
obj->showPluginName = false;
|
||||
auto l = (new LayoutGadget(obj))->setMetrics(12);
|
||||
auto l = (new LayoutGadget(obj))->setMetrics(8, 10);
|
||||
|
||||
(new KnobGadget(l))->bind(amount)->setLabel("Transpose")->setRange(-24, 24, 1);
|
||||
(new KnobGadget(l))->bind(amount)->setLabel("Transpose")->setTextFunc(KnobGadget::textOffset)->setRange(-12, 12, 1, KnobGadget::MedStep);
|
||||
(new KnobGadget(l))->bind(octave)->setLabel("Octave")->setTextFunc(KnobGadget::textOffset)->setRange(-5, 5, 1, KnobGadget::BigStep);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
namespace Xybrid::Gadgets {
|
||||
class Transpose : public Data::Node {
|
||||
std::atomic<int> amount = 0;
|
||||
std::atomic<int> octave = 0;
|
||||
public:
|
||||
Transpose();
|
||||
~Transpose() override = default;
|
||||
|
|
|
@ -18,6 +18,7 @@ using namespace Xybrid::Audio;
|
|||
using namespace Xybrid::UI;
|
||||
|
||||
#include "util/strings.h"
|
||||
#include "util/ext.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
|
@ -26,18 +27,14 @@ using namespace Xybrid::UI;
|
|||
#include <QCborValue>
|
||||
#include <QCborArray>
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "plug:2x03";
|
||||
i->displayName = "2x03";
|
||||
i->category = "Instrument";
|
||||
//i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<I2x03>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
//inf = i;
|
||||
});
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(I2x03, {
|
||||
i->id = "plug:2x03";
|
||||
i->displayName = "2x03";
|
||||
i->category = "Instrument";
|
||||
})
|
||||
|
||||
namespace {
|
||||
std::unordered_map<int8_t, QString> waveNames = [] {
|
||||
std::unordered_map<int8_t, QString> m;
|
||||
|
||||
|
@ -53,12 +50,8 @@ namespace {
|
|||
return m;
|
||||
}();
|
||||
|
||||
// silence qtcreator warnings about gcc optimize attributes
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wattributes"
|
||||
|
||||
// polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||
[[gnu::optimize("O3")]] double polyblep(double t, double dt) {
|
||||
force_opt double polyblep(double t, double dt) {
|
||||
// 0 <= t < 1
|
||||
if (t < dt) {
|
||||
t /= dt;
|
||||
|
@ -73,22 +66,22 @@ namespace {
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double oscPulse(double phase, double delta, double duty = 0.5) {
|
||||
force_opt double oscPulse(double phase, double delta, double duty = 0.5) {
|
||||
//double duty = 0.5 + std::cos(time * 2.5) * (1 - 0.125*2) * 0.5;
|
||||
double d = 1.0;
|
||||
if (std::fmod(phase, 1.0) >= duty) d = -1.0;
|
||||
d += polyblep(std::fmod(phase, 1.0), delta);
|
||||
d -= polyblep(std::fmod(phase + (1.0 - duty), 1.0), delta);
|
||||
return d;
|
||||
return d - (duty-0.5)*2; // DC offset compensation
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double oscTri(double phase) {
|
||||
force_opt double oscTri(double phase) {
|
||||
phase = std::fmod(phase + 0.75, 1.0);
|
||||
phase = phase * 0.2 + (std::floor(phase*32.0) / 32.0) * 0.8;
|
||||
return std::abs(phase*2.0 - 1.0)*2.0 - 1.0;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta) {
|
||||
force_opt double oscSaw(double phase, double delta) {
|
||||
phase = std::fmod(phase + 0.5, 1.0);
|
||||
double d = phase * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8;
|
||||
d = d * 2.0 - 1.0;
|
||||
|
@ -96,7 +89,6 @@ namespace {
|
|||
return d;
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
I2x03::I2x03() {
|
||||
|
@ -207,10 +199,7 @@ void I2x03::onGadgetCreated() {
|
|||
|
||||
auto wavetxt = [](double inp) {
|
||||
if (auto f = waveNames.find(static_cast<int8_t>(inp)); f != waveNames.end()) return f->second;
|
||||
return QString("?");
|
||||
};
|
||||
auto percenttxt = [](double d) {
|
||||
return QString("%1%").arg(d*100, 0);
|
||||
return qs("?");
|
||||
};
|
||||
|
||||
auto ol = new LayoutGadget(obj, true);
|
||||
|
@ -226,14 +215,14 @@ void I2x03::onGadgetCreated() {
|
|||
// blip group
|
||||
(new KnobGadget(l2))->bind(blipTime)->setLabel(qs("Blip"))->setRange(0.0, 0.1, 0.001);
|
||||
(new KnobGadget(l2))->bind(blipWave)->setLabel(qs("Wave"))->setTextFunc(wavetxt)->setRange(-1, 4, 1, KnobGadget::BigStep)->setDefault(-1);
|
||||
(new KnobGadget(l2))->bind(blipNote)->setLabel(qs("Note"))->setRange(-12, 12, 1);
|
||||
(new KnobGadget(l2))->bind(blipNote)->setLabel(qs("Note"))->setTextFunc(KnobGadget::textOffset)->setRange(-12, 12, 1);
|
||||
|
||||
l2->addSpacer();
|
||||
|
||||
// pwm group
|
||||
(new KnobGadget(l2))->bind(pwmDepth)->setLabel(qs("PWM"))->setTextFunc(percenttxt)->setRange(0.0, 1.0, 0.01)->setDefault(0.75);
|
||||
KnobGadget::autoPercent(l2, pwmDepth)->setLabel(qs("PWM"))->setDefault(0.75);
|
||||
(new KnobGadget(l2))->bind(pwmTime)->setLabel(qs("Time"))->setRange(0.01, 5.0, 0.01)->setDefault(3.0);
|
||||
(new KnobGadget(l2))->bind(pwmPhase)->setLabel(qs("Phase"))->setRange(0.0, 1.0, 0.01);
|
||||
KnobGadget::autoPercent(l2, pwmPhase)->setLabel(qs("Phase"));
|
||||
|
||||
//
|
||||
}
|
||||
|
|
|
@ -41,4 +41,3 @@ namespace Xybrid::Instruments {
|
|||
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -23,22 +23,19 @@ using namespace Xybrid::UI;
|
|||
|
||||
#include "nodelib/resampler.h"
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "plug:testsynth";
|
||||
i->displayName = "The Testron";
|
||||
i->category = "Instrument";
|
||||
//i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<TestSynth>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
//inf = i;
|
||||
});
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(TestSynth, {
|
||||
i->id = "plug:testsynth";
|
||||
i->displayName = "The Testron";
|
||||
i->category = "Instrument";
|
||||
i->hidden = true;
|
||||
})
|
||||
|
||||
namespace {
|
||||
const double PI = std::atan(1)*4;
|
||||
const double SEMI = std::pow(2.0, 1.0/12.0);
|
||||
|
||||
double fOsc(double& time) {
|
||||
[[maybe_unused]] double fOsc(double& time) {
|
||||
time = std::fmod(time, 2.0);
|
||||
|
||||
auto a = 0.0;
|
||||
|
@ -78,7 +75,7 @@ void TestSynth::process() {
|
|||
auto smp = *(project->samples.begin());
|
||||
|
||||
size_t mi = 0;
|
||||
while (cp->dataSize >= mi+5) {
|
||||
while (cp->size >= mi+5) {
|
||||
uint16_t id = reinterpret_cast<uint16_t&>(cp->data[mi]);
|
||||
int16_t n = reinterpret_cast<int16_t&>(cp->data[mi+2]);
|
||||
if (n > -1) {
|
||||
|
@ -100,7 +97,7 @@ void TestSynth::process() {
|
|||
//qDebug() << "rate" << rate << "note" << note;
|
||||
|
||||
for (size_t s = 0; s < ts; s++) {
|
||||
double ip = std::floor(osc);
|
||||
/*double ip = std::floor(osc);
|
||||
double fp = osc - ip;
|
||||
size_t lutIndex = static_cast<size_t>(fp*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS;
|
||||
auto& pt = NodeLib::resamplerLUT[lutIndex];
|
||||
|
@ -108,9 +105,9 @@ void TestSynth::process() {
|
|||
auto ii = static_cast<ptrdiff_t>(ip);
|
||||
for (size_t i = 0; i < 8; i++) {
|
||||
auto si = ii+static_cast<ptrdiff_t>(i);
|
||||
if (si >= 0 && si < static_cast<ptrdiff_t>(smp->length())) out += (*smp)[static_cast<size_t>(si)] * pt[i];
|
||||
//if (si >= 0 && si < static_cast<ptrdiff_t>(smp->length())) out += (*smp)[static_cast<size_t>(si)] * pt[i];
|
||||
}
|
||||
(*p)[s] = out;
|
||||
(*p)[s] = out;*/
|
||||
|
||||
|
||||
osc += rate;
|
||||
|
|
|
@ -18,6 +18,7 @@ using namespace Xybrid::Audio;
|
|||
using namespace Xybrid::UI;
|
||||
|
||||
#include "util/strings.h"
|
||||
#include "util/ext.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
|
@ -28,16 +29,14 @@ using namespace Xybrid::UI;
|
|||
#include <QCborValue>
|
||||
#include <QCborArray>
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "plug:thicc";
|
||||
i->displayName = "THiCC";
|
||||
i->category = "Instrument";
|
||||
i->createInstance = []{ return std::make_shared<Thicc>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
});
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Thicc, {
|
||||
i->id = "plug:thicc";
|
||||
i->displayName = "THiCC";
|
||||
i->category = "Instrument";
|
||||
})
|
||||
|
||||
namespace {
|
||||
[[maybe_unused]] inline double wrap(double d) {
|
||||
while (true) {
|
||||
if (d > 1.0) d = (d - 2.0) * -1; //d-=2.0;
|
||||
|
@ -49,12 +48,8 @@ namespace {
|
|||
return b * p + a * (1.0 - p);
|
||||
}
|
||||
|
||||
// silence qtcreator warnings about gcc optimize attributes
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wattributes"
|
||||
|
||||
// polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||
[[gnu::optimize("O3")]] double polyblep(double t, double dt) {
|
||||
force_opt double polyblep(double t, double dt) {
|
||||
// 0 <= t < 1
|
||||
if (t < dt) {
|
||||
t /= dt;
|
||||
|
@ -69,7 +64,7 @@ namespace {
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double push(double in, double mod, double factor) {
|
||||
force_opt inline double push(double in, double mod, double factor) {
|
||||
double s = in < 0 ? -1 : 1;
|
||||
in *= s;
|
||||
//if (mod < 0) mod = 1.0/-mod;
|
||||
|
@ -78,7 +73,7 @@ namespace {
|
|||
return std::pow(in, mod)*s;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta, double mod) {
|
||||
force_opt double oscSaw(double phase, double delta, double mod) {
|
||||
phase = std::fmod(phase + 0.5, 1.0);
|
||||
double d = phase;// * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8;
|
||||
d = d * 2.0 - 1.0;
|
||||
|
@ -87,9 +82,9 @@ namespace {
|
|||
return d;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); }
|
||||
force_opt double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); }
|
||||
|
||||
[[gnu::optimize("O3")]] double oscPulse(double phase, double delta, double mod) {
|
||||
force_opt double oscPulse(double phase, double delta, double mod) {
|
||||
double duty = (mod+1.0)/2.0;
|
||||
double d = 1.0;
|
||||
if (std::fmod(phase, 1.0) >= duty) d = -1.0;
|
||||
|
@ -98,9 +93,6 @@ namespace {
|
|||
return d;
|
||||
}
|
||||
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
// for clang on freebsd (and possibly other non-apple llvm sources) it seems we need to specify more.
|
||||
// wave function list(s)
|
||||
const constexpr std::array<double(*)(double,double,double),3> waveFunc = {
|
||||
|
@ -203,7 +195,7 @@ void Thicc::onGadgetCreated() {
|
|||
(new KnobGadget(l))->bind(mod)->setLabel(qs("W. Mod"))->setRange(-1.0, 1.0, 0.01);
|
||||
l->addSpacer();
|
||||
(new KnobGadget(l))->bind(voices)->setLabel(qs("Voices"))->setRange(1, 16, 1, KnobGadget::BigStep)->setDefault(1);
|
||||
(new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setRange(0.0, 1.0, 0.001);
|
||||
(new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setTextFunc(KnobGadget::textPercent)->setRange(0.0, 1.0, 0.001);
|
||||
l->addSpacer();
|
||||
KnobGadget::autoCreate(l, adsr);
|
||||
}
|
||||
|
|
|
@ -29,4 +29,3 @@ namespace Xybrid::Instruments {
|
|||
void onGadgetCreated() override;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -26,16 +26,14 @@ using namespace Xybrid::UI;
|
|||
#include <QCborValue>
|
||||
#include <QCborArray>
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "plug:xriek";
|
||||
i->displayName = "Xriek";
|
||||
i->category = "Instrument";
|
||||
i->createInstance = []{ return std::make_shared<Xriek>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
});
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Xriek, {
|
||||
i->id = "plug:xriek";
|
||||
i->displayName = "Xriek";
|
||||
i->category = "Instrument";
|
||||
})
|
||||
|
||||
namespace {
|
||||
[[maybe_unused]] inline double wrap(double d) {
|
||||
while (true) {
|
||||
if (d > 1.0) d = (d - 2.0) * -1; //d-=2.0;
|
||||
|
@ -134,6 +132,7 @@ void Xriek::onGadgetCreated() {
|
|||
k->step = .01;
|
||||
k->bind(drive);
|
||||
k->setLabel("Drive");
|
||||
k->setTextFunc(KnobGadget::textPercent);
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -143,6 +142,7 @@ void Xriek::onGadgetCreated() {
|
|||
k->step = .01;
|
||||
k->bind(saturation);
|
||||
k->setLabel("Saturate");
|
||||
k->setTextFunc(KnobGadget::textPercent);
|
||||
}
|
||||
|
||||
l->addSpacer();
|
||||
|
|
|
@ -25,4 +25,3 @@ namespace Xybrid::Instruments {
|
|||
void onGadgetCreated() override;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ using namespace Xybrid::Audio;
|
|||
#include "ui/waveformpreviewwidget.h"
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
#include "ui/gadgets/layoutgadget.h"
|
||||
#include "ui/gadgets/knobgadget.h"
|
||||
#include "ui/gadgets/selectorgadget.h"
|
||||
#include "ui/gadgets/sampleselectorgadget.h"
|
||||
|
@ -27,9 +28,11 @@ using namespace Xybrid::UI;
|
|||
#include "ui/patchboard/nodeuiscene.h"
|
||||
#include "uisocket.h"
|
||||
|
||||
#include "util/ext.h"
|
||||
#include "util/strings.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QCborMap>
|
||||
|
@ -39,21 +42,12 @@ using namespace Xybrid::UI;
|
|||
#include <QMenu>
|
||||
#include <QGraphicsProxyWidget>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
||||
namespace {
|
||||
bool _ = PluginRegistry::enqueueRegistration([] {
|
||||
auto i = std::make_shared<PluginInfo>();
|
||||
i->id = "plug:beatpad";
|
||||
i->displayName = "BeatPad";
|
||||
i->category = "Sampler";
|
||||
//i->hidden = true;
|
||||
i->createInstance = []{ return std::make_shared<BeatPad>(); };
|
||||
PluginRegistry::registerPlugin(i);
|
||||
//inf = i;
|
||||
});
|
||||
|
||||
}
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(BeatPad, {
|
||||
i->id = "plug:beatpad";
|
||||
i->displayName = "BeatPad";
|
||||
i->category = "Sampler";
|
||||
})
|
||||
|
||||
BeatPad::BeatPad() {
|
||||
|
||||
|
@ -64,7 +58,7 @@ void BeatPad::init() {
|
|||
addPort(Port::Output, Port::Audio, 0);
|
||||
|
||||
core.onNoteOn = [this](Note& note) {
|
||||
auto& data = *reinterpret_cast<NoteData*>(¬e.scratch);
|
||||
auto& data = *hard_cast<NoteData*>(¬e.scratch);
|
||||
new (&data) NoteData(); // construct in-place
|
||||
|
||||
// look up config for note
|
||||
|
@ -77,7 +71,7 @@ void BeatPad::init() {
|
|||
};
|
||||
|
||||
core.onDeleteNote = [](Note& note) {
|
||||
auto& data = *reinterpret_cast<NoteData*>(¬e.scratch);
|
||||
auto& data = *hard_cast<NoteData*>(¬e.scratch);
|
||||
data.~NoteData(); // destroy
|
||||
};
|
||||
|
||||
|
@ -87,12 +81,13 @@ void BeatPad::init() {
|
|||
};*/
|
||||
|
||||
core.processNote = [this](Note& note, AudioPort* p) {
|
||||
auto& data = *reinterpret_cast<NoteData*>(¬e.scratch);
|
||||
auto& data = *hard_cast<NoteData*>(¬e.scratch);
|
||||
if (!data.config) return core.deleteNote(note);
|
||||
auto smp = data.config->smp.lock();
|
||||
if (!smp) return core.deleteNote(note);
|
||||
|
||||
double rate = static_cast<double>(smp->sampleRate) / static_cast<double>(audioEngine->curSampleRate());
|
||||
//std::cout << "rate: " << rate << std::endl;
|
||||
auto start = data.config->start;
|
||||
if (start < 0) start = 0;
|
||||
auto end = data.config->end;
|
||||
|
@ -105,21 +100,11 @@ void BeatPad::init() {
|
|||
|
||||
// actual sample pos
|
||||
double sp = static_cast<double>(start) + data.sampleTime * rate;
|
||||
if (sp >= static_cast<double>(end)) return core.deleteNote(note);
|
||||
|
||||
double ip = std::floor(sp);
|
||||
double fp = sp - ip;
|
||||
size_t lutIndex = static_cast<size_t>(fp*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS;
|
||||
auto& pt = NodeLib::resamplerLUT[lutIndex];
|
||||
AudioFrame out(0.0);
|
||||
auto ii = static_cast<ptrdiff_t>(ip) - 3;
|
||||
for (size_t i = 0; i < 8; i++) {
|
||||
auto si = ii+static_cast<ptrdiff_t>(i);
|
||||
if (si >= start && si < static_cast<ptrdiff_t>(end)) out += (*smp)[static_cast<size_t>(si)] * pt[i];
|
||||
}
|
||||
auto out = NodeLib::resamp(smp.get(), sp, rate);
|
||||
|
||||
// stuff
|
||||
|
||||
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
|
||||
(*p)[i] += out.gainBalance(data.config->gain, note.pan) * note.ampMult();
|
||||
data.sampleTime += 1;
|
||||
}
|
||||
};
|
||||
|
@ -131,12 +116,13 @@ void BeatPad::process() { core.process(this); }
|
|||
|
||||
void BeatPad::saveData(QCborMap& m) const {
|
||||
QCborMap cm;
|
||||
for (auto c : cfg) {
|
||||
for (auto& c : cfg) {
|
||||
if (auto smp = c.second->smp.lock(); smp) {
|
||||
QCborMap e;
|
||||
e[qs("sample")] = QCborValue(smp->uuid);
|
||||
e[qs("start")] = static_cast<qint64>(c.second->start);
|
||||
e[qs("end")] = static_cast<qint64>(c.second->end);
|
||||
e[qs("gain")] = c.second->gain;
|
||||
cm[c.first] = e;
|
||||
smp->markForExport();
|
||||
}
|
||||
|
@ -153,6 +139,7 @@ void BeatPad::loadData(const QCborMap& m) {
|
|||
c->smp = f.value();
|
||||
c->start = cm.value("start").toInteger(-1);
|
||||
c->end = cm.value("end").toInteger(-1);
|
||||
c->gain = cm.value("gain").toDouble(0.0);
|
||||
cfg[static_cast<int16_t>(ce.first.toInteger())] = c;
|
||||
}
|
||||
}
|
||||
|
@ -180,10 +167,11 @@ void BeatPad::initUI(NodeUIScene* scene) {
|
|||
};
|
||||
auto state = scene->makeStateObject<UIState>();
|
||||
|
||||
// init layout
|
||||
auto ol = (new LayoutGadget(scene, true))->setPanel(true);
|
||||
|
||||
// set up gadgets
|
||||
auto noteSelector = new SelectorGadget();
|
||||
scene->addItem(noteSelector);
|
||||
noteSelector->setPos(0, 0);
|
||||
auto noteSelector = new SelectorGadget(ol);
|
||||
noteSelector->setWidth(320);
|
||||
|
||||
noteSelector->fGetList = [=] {
|
||||
|
@ -194,7 +182,7 @@ void BeatPad::initUI(NodeUIScene* scene) {
|
|||
auto c = f->second;
|
||||
QString n;
|
||||
if (auto smp = c->smp.lock(); smp) n = smp->name.section('/', -1, -1);
|
||||
v.push_back({ f->first, qs("%1 %2").arg(Util::noteName(i)).arg(n) });
|
||||
v.push_back({ f->first, qs("%1 %2").arg(Util::noteName(i), n) });
|
||||
}
|
||||
}
|
||||
return v;
|
||||
|
@ -209,16 +197,17 @@ void BeatPad::initUI(NodeUIScene* scene) {
|
|||
for (int16_t i = 0; i < 12; i++) {
|
||||
int16_t n = oct*12+i;
|
||||
if (auto f = cfg.find(n); f != cfg.end()) mo->addAction(Util::noteName(n))->setDisabled(true);
|
||||
else mo->addAction(Util::noteName(n), [=] { state->selectNote(n); });
|
||||
else mo->addAction(Util::noteName(n), m, [=] { state->selectNote(n); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto sampleSelector = new SampleSelectorGadget(project);
|
||||
scene->addItem(sampleSelector);
|
||||
sampleSelector->setPos(0, 28);
|
||||
auto sampleSelector = new SampleSelectorGadget(project, ol);
|
||||
sampleSelector->setSize(320, 96);
|
||||
|
||||
auto r1 = (new LayoutGadget(ol))->setMetrics(0, -1, 0.0);
|
||||
auto gain = KnobGadget::autoGain(r1);
|
||||
|
||||
// create functions now that all UI elements exist to be referenced
|
||||
state->selectNote = [=](int16_t n) {
|
||||
if (auto f = cfg.find(n); f != cfg.end()) state->cfg = f->second;
|
||||
|
@ -226,8 +215,9 @@ void BeatPad::initUI(NodeUIScene* scene) {
|
|||
state->note = n;
|
||||
auto smp = state->cfg->smp.lock();
|
||||
sampleSelector->setSample(smp);
|
||||
gain->bind(state->cfg->gain);
|
||||
|
||||
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name.section('/', -1, -1) : "")}, false);
|
||||
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n), smp ? smp->name.section('/', -1, -1) : "")}, false);
|
||||
};
|
||||
state->setSample = [=](std::shared_ptr<Sample> smp) {
|
||||
state->cfg->smp = smp;
|
||||
|
@ -235,9 +225,11 @@ void BeatPad::initUI(NodeUIScene* scene) {
|
|||
if (smp) cfg[n] = state->cfg;
|
||||
else if (auto f = cfg.find(n); f != cfg.end()) cfg.erase(f);
|
||||
|
||||
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name : "")}, false);
|
||||
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n), smp ? smp->name : "")}, false);
|
||||
};
|
||||
|
||||
//ol->updateGeometry();
|
||||
|
||||
// hook up relevantsignals
|
||||
QObject::connect(scene, &NodeUIScene::notePreview, [=](int16_t note) { state->selectNote(note); });
|
||||
QObject::connect(noteSelector, &SelectorGadget::onSelect, [=](auto e) { state->selectNote(e.first); });
|
||||
|
|
|
@ -11,6 +11,8 @@ namespace Xybrid::Instruments {
|
|||
std::weak_ptr<Data::Sample> smp = std::weak_ptr<Data::Sample>();
|
||||
ptrdiff_t start = -1;
|
||||
ptrdiff_t end = -1;
|
||||
|
||||
double gain = 0.0;
|
||||
};
|
||||
|
||||
struct NoteData {
|
||||
|
@ -49,4 +51,3 @@ namespace Xybrid::Instruments {
|
|||
void initUI(UI::NodeUIScene*) override;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
#include "capaxitor.h"
|
||||
using Xybrid::Instruments::Capaxitor;
|
||||
using namespace Xybrid::NodeLib;
|
||||
using Note = InstrumentCore::Note;
|
||||
using namespace Xybrid::Data;
|
||||
|
||||
#include "nodelib/commandreader.h"
|
||||
#include "nodelib/resampler.h"
|
||||
|
||||
#include "data/project.h"
|
||||
#include "data/sample.h"
|
||||
|
||||
#include "data/porttypes.h"
|
||||
#include "config/pluginregistry.h"
|
||||
using namespace Xybrid::Config;
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "ui/waveformpreviewwidget.h"
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
#include "ui/gadgets/layoutgadget.h"
|
||||
#include "ui/gadgets/knobgadget.h"
|
||||
#include "ui/gadgets/selectorgadget.h"
|
||||
#include "ui/gadgets/sampleselectorgadget.h"
|
||||
using namespace Xybrid::UI;
|
||||
|
||||
#include "ui/patchboard/nodeuiscene.h"
|
||||
#include "uisocket.h"
|
||||
|
||||
#include "util/ext.h"
|
||||
#include "util/strings.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QCborMap>
|
||||
#include <QCborValue>
|
||||
#include <QCborArray>
|
||||
|
||||
#include <QMenu>
|
||||
#include <QGraphicsProxyWidget>
|
||||
|
||||
// clazy:excludeall=non-pod-global-static
|
||||
RegisterPlugin(Capaxitor, {
|
||||
i->id = "plug:capaxitor";
|
||||
i->displayName = "CapaXitor";
|
||||
i->category = "Sampler";
|
||||
})
|
||||
|
||||
Capaxitor::Capaxitor() {
|
||||
|
||||
}
|
||||
|
||||
void Capaxitor::init() {
|
||||
addPort(Port::Input, Port::Command, 0);
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
|
||||
core.onNoteOn = [this](Note& note) {
|
||||
auto& data = *hard_cast<NoteData*>(¬e.scratch);
|
||||
new (&data) NoteData(); // construct in-place
|
||||
|
||||
note.adsr = adsr;
|
||||
};
|
||||
|
||||
core.onDeleteNote = [](Note& note) {
|
||||
auto& data = *hard_cast<NoteData*>(¬e.scratch);
|
||||
data.~NoteData(); // destroy
|
||||
};
|
||||
|
||||
core.processNote = [this](Note& note, AudioPort* p) {
|
||||
auto& data = *hard_cast<NoteData*>(¬e.scratch);
|
||||
auto smp = this->smp.lock();
|
||||
if (!smp) return core.deleteNote(note);
|
||||
|
||||
double rate = static_cast<double>(smp->sampleRate) / static_cast<double>(audioEngine->curSampleRate());
|
||||
double baseNote = smp->getNote();
|
||||
|
||||
bool loop = smp->loopStart >= 0;
|
||||
auto len = static_cast<double>(smp->length());
|
||||
|
||||
size_t ts = p->size;
|
||||
for (size_t i = 0; i < ts; i++) {
|
||||
core.advanceNote(note);
|
||||
|
||||
double n = note.effectiveNote();
|
||||
double fr = std::pow(SEMI, n - baseNote);
|
||||
|
||||
// actual sample pos
|
||||
double sp = data.sampleTime * rate;
|
||||
if (!loop && sp >= len) return core.deleteNote(note);
|
||||
|
||||
auto out = NodeLib::resamp(smp.get(), sp, rate*fr);
|
||||
|
||||
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
|
||||
data.sampleTime += fr;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void Capaxitor::reset() { core.reset(); }
|
||||
void Capaxitor::release() { core.release(); }
|
||||
void Capaxitor::process() { core.process(this); }
|
||||
|
||||
void Capaxitor::saveData(QCborMap& m) const {
|
||||
if (auto smp = this->smp.lock(); smp) {
|
||||
m[qs("sample")] = QCborValue(smp->uuid);
|
||||
smp->markForExport();
|
||||
}
|
||||
m[qs("adsr")] = adsr;
|
||||
}
|
||||
|
||||
void Capaxitor::loadData(const QCborMap& m) {
|
||||
auto id = m.value("sample").toUuid();
|
||||
if (auto f = project->samples.find(id); f != project->samples.end()) {
|
||||
smp = f.value();
|
||||
}
|
||||
adsr = m.value("adsr");
|
||||
}
|
||||
|
||||
void Capaxitor::onGadgetCreated() {
|
||||
if (!obj) return;
|
||||
|
||||
auto l = new LayoutGadget(obj);
|
||||
auto sampleSelector = new SampleSelectorGadget(project, l);
|
||||
sampleSelector->setSize(128, 48);
|
||||
sampleSelector->setSample(smp.lock(), false);
|
||||
|
||||
KnobGadget::autoCreate(l, adsr);
|
||||
|
||||
QObject::connect(sampleSelector, &SampleSelectorGadget::sampleSelected, sampleSelector, [=](auto smp) { this->smp = smp; });
|
||||
}
|
||||
|
||||
void Capaxitor::onDoubleClick() { emit project->socket->openNodeUI(this); }
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include "nodelib/instrumentcore.h"
|
||||
#include "data/audioframe.h"
|
||||
|
||||
namespace Xybrid::Data { class Sample; }
|
||||
namespace Xybrid::Instruments {
|
||||
class Capaxitor : public Data::Node {
|
||||
NodeLib::InstrumentCore core;
|
||||
|
||||
struct NoteData {
|
||||
double sampleTime = 0;
|
||||
|
||||
NoteData() = default;
|
||||
~NoteData() = default;
|
||||
};
|
||||
static_assert (sizeof(NoteData) <= sizeof(NodeLib::InstrumentCore::Note::scratch), "Note data overflows scratch space!");
|
||||
|
||||
std::weak_ptr<Data::Sample> smp = std::weak_ptr<Data::Sample>();
|
||||
|
||||
NodeLib::ADSR adsr;
|
||||
|
||||
|
||||
|
||||
public:
|
||||
Capaxitor();
|
||||
~Capaxitor() override = default;
|
||||
|
||||
void init() override;
|
||||
void reset() override;
|
||||
void release() override;
|
||||
void process() override;
|
||||
|
||||
//void onRename() override;
|
||||
|
||||
void saveData(QCborMap&) const override;
|
||||
void loadData(const QCborMap&) override;
|
||||
|
||||
//void onUnparent(std::shared_ptr<Data::Graph>) override;
|
||||
//void onParent(std::shared_ptr<Data::Graph>) override;
|
||||
|
||||
void onGadgetCreated() override;
|
||||
|
||||
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
|
||||
|
||||
void onDoubleClick() override;
|
||||
//void initUI(UI::NodeUIScene*) override;
|
||||
};
|
||||
}
|
Binary file not shown.
|
@ -6,4 +6,7 @@
|
|||
<qresource prefix="/img">
|
||||
<file>xybrid-logo-tiny.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="/template">
|
||||
<file>default.xyp</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
#include "settingsdialog.h"
|
||||
#include "ui_settingsdialog.h"
|
||||
|
||||
using namespace Xybrid;
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
|
||||
#include "fileops.h"
|
||||
#include "config/audioconfig.h"
|
||||
#include "config/uiconfig.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
SettingsDialog* SettingsDialog::instance = nullptr;
|
||||
|
||||
namespace { // clazy:excludeall=non-pod-global-static
|
||||
std::vector<std::function<void()>>* bnd;
|
||||
|
||||
void bind(QCheckBox* o, bool& v) {
|
||||
o->setChecked(v);
|
||||
bnd->push_back([o, &v] {
|
||||
v = o->isChecked();
|
||||
});
|
||||
}
|
||||
|
||||
const QRegularExpression numeric("[0-9.]+");
|
||||
|
||||
void bind(QComboBox* o, int& v, const QStringList& items) {
|
||||
o->clear();
|
||||
o->addItems(items);
|
||||
int ld = 100000000;
|
||||
QString cm;
|
||||
for (auto& i : items) { // find closest match
|
||||
auto q = numeric.match(i).captured().toInt();
|
||||
int id = std::abs(q - v);
|
||||
if (id < ld) {
|
||||
ld = id;
|
||||
cm = i;
|
||||
}
|
||||
}
|
||||
o->setCurrentText(cm);
|
||||
bnd->push_back([o, &v] { // convert back to int
|
||||
v = numeric.match(o->currentText()).captured().toInt();
|
||||
});
|
||||
}
|
||||
|
||||
void bind(QSpinBox* o, int& v, int min, int max, const QString& suffix = { }) {
|
||||
o->setRange(min, max);
|
||||
o->setValue(std::clamp(v, min, max));
|
||||
o->setSuffix(suffix);
|
||||
bnd->push_back([o, &v] {
|
||||
v = o->value();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::SettingsDialog) {
|
||||
ui->setupUi(this);
|
||||
|
||||
connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &SettingsDialog::apply);
|
||||
|
||||
instance = this;
|
||||
bnd = &this->binds;
|
||||
|
||||
// audio page
|
||||
QStringList sampleRates = { qs("44100Hz"), qs("48000Hz"), qs("96000Hz") };
|
||||
const constexpr int minBufMs = 25, maxBufMs = 250;
|
||||
const QString ms = qs("ms");
|
||||
|
||||
bind(ui->playbackSampleRate, AudioConfig::playbackSampleRate, sampleRates);
|
||||
bind(ui->playbackBufferMs, AudioConfig::playbackBufferMs, minBufMs, maxBufMs, ms);
|
||||
bind(ui->previewSampleRate, AudioConfig::previewSampleRate, sampleRates);
|
||||
bind(ui->previewBufferMs, AudioConfig::previewBufferMs, minBufMs, maxBufMs, ms);
|
||||
bind(ui->renderSampleRate, AudioConfig::renderSampleRate, sampleRates);
|
||||
|
||||
// UI page
|
||||
bind(ui->verticalKnobs, UIConfig::verticalKnobs);
|
||||
bind(ui->invertScrollWheel, UIConfig::invertScrollWheel);
|
||||
}
|
||||
|
||||
SettingsDialog::~SettingsDialog() {
|
||||
if (instance == this) instance = nullptr;
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void SettingsDialog::apply() {
|
||||
for (auto& f : binds) f();
|
||||
FileOps::saveConfig();
|
||||
|
||||
// if left in preview mode, stop to allow settings to take
|
||||
if (audioEngine->playbackMode() == audioEngine->Previewing) audioEngine->stop();
|
||||
}
|
||||
|
||||
void SettingsDialog::reject() {
|
||||
QDialog::reject();
|
||||
if (instance == this) instance = nullptr;
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void SettingsDialog::tryOpen() {
|
||||
if (!instance) {
|
||||
(new SettingsDialog(nullptr))->show();
|
||||
} else {
|
||||
instance->show();
|
||||
instance->raise();
|
||||
instance->activateWindow();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace Ui {
|
||||
class SettingsDialog;
|
||||
}
|
||||
|
||||
namespace Xybrid {
|
||||
class SettingsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
std::vector<std::function<void()>> binds;
|
||||
|
||||
public:
|
||||
static SettingsDialog* instance;
|
||||
|
||||
explicit SettingsDialog(QWidget *parent = nullptr);
|
||||
~SettingsDialog() override;
|
||||
|
||||
static void tryOpen();
|
||||
|
||||
public slots:
|
||||
|
||||
void apply();
|
||||
void reject() override;
|
||||
|
||||
private:
|
||||
Ui::SettingsDialog *ui;
|
||||
};
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SettingsDialog</class>
|
||||
<widget class="QDialog" name="SettingsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>350</width>
|
||||
<height>360</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Xybrid Settings</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<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="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tabAudio">
|
||||
<attribute name="title">
|
||||
<string>Audio</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>4</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="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Playback Sample Rate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="playbackSampleRate"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_2" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>4</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="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Playback Buffer Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="playbackBufferMs"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="spacing">
|
||||
<number>4</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="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Preview Sample Rate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="previewSampleRate"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_5" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<property name="spacing">
|
||||
<number>4</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="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Preview Buffer Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="previewBufferMs"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<property name="spacing">
|
||||
<number>4</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="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Rendering Sample Rate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="renderSampleRate"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tabUI">
|
||||
<attribute name="title">
|
||||
<string>UI</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="verticalKnobs">
|
||||
<property name="text">
|
||||
<string>Knobs scroll vertically</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="invertScrollWheel">
|
||||
<property name="text">
|
||||
<string>Invert scroll wheel for knobs, sliders, etc.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tabAbout">
|
||||
<attribute name="title">
|
||||
<string>About</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="buttonBoxContainer" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Apply|QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SettingsDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SettingsDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
|
@ -58,6 +58,6 @@ QVariant BreadcrumbModel::data(const QModelIndex& index, int role) const {
|
|||
if (role == Qt::DisplayRole) {
|
||||
return actions[static_cast<size_t>(index.column())]->text();
|
||||
}
|
||||
if (role == Qt::TextAlignmentRole) return Qt::AlignHCenter + Qt::AlignVCenter;
|
||||
if (role == Qt::TextAlignmentRole) return {Qt::AlignHCenter | Qt::AlignVCenter};
|
||||
return QVariant();
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ void DirectoryNode::sortChildren() {
|
|||
|
||||
void DirectoryNode::sortTree() {
|
||||
sortChildren();
|
||||
for (auto c : children) c->sortTree();
|
||||
for (auto c : qAsConst(children)) c->sortTree();
|
||||
}
|
||||
|
||||
DirectoryNode* DirectoryNode::subdir(QString name) {
|
||||
|
@ -47,7 +47,7 @@ DirectoryNode* DirectoryNode::subdir(QString name) {
|
|||
QString first = name.section('/', 0, 0, QString::SectionSkipEmpty);
|
||||
QString rest = name.section('/', 1, -1, QString::SectionSkipEmpty);
|
||||
DirectoryNode* sd = nullptr;
|
||||
for (auto c : children) if (c->isDirectory() && c->name == first) { sd = c; break; }
|
||||
for (auto c : qAsConst(children)) if (c->isDirectory() && c->name == first) { sd = c; break; }
|
||||
if (!sd) sd = new DirectoryNode(this, first);
|
||||
return sd->subdir(rest);
|
||||
}
|
||||
|
@ -69,14 +69,14 @@ DirectoryNode* DirectoryNode::findPath(QString path) {
|
|||
QString first = path.section('/', 0, 0, QString::SectionSkipEmpty);
|
||||
QString rest = path.section('/', 1, -1, QString::SectionSkipEmpty);
|
||||
DirectoryNode* sd = nullptr;
|
||||
for (auto c : children) if (c->name == first) { sd = c; break; }
|
||||
for (auto c : qAsConst(children)) if (c->name == first) { sd = c; break; }
|
||||
if (!sd) return nullptr;
|
||||
return sd->findPath(rest);
|
||||
}
|
||||
|
||||
DirectoryNode* DirectoryNode::findData(const QVariant& d) {
|
||||
if (data == d) return this;
|
||||
for (auto c : children) {
|
||||
for (auto c : qAsConst(children)) {
|
||||
if (auto cd = c->findData(d); cd) return cd;
|
||||
}
|
||||
return nullptr;
|
||||
|
@ -92,7 +92,7 @@ bool DirectoryNode::isChildOf(Xybrid::UI::DirectoryNode* dn) const {
|
|||
}
|
||||
|
||||
void DirectoryNode::treeExec(const std::function<void(Xybrid::UI::DirectoryNode*)>& f) {
|
||||
for (auto c : children) c->treeExec(f);
|
||||
for (auto c : qAsConst(children)) c->treeExec(f);
|
||||
f(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
#include "floaterbg.h"
|
||||
|
||||
#include <QVariant>
|
||||
|
||||
using Xybrid::UI::FloaterBG;
|
||||
|
||||
FloaterBG::FloaterBG(QWidget* parent) : QWidget(parent) {
|
||||
// set custom appearance
|
||||
setStyleSheet(R"css(
|
||||
background: palette(window);
|
||||
border: 1px solid palette(mid);
|
||||
border-radius: 5;
|
||||
)css");
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace Xybrid::UI {
|
||||
class FloaterBG : public QWidget {
|
||||
Q_OBJECT
|
||||
// this is really just here for QSS purposes
|
||||
public:
|
||||
FloaterBG(QWidget* parent);
|
||||
~FloaterBG() override = default;
|
||||
};
|
||||
}
|
|
@ -9,7 +9,7 @@ using Xybrid::UI::ButtonGadget;
|
|||
|
||||
#include <QMenu>
|
||||
|
||||
namespace {
|
||||
namespace { // clazy:excludeall=non-pod-global-static
|
||||
const QFont font("Arcon Rounded", 8);
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ void ButtonGadget::paint(QPainter* p, const QStyleOptionGraphicsItem* opt, QWidg
|
|||
p->setPen(QColor(255, 255, 255));
|
||||
QFontMetricsF f(font);
|
||||
auto t = f.elidedText(text, Qt::ElideRight, static_cast<int>(r.width() - 8));
|
||||
p->drawText(QPointF(r.center().x() - f.width(t)/2, r.center().y() + f.ascent()/2), t);
|
||||
p->drawText(QPointF(r.center().x() - f.horizontalAdvance(t)/2, r.center().y() + f.ascent()/2), t);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
#include "knobgadget.h"
|
||||
using namespace Xybrid::UI;
|
||||
|
||||
#include "util/strings.h"
|
||||
#include "ui/gadgets/layoutgadget.h"
|
||||
#include "nodelib/basics.h"
|
||||
|
||||
#include "config/uiconfig.h"
|
||||
using namespace Xybrid::Config;
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QDebug>
|
||||
|
@ -113,13 +117,37 @@ void KnobGadget::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidg
|
|||
painter->drawPie(ir, 250*16 + static_cast<int>(-320.0*16*proportion), 0);
|
||||
}
|
||||
|
||||
namespace { // interaction tracking vars
|
||||
int acc = 0;
|
||||
int wAcc = 0;
|
||||
double trackVal;
|
||||
|
||||
inline double stepRound(double v, double s) { return std::round(v / s) * s; }
|
||||
|
||||
int accumulate(int& acc, int step) {
|
||||
int sgn = acc < 0 ? -1 : 1;
|
||||
int rm = std::abs(acc) % step;
|
||||
int diff = (std::abs(acc)-rm)*sgn / step;
|
||||
acc = rm*sgn;
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
|
||||
void KnobGadget::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
|
||||
wAcc = 0; //reset wheel accumulator on hovering over a new knob
|
||||
}
|
||||
|
||||
void KnobGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
highlighted = true;
|
||||
startVal = get();
|
||||
if (step != 0.0) {
|
||||
startVal = std::round(startVal / step) * step;
|
||||
}
|
||||
trackVal = get();
|
||||
auto mod = e->modifiers();
|
||||
if (mod.testFlag(Qt::AltModifier)) ; // no rounding until alt released
|
||||
else if (mod.testFlag(Qt::ShiftModifier) && subStep > 0.0)
|
||||
trackVal = stepRound(trackVal, subStep);
|
||||
else if (step > 0.0)
|
||||
trackVal = stepRound(trackVal, step);
|
||||
fSet(trackVal);
|
||||
} else if (e->button() == Qt::RightButton) {
|
||||
fSet(defaultVal);
|
||||
e->accept();
|
||||
|
@ -136,46 +164,69 @@ void KnobGadget::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
|
|||
|
||||
void KnobGadget::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
||||
if (highlighted) {
|
||||
auto tdelta = -(e->screenPos().y() - e->buttonDownScreenPos(Qt::LeftButton).y());
|
||||
tdelta /= stepPx;
|
||||
fSet(std::clamp(startVal + tdelta * step, min, max));
|
||||
auto curPos = e->pos().toPoint();
|
||||
auto lastPos = e->lastPos().toPoint();
|
||||
if (UIConfig::verticalKnobs) acc -= curPos.y() - lastPos.y();
|
||||
else acc += curPos.x() - lastPos.x();
|
||||
|
||||
auto d = accumulate(acc, stepPx);
|
||||
|
||||
auto mod = e->modifiers();
|
||||
if (mod.testFlag(Qt::AltModifier)) trackVal = std::clamp(trackVal, min, max); // soft release; just keep track within range
|
||||
else if (mod.testFlag(Qt::ShiftModifier) && subStep > 0) {
|
||||
trackVal = stepRound(trackVal + subStep*d, subStep);
|
||||
fSet(std::clamp(trackVal, min, max));
|
||||
} else if (step > 0) {
|
||||
trackVal = stepRound(trackVal + step*d, step);
|
||||
fSet(std::clamp(trackVal, min, max));
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void KnobGadget::wheelEvent(QGraphicsSceneWheelEvent* e) {
|
||||
e->accept(); // never pass through
|
||||
|
||||
wAcc += e->delta();
|
||||
auto d = accumulate(wAcc, 120);
|
||||
if (d == 0) return; // don't need to update anything if incomplete accumulation
|
||||
if (UIConfig::invertScrollWheel) d *= -1;
|
||||
|
||||
auto mod = e->modifiers();
|
||||
if (highlighted) { // wheel while dragging
|
||||
/*if (mod.testFlag(Qt::ShiftModifier) && subStep > 0) {
|
||||
trackVal = stepRound(trackVal + subStep*d, subStep);
|
||||
fSet(std::clamp(trackVal, min, max));
|
||||
} else if (step > 0) {
|
||||
trackVal = stepRound(trackVal + step*d, step);
|
||||
fSet(std::clamp(trackVal, min, max));
|
||||
}*/
|
||||
return;
|
||||
}
|
||||
|
||||
if (mod.testFlag(Qt::ShiftModifier) && subStep > 0)
|
||||
fSet(std::clamp(stepRound(get()+subStep*d, subStep), min, max));
|
||||
else if (step > 0)
|
||||
fSet(std::clamp(stepRound(get()+step*d, step), min, max));
|
||||
update();
|
||||
}
|
||||
|
||||
void KnobGadget::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
|
||||
e->accept();
|
||||
}
|
||||
|
||||
QString KnobGadget::textPercent(double d) { return qs("%1%").arg(d*100); }
|
||||
QString KnobGadget::textOffset(double d) { return (d > 0 ? qs("+%1") : qs("%1")).arg(d); }
|
||||
QString KnobGadget::textFrequency(double d) { return qs("%1Hz").arg(d); }
|
||||
|
||||
QString KnobGadget::textGain(double d) { return (d > 0 ? qs("+%1dB") : qs("%1dB")).arg(d); }
|
||||
QString KnobGadget::textBalance(double d) { return (d > 0 ? qs("+%1%") : qs("%1%")).arg(d*100); }
|
||||
|
||||
void KnobGadget::autoCreate(LayoutGadget* l, NodeLib::ADSR& adsr) {
|
||||
KnobGadget* k;
|
||||
KnobGadget* k [[maybe_unused]];
|
||||
|
||||
k = new KnobGadget(l);
|
||||
k->min = 0.0;
|
||||
k->max = 5.0;
|
||||
k->step = .01;
|
||||
k->bind(adsr.a);
|
||||
k->setLabel("Attack");
|
||||
|
||||
k = new KnobGadget(l);
|
||||
k->min = 0.0;
|
||||
k->max = 5.0;
|
||||
k->step = .01;
|
||||
k->bind(adsr.d);
|
||||
k->setLabel("Decay");
|
||||
|
||||
k = new KnobGadget(l);
|
||||
k->min = 0.0;
|
||||
k->max = 1.0;
|
||||
k->defaultVal = 1.0;
|
||||
k->step = .01;
|
||||
k->bind(adsr.s);
|
||||
k->setLabel("Sustain");
|
||||
|
||||
k = new KnobGadget(l);
|
||||
k->min = 0.0;
|
||||
k->max = 5.0;
|
||||
k->step = .01;
|
||||
k->bind(adsr.r);
|
||||
k->setLabel("Release");
|
||||
k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.a)->setLabel(qs("Attack"));
|
||||
k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.d)->setLabel(qs("Decay"));
|
||||
k = KnobGadget::autoPercent(l, adsr.s)->setLabel(qs("Sustain"));
|
||||
k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.r)->setLabel(qs("Release"));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "ui/gadgets/gadget.h"
|
||||
#include "nodelib/param.h"
|
||||
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
namespace Xybrid::NodeLib { class ADSR; }
|
||||
namespace Xybrid::UI {
|
||||
class LayoutGadget;
|
||||
|
@ -13,7 +16,6 @@ namespace Xybrid::UI {
|
|||
Q_OBJECT
|
||||
|
||||
bool highlighted = false;
|
||||
double startVal;
|
||||
double lastVal = 0.0;
|
||||
bool dirty = true;
|
||||
|
||||
|
@ -25,6 +27,7 @@ namespace Xybrid::UI {
|
|||
enum Step : int {
|
||||
NoStep = 1,
|
||||
SmallStep = 3,
|
||||
MedStep = 7,
|
||||
BigStep = 15,
|
||||
};
|
||||
|
||||
|
@ -35,6 +38,7 @@ namespace Xybrid::UI {
|
|||
double min = 0.0;
|
||||
double max = 1.0;
|
||||
double step = 0.01;
|
||||
double subStep = -1;
|
||||
int stepPx = SmallStep;
|
||||
double defaultVal = 0.0;
|
||||
|
||||
|
@ -48,18 +52,22 @@ namespace Xybrid::UI {
|
|||
QPainterPath shape() const override;
|
||||
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
|
||||
|
||||
void hoverEnterEvent(QGraphicsSceneHoverEvent*) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent*) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent*) override;
|
||||
void wheelEvent(QGraphicsSceneWheelEvent*) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
|
||||
|
||||
inline KnobGadget* setLabel(const QString& s) { label->setText(s); dirty = true; update(); return this; }
|
||||
inline KnobGadget* setTextFunc(const std::function<QString(double)>& f) { fText = f; dirty = true; update(); return this; }
|
||||
inline KnobGadget* setRange(double min, double max, double step = -1, int px = -1) {
|
||||
inline KnobGadget* setSize(decltype(size) s) { this->size = s; return this; }
|
||||
inline KnobGadget* setRange(double min, double max, double step = -1, int px = -1, double sub = -1) {
|
||||
this->min = min;
|
||||
this->max = max;
|
||||
if (step > 0) this->step = step;
|
||||
if (px > 0) stepPx = px;
|
||||
if (sub > 0) subStep = sub;
|
||||
update();
|
||||
return this;
|
||||
}
|
||||
|
@ -80,8 +88,48 @@ namespace Xybrid::UI {
|
|||
update();
|
||||
return this;
|
||||
}
|
||||
KnobGadget* bind(NodeLib::Param& par) {
|
||||
auto p = ∥
|
||||
fGet = [p] { return p->value; };
|
||||
fSet = [p](double d) {
|
||||
p->value = d;
|
||||
p->vt = std::numeric_limits<double>::quiet_NaN();
|
||||
};
|
||||
|
||||
// binding to one of these populates metadata
|
||||
min = p->min;
|
||||
max = p->max;
|
||||
defaultVal = p->def;
|
||||
label->setText(p->name);
|
||||
|
||||
dirty = true;
|
||||
update();
|
||||
return this;
|
||||
}
|
||||
|
||||
static QString textPercent(double);
|
||||
static QString textOffset(double);
|
||||
static QString textFrequency(double);
|
||||
|
||||
static QString textGain(double);
|
||||
static QString textBalance(double);
|
||||
|
||||
static void autoCreate(LayoutGadget*, NodeLib::ADSR&);
|
||||
};
|
||||
|
||||
// macro to vastly reduce boilerplate
|
||||
#define preset(NAME) \
|
||||
template<typename T> static inline KnobGadget* auto##NAME (QGraphicsItem* p, T& v) { return auto##NAME (p)->bind(v); } \
|
||||
static inline KnobGadget* auto##NAME (QGraphicsItem* p)
|
||||
|
||||
// common presets
|
||||
preset(Percent) { return (new KnobGadget(p))->setRange(0.0, 1.0, .01)->setTextFunc(KnobGadget::textPercent); }
|
||||
|
||||
preset(Gain) { return (new KnobGadget(p))->setRange(-60, 12, .1)->setLabel(qs("Gain"))->setTextFunc(KnobGadget::textGain); }
|
||||
preset(Balance) { return (new KnobGadget(p))->setRange(-1.0, 1.0, .01)->setLabel(qs("Balance"))->setTextFunc(KnobGadget::textBalance); }
|
||||
preset(Cutoff) { return (new KnobGadget(p))->setRange(0, 16000, 10, NoStep, 1)->setLabel(qs("Cutoff"))->setTextFunc(KnobGadget::textFrequency); }
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// keep this local
|
||||
#undef preset
|
||||
|
|
|
@ -6,7 +6,7 @@ using Xybrid::UI::LabelGadget;
|
|||
|
||||
#include <QPainter>
|
||||
|
||||
namespace {
|
||||
namespace { // clazy:excludeall=non-pod-global-static
|
||||
const QFont font("Arcon Rounded", 8);
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ void LabelGadget::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*)
|
|||
LabelGadget* LabelGadget::setText(const QString& t) {
|
||||
text = t;
|
||||
QFontMetricsF fm(font);
|
||||
size = {fm.width(text) + 2, fm.height() + 2};
|
||||
size = {fm.horizontalAdvance(text) + 2, fm.height() + 2};
|
||||
update();
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using Xybrid::UI::LayoutGadget;
|
||||
|
||||
#include "ui/patchboard/nodeobject.h"
|
||||
#include "ui/patchboard/nodeuiscene.h"
|
||||
|
||||
namespace Xybrid::UI {
|
||||
class LayoutSpacerGadget : public Gadget {
|
||||
|
@ -51,19 +52,26 @@ LayoutGadget::LayoutGadget(NodeObject* parent, bool vertical) : LayoutGadget(par
|
|||
});
|
||||
}
|
||||
|
||||
LayoutGadget::LayoutGadget(NodeUIScene* parent, bool vertical) : LayoutGadget(static_cast<QGraphicsItem*>(nullptr), vertical) {
|
||||
QObject::connect(parent, &NodeUIScene::finalized, this, [this] {
|
||||
updateGeometry();
|
||||
});
|
||||
parent->addItem(this);
|
||||
}
|
||||
|
||||
LayoutGadget* LayoutGadget::addSpacer() { new LayoutSpacerGadget(this); return this; }
|
||||
|
||||
void LayoutGadget::updateGeometry() {
|
||||
qreal cur = margin;
|
||||
auto ms = orient(minSize);
|
||||
qreal h = ms.height();
|
||||
for (auto c : childItems()) {
|
||||
for (auto c : childItems()) { // clazy:exclude=range-loop-detach
|
||||
auto g = static_cast<Gadget*>(c);
|
||||
g->updateGeometry();
|
||||
auto r = orient(g->layoutBoundingRect());
|
||||
h = std::max(h, r.height());
|
||||
}
|
||||
for (auto c : childItems()) {
|
||||
for (auto c : childItems()) { // clazy:exclude=range-loop-detach
|
||||
auto g = static_cast<Gadget*>(c);
|
||||
auto r = orient(g->layoutBoundingRect());
|
||||
g->centerOn(orient(QPointF(cur + r.width()/2, r.height()/2 + (h - r.height())*bias)));
|
||||
|
@ -74,3 +82,7 @@ void LayoutGadget::updateGeometry() {
|
|||
}
|
||||
|
||||
QRectF LayoutGadget::boundingRect() const { return { QPointF(), size }; }
|
||||
|
||||
void LayoutGadget::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget*) {
|
||||
if (drawPanel) NodeObject::drawPanel(painter, opt, boundingRect().adjusted(-panelMargin, -panelMargin, panelMargin, panelMargin));
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace Xybrid::UI {
|
||||
class NodeObject;
|
||||
class NodeUIScene;
|
||||
class LayoutGadget : public Gadget {
|
||||
QSizeF size;
|
||||
|
||||
|
@ -19,9 +20,12 @@ namespace Xybrid::UI {
|
|||
qreal bias = 0.5;
|
||||
|
||||
bool vertical = false;
|
||||
bool drawPanel = false;
|
||||
qreal panelMargin = 6;
|
||||
|
||||
LayoutGadget(QGraphicsItem* parent = nullptr, bool vertical = false);
|
||||
LayoutGadget(NodeObject* parent, bool vertical = false);
|
||||
LayoutGadget(NodeUIScene* parent, bool vertical = false);
|
||||
~LayoutGadget() override = default;
|
||||
|
||||
inline LayoutGadget* setMetrics(qreal margin = -1, qreal spacing = -1, qreal bias = -1) {
|
||||
|
@ -30,11 +34,17 @@ namespace Xybrid::UI {
|
|||
if (bias >= 0) this->bias = std::clamp(bias, 0.0, 1.0);
|
||||
return this;
|
||||
}
|
||||
inline LayoutGadget* setPanel(bool draw, qreal margin = 6) {
|
||||
drawPanel = draw;
|
||||
panelMargin = margin;
|
||||
return this;
|
||||
}
|
||||
|
||||
LayoutGadget* addSpacer();
|
||||
|
||||
void updateGeometry() override;
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -86,7 +86,6 @@ void SampleSelectorGadget::paint(QPainter* p, const QStyleOptionGraphicsItem* op
|
|||
}
|
||||
}
|
||||
p->setRenderHint(QPainter::Antialiasing, true);
|
||||
p->setRenderHint(QPainter::HighQualityAntialiasing, true);
|
||||
p->setPen(QPen(outline, 2));
|
||||
p->setBrush(QBrush(Qt::NoBrush));
|
||||
p->drawRoundedRect(br, corner, corner);
|
||||
|
@ -106,7 +105,7 @@ void SampleSelectorGadget::paint(QPainter* p, const QStyleOptionGraphicsItem* op
|
|||
void SampleSelectorGadget::buildSubmenu(DirectoryNode* dir, QMenu* menu) {
|
||||
auto smp = currentSample.lock();
|
||||
bool needSeparator = false;
|
||||
for (auto c : dir->children) {
|
||||
for (auto c : qAsConst(dir->children)) {
|
||||
if (c->isDirectory()) {
|
||||
needSeparator = true;
|
||||
buildSubmenu(c, menu->addMenu(c->name));
|
||||
|
@ -118,11 +117,12 @@ void SampleSelectorGadget::buildSubmenu(DirectoryNode* dir, QMenu* menu) {
|
|||
wa->setDefaultWidget(wfp);
|
||||
wfp->showName = true;
|
||||
wfp->highlightable = true;
|
||||
wfp->showLoopPoints = false;
|
||||
wfp->setSample(s);
|
||||
wfp->setMinimumSize(192, 48);
|
||||
menu->addAction(wa);
|
||||
if (s == smp) wa->setDisabled(true);
|
||||
else connect(wa, &QWidgetAction::triggered, [this, s] { setSample(s); });
|
||||
else connect(wa, &QWidgetAction::triggered, this, [this, s] { setSample(s); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,10 +135,10 @@ void SampleSelectorGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
|||
auto* m = new QMenu();
|
||||
if (project->samples.empty()) m->addAction("(no samples in project)")->setDisabled(true);
|
||||
else {
|
||||
m->addAction("(no sample)", [this] { setSample(nullptr); });
|
||||
m->addAction("(no sample)", this, [this] { setSample(nullptr); });
|
||||
m->addSeparator();
|
||||
DirectoryNode root;
|
||||
for (auto s : project->samples) root.placeData(s->name, s->uuid);
|
||||
for (auto& s : qAsConst(project->samples)) root.placeData(s->name, s->uuid);
|
||||
root.sortTree();
|
||||
|
||||
buildSubmenu(&root, m);
|
||||
|
|
|
@ -57,10 +57,10 @@ void SelectorGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
|
|||
std::vector<Entry> v = fGetList ? fGetList() : std::vector<Entry>();
|
||||
if (v.empty()) m->addAction("(no entries)")->setDisabled(true);
|
||||
else {
|
||||
for (auto e : v) {
|
||||
for (auto& e : v) {
|
||||
if (_entry.first == e.first) {
|
||||
m->addAction(e.second)->setDisabled(true);
|
||||
} else m->addAction(e.second, [this, e] {
|
||||
} else m->addAction(e.second, this, [this, e] {
|
||||
setEntry(e);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,6 +47,6 @@ namespace Xybrid::UI {
|
|||
inline SelectorGadget* setEditFunc(const std::function<void(QMenu*)>& f) { fEditMenu = f; return this; }
|
||||
|
||||
signals:
|
||||
void onSelect(const Entry&);
|
||||
void onSelect(const Xybrid::UI::SelectorGadget::Entry&);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,7 +12,10 @@ void GadgetScene::toolTip(QGraphicsItem* g, const QString& s, const QPointF& pos
|
|||
if (s.isEmpty()) {
|
||||
if (toolTipSource && toolTipSource != g) return;
|
||||
// remove existing tool tip
|
||||
if (toolTipObject) toolTipObject->deleteLater();
|
||||
if (toolTipObject) {
|
||||
toolTipObject->setVisible(false); // hide immediately to ensure ghosting doesn't happen
|
||||
toolTipObject->deleteLater();
|
||||
}
|
||||
toolTipObject = nullptr;
|
||||
} else {
|
||||
// create/set up
|
||||
|
|
|
@ -17,6 +17,6 @@ namespace Xybrid::UI {
|
|||
GadgetScene(QGraphicsView* view);
|
||||
~GadgetScene() override = default;
|
||||
|
||||
void toolTip(QGraphicsItem*, const QString&, const QPointF&, const QColor&);
|
||||
void toolTip(QGraphicsItem*, const QString& = { }, const QPointF& = {0, 0}, const QColor& = {255, 255, 255});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -48,6 +48,20 @@ void PortObject::connectTo(PortObject* o) {
|
|||
if (port->type == Port::Input) { in = this; out = o; }
|
||||
else { out = this; in = o; }
|
||||
|
||||
// splice-between logic
|
||||
auto dt = port->dataType();
|
||||
if (in->port->type == Port::Input && in->port->singleInput() && in->port->isConnected() && out->port->dataType() == dt) {
|
||||
if (auto oi = out->port->owner.lock()->port(Port::Input, dt, 0); oi) {
|
||||
auto c = in->port->connections[0].lock();
|
||||
if (!oi->isConnected() || oi->connections[0].lock() == in->port->connections[0].lock()) {
|
||||
if (auto pc = in->connections[c->obj]; pc) delete pc;
|
||||
in->port->disconnect(c);
|
||||
|
||||
if (!oi->isConnected() && oi->connect(c)) new PortConnectionObject(oi->obj, c->obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (out->port->connect(in->port)) {
|
||||
/*auto* pc =*/ new PortConnectionObject(in, out);
|
||||
}
|
||||
|
@ -60,11 +74,11 @@ void PortObject::setHighlighted(bool h, bool hideLabel) {
|
|||
auto gs = static_cast<GadgetScene*>(scene());
|
||||
if (h && !hideLabel) {
|
||||
auto c = tcolor[port->dataType()];
|
||||
auto txt = qs("%1 %2").arg(Util::enumName(port->dataType()).toLower()).arg(Util::hex(port->index));
|
||||
if (!port->name.isEmpty()) txt = qs("%1 (%2)").arg(port->name).arg(txt);
|
||||
auto txt = qs("%1 %2").arg(Util::enumName(port->dataType()).toLower(), Util::hex(port->index));
|
||||
if (!port->name.isEmpty()) txt = qs("%1 (%2)").arg(port->name, txt);
|
||||
double side = port->type == Port::Input ? -1.0 : 1.0;
|
||||
gs->toolTip(this, txt, {side, 0}, c);
|
||||
} else gs->toolTip(this, { }, { }, { });
|
||||
} else gs->toolTip(this);
|
||||
|
||||
update();
|
||||
}
|
||||
|
@ -76,7 +90,7 @@ PortObject::PortObject(const std::shared_ptr<Data::Port>& p) {
|
|||
setAcceptedMouseButtons(Qt::LeftButton);
|
||||
setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
|
||||
|
||||
for (auto c : port->connections) {
|
||||
for (auto& c : port->connections) {
|
||||
if (auto cc = c.lock(); cc && cc->obj) connectTo(cc->obj);
|
||||
}
|
||||
|
||||
|
@ -175,7 +189,7 @@ NodeObject::NodeObject(const std::shared_ptr<Data::Node>& n) {
|
|||
|
||||
node->onGadgetCreated();
|
||||
|
||||
emit finalized();
|
||||
emit finalized(); // clazy:exclude=incorrect-emit
|
||||
}
|
||||
|
||||
void NodeObject::setGadgetSize(QPointF p) {
|
||||
|
@ -204,7 +218,7 @@ void NodeObject::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
|||
setPos(std::round((x()+cx)/grid)*grid-cx, std::round((y()+cy)/grid)*grid-cy);
|
||||
|
||||
auto posDelta = pos() - oPos;
|
||||
for (auto itm : scene()->selectedItems()) {
|
||||
for (auto itm : scene()->selectedItems()) { // clazy:exclude=range-loop-detach
|
||||
if (itm == this) continue;
|
||||
itm->moveBy(posDelta.x(), posDelta.y()); // apply snap to everything else, in relative terms
|
||||
}
|
||||
|
@ -217,7 +231,7 @@ void NodeObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) {
|
|||
|
||||
void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
|
||||
if (!isSelected()) {
|
||||
for (auto* s : scene()->selectedItems()) s->setSelected(false);
|
||||
for (auto* s : scene()->selectedItems()) s->setSelected(false); // clazy:exclude=range-loop-detach
|
||||
setSelected(true);
|
||||
}
|
||||
auto* m = new QMenu();
|
||||
|
@ -247,7 +261,7 @@ void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
|
|||
}
|
||||
|
||||
void NodeObject::bringToTop(bool force) {
|
||||
for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this);
|
||||
for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this); // clazy:exclude=range-loop-detach
|
||||
}
|
||||
|
||||
void NodeObject::createPorts() {
|
||||
|
@ -261,8 +275,8 @@ void NodeObject::createPorts() {
|
|||
QPointF inc(0, PortObject::portSize + PortObject::portSpacing);
|
||||
|
||||
QPointF cursor = QPointF(0, 0);
|
||||
for (auto mdt : node->inputs) {
|
||||
for (auto pp : mdt.second) {
|
||||
for (auto& mdt : node->inputs) {
|
||||
for (auto& pp : mdt.second) {
|
||||
auto* p = new PortObject(pp.second);
|
||||
p->setParentItem(ipc);
|
||||
p->setPos(cursor);
|
||||
|
@ -275,8 +289,8 @@ void NodeObject::createPorts() {
|
|||
ipc->setPen(p);
|
||||
|
||||
cursor = QPointF(0, 0);
|
||||
for (auto mdt : node->outputs) {
|
||||
for (auto pp : mdt.second) {
|
||||
for (auto& mdt : node->outputs) {
|
||||
for (auto& pp : mdt.second) {
|
||||
auto* p = new PortObject(pp.second);
|
||||
p->setParentItem(opc);
|
||||
p->setPos(cursor);
|
||||
|
@ -297,7 +311,7 @@ void NodeObject::updateGeometry() {
|
|||
if (autoPositionPorts) {
|
||||
qreal pm = PortObject::portSize * .5 + PortObject::portSpacing;
|
||||
if (inputPortContainer) inputPortContainer->setPos(QPointF(-pm, PortObject::portSize));
|
||||
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize));
|
||||
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize)); // NOLINT we're calling this on *this*
|
||||
}
|
||||
emit postGeometryUpdate();
|
||||
}
|
||||
|
@ -334,13 +348,7 @@ void NodeObject::focusInEvent(QFocusEvent *) {
|
|||
bringToTop(true);
|
||||
}
|
||||
|
||||
void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
|
||||
if (customChrome) {
|
||||
node->drawCustomChrome(painter, opt);
|
||||
return;
|
||||
}
|
||||
QRectF r = boundingRect();
|
||||
|
||||
void NodeObject::drawPanel(QPainter* painter, const QStyleOptionGraphicsItem* opt, QRectF r, double rad) {
|
||||
QColor outline = QColor(31, 31, 31);
|
||||
if (opt->state & QStyle::State_Selected) outline = QColor(127, 127, 255);
|
||||
|
||||
|
@ -353,7 +361,17 @@ void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, Q
|
|||
painter->setRenderHint(QPainter::RenderHint::Antialiasing);
|
||||
painter->setBrush(QBrush(fill));
|
||||
painter->setPen(QPen(QBrush(outline), 2));
|
||||
painter->drawRoundedRect(r, 8, 8);
|
||||
painter->drawRoundedRect(r, rad, rad);
|
||||
}
|
||||
|
||||
void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
|
||||
if (customChrome) {
|
||||
node->drawCustomChrome(painter, opt);
|
||||
return;
|
||||
}
|
||||
QRectF r = boundingRect();
|
||||
|
||||
drawPanel(painter, opt, r);
|
||||
|
||||
if (showName) {
|
||||
static QFont f = [] {
|
||||
|
@ -394,12 +412,11 @@ PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) {
|
|||
in->connections[out] = this;
|
||||
out->connections[in] = this;
|
||||
|
||||
QTimer::singleShot(1, [this] { this->in->scene()->addItem(this); });
|
||||
QTimer::singleShot(1, this, [this] { this->in->scene()->addItem(this); });
|
||||
setZValue(-100);
|
||||
setAcceptHoverEvents(true);
|
||||
//setFlag(QGraphicsItem::GraphicsItemFlag::)
|
||||
|
||||
QTimer::singleShot(1, [this] {
|
||||
QTimer::singleShot(1, this, [this] {
|
||||
auto* op = static_cast<QGraphicsObject*>(this->out->parentItem()->parentItem());
|
||||
auto* ip = static_cast<QGraphicsObject*>(this->in->parentItem()->parentItem());
|
||||
connect(op, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds);
|
||||
|
|
|
@ -126,6 +126,7 @@ namespace Xybrid::UI {
|
|||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
|
||||
|
||||
static void drawPanel(QPainter*, const QStyleOptionGraphicsItem*, QRectF, double radius = 8);
|
||||
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
|
||||
QRectF boundingRect() const override;
|
||||
|
||||
|
|
|
@ -29,7 +29,14 @@ NodeUIScene::NodeUIScene(QGraphicsView* v, const std::shared_ptr<Xybrid::Data::N
|
|||
connect(view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &NodeUIScene::queueResize);
|
||||
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &NodeUIScene::queueResize);
|
||||
|
||||
autoResize();
|
||||
// force full redraw to eliminate graphical glitches
|
||||
connect(this, &QGraphicsScene::changed, this, [this] { update(); });
|
||||
|
||||
// queue up final setup in event loop; this should happen after code surrounding creation but before display
|
||||
QMetaObject::invokeMethod(this, [this] {
|
||||
emit finalized(); // emit before display
|
||||
autoResize();
|
||||
}, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
NodeUIScene::~NodeUIScene() {
|
||||
|
|
|
@ -45,6 +45,7 @@ namespace Xybrid::UI {
|
|||
template<typename T> inline T* makeStateObject() { auto o = std::make_shared<T>(); stateObject = o; return o.get(); }
|
||||
|
||||
signals:
|
||||
void finalized();
|
||||
void notePreview(int16_t);
|
||||
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ using Xybrid::UI::PatchboardScene;
|
|||
#include "data/graph.h"
|
||||
#include "data/project.h"
|
||||
using namespace Xybrid::Data;
|
||||
#include "config/uiconfig.h"
|
||||
#include "config/pluginregistry.h"
|
||||
using namespace Xybrid::Config;
|
||||
#include "fileops.h"
|
||||
|
@ -114,7 +115,7 @@ PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr<Xy
|
|||
auto v = Node::multiFromCbor(QCborValue::fromCbor(data->data("xybrid-internal/x-graph-copy")), graph, center);
|
||||
|
||||
setSelectionArea(QPainterPath()); // deselect all
|
||||
for (auto n : v) { // and select pasted objects
|
||||
for (auto& n : v) { // and select pasted objects
|
||||
auto o = new NodeObject(n);
|
||||
addItem(o);
|
||||
o->setSelected(true);
|
||||
|
@ -131,9 +132,9 @@ void PatchboardScene::drawBackground(QPainter* painter, const QRectF& rect) {
|
|||
|
||||
const constexpr int step = 32; // grid size
|
||||
painter->setPen(QPen(QColor(127, 127, 127, 63), 1));
|
||||
for (auto y = std::floor(rect.top() / step) * step + .5; y < rect.bottom(); y += step)
|
||||
for (auto y = std::floor(rect.top() / step) * step + .5; y < rect.bottom(); y += step) // NOLINT
|
||||
painter->drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y));
|
||||
for (auto x = std::floor(rect.left() / step) * step + .5; x < rect.right(); x += step)
|
||||
for (auto x = std::floor(rect.left() / step) * step + .5; x < rect.right(); x += step) // NOLINT
|
||||
painter->drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()));
|
||||
}
|
||||
|
||||
|
@ -151,7 +152,7 @@ void PatchboardScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
|
|||
|
||||
addItem(new NodeObject(n));
|
||||
}, graph.get());
|
||||
m->addAction("Import...", [this, p] {
|
||||
m->addAction("Import...", this, [this, p] {
|
||||
if (auto fileName = FileOps::showOpenDialog(nullptr, "Import node...", Config::Directories::presets, FileOps::Filter::node); !fileName.isEmpty()) {
|
||||
auto n = FileOps::loadNode(fileName, graph);
|
||||
if (!n) return; // right, that can return null
|
||||
|
@ -224,7 +225,7 @@ void PatchboardScene::refresh() {
|
|||
// build scene from graph
|
||||
clear();
|
||||
|
||||
for (auto n : graph->children) {
|
||||
for (auto& n : graph->children) {
|
||||
auto* o = new NodeObject(n);
|
||||
addItem(o);
|
||||
}
|
||||
|
|
|
@ -136,7 +136,7 @@ void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewI
|
|||
|
||||
// and main data
|
||||
QString s = index.data().toString();
|
||||
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
//auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
int cc = index.column() % PatternEditorModel::colsPerChannel;
|
||||
int align = Qt::AlignCenter;
|
||||
if (cc > 1) { // param field
|
||||
|
@ -170,7 +170,7 @@ 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::KeyRelease) qDebug() << "key release";
|
||||
if (type == QEvent::KeyPress) {
|
||||
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
|
||||
|
||||
|
@ -194,10 +194,11 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
|
||||
} else {
|
||||
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?
|
||||
if (mod & Qt::Modifier::SHIFT) return dc->cancel(); // nothing on shift yet
|
||||
|
||||
dc->cancel();
|
||||
SelectionBounds s(sel);
|
||||
if (s.x1 == s.x2 && s.x1 % PatternEditorModel::colsPerChannel <= 1) return false; // just previewing the note column
|
||||
auto* cc = (new CompositeCommand())->reserve((1 + s.y2 - s.y1) * (1 + s.ch2 - s.ch1));
|
||||
|
||||
for (int c = s.ch1; c <= s.ch2; c++) {
|
||||
|
@ -206,6 +207,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
auto mp = s.maxParamSelected(c);
|
||||
if (mp < 0) continue;
|
||||
auto mps = std::min(mpc, static_cast<size_t>(mp));
|
||||
if (!multi) mps++; // allow strutting into new columns if not multiselecting
|
||||
for (int r = s.y1; r <= s.y2; r++) {
|
||||
if (multi && mps <= p->rowAt(c, r).numParams()) continue;
|
||||
auto* dc = new PatternDeltaCommand(p, c, r-1);
|
||||
|
@ -236,6 +238,30 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
|
||||
return cc->commit("delete selection");
|
||||
}
|
||||
if (k == Qt::Key_Minus) {
|
||||
dc->cancel();
|
||||
SelectionBounds s(sel);
|
||||
auto cc = (new CompositeCommand())->reserve((1 + s.y2 - s.y1) * (1 + s.ch2 - s.ch1));
|
||||
|
||||
for (int c = s.ch1; c <= s.ch2; c++) {
|
||||
if (c == s.ch2 && s.x2 < 2) continue; // no params selected here
|
||||
for (int r = s.y1; r <= s.y2; r++) {
|
||||
auto dc = new PatternDeltaCommand(p, c, r-1);
|
||||
//auto min = c == s.ch1 ? std::max(0, s.x1 - 2) : 0;
|
||||
//auto max = c == s.ch2 ? s.x2 - 2 : PatternEditorModel::paramSoftCap;
|
||||
|
||||
for (auto i = 0; i < static_cast<int>(dc->row.numParams()); i++) {
|
||||
if (s.paramSelected(c, i)) {
|
||||
auto& pr = dc->row.param(i);
|
||||
if (pr[0] != ' ') pr[1] *= -1;
|
||||
}
|
||||
}
|
||||
cc->compose(dc);
|
||||
}
|
||||
}
|
||||
|
||||
return cc->commit("negate selection");
|
||||
}
|
||||
|
||||
// for all other commands, reset selection to cursor and defer
|
||||
sm->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
|
||||
|
@ -296,7 +322,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
} else { // param column
|
||||
size_t par = static_cast<size_t>((cc - (cc % 2)) / 2 - 1);
|
||||
if (k == Qt::Key_Insert) { // insert from within any place in the param columns
|
||||
if (row.numParams() >= PatternEditorModel::paramSoftCap) return false; // no overruns
|
||||
if (row.numParams() >= PatternEditorModel::paramSoftCap) return dc->cancel(); // no overruns
|
||||
row.insertParam(par, ' ');
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
size_t cpar = row.numParams() - 1;
|
||||
|
@ -321,6 +347,10 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
view->setCurrentIndex(index.siblingAtColumn(index.column()+1));
|
||||
return dc->commit();
|
||||
} else {
|
||||
if (k == Qt::Key_Minus) { // negate current value
|
||||
row.param(par)[1] *= -1;
|
||||
return dc->commit();
|
||||
}
|
||||
if (k == Qt::Key_Comma) { // convenience; allow inserting an extend from number column
|
||||
if (row.numParams() >= PatternEditorModel::paramSoftCap) return dc->cancel();
|
||||
row.insertParam(par+1, ',');
|
||||
|
@ -336,7 +366,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
}
|
||||
}
|
||||
} else { // new param; set to key pressed and move forward
|
||||
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return false;
|
||||
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return dc->cancel();
|
||||
char chr = static_cast<QKeyEvent*>(event)->text().toUtf8()[0];
|
||||
row.addParam(chr);
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace { // helper functions
|
|||
QString s(3, ' ');
|
||||
int nn = n % 12;
|
||||
int oc = (n - nn) / 12;
|
||||
s[2] = '0' + static_cast<char>(oc);
|
||||
s[2] = static_cast<QChar>('0' + static_cast<char>(oc));
|
||||
s[0] = notemap[nn*2];
|
||||
s[1] = notemap[nn*2+1];
|
||||
return s;
|
||||
|
@ -112,13 +112,13 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
|
|||
if (cc % 2 == 0) {
|
||||
if (row.numParams() > cp) return QString(1,static_cast<char>(row.params->at(cp)[0]));
|
||||
if (row.numParams() == cp) return qs("» ");
|
||||
return qs("");
|
||||
return qs(" ");
|
||||
}
|
||||
if (row.numParams() > cp) {
|
||||
if (row.params->at(cp)[0] == ' ') return qs("- ");
|
||||
return byteStr(row.params->at(cp)[1]);
|
||||
}
|
||||
return qs("");
|
||||
return qs(" ");
|
||||
}
|
||||
} else if (role == Qt::SizeHintRole) {
|
||||
if (index.row() >= pattern->rows) return QSize(-1, -1);
|
||||
|
|
|
@ -135,7 +135,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
} else { // note(s)
|
||||
amt = std::clamp(amt, -12, 12);
|
||||
auto cc = new CompositeCommand();
|
||||
for (auto s : sel.indexes()) {
|
||||
for (auto s : sel.indexes()) { // clazy:exclude=range-loop-detach
|
||||
if (s.column() % Util::colsPerChannel != 1) continue;
|
||||
int ch = Util::channelForColumn(s.column());
|
||||
auto c = new PatternDeltaCommand(p, ch, s.row()-1);
|
||||
|
@ -307,7 +307,7 @@ void PatternEditorView::startPreview(int key) {
|
|||
auto ind = currentIndex();
|
||||
int cc = ind.column() % PatternEditorModel::colsPerChannel;
|
||||
int ch = (ind.column() - cc) / PatternEditorModel::colsPerChannel;
|
||||
if (cc == 1) { // note column
|
||||
if (cc == 1 || (cc == 0 && key == Qt::Key_Space)) { // note column or space on port
|
||||
stopPreview(key); // end current preview first, if applicable
|
||||
auto& r = mdl->getPattern()->rowAt(ch, ind.row()-1);
|
||||
auto p = mdl->getPattern()->project->shared_from_this();
|
||||
|
@ -401,7 +401,7 @@ void PatternEditorView::headerContextMenu(QPoint pt) {
|
|||
});
|
||||
if (idx < hdr->count() - 1) {
|
||||
menu->addAction("Delete Channel", this, [this, idx, p]() {
|
||||
if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name)).arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
|
||||
if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name), Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
|
||||
(new PatternChannelDeleteCommand(p, idx))->commit();
|
||||
});
|
||||
menu->addAction("Rename Channel...", this, [this, idx, p]() {
|
||||
|
|
|
@ -10,6 +10,7 @@ using namespace Xybrid::Editing;
|
|||
|
||||
#include <QDebug>
|
||||
#include <QMimeData>
|
||||
#include <QIODevice>
|
||||
|
||||
#include "mainwindow.h"
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using Xybrid::UI::PatternSequencerModel;
|
||||
|
||||
#include <QMimeData>
|
||||
#include <QIODevice>
|
||||
|
||||
using Xybrid::Data::Pattern;
|
||||
using Xybrid::Data::Project;
|
||||
|
@ -40,7 +41,7 @@ QVariant PatternSequencerModel::data(const QModelIndex &index, int role) const {
|
|||
if (pattern->name.isEmpty()) return QVariant(); // no tool tip without name
|
||||
return QString("(%1) %2").arg(pattern->index, 1, 10, QChar('0')).arg(pattern->name);*/
|
||||
}
|
||||
if (role == Qt::TextAlignmentRole ) return Qt::AlignHCenter + Qt::AlignVCenter;
|
||||
if (role == Qt::TextAlignmentRole ) return {Qt::AlignHCenter | Qt::AlignVCenter};
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ using namespace Xybrid::Editing;
|
|||
#include <QTimer>
|
||||
#include <QStringBuilder>
|
||||
#include <QMimeData>
|
||||
#include <QIODevice>
|
||||
#include <QUrl>
|
||||
|
||||
#include <QMenu>
|
||||
|
@ -98,7 +99,7 @@ void SampleListModel::refresh() {
|
|||
root = std::make_shared<DirectoryNode>();
|
||||
auto* project = window->getProject().get();
|
||||
if (!project) return;
|
||||
for (auto s : project->samples) root->placeData(s->name, s->uuid);
|
||||
for (auto& s : qAsConst(project->samples)) root->placeData(s->name, s->uuid);
|
||||
root->sortTree();
|
||||
|
||||
view->setCurrentIndex(QModelIndex());
|
||||
|
@ -146,7 +147,7 @@ void SampleListModel::propagateSampleNames(DirectoryNode* dn) {
|
|||
if (!dn->data.isNull()) {
|
||||
auto* project = window->getProject().get();
|
||||
project->samples[dn->data.toUuid()]->name = dn->path();
|
||||
} else for (auto c : dn->children) propagateSampleNames(c);
|
||||
} else for (auto c : qAsConst(dn->children)) propagateSampleNames(c);
|
||||
}
|
||||
|
||||
bool SampleListModel::setData(const QModelIndex& index, const QVariant& value, int role) {
|
||||
|
@ -236,7 +237,7 @@ bool SampleListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
|
|||
|
||||
QList<QUrl> urls = data->urls();
|
||||
bool success = false;
|
||||
for (auto u : urls) {
|
||||
for (auto& u : urls) {
|
||||
if (!u.isLocalFile()) continue;
|
||||
auto smp = Sample::fromFile(u.toLocalFile());
|
||||
if (smp) { // valid sample returned
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue