Compare commits
25 Commits
fe0a5e2096
...
9460490bce
Author | SHA1 | Date |
---|---|---|
Rachel Fae Fox (foxiepaws) | 9460490bce | |
Rachel Fae Fox (foxiepaws) | 69215761d9 | |
Rachel Fae Fox | 30cd64d942 | |
Rachel Fae Fox | dfbd976ef1 | |
Rachel Fae Fox (foxiepaws) | 069f2971ce | |
Rachel Fae Fox (foxiepaws) | 55b71f449f | |
Rachel Fae Fox (foxiepaws) | 713d952cef | |
Rachel Fae Fox (foxiepaws) | a95df29d8f | |
zetaPRIME | b1449ebcf6 | |
zetaPRIME | 43f6374fc2 | |
zetaPRIME | ffc7be1783 | |
zetaPRIME | 3d678c173b | |
zetaPRIME | 914f987487 | |
zetaPRIME | b380959241 | |
zetaPRIME | 24b7c72ef9 | |
zetaPRIME | 7af2ec5034 | |
zetaPRIME | 70da8cefd6 | |
zetaPRIME | 8f55ef577b | |
zetaPRIME | b65ce423a7 | |
zetaPRIME | 707a6169a2 | |
Rachel Fae Fox (foxiepaws) | b91a3ee5d5 | |
Rachel Fae Fox (foxiepaws) | 994deb8d54 | |
Rachel Fae Fox (foxiepaws) | fed1365b14 | |
Rachel Fae Fox (foxiepaws) | d4595bc022 | |
Rachel Fae Fox (foxiepaws) | 2e1f3e04cb |
|
@ -1,5 +1,6 @@
|
|||
# ignore build artifacts
|
||||
build-*/*
|
||||
*.o
|
||||
|
||||
# user config
|
||||
*.codekit
|
||||
|
@ -7,3 +8,4 @@ build-*/*
|
|||
*.pro.user.*
|
||||
*.stash
|
||||
*.autosave
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
pipeline {
|
||||
agent any
|
||||
stages {
|
||||
stage('Clean Up') {
|
||||
steps {
|
||||
sh '''cd xybrid
|
||||
make clean || true
|
||||
rm Makefile || true'''
|
||||
}
|
||||
}
|
||||
stage('qmake') {
|
||||
steps {
|
||||
sh 'qmake'
|
||||
}
|
||||
}
|
||||
stage('Make') {
|
||||
steps {
|
||||
sh 'make'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
notes
14
notes
|
@ -32,13 +32,13 @@ parameters {
|
|||
|
||||
TODO {
|
||||
immediate frontburner {
|
||||
spacer rows on top/bottom of pattern editor (keep centered)
|
||||
make ctrl+pgup/dn jump to the next *actual pattern*, looping around if necessary
|
||||
|
||||
global pan (PXX) for InstrumentCore? *default* pan
|
||||
distortion effect
|
||||
single-selection sampler
|
||||
|
||||
- node function to release unneeded old data when stopping playback
|
||||
...
|
||||
global (default) pan (PXX) for InstrumentCore
|
||||
|
||||
add ,XX support to global tempo
|
||||
}
|
||||
|
||||
- actual config file loading/saving
|
||||
|
@ -51,9 +51,13 @@ TODO {
|
|||
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 {
|
||||
expand/compact pattern 2x/3x, keeping fold interval
|
||||
|
||||
at *least* js plugin support, with lua+lv2 highly preferable
|
||||
|
||||
different context menu for multiple selected nodes
|
||||
|
|
|
@ -236,7 +236,8 @@ void AudioEngine::render(std::shared_ptr<Project> p, QString filename) {
|
|||
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" << filename;
|
||||
param << "-f" << "mp3" << "-codec:a" << "libmp3lame"<< "-q:a" << "0"; // specify mp3, vbr v0
|
||||
param << filename;
|
||||
|
||||
enc.start("ffmpeg", param);
|
||||
enc.waitForStarted();
|
||||
|
|
|
@ -7,22 +7,24 @@ namespace Xybrid::Config {
|
|||
public:
|
||||
ColorScheme() = default;
|
||||
|
||||
QColor patternSelection = QColor(127, 63, 255, 63);
|
||||
QColor patternFoldIndicator = QColor(95, 95, 95);
|
||||
QColor patternSelection = {127, 63, 255, 63};
|
||||
QColor patternFoldIndicator = {95, 95, 95};
|
||||
QColor patternFoldShade = {0, 0, 0, 31};
|
||||
QColor patternOuterShade = {0, 0, 0, 63};
|
||||
|
||||
QColor patternBg = QColor(23, 23, 23);
|
||||
QColor patternBgBeat = QColor(31, 31, 31);
|
||||
QColor patternBgMeasure = QColor(39, 39, 39);
|
||||
QColor patternBg = {23, 23, 23};
|
||||
QColor patternBgBeat = {31, 31, 31};
|
||||
QColor patternBgMeasure = {39, 39, 39};
|
||||
|
||||
QColor patternFgBlank = QColor(127, 127, 127);
|
||||
QColor patternFgPort = QColor(191, 191, 191);
|
||||
QColor patternFgNote = QColor(255, 255, 255);
|
||||
QColor patternFgParamCmd = QColor(191,163,255);
|
||||
QColor patternFgParamAmt = QColor(191,222,255);
|
||||
QColor patternFgBlank = {127, 127, 127};
|
||||
QColor patternFgPort = {191, 191, 191};
|
||||
QColor patternFgNote = {255, 255, 255};
|
||||
QColor patternFgParamCmd = {191, 163, 255};
|
||||
QColor patternFgParamAmt = {191, 222, 255};
|
||||
|
||||
QColor waveformBg = QColor(23, 23, 23);
|
||||
QColor waveformBgHighlight = QColor(31, 31, 47);
|
||||
QColor waveformFgPrimary = QColor(191, 163, 255);
|
||||
QColor waveformBg = {23, 23, 23};
|
||||
QColor waveformBgHighlight = {31, 31, 47};
|
||||
QColor waveformFgPrimary = {191, 163, 255};
|
||||
};
|
||||
extern ColorScheme colorScheme;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ namespace Xybrid::Data {
|
|||
QString comment;
|
||||
|
||||
QString fileName;
|
||||
QString exportFileName;
|
||||
|
||||
double tempo = 140.0;
|
||||
TimeSignature time;
|
||||
|
|
|
@ -9,11 +9,18 @@ using namespace Xybrid::Data;
|
|||
#include <QCborMap>
|
||||
#include <QCborArray>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <QAudioDecoder>
|
||||
#include <QAudioFormat>
|
||||
|
||||
#include <QProcess>
|
||||
#include <QDataStream>
|
||||
|
||||
#include <QEventLoop>
|
||||
|
||||
#define qs QStringLiteral
|
||||
|
@ -88,6 +95,7 @@ bool Sample::changeUuid(QUuid newUuid) {
|
|||
|
||||
void Sample::newUuid() { changeUuid(QUuid::createUuid()); }
|
||||
|
||||
#ifdef OLD_SAMPLE_IMPORT
|
||||
namespace {
|
||||
bool blah [[maybe_unused]];
|
||||
template<typename T> void insertbuf(std::shared_ptr<Sample> smp, const T* data, size_t len, size_t channels) {
|
||||
|
@ -154,6 +162,63 @@ std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
|
|||
|
||||
return smp;
|
||||
}
|
||||
#else
|
||||
std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
|
||||
QJsonObject info;
|
||||
{
|
||||
// get stream info
|
||||
QProcess probe;
|
||||
QStringList param;
|
||||
param << "-v" << "quiet" << "-show_streams" << "-select_streams" << "a" << "-of" << "json";
|
||||
param << fileName;
|
||||
probe.start("ffprobe", param);
|
||||
probe.waitForFinished();
|
||||
|
||||
auto doc = QJsonDocument::fromJson(probe.readAllStandardOutput());
|
||||
info = doc.object()["streams"].toArray().first().toObject();
|
||||
}
|
||||
if (!info.contains("sample_rate") || !info.contains("channels")) return nullptr; // no/invalid audio streams
|
||||
|
||||
int channels = info["channels"].toInt();
|
||||
int sampleRate = info["sample_rate"].toString().toInt(); // for some reason ffprobe stores this as a string??
|
||||
|
||||
auto smp = std::make_shared<Sample>();
|
||||
smp->sampleRate = sampleRate;
|
||||
|
||||
// grab raw pcm_f32le via ffmpeg
|
||||
QByteArray raw;
|
||||
{
|
||||
QProcess dec;
|
||||
QStringList param;
|
||||
param << "-i" << fileName << "-f" << "f32le" << "-acodec" << "pcm_f32le" << "-";
|
||||
dec.start("ffmpeg", param);
|
||||
dec.waitForFinished();
|
||||
raw = dec.readAllStandardOutput();
|
||||
}
|
||||
// pre-size sample data buffers
|
||||
auto len = static_cast<size_t>((raw.length() / channels) / 4);
|
||||
smp->data[0].reserve(len);
|
||||
if (channels > 1) smp->data[1].reserve(len);
|
||||
|
||||
// read raw bytes into channels
|
||||
QDataStream r(&raw, QIODevice::ReadOnly);
|
||||
size_t chs = static_cast<size_t>(channels);
|
||||
size_t ch = chs;
|
||||
float f;
|
||||
while (!r.atEnd()) {
|
||||
r.readRawData(reinterpret_cast<char*>(&f), sizeof(f));
|
||||
ch = (ch+1) % chs;
|
||||
if (ch < 2) smp->data[ch].push_back(f);
|
||||
}
|
||||
|
||||
// add info
|
||||
smp->uuid = QUuid::createUuid();
|
||||
smp->name = QFileInfo(fileName).baseName();
|
||||
|
||||
return smp;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
bool exporting = false;
|
||||
|
|
|
@ -38,8 +38,11 @@ namespace {
|
|||
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,1);
|
||||
}
|
||||
|
||||
const QString FileOps::Filter::project = u8"Xybrid project (*.xyp);;All files (*)";
|
||||
const QString FileOps::Filter::node = u8"Xybrid node (*.xyn);;All files (*)";
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -16,6 +16,9 @@ namespace Xybrid::FileOps {
|
|||
namespace Filter {
|
||||
extern const QString project;
|
||||
extern const QString node;
|
||||
|
||||
extern const QString audioIn;
|
||||
extern const QString audioOut;
|
||||
}
|
||||
QString showOpenDialog(QWidget* parent = nullptr, const QString& caption = QString(), const QString& directory = QString(), const QString& filter = QString());
|
||||
QString showSaveAsDialog(QWidget* parent = nullptr, const QString& caption = QString(), const QString& directory = QString(), const QString& filter = QString(), const QString& suffix = QString());
|
||||
|
|
|
@ -13,13 +13,15 @@
|
|||
|
||||
int main(int argc, char *argv[]) {
|
||||
qRegisterMetaType<Xybrid::Data::Port>();
|
||||
|
||||
QApplication a(argc, argv);
|
||||
|
||||
// enable antialiasing on accelerated graphicsview
|
||||
QSurfaceFormat fmt;
|
||||
fmt.setSamples(10);
|
||||
|
||||
QSurfaceFormat::setDefaultFormat(fmt);
|
||||
QApplication a(argc, argv);
|
||||
|
||||
|
||||
|
||||
|
||||
// make sure bundled fonts are loaded
|
||||
QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf");
|
||||
|
|
|
@ -121,11 +121,6 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
}
|
||||
});
|
||||
|
||||
// TODO: temp: render shortcut
|
||||
connect(new QShortcut(QKeySequence("Ctrl+E"), this), &QShortcut::activated, this, [this]() {
|
||||
audioEngine->render(project, Config::Directories::projects % "/testOut.mp3");
|
||||
});
|
||||
|
||||
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
|
||||
}
|
||||
|
||||
|
@ -325,8 +320,11 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
view->setViewport(vp); // enable hardware acceleration
|
||||
}
|
||||
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
|
||||
// Under OSX these cause Xybrid to crash.
|
||||
#ifndef __APPLE__
|
||||
glEnable(GL_MULTISAMPLE);
|
||||
glEnable(GL_LINE_SMOOTH);
|
||||
#endif
|
||||
//QGL::FormatOption::Rgba
|
||||
|
||||
|
||||
|
@ -483,13 +481,37 @@ void MainWindow::menuFileSave() {
|
|||
}
|
||||
|
||||
void MainWindow::menuFileSaveAs() {
|
||||
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", Config::Directories::projects, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) {
|
||||
QString saveDir = Config::Directories::projects;
|
||||
if (!project->fileName.isEmpty()) {
|
||||
QFileInfo f(project->fileName);
|
||||
saveDir = f.dir().filePath(f.baseName());
|
||||
}
|
||||
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) {
|
||||
FileOps::saveProject(project, fileName);
|
||||
undoStack->setClean();
|
||||
updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::menuFileExport() {
|
||||
if (project->exportFileName.isEmpty()) menuFileExportAs();
|
||||
else {
|
||||
audioEngine->render(project, project->exportFileName);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::menuFileExportAs() {
|
||||
QString saveDir = Config::Directories::projects;
|
||||
if (!project->fileName.isEmpty()) {
|
||||
QFileInfo f(project->fileName);
|
||||
saveDir = f.dir().filePath(f.baseName());
|
||||
}
|
||||
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::audioOut, "mp3"); !fileName.isEmpty()) {
|
||||
project->exportFileName = fileName;
|
||||
audioEngine->render(project, project->exportFileName);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::menuFileNewWindow() {
|
||||
auto w = new MainWindow();
|
||||
w->show();
|
||||
|
@ -541,9 +563,9 @@ int MainWindow::sequenceSelection(int n) {
|
|||
void MainWindow::playbackPosition(int seq, int row) {
|
||||
sequenceSelection(seq);
|
||||
if (ui->patternEditor->isFolded() && editingPattern->fold > 1) row -= row % editingPattern->fold;
|
||||
auto mi = ui->patternEditor->currentIndex().siblingAtRow(row);
|
||||
auto mi = ui->patternEditor->currentIndex().siblingAtRow(row+1);
|
||||
|
||||
if (!mi.isValid()) mi = ui->patternEditor->model()->index(row, 0);
|
||||
if (!mi.isValid()) mi = ui->patternEditor->model()->index(row+1, 0);
|
||||
ui->patternEditor->setCurrentIndex(mi);
|
||||
ui->patternEditor->selectionModel()->select(QItemSelection(mi.siblingAtColumn(0), mi.siblingAtColumn(ui->patternEditor->horizontalHeader()->count()-1)), QItemSelectionModel::SelectionFlag::ClearAndSelect);
|
||||
ui->patternEditor->scrollTo(mi, QAbstractItemView::PositionAtCenter);
|
||||
|
|
|
@ -65,6 +65,9 @@ namespace Xybrid {
|
|||
void menuFileSave();
|
||||
void menuFileSaveAs();
|
||||
|
||||
void menuFileExport();
|
||||
void menuFileExportAs();
|
||||
|
||||
void menuFileNewWindow();
|
||||
|
||||
signals:
|
||||
|
|
|
@ -835,6 +835,9 @@
|
|||
<addaction name="actionSave"/>
|
||||
<addaction name="actionSave_As"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionExport"/>
|
||||
<addaction name="actionExport_As"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionNew_Window"/>
|
||||
<addaction name="actionClose_Window"/>
|
||||
</widget>
|
||||
|
@ -898,7 +901,7 @@
|
|||
</action>
|
||||
<action name="actionNew_Window">
|
||||
<property name="text">
|
||||
<string>New Window</string>
|
||||
<string>New &Window</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+N</string>
|
||||
|
@ -906,12 +909,28 @@
|
|||
</action>
|
||||
<action name="actionClose_Window">
|
||||
<property name="text">
|
||||
<string>Close Window</string>
|
||||
<string>&Close Window</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+W</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExport">
|
||||
<property name="text">
|
||||
<string>&Export</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+E</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExport_As">
|
||||
<property name="text">
|
||||
<string>E&xport As...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+E</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
|
@ -1032,6 +1051,38 @@
|
|||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionExport</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuFileExport()</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>actionExport_As</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>menuFileExportAs()</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>
|
||||
|
@ -1039,5 +1090,7 @@
|
|||
<slot>menuFileSave()</slot>
|
||||
<slot>menuFileSaveAs()</slot>
|
||||
<slot>menuFileNewWindow()</slot>
|
||||
<slot>menuFileExport()</slot>
|
||||
<slot>menuFileExportAs()</slot>
|
||||
</slots>
|
||||
</ui>
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
#include "resampler.h"
|
||||
using namespace Xybrid::NodeLib;
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
|
||||
#ifdef WITH_BOOST
|
||||
#include <boost/math/special_functions/bessel.hpp>
|
||||
#define cyl_bessel_i boost::math::cyl_bessel_i
|
||||
#else
|
||||
#include <cmath>
|
||||
#define cyl_bessel_i std::cyl_bessel_i
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
|
||||
|
||||
|
@ -18,9 +26,12 @@ namespace {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// generate
|
||||
const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> Xybrid::NodeLib::resamplerLUT = [] {
|
||||
double denom = std::cyl_bessel_i(0, KAISER_BETA);
|
||||
|
||||
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
|
||||
|
@ -28,7 +39,7 @@ const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> Xybrid::NodeLib::resam
|
|||
double sv = static_cast<double>(step) / LUT_STEPS;
|
||||
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)) * (std::cyl_bessel_i(0, KAISER_BETA * std::sqrt(1 - std::pow(((2 * (x+1)) / (LUT_TAPS)) - 1, 2))) / denom);
|
||||
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] << " ";
|
||||
}
|
||||
|
|
|
@ -102,10 +102,10 @@ void Delay::onGadgetCreated() {
|
|||
if (!obj) return;
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
(new KnobGadget(l))->bind(delayTime)->setLabel("Time")->setRange(0.0, 5.0)->setDefault(0.5);
|
||||
(new ToggleGadget(l))->bind(timeInBeats)->setColor({191, 127, 255})->setToolTip("BPM-relative");
|
||||
(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("Amount")->setDefault(0.5);
|
||||
(new KnobGadget(l))->bind(amount)->setLabel(qs("Amount"))->setDefault(0.5);
|
||||
l->addSpacer();
|
||||
(new KnobGadget(l))->bind(feedback)->setLabel("Feedback")->setDefault(0.0);
|
||||
(new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setDefault(0.0);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ using namespace Xybrid::UI;
|
|||
#include "util/strings.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QRandomGenerator>
|
||||
|
@ -44,7 +45,7 @@ namespace {
|
|||
else return d;
|
||||
}
|
||||
}
|
||||
[[maybe_unused]] inline double lerp(double p, double a, double b) {
|
||||
[[maybe_unused]] inline double lerp(double a, double b, double p) {
|
||||
return b * p + a * (1.0 - p);
|
||||
}
|
||||
|
||||
|
@ -68,24 +69,54 @@ namespace {
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta) {
|
||||
[[gnu::optimize("O3")]] double push(double in, double mod, double factor) {
|
||||
double s = in < 0 ? -1 : 1;
|
||||
in *= s;
|
||||
//if (mod < 0) mod = 1.0/-mod;
|
||||
//else mod += 1.0;
|
||||
mod = mod < 0 ? lerp(1.0, 1.0/factor, -mod) : lerp(1.0, factor, mod);
|
||||
return std::pow(in, mod)*s;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] 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;
|
||||
d = push(d, -mod, 5);
|
||||
d -= polyblep(phase, delta);
|
||||
return d;
|
||||
}
|
||||
|
||||
[[gnu::optimize("O3")]] 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) {
|
||||
double duty = (mod+1.0)/2.0;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
// wave function list(s)
|
||||
const constexpr std::array waveFunc = {
|
||||
&oscSaw,
|
||||
&oscSine,
|
||||
&oscPulse,
|
||||
};
|
||||
const std::array waveName = {
|
||||
qs("saw"),
|
||||
qs("sine"),
|
||||
qs("pulse"),
|
||||
};
|
||||
}
|
||||
|
||||
Thicc::Thicc() { }
|
||||
|
||||
void Thicc::init() {
|
||||
double avg = 0;
|
||||
for (int i = 0; i < 4096; i++) avg += std::abs(std::fmod(static_cast<double>(i) / 4096.0, 1.0) * 2.0 - 1.0);
|
||||
qDebug() << "average of" << (avg / 4096.0);
|
||||
|
||||
addPort(Port::Input, Port::Command, 0);
|
||||
addPort(Port::Output, Port::Audio, 0);
|
||||
|
||||
|
@ -95,14 +126,16 @@ void Thicc::init() {
|
|||
note.scratch[0] = static_cast<double>(QRandomGenerator::global()->generate() % 573000);
|
||||
};
|
||||
|
||||
core.globalParam['Q'] = [](const ParamReader& pr) {
|
||||
/*core.globalParam['Q'] = [](const ParamReader& pr) {
|
||||
qDebug() << "global recieved" << pr.param() << pr.val();
|
||||
return true;
|
||||
};
|
||||
};*/
|
||||
|
||||
core.processNote = [this](Note& note, AudioPort* p) {
|
||||
double freq;
|
||||
|
||||
auto osc = waveFunc[static_cast<size_t>(wave)];
|
||||
|
||||
int vc = voices;
|
||||
double vf = static_cast<double>(vc);
|
||||
double spr = std::pow(SEMI, detune/(vf/2.0));
|
||||
|
@ -118,8 +151,6 @@ void Thicc::init() {
|
|||
double delta = smpTime * freq;
|
||||
note.scratch[0] += delta;
|
||||
|
||||
// s
|
||||
|
||||
double o = 0;
|
||||
for (int i = 0; i < vc; i++) {
|
||||
auto ii = static_cast<double>(i);
|
||||
|
@ -128,18 +159,13 @@ void Thicc::init() {
|
|||
double dm = std::pow(spr, dc);
|
||||
double sg = i % 2 == 0 ? 1.0 : -1.0;
|
||||
//o += (std::fmod(pc + si * dm, 1.0) * 2.0 - 1.0) * sg;
|
||||
o += oscSaw(pc + si * dm, delta * dm) * sg;
|
||||
o += osc(pc + si * dm, delta * dm, mod) * sg;
|
||||
|
||||
}
|
||||
|
||||
o /= 1.0 + ((vf - 1.0)/3.0);
|
||||
|
||||
// e
|
||||
|
||||
note.scratch[3] += /*(smpTime / (1.0/20000.0))*/0.5 * (o - note.scratch[3]); // simple low pass;
|
||||
if (note.scratch[3] != note.scratch[3]) note.scratch[3] = 0; // nan!? WHY
|
||||
AudioFrame out = o;//note.scratch[3];
|
||||
o /= 1.0 + ((vf - 1.0)/5.0);
|
||||
|
||||
AudioFrame out = o;
|
||||
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
|
||||
}
|
||||
};
|
||||
|
@ -152,25 +178,31 @@ void Thicc::process() { core.process(this); }
|
|||
void Thicc::saveData(QCborMap& m) const {
|
||||
m[qs("adsr")] = adsr;
|
||||
|
||||
m[qs("wave")] = wave;
|
||||
m[qs("voices")] = voices;
|
||||
m[qs("mod")] = mod;
|
||||
m[qs("detune")] = detune;
|
||||
//m[qs("shift")] = shift;
|
||||
}
|
||||
|
||||
void Thicc::loadData(const QCborMap& m) {
|
||||
adsr = m.value("adsr");
|
||||
|
||||
wave = static_cast<int>(m.value("wave").toInteger(wave));
|
||||
voices = static_cast<int>(m.value("voices").toInteger(voices));
|
||||
mod = m.value("mod").toDouble(mod);
|
||||
detune = m.value("detune").toDouble(detune);
|
||||
//shift = m.value("shift").toDouble(shift);
|
||||
}
|
||||
|
||||
void Thicc::onGadgetCreated() {
|
||||
auto wn = [](size_t i) { if (i >= waveName.size()) return QString::number(i); return waveName[i]; };
|
||||
|
||||
auto l = new LayoutGadget(obj);
|
||||
|
||||
(new KnobGadget(l))->bind(wave)->setLabel(qs("Wave"))->setTextFunc(wn)->setRange(0, waveFunc.size()-1, 1, KnobGadget::BigStep);
|
||||
(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(shift)->setLabel(qs("Shift"))->setRange(0.0, 25.0, 0.01);
|
||||
l->addSpacer();
|
||||
KnobGadget::autoCreate(l, adsr);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,11 @@ namespace Xybrid::Instruments {
|
|||
|
||||
NodeLib::ADSR adsr;
|
||||
|
||||
int wave = 0;
|
||||
int voices = 1;
|
||||
|
||||
double mod = 0.0;
|
||||
double detune = 0.0;
|
||||
//double shift = 0.0;
|
||||
|
||||
public:
|
||||
Thicc();
|
||||
|
|
|
@ -91,19 +91,22 @@ namespace {
|
|||
}
|
||||
|
||||
void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
||||
int row = index.row() - 1;
|
||||
auto mdl = const_cast<PatternEditorModel*>(static_cast<const PatternEditorModel*>(index.model()));
|
||||
auto p = mdl->getPattern();
|
||||
{ /* background */ } {
|
||||
painter->fillRect(option.rect, Config::colorScheme.patternBg);
|
||||
if (row == -1 || row >= p->rows) painter->fillRect(option.rect, Config::colorScheme.patternOuterShade);
|
||||
else if (p->fold > 1 && row % p->fold != 0) painter->fillRect(option.rect, Config::colorScheme.patternFoldShade);
|
||||
if (option.state & QStyle::State_Enabled) {
|
||||
if (index.row() % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgMeasure);
|
||||
else if (index.row() % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgBeat);
|
||||
if (row % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgMeasure);
|
||||
else if (row % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgBeat);
|
||||
}
|
||||
}
|
||||
|
||||
// selection/cursor highlight
|
||||
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, Config::colorScheme.patternSelection);
|
||||
if (option.state & QStyle::State_HasFocus) {
|
||||
if (option.state & QStyle::State_HasFocus && p->channels.size() > 0) {
|
||||
painter->setPen(Config::colorScheme.patternSelection);
|
||||
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
|
||||
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
|
||||
|
@ -111,15 +114,15 @@ void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewI
|
|||
//painter->fillRect(option.rect, Config::colorScheme.patternSelection);
|
||||
}
|
||||
|
||||
if (mdl->folded && p->fold > 1) {
|
||||
if (row > 0 && mdl->folded && p->fold > 1) {
|
||||
bool show = false;
|
||||
|
||||
int cc = index.column() % PatternEditorModel::colsPerChannel;
|
||||
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
|
||||
if (ch < static_cast<int>(p->numChannels())) {
|
||||
int row = index.row();
|
||||
int rowEnd = row + p->fold;
|
||||
for (int i = row + 1; i < rowEnd; i++) {
|
||||
int rs = row + 1;
|
||||
int rowEnd = rs + p->fold;
|
||||
for (int i = rs + 1; i < rowEnd; i++) {
|
||||
QString v = index.siblingAtRow(i).data(Qt::DisplayRole).toString();
|
||||
if (!(v.isEmpty() or v == " - " or v == "- " or v == "» ")) { show = true; break; }
|
||||
}
|
||||
|
@ -178,7 +181,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
auto p = m->getPattern();
|
||||
int cc = index.column() % PatternEditorModel::colsPerChannel;
|
||||
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
|
||||
auto* dc = new PatternDeltaCommand(p, ch, index.row());
|
||||
auto* dc = new PatternDeltaCommand(p, ch, index.row() - 1);
|
||||
auto& row = dc->row;//p->rowAt(ch, index.row());
|
||||
|
||||
auto* sm = static_cast<PatternEditorView*>(parent())->selectionModel();
|
||||
|
@ -205,7 +208,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
auto mps = std::min(mpc, static_cast<size_t>(mp));
|
||||
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);
|
||||
auto* dc = new PatternDeltaCommand(p, c, r-1);
|
||||
for (size_t i = dc->row.numParams(); i < mps; i++) dc->row.addParam(' ');
|
||||
if (!multi) dc->row.param(mps) = {' ', 0};
|
||||
cc->compose(dc);
|
||||
|
@ -221,7 +224,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
|
||||
for (int c = s.ch1; c <= s.ch2; c++) {
|
||||
for (int r = s.y1; r <= s.y2; r++) {
|
||||
auto* dc = new PatternDeltaCommand(p, c, r);
|
||||
auto* dc = new PatternDeltaCommand(p, c, r-1);
|
||||
if (s.portSelected(c)) dc->row.port = -1;
|
||||
if (s.noteSelected(c)) dc->row.note = -1;
|
||||
for (int i = static_cast<int>(dc->row.numParams()) - 1; i >= 0; i--) {
|
||||
|
|
|
@ -5,16 +5,20 @@ using Xybrid::Data::Pattern;
|
|||
#include "ui/patterneditorview.h"
|
||||
using Xybrid::UI::PatternEditorView;
|
||||
|
||||
#include "util/strings.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
#include <QFontMetrics>
|
||||
#include <QTimer>
|
||||
#include <QScrollBar>
|
||||
|
||||
namespace { // helper functions
|
||||
int cellWidthBase = -1;
|
||||
int cellWidthParam;
|
||||
int cellWidthParamTab;
|
||||
int headerHeight;
|
||||
QSize hsz, hhsz;
|
||||
|
||||
constexpr char hexmap[] = {'0', '1', '2', '3', '4', '5', '6', '7',
|
||||
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
|
||||
|
@ -22,21 +26,21 @@ namespace { // helper functions
|
|||
|
||||
constexpr char notemap[] = "C-C#D-D#E-F-F#G-G#A-A#B-";
|
||||
|
||||
std::string hexStr(unsigned char *data, uint len) {
|
||||
std::string s(len * 2, ' ');
|
||||
for (uint i = 0; i < len; ++i) {
|
||||
QString hexStr(unsigned char *data, int len) {
|
||||
QString s(len * 2, ' ');
|
||||
for (int i = 0; i < len; ++i) {
|
||||
s[2 * i] = hexmap[(data[i] & 0xF0) >> 4];
|
||||
s[2 * i + 1] = hexmap[data[i] & 0x0F];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
std::string byteStr(int t) {
|
||||
QString byteStr(int t) {
|
||||
unsigned char c = static_cast<unsigned char>(t & 255);
|
||||
return hexStr(&c, 1);
|
||||
}
|
||||
|
||||
std::string noteStr(int n) {
|
||||
std::string s(3, ' ');
|
||||
QString noteStr(int n) {
|
||||
QString s(3, ' ');
|
||||
int nn = n % 12;
|
||||
int oc = (n - nn) / 12;
|
||||
s[2] = '0' + static_cast<char>(oc);
|
||||
|
@ -63,11 +67,22 @@ QVariant PatternEditorHeaderProxyModel::headerData(int section, Qt::Orientation
|
|||
PatternEditorModel::PatternEditorModel(QObject *parent)
|
||||
:QAbstractTableModel(parent) {
|
||||
hprox = new PatternEditorHeaderProxyModel(parent, this);
|
||||
|
||||
auto view = static_cast<PatternEditorView*>(parent);
|
||||
int rowHeight = [] {
|
||||
QFontMetrics fm = QFontMetrics(QFont());
|
||||
return (fm.boundingRect("255").size() + QSize(8, 4)).height();
|
||||
}();
|
||||
endHeight = (view->viewport()->height() - rowHeight) / 2;
|
||||
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this, view, rowHeight] {
|
||||
endHeight = (view->viewport()->height() - rowHeight) / 2;
|
||||
emit this->layoutChanged();
|
||||
});
|
||||
}
|
||||
|
||||
int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {
|
||||
//if (pattern->channels.size() == 0) return 1;
|
||||
return pattern->rows + 1;
|
||||
return pattern->rows + 2;
|
||||
}
|
||||
|
||||
int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
|
||||
|
@ -77,34 +92,33 @@ int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
|
|||
|
||||
QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
|
||||
if (role == Qt::DisplayRole) {
|
||||
if (index.row() >= pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) return QVariant();
|
||||
if (index.row() == 0) return QVariant();
|
||||
if (index.row() > pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) return QVariant();
|
||||
int cc = index.column() % colsPerChannel;
|
||||
int ch = (index.column() - cc) / colsPerChannel;
|
||||
auto& row = pattern->rowAt(ch, index.row());
|
||||
auto& row = pattern->rowAt(ch, index.row()-1);
|
||||
if (cc == 0) { // port
|
||||
if (row.port >= 0 && row.port < 256) return QString::fromStdString(byteStr(row.port));
|
||||
if (row.port == -2) return QString("(G)");
|
||||
if (row.port == -3) return QString("L");
|
||||
return QString(" - ");
|
||||
if (row.port >= 0 && row.port < 256) return byteStr(row.port);
|
||||
if (row.port == -2) return qs("(G)");
|
||||
if (row.port == -3) return qs("L");
|
||||
return qs(" - ");
|
||||
} else if (cc == 1) { // note
|
||||
if (row.note >= 0) return QString::fromStdString(noteStr(row.note));
|
||||
if (row.note == -2) return QString(" ^ "); // note off
|
||||
if (row.note == -3) return QString(" x "); // hard cut
|
||||
return QString(" - ");
|
||||
if (row.note >= 0) return noteStr(row.note);
|
||||
if (row.note == -2) return qs(" ^ "); // note off
|
||||
if (row.note == -3) return qs(" x "); // hard cut
|
||||
return qs(" - ");
|
||||
} else {
|
||||
size_t cp = static_cast<size_t>(((cc - 2) - (cc % 2)) / 2);
|
||||
//return QString::number((cp));
|
||||
if (cc % 2 == 0) {
|
||||
if (row.numParams() > cp) return QString::fromStdString(std::string(1,static_cast<char>(row.params->at(cp)[0])));
|
||||
if (row.numParams() == cp) return QString("» ");
|
||||
return QString("");
|
||||
if (row.numParams() > cp) return QString(1,static_cast<char>(row.params->at(cp)[0]));
|
||||
if (row.numParams() == cp) return qs("» ");
|
||||
return qs("");
|
||||
}
|
||||
if (row.numParams() > cp) {
|
||||
if (row.params->at(cp)[0] == ' ') return QString("- ");
|
||||
return QString::fromStdString(byteStr(row.params->at(cp)[1]));
|
||||
if (row.params->at(cp)[0] == ' ') return qs("- ");
|
||||
return byteStr(row.params->at(cp)[1]);
|
||||
}
|
||||
return QString("");
|
||||
//return QString("--");
|
||||
return qs("");
|
||||
}
|
||||
} else if (role == Qt::SizeHintRole) {
|
||||
if (index.row() >= pattern->rows) return QSize(-1, -1);
|
||||
|
@ -115,21 +129,24 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
|
|||
QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (role == Qt::DisplayRole) {
|
||||
if (orientation == Qt::Orientation::Horizontal) return QVariant(); // blank actual-header
|
||||
if (section >= pattern->rows) return QVariant(); // blank end section
|
||||
return QString::number(section);
|
||||
if (section == 0 || section > pattern->rows) return QVariant(); // blank end section
|
||||
return QString::number(section-1);
|
||||
} else if (role == Qt::SizeHintRole) {
|
||||
auto fm = QFontMetrics(QFont(/*"Iosevka Term Light", 9*/));
|
||||
if (orientation == Qt::Orientation::Vertical) {
|
||||
if (section >= pattern->rows) return QSize(-1, -1); // take no space if no hanging room
|
||||
return fm.boundingRect("255").size() + QSize(8, 4); // this should fit 0-999
|
||||
if (orientation == Qt::Orientation::Vertical) if (section == 0 || section > pattern->rows) return QSize(-1, endHeight); // fill ends to center
|
||||
if (hsz.isEmpty()) {
|
||||
auto fm = QFontMetrics(QFont(/*"Iosevka Term Light", 9*/));
|
||||
hsz = fm.boundingRect("255").size() + QSize(8, 4); // this should fit 0-999
|
||||
hhsz = QSize(0, hsz.height());
|
||||
}
|
||||
return QSize(0, fm.height() + 4);
|
||||
return orientation == Qt::Vertical ? hsz : hhsz;
|
||||
|
||||
//return QSize(0, fm.height() + 4);
|
||||
} else if (role == Qt::TextAlignmentRole) return Qt::AlignCenter;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags PatternEditorModel::flags(const QModelIndex &index) const {
|
||||
if (index.row() >= pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) {
|
||||
if (index.row() == 0 || index.row() >= pattern->rows + 1 || (pattern->channels.size() > 0 && index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size()))) {
|
||||
return QAbstractTableModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
}
|
||||
return QAbstractTableModel::flags(index);
|
||||
|
@ -150,19 +167,10 @@ QVariant PatternEditorModel::hdrData(int section, Qt::Orientation, int role) con
|
|||
if (cellWidthBase <= 0) {
|
||||
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
headerHeight = fm.height()+4;
|
||||
cellWidthBase = fm.width(QString("FF")) + fm.width(QString("C#2")) + cellPadding*4;
|
||||
cellWidthParamTab = fm.width(QString("v")) + cellPadding;
|
||||
cellWidthParam = cellWidthParamTab + fm.width(QString("FF")) + cellPadding;
|
||||
}/*
|
||||
auto& c = pattern->channels.at(static_cast<size_t>(section));
|
||||
size_t maxParams = 0;
|
||||
for (auto& r : c.rows) {
|
||||
if (r.numParams() > maxParams) maxParams = r.numParams();
|
||||
cellWidthBase = fm.horizontalAdvance(QString("FF")) + fm.horizontalAdvance(QString("C#2")) + cellPadding*4;
|
||||
cellWidthParamTab = fm.horizontalAdvance(QString("v")) + cellPadding;
|
||||
cellWidthParam = cellWidthParamTab + fm.horizontalAdvance(QString("FF")) + cellPadding;
|
||||
}
|
||||
int width = cellWidthBase;
|
||||
width += static_cast<int>(maxParams) * cellWidthParam;
|
||||
if (maxParams < paramSoftCap) width += cellWidthParamTab;
|
||||
return QSize(width, headerHeight);*/
|
||||
return QSize(0, headerHeight);
|
||||
}
|
||||
return QVariant();
|
||||
|
@ -175,12 +183,10 @@ void PatternEditorModel::setPattern(const std::shared_ptr<Pattern>& pattern) {
|
|||
}
|
||||
|
||||
void PatternEditorModel::updateColumnDisplay() {
|
||||
/*static int qi = 0;
|
||||
qDebug() << QString("column display request #%1").arg(qi++);//*/
|
||||
if (pattern == nullptr) return;
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
view->setUpdatesEnabled(false);
|
||||
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
//view->setUpdatesEnabled(false);
|
||||
//auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
||||
for (size_t ch = 0; ch < pattern->channels.size(); ch++) {
|
||||
auto& c = pattern->channels.at(ch);
|
||||
size_t maxParams = 0;
|
||||
|
@ -213,21 +219,23 @@ void PatternEditorModel::updateColumnDisplay() {
|
|||
int lsw = view->columnWidth(lastShown);
|
||||
view->setColumnWidth(lastShown, std::max(lsw + 3, minWidth - (chWidth - lsw)));
|
||||
}
|
||||
view->setUpdatesEnabled(true);
|
||||
//view->setUpdatesEnabled(true);
|
||||
view->updateHeader(true);
|
||||
}
|
||||
|
||||
void PatternEditorModel::updateFold() {
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
view->setUpdatesEnabled(false);
|
||||
//view->setUpdatesEnabled(false);
|
||||
int ifold = 1;
|
||||
if (folded && pattern->fold > 1) ifold = pattern->fold;
|
||||
int rows = this->rowCount();
|
||||
int rows = this->rowCount()-2;
|
||||
view->setRowHidden(0, false);
|
||||
view->setRowHidden(rows+1, false);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
view->setRowHidden(i, false); // dispel any "phantoms" we might end up having
|
||||
view->setRowHidden(i, (i < pattern->rows && i % ifold != 0));
|
||||
view->setRowHidden(i+1, false); // dispel any "phantoms" we might end up having
|
||||
if (i < pattern->rows && i % ifold != 0) view->setRowHidden(i+1, true);
|
||||
}
|
||||
view->setUpdatesEnabled(true);
|
||||
//view->setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
void PatternEditorModel::toggleFold() {
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace Xybrid::UI {
|
|||
|
||||
bool fitHeaderToName = false;
|
||||
bool folded = true;
|
||||
int endHeight = -1;
|
||||
|
||||
PatternEditorModel(QObject *parent);
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
|
|
@ -71,7 +71,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
horizontalHeader()->setStretchLastSection(true);
|
||||
verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
verticalHeader()->setMinimumSectionSize(0);
|
||||
verticalHeader()->setStretchLastSection(true);
|
||||
//verticalHeader()->setStretchLastSection(true);
|
||||
verticalHeader()->setSectionsClickable(false);
|
||||
setCornerButtonEnabled(false);
|
||||
//verticalHeader()->setDefaultAlignment(Qt::AlignTop);
|
||||
|
@ -96,6 +96,10 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
setModel(&*mdl);
|
||||
hdr->setModel(&*mdl->hprox);
|
||||
|
||||
// smooth scroll
|
||||
setVerticalScrollMode(ScrollMode::ScrollPerPixel);
|
||||
setHorizontalScrollMode(ScrollMode::ScrollPerPixel);
|
||||
|
||||
{ /* set up hotkeys */ } {
|
||||
// transpose notes
|
||||
auto transpose = [this](int amt, int key = Qt::Key_Alt) {
|
||||
|
@ -109,8 +113,8 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
auto ch = Util::channelForColumn(sel.left());
|
||||
auto last = sel.bottom();
|
||||
for (auto i = sel.top(); i <= last; i++) {
|
||||
if (p->rowAt(ch, i).port >= 0) {
|
||||
auto c = new PatternDeltaCommand(p, ch, i);
|
||||
if (p->rowAt(ch, i-1).port >= 0) {
|
||||
auto c = new PatternDeltaCommand(p, ch, i-1);
|
||||
auto op = c->row.port;
|
||||
c->row.port = static_cast<int16_t>(std::clamp(op + amt, 0, 255));
|
||||
if (c->row.port == op) c->cancel();
|
||||
|
@ -120,7 +124,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
cc->commit("change note port(s)");
|
||||
} else if (startCol >= 2 && sel.height() == 1) { // single param
|
||||
amt = std::clamp(amt, -16, 16);
|
||||
auto c = new PatternDeltaCommand(p, Util::channelForColumn(sel.left()), sel.top());
|
||||
auto c = new PatternDeltaCommand(p, Util::channelForColumn(sel.left()), sel.top()-1);
|
||||
auto& r = c->row;
|
||||
auto par = static_cast<size_t>((sel.left() % Util::colsPerChannel) - 2) / 2;
|
||||
if (r.numParams() > par) {
|
||||
|
@ -134,7 +138,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
for (auto s : sel.indexes()) {
|
||||
if (s.column() % Util::colsPerChannel != 1) continue;
|
||||
int ch = Util::channelForColumn(s.column());
|
||||
auto c = new PatternDeltaCommand(p, ch, s.row());
|
||||
auto c = new PatternDeltaCommand(p, ch, s.row()-1);
|
||||
if (c->row.note >= 0) {
|
||||
c->row.note = static_cast<int16_t>(std::max(c->row.note + amt, 0));
|
||||
cc->compose(c);
|
||||
|
@ -154,15 +158,19 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
|
||||
// fold
|
||||
connect(new QShortcut(QKeySequence("Ctrl+Space"), this), &QShortcut::activated, this, [this] {
|
||||
setUpdatesEnabled(false);
|
||||
mdl->toggleFold();
|
||||
auto p = mdl->getPattern();
|
||||
auto ind = currentIndex();
|
||||
if (mdl->folded && p->fold > 1 && ind.row() % p->fold != 0) {
|
||||
ind = ind.siblingAtRow(ind.row() - (ind.row() % p->fold));
|
||||
int row = ind.row() - 1;
|
||||
if (mdl->folded && p->fold > 1 && row % p->fold != 0) {
|
||||
ind = ind.siblingAtRow(1 + row - (row % p->fold));
|
||||
setCurrentIndex(ind);
|
||||
if (selectedIndexes().count() <= 1) selectionModel()->select(ind, QItemSelectionModel::SelectionFlag::ClearAndSelect);
|
||||
}
|
||||
QTimer::singleShot(1, [this, ind]{ scrollTo(ind, ScrollHint::PositionAtCenter); });
|
||||
scrollTo(ind, ScrollHint::PositionAtCenter);
|
||||
setUpdatesEnabled(true);
|
||||
//QTimer::singleShot(1, [this, ind]{ scrollTo(ind, ScrollHint::PositionAtCenter); });
|
||||
});
|
||||
|
||||
// cut/copy/paste
|
||||
|
@ -188,7 +196,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
chm << first << last;
|
||||
for (int r = sel.top(); r <= sel.bottom(); r++) {
|
||||
QCborArray rm;
|
||||
auto& row = p->rowAt(ch, r);
|
||||
auto& row = p->rowAt(ch, r-1);
|
||||
rm << row.port;
|
||||
rm << row.note;
|
||||
if (row.params) for (auto p : *row.params) rm << p[0] << p[1];
|
||||
|
@ -215,7 +223,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
|
|||
auto p = mdl->getPattern();
|
||||
auto idx = currentIndex();
|
||||
auto chMin = Util::channelForColumn(idx.column());
|
||||
auto rMin = idx.row();
|
||||
auto rMin = idx.row()-1;
|
||||
|
||||
auto root = QCborValue::fromCbor(data->data("xybrid-internal/x-pattern-copy")).toArray();
|
||||
|
||||
|
@ -271,7 +279,9 @@ void PatternEditorView::keyPressEvent(QKeyEvent* e) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
QAbstractItemView::keyPressEvent(e);
|
||||
auto prevInd = currentIndex();
|
||||
QTableView::keyPressEvent(e);
|
||||
if (auto ind = currentIndex(); ind != prevInd) scrollTo(ind, ScrollHint::PositionAtCenter); // cursor position changed, scroll to keep up
|
||||
if (!e->isAutoRepeat()) {
|
||||
if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
|
||||
startPreview(Util::unshiftedKey(e->key()));
|
||||
|
@ -284,13 +294,22 @@ void PatternEditorView::keyReleaseEvent(QKeyEvent* e) {
|
|||
if (!e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
|
||||
}
|
||||
|
||||
void PatternEditorView::mouseReleaseEvent(QMouseEvent* e) {
|
||||
QTableView::mouseReleaseEvent(e);
|
||||
auto sm = selectionModel();
|
||||
if (sm->selectedIndexes().size() <= 1) {
|
||||
auto ind = indexAt(e->localPos().toPoint());
|
||||
if (ind.isValid() && model()->flags(ind) & Qt::ItemFlag::ItemIsEnabled) scrollTo(ind, ScrollHint::PositionAtCenter);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
stopPreview(key); // end current preview first, if applicable
|
||||
auto& r = mdl->getPattern()->rowAt(ch, ind.row());
|
||||
auto& r = mdl->getPattern()->rowAt(ch, ind.row()-1);
|
||||
auto p = mdl->getPattern()->project->shared_from_this();
|
||||
previewKey[key] = {r.port, audioEngine->preview(p, r.port, r.note)};
|
||||
}
|
||||
|
@ -305,10 +324,19 @@ void PatternEditorView::stopPreview(int key) {
|
|||
}
|
||||
|
||||
void PatternEditorView::setPattern(const std::shared_ptr<Pattern>& pattern) {
|
||||
setUpdatesEnabled(false);
|
||||
mdl->setPattern(pattern);
|
||||
//horizontalHeader()->isSectionHidden()
|
||||
//columnCountChanged(0, mdl->columnCount());
|
||||
rowCountChanged(0, mdl->rowCount());
|
||||
|
||||
//rowCountChanged(0, mdl->rowCount());
|
||||
|
||||
auto cur = currentIndex();
|
||||
auto ind = model()->index(std::clamp(cur.row(), 1, pattern->rows), std::clamp(cur.column(), 0, std::max(0, mdl->columnCount() - 2)));
|
||||
while (isColumnHidden(ind.column())) ind = ind.siblingAtColumn(ind.column()-1);
|
||||
while (isRowHidden(ind.row())) ind = ind.siblingAtRow(ind.row()-1);
|
||||
setCurrentIndex(ind);
|
||||
selectionModel()->select(ind, QItemSelectionModel::ClearAndSelect);
|
||||
scrollTo(ind, ScrollHint::PositionAtCenter);
|
||||
setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
void PatternEditorView::updateGeometries() {
|
||||
|
|
|
@ -36,6 +36,8 @@ namespace Xybrid::UI {
|
|||
void keyReleaseEvent(QKeyEvent*) override;
|
||||
void keyboardSearch(const QString&) override {} // disable accidental search
|
||||
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
|
||||
void setPattern(const std::shared_ptr<Data::Pattern>& pattern);
|
||||
|
||||
void updateHeader(bool full = false);
|
||||
|
|
|
@ -22,9 +22,10 @@ using namespace Xybrid::Editing;
|
|||
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "util/macos_urlhelper.h"
|
||||
#include "mainwindow.h"
|
||||
|
||||
|
||||
SampleListModel::SampleListModel(QObject* parent, MainWindow* window) : QAbstractItemModel (parent) {
|
||||
this->window = window;
|
||||
root = std::make_shared<DirectoryNode>();
|
||||
|
@ -223,6 +224,7 @@ bool SampleListModel::canDropMimeData(const QMimeData *data, Qt::DropAction acti
|
|||
return data->hasUrls() || data->hasFormat("xybrid-internal/x-sample-entry-move");
|
||||
}
|
||||
|
||||
|
||||
bool SampleListModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row [[maybe_unused]], int column [[maybe_unused]], const QModelIndex &parent [[maybe_unused]]) {
|
||||
if (data->hasUrls()) {
|
||||
if (action == Qt::IgnoreAction) return true; // can accept type
|
||||
|
@ -232,7 +234,16 @@ bool SampleListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
|
|||
if (!tdn->isDirectory()) tdn = tdn->parent;
|
||||
QString p = tdn->path();
|
||||
|
||||
|
||||
QList<QUrl> urls = data->urls();
|
||||
#ifdef Q_OS_MAC
|
||||
QList<QUrl> localUrls;
|
||||
foreach (const QUrl &url, urls) {
|
||||
QUrl localUrl = fromNSUrl(url);
|
||||
localUrls.append(localUrl);
|
||||
urls = localUrls;
|
||||
}
|
||||
#endif
|
||||
bool success = false;
|
||||
for (auto u : urls) {
|
||||
if (!u.isLocalFile()) continue;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#ifdef __APPLE__
|
||||
#ifndef MACOS_URLHELPER_H
|
||||
#define MACOS_URLHELPER_H
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
QUrl fromNSUrl(const QUrl &url);
|
||||
|
||||
#endif // MACOS_URLHELPER_H
|
||||
#endif
|
|
@ -0,0 +1,11 @@
|
|||
#ifdef __APPLE__
|
||||
#include "macos_urlhelper.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
QUrl fromNSUrl(const QUrl &url) {
|
||||
NSURL *nsUrl = url.toNSURL();
|
||||
NSString *path = nsUrl.path;
|
||||
QString qtString = QString::fromNSString(path);
|
||||
return QUrl::fromLocalFile(qtString);
|
||||
}
|
||||
#endif
|
|
@ -17,6 +17,7 @@ TEMPLATE = app
|
|||
# deprecated API in order to know how to port your code away from it.
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
|
||||
|
||||
# You can also make your code fail to compile if you use deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
||||
|
@ -30,12 +31,37 @@ CONFIG += c++17
|
|||
QMAKE_CXXFLAGS_DEBUG += -Og
|
||||
|
||||
# automatically build file lists
|
||||
SOURCES += $$files(*.cpp, true)
|
||||
SOURCES += $$files(*.cpp, true) \
|
||||
util/macos_urlhelper.mm
|
||||
HEADERS += $$files(*.h, true) \
|
||||
$$files(*.hpp, true)
|
||||
$$files(*.hpp, true) \
|
||||
util/macos_urlhelper.h
|
||||
FORMS += $$files(*.ui, true)
|
||||
RESOURCES += res/resources.qrc
|
||||
|
||||
|
||||
|
||||
freebsd-clang {
|
||||
DEFINES += WITH_BOOST
|
||||
LIBS += -lboost_math_tr1
|
||||
}
|
||||
|
||||
macx: {
|
||||
DEFINES += WITH_BOOST
|
||||
LIBS += -L/usr/local/Cellar/boost/1.70.0/lib/ -lboost_math_tr1
|
||||
LIBS += -framework OpenGL
|
||||
LIBS += -framework Foundation
|
||||
OBJECTIVE_SOURCES += util/macos_urlhelper.h
|
||||
QMAKE_CXXFLAGS += -I/usr/local/Cellar/boost/1.70.0/include/
|
||||
}
|
||||
|
||||
# TODO: make this work.
|
||||
CONFIG (boost) {
|
||||
DEFINES += WITH_BOOST
|
||||
LIBS += -L$${BOOSTPATH}
|
||||
QMAKE_CXXFLAGS += -I$${BOOSTINCLUDE}
|
||||
}
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
|
|
Loading…
Reference in New Issue