Compare commits

...

25 Commits

Author SHA1 Message Date
Rachel Fae Fox (foxiepaws) 9460490bce freebsd-clang added to qmake project 2019-07-22 10:34:51 -04:00
Rachel Fae Fox (foxiepaws) 69215761d9 Merge branch 'portability/macos' of https://git.foxiepa.ws/foxiepaws/xybrid into portability/clang 2019-07-22 10:30:09 -04:00
Rachel Fae Fox 30cd64d942 its qmake not build 2019-07-22 10:05:02 -04:00
Rachel Fae Fox dfbd976ef1 don't freak out if the makefile isn't there. 2019-07-22 09:50:33 -04:00
Rachel Fae Fox (foxiepaws) 069f2971ce fix jenkinsfile 2019-07-22 09:49:07 -04:00
Rachel Fae Fox (foxiepaws) 55b71f449f Add Jenkinsfile that i hope is good enough 2019-07-22 09:37:05 -04:00
Rachel Fae Fox (foxiepaws) 713d952cef LIBS += framework foundation 2019-07-22 09:21:23 -04:00
Rachel Fae Fox (foxiepaws) a95df29d8f fixing drag/drop 2019-07-22 09:19:46 -04:00
zetaPRIME b1449ebcf6 attempt to speed up pattern switching 2019-07-22 03:26:39 -04:00
zetaPRIME 43f6374fc2 Merge branch 'portability/macos' of https://git.foxiepa.ws/foxiepaws/xybrid
macOS compatibility fixes
2019-07-21 19:21:10 -04:00
zetaPRIME ffc7be1783 reimplement sample import using ffmpeg 2019-07-21 16:54:39 -04:00
zetaPRIME 3d678c173b bump up delay time fine tuning 2019-07-21 04:19:23 -04:00
zetaPRIME 914f987487 fix fold indicators 2019-07-21 04:13:16 -04:00
zetaPRIME b380959241 fix remaining off-by-ones in the pattern editor 2019-07-21 02:33:07 -04:00
zetaPRIME 24b7c72ef9 waveform and mod dial for THiCC 2019-07-20 20:24:13 -04:00
zetaPRIME 7af2ec5034 separate extension filters for audio import/export 2019-07-20 15:28:04 -04:00
zetaPRIME 70da8cefd6 pick your export filename; better file save defaults 2019-07-20 15:25:35 -04:00
zetaPRIME 8f55ef577b shade outside pattern 2019-07-20 01:14:45 -04:00
zetaPRIME b65ce423a7 follow cursor, spacer rows, shade rows within fold 2019-07-20 01:09:59 -04:00
zetaPRIME 707a6169a2 use VBR V0 for mp3 export 2019-07-19 18:50:04 -04:00
Rachel Fae Fox (foxiepaws) b91a3ee5d5 Merge branch 'master' of https://gitlab.com/zetaPRIME/xybrid into portability/macos 2019-07-18 06:20:58 -04:00
Rachel Fae Fox (foxiepaws) 994deb8d54 Merge branch 'portability/boost' of https://git.foxiepa.ws/foxiepaws/xybrid into portability/macos 2019-07-18 06:20:33 -04:00
Rachel Fae Fox (foxiepaws) fed1365b14 Modified code style on the resampler to make it easier to read. 2019-07-18 06:19:05 -04:00
Rachel Fae Fox (foxiepaws) d4595bc022 Modifications required to build under OSX
- moved QSurfaceFormat::setDefaultFormat(fmt); higher up in main, as
  osx requires this.
- ifdef'ed out two glEnable calls as they cause Xybrid to segfault on OSX
2019-07-18 05:11:43 -04:00
Rachel Fae Fox (foxiepaws) 2e1f3e04cb WITH_BOOST define added
- some platforms don't support C++17 Special Mathmatical functions,
  most notibly macOS and llvm. This enables this to work by using
  boost for the bessel functions
2019-07-18 05:08:22 -04:00
26 changed files with 473 additions and 146 deletions

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
# ignore build artifacts
build-*/*
*.o
# user config
*.codekit
@ -7,3 +8,4 @@ build-*/*
*.pro.user.*
*.stash
*.autosave

22
Jenkinsfile vendored Normal file
View File

@ -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
View File

@ -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

View File

@ -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();

View File

@ -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;
}

View File

@ -31,6 +31,7 @@ namespace Xybrid::Data {
QString comment;
QString fileName;
QString exportFileName;
double tempo = 140.0;
TimeSignature time;

View File

@ -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;

View File

@ -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

View File

@ -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());

View File

@ -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");

View File

@ -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);

View File

@ -65,6 +65,9 @@ namespace Xybrid {
void menuFileSave();
void menuFileSaveAs();
void menuFileExport();
void menuFileExportAs();
void menuFileNewWindow();
signals:

View File

@ -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 &amp;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>&amp;Close Window</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="actionExport">
<property name="text">
<string>&amp;Export</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action>
<action name="actionExport_As">
<property name="text">
<string>E&amp;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>

View File

@ -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] << " ";
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();

View File

@ -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--) {

View File

@ -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() {

View File

@ -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;

View File

@ -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() {

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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