Compare commits

...

168 Commits

Author SHA1 Message Date
Rachel Fae Fox (foxiepaws) 7aa71c5cc2 macOS/clang++ Porting work
- std=c++2a instead of c++20. QT Creator mkspec for mac requires this
- added missing headers in some places. likely other headers are missing
- expanded WITH_BOOST segments to include memory_resource stuff
- moved SVFilter template process() into header.
2022-03-31 03:22:59 -04:00
Zithia Satazaki b92dd4f3c5 get rid of ugly gcc diagnostic pragmas 2022-03-30 15:55:43 -04:00
Zithia Satazaki a7ece838f1 invert scrollwheel option (for knobs etc.); some magic to make config less annoying 2022-03-29 17:36:10 -04:00
Zithia Satazaki ab811c363c negate selection 2022-03-29 15:58:16 -04:00
Zithia Satazaki 29b0367dfb value negation 2022-03-29 14:54:41 -04:00
Zithia Satazaki 00cfb9df3a allow strutting into new columns if not multiselecting 2022-03-29 14:34:31 -04:00
Zithia Satazaki 9bed15f564 fix nullity on struttable cells short-circuiting key input 2022-03-29 14:28:41 -04:00
Zithia Satazaki e6fa6d33fa fix note preview stuff on pattern editor 2022-03-29 14:06:12 -04:00
Zithia Satazaki e845325178 bump shape range a bit 2022-03-29 02:21:24 -04:00
Zithia Satazaki 25b12be6ea use lerp for ringmod 2022-03-29 00:43:22 -04:00
Zithia Satazaki f969aa0d2e distortion effect 2022-03-29 00:42:12 -04:00
Zithia Satazaki b0b754a37d AudioFrame::lerp 2022-03-28 20:06:21 -04:00
Zithia Satazaki 3ff986d90c move pool reservation out of static init 2022-03-28 18:28:31 -04:00
Zithia Satazaki 5385740516 slight rework of RegisterPlugin (leave a pointer to the plugin info instead of a useless bool) 2022-03-28 18:15:32 -04:00
Zithia Satazaki 5f1b9d03b4 and finally, ref-ify all the range-fors 2022-03-28 17:45:13 -04:00
Zithia Satazaki 042e13eefb fix another deltacommand leak 2022-03-28 17:30:27 -04:00
Zithia Satazaki bf84a1dc1f it does make sense to do this here 2022-03-28 17:27:41 -04:00
Zithia Satazaki 7809e2ab58 fix PatternDeltaCommand leak 2022-03-28 17:23:44 -04:00
Zithia Satazaki ab3cad2c22 nuke unused metrics 2022-03-28 17:21:15 -04:00
Zithia Satazaki 59de376d05 kill "might detach" warnings 2022-03-28 17:20:28 -04:00
Zithia Satazaki 95c0af06be qstring multi-arg, connect/action context object, signal qualification 2022-03-28 17:13:33 -04:00
Zithia Satazaki 22b06c465a switch subgraph to RegisterPlugin macro 2022-03-28 16:49:13 -04:00
Zithia Satazaki b615b18d45 give that a default 2022-03-28 16:43:55 -04:00
Zithia Satazaki 9b131e5322 nuke non-pod global static warnings (we're an executable) 2022-03-28 16:40:13 -04:00
Zithia Satazaki acbba0403b util/mem; use a pool for playtime allocations 2022-03-28 16:15:55 -04:00
Zithia Satazaki 15c0aaed82 use c++20 2022-03-28 13:24:40 -04:00
Zithia Satazaki 7e495b5645 unionize note scratch 2022-03-27 20:44:04 -04:00
Zithia Satazaki e007063e92 shut up testsynth 2022-03-27 20:01:44 -04:00
zetaPRIME 2bfd06acf2 AM mode for ringmod effect 2022-03-25 08:15:25 -04:00
zetaPRIME a6032be6a9 knobGadget.setSize, autopan phase 2022-03-25 04:34:05 -04:00
zetaPRIME d75b7878f8 make adsr autoCreate more concise 2022-03-25 03:38:08 -04:00
zetaPRIME aa264a8065 use autoPercent for 2x03 2022-03-25 03:34:50 -04:00
zetaPRIME 369a7d979d switch cutoff to a Param 2022-03-25 02:25:14 -04:00
zetaPRIME a7c26b9722 fix explicit instantiation declarations 2022-03-25 00:52:37 -04:00
zetaPRIME 779af6bdab automatable param stuff 2022-03-24 02:27:04 -04:00
zetaPRIME 853ba8a901 ...and flip the switch 2022-03-23 19:25:26 -04:00
zetaPRIME 8b020975cc prep for double-buffer switchover... 2022-03-23 19:25:16 -04:00
zetaPRIME be67e02004 port member name consistency 2022-03-23 18:47:37 -04:00
zetaPRIME e63c93e146 ParameterPort 2022-03-23 18:44:04 -04:00
zetaPRIME a28d6e48b6 template wizardry resulting in a mono version of SVFilter for synth use 2022-03-23 01:32:07 -04:00
zetaPRIME acbca4ae0b fix potential overruns on sample preview 2022-03-22 22:23:31 -04:00
zetaPRIME 0a14aec9e5 sample formats, threshold for saving as s16, slightly more "correct" playback output 2022-03-22 22:18:57 -04:00
zetaPRIME cbce51744c easy splice-between for single-input node types 2022-03-22 17:49:01 -04:00
zetaPRIME 4cbde894c4 ringmod 2022-03-22 16:28:28 -04:00
zetaPRIME 4330ab847f fix fonts breaking in pattern editor 2022-03-22 05:15:02 -04:00
zetaPRIME e962cda5cc adjust menu accelerators 2022-03-21 23:15:32 -04:00
zetaPRIME e87471c39a applying settings stops preview to let new sample rates take 2022-03-21 23:10:34 -04:00
zetaPRIME f430ebab00 fix missing opening notes on non-first play per session (!) 2022-03-21 22:27:27 -04:00
zetaPRIME d95e6ce1d5 fix sample rate mismatch 2022-03-21 19:48:43 -04:00
zetaPRIME 41a591a957 audio settings kind of works now 2022-03-21 19:40:08 -04:00
zetaPRIME 1ebc8f04c5 audio settings backend 2022-03-21 18:28:01 -04:00
zetaPRIME c6e22d3521 slight tweaks to knob wheel flow 2022-03-21 15:54:32 -04:00
zetaPRIME cf81f91a0b bpm relativity for auto pan (and consistency in delay) 2022-03-21 00:32:02 -04:00
zetaPRIME 5d9b39f84b auto pan effect~ 2022-03-20 19:27:25 -04:00
zetaPRIME 7abccd7a38 these should probably be QStringLiterals 2022-03-20 18:58:48 -04:00
zetaPRIME 5dca500640 generic percentage knob preset 2022-03-20 18:46:24 -04:00
zetaPRIME 3caf08c3db clazy cleanup 2022-03-20 15:34:32 -04:00
zetaPRIME df520dfd2d dc offset compensation for 2x03 2022-03-20 07:35:23 -04:00
zetaPRIME 33916acc65 fix QuickLevel artifacting 2022-03-20 06:29:56 -04:00
zetaPRIME dd6c9009a7 new syntax sugar for plugin registration 2022-03-20 06:14:42 -04:00
zetaPRIME e4475cbce0 ...why were those `gadget:` 2022-03-20 05:16:51 -04:00
zetaPRIME 987d22d332 correct svf cutoff var names 2022-03-20 04:56:02 -04:00
zetaPRIME 79b1d9239f KnobGadget: filter cutoff preset, boilerplate reduction macro :D 2022-03-20 04:49:19 -04:00
zetaPRIME 0008997fe7 move config default vars into a single file for cleanliness 2022-03-20 03:34:26 -04:00
zetaPRIME dc042ae1ac cleaning this file up a bit while I'm in here 2022-03-20 03:20:06 -04:00
zetaPRIME 3eb25120ce actually never mind, fixed the underlying bugs (and made QuickLevel much more efficient in the process) 2022-03-20 03:09:33 -04:00
zetaPRIME 7f471bf5ce quick fix for incomplete drawing wonk 2022-03-20 02:26:06 -04:00
zetaPRIME b1c8377db5 force at least 1hz cutoff in svfilter so it doesn't bias 2022-03-20 01:43:42 -04:00
zetaPRIME 3137c5e699 hard_cast to fix type punning warnings 2022-03-19 21:19:03 -04:00
zetaPRIME 4db24fb188 make things a bit more "correct" I guess 2022-03-19 18:20:05 -04:00
zetaPRIME 58826188c6 clean up flailing 2022-03-19 16:43:31 -04:00
zetaPRIME 6fcc6db4e6 nonfunctional flailing 2022-03-19 16:25:31 -04:00
zetaPRIME e78cebfd77 substep for delay time (faster) 2022-03-19 03:45:23 -04:00
zetaPRIME 3af34095d2 stereo ping pong for delay 2022-03-19 03:21:04 -04:00
zetaPRIME 3af05f2cc3 d'oh 2022-03-19 02:09:29 -04:00
zetaPRIME 1a6f85e0fa trying something 2022-03-19 02:09:01 -04:00
zetaPRIME a3be1bded0 vertical knob option exists 2022-03-19 00:58:45 -04:00
zetaPRIME 0f3da1f094 beginning of settings dialog 2022-03-18 22:59:50 -04:00
zetaPRIME b4b9918c7d wheel accumulator (smooth wheel support) 2022-03-18 21:41:59 -04:00
zetaPRIME 7b566929a2 initial scrollwheel functionality, simplified position tracking slightly 2022-03-18 21:29:21 -04:00
zetaPRIME fd81de5040 most of KnobGadget rework 2022-03-18 21:05:56 -04:00
zetaPRIME 930992025d trying horizontal drag for knobs 2022-03-18 02:45:46 -04:00
zetaPRIME bf793d20fd AudioFrame.clamp, SVFilter.normalize 2022-03-17 19:23:21 -04:00
zetaPRIME 4822ef1bb6 gain/balance presets in KnobGadget 2022-03-17 17:45:03 -04:00
zetaPRIME 401ee05aa4 clean up warnings in mixboard 2022-03-17 16:44:32 -04:00
zetaPRIME cf75f22403 stock text functions for KnobGadgets (the percentening) 2022-03-17 07:26:08 -04:00
zetaPRIME e5c664a5ad svf scaledResonance 2022-03-17 06:48:43 -04:00
zetaPRIME b8f9664f7c more correct SVF 2022-03-16 23:04:06 -04:00
zetaPRIME d360074e81 reorder node categories 2022-03-16 04:43:20 -04:00
zetaPRIME 7e1e80eaea shut up -Wclass-memaccess 2022-03-16 04:36:50 -04:00
zetaPRIME e39f8603f6 nuke some warnings 2022-03-16 04:32:35 -04:00
zetaPRIME a2a643a80c list command ports before audio for less annoying instrument placement 2022-03-16 04:27:06 -04:00
zetaPRIME a9cbd629dc give patchboard keyboard focus back on project load if it had it before 2022-03-16 04:13:18 -04:00
zetaPRIME 19a82764cc use std:: math funcs in svf (mostly code style) 2022-03-16 04:03:47 -04:00
zetaPRIME d960da0775 compact delay UI a bit 2022-03-16 03:49:32 -04:00
zetaPRIME f7212a222d wet-only output for delay 2022-03-16 03:42:13 -04:00
zetaPRIME 31b13ad3f1 bit of sugar 2022-03-16 03:02:24 -04:00
zetaPRIME 6fd5276aaf those don't need the constructor (only std::atomic) 2022-03-16 02:55:33 -04:00
zetaPRIME 0551c4f7e7 this at least sounds more accurate? 2022-03-16 02:20:35 -04:00
zetaPRIME 18557d933b rework svf effect using nodelib object 2022-03-16 00:34:08 -04:00
zetaPRIME 2d077d90c0 whoops 2022-03-15 23:41:28 -04:00
zetaPRIME 6b126db234 SVF library element 2022-03-15 22:22:23 -04:00
zetaPRIME e1e4089b88 just hit svf with astyle 2022-03-15 20:32:45 -04:00
zetaPRIME 9455834b2a ,XX support for global tempo 2022-03-15 19:54:57 -04:00
zetaPRIME c168ed95d3 disable UI and show a floater while rendering 2022-03-15 05:43:04 -04:00
zetaPRIME 7858449637 handle rendering from audio engine thread (UI can update) 2022-03-15 03:56:32 -04:00
zetaPRIME ff4ffaac61 commas break things in filters; slightly better behaviour for export dialog 2022-03-15 03:18:20 -04:00
zetaPRIME 9ed8d6039d flac export 2022-03-15 00:59:05 -04:00
zetaPRIME 34b4721f69 that's not happening any time soon 2022-03-14 16:58:05 -04:00
zetaPRIME 9590462891 we have that now 2022-03-14 10:39:19 -04:00
zetaPRIME 394b24223f user default template (jankish for now but whatever) 2022-03-13 23:14:34 -04:00
zetaPRIME 7fa610f4af oh right. duplicate protection 2022-03-13 23:07:24 -04:00
zetaPRIME f9f8391bba match sequencer font size to pattern editor 2022-03-13 22:59:21 -04:00
zetaPRIME 30b9db6a1a we support file lists now, so adjust desktop file to fit 2022-03-13 22:54:19 -04:00
zetaPRIME 53f2f27285 that should be a literal 2022-03-13 22:53:47 -04:00
zetaPRIME 06230dde55 rewrite signaling to CBOR; support multiple file signaling 2022-03-13 22:52:55 -04:00
zetaPRIME 22f5b0502d support multiple file opening on launch 2022-03-13 22:32:12 -04:00
zetaPRIME 8db7adfa74 additional robustness with ipc 2022-03-13 21:05:34 -04:00
zetaPRIME 71ec8dba73 instance per user 2022-03-13 20:49:12 -04:00
zetaPRIME 63c09b46aa mime entry and file association 2022-03-13 20:36:41 -04:00
zetaPRIME 3b5bdd2e07 IPC socket, single instance, open in existing instance 2022-03-13 20:26:00 -04:00
zetaPRIME ba6683cd2d open project from command line 2022-03-13 19:04:27 -04:00
zetaPRIME 59f45ab382 focus existing window instead of opening duplicate project instance 2022-03-13 18:38:10 -04:00
zetaPRIME 4782eedf9c projectWindow 2022-03-13 18:00:02 -04:00
zetaPRIME 70a6edf824 specify file on opening error 2022-03-13 17:53:48 -04:00
zetaPRIME 638c7e5c12 groundwork for opening window with project 2022-03-13 17:51:06 -04:00
zetaPRIME c41ffbfcce quit action 2022-03-13 16:34:07 -04:00
zetaPRIME 46a73bef74 keep list of open windows 2022-03-13 16:18:35 -04:00
zetaPRIME c027c422fd 16 inputs in default project 2022-03-13 07:33:23 -04:00
zetaPRIME a48e9d4583 refine save prompt 2022-03-13 07:28:29 -04:00
zetaPRIME bac05e3f68 save prompt 2022-03-13 07:24:28 -04:00
zetaPRIME 10bcacf8c1 recent files tracking and menu items! 2022-03-13 06:47:17 -04:00
zetaPRIME d3521416f2 transpose gadget revamp 2022-03-10 21:09:40 -05:00
zetaPRIME 8a7cb67bf3 add built-in default template 2022-03-10 19:43:38 -05:00
zetaPRIME c069f5b9f3 *why* did I think I wanted to separately allocate there?? 2022-03-10 19:27:29 -05:00
zetaPRIME 93c5fc0611 loadProject asTemplate 2022-03-10 19:26:43 -05:00
zetaPRIME 98f157fb01 <_< 2022-03-09 19:06:18 -05:00
zetaPRIME e36f2c110c complaining about these not being refs. does it help? who knows! 2022-03-09 19:02:20 -05:00
zetaPRIME d28e1837d4 sure, context object 2022-03-09 19:01:38 -05:00
zetaPRIME 12ba77fc96 you can just start it at full prio 2022-03-09 00:59:52 -05:00
zetaPRIME 4cf9a61a56 idk 2022-03-08 18:55:18 -05:00
zetaPRIME d8d7fac590 slight opt for transpose (skip processing if offset=0) 2022-03-07 20:07:16 -05:00
zetaPRIME 149ab65c08 font rendering changed?? fix that oddness 2022-03-07 19:39:29 -05:00
zetaPRIME affb86d76a add gain to beatpad, layoutgadget panel flag, nodeuiscene improvements 2021-11-12 22:26:13 -05:00
zetaPRIME d0db5a6b4d missed one 2021-11-12 21:46:51 -05:00
zetaPRIME 6ee9a0db6b NodeObject::drawPanel 2021-11-12 04:29:23 -05:00
zetaPRIME 80c90451f3 quicklevel: thread safety (oops), don't run if no UI instance 2021-11-12 03:42:12 -05:00
zetaPRIME d4a12647d2 capital x looks better here 2021-11-11 15:04:31 -05:00
zetaPRIME 25408ba776 quicklevel polish 2021-11-11 14:26:48 -05:00
zetaPRIME b86452b1af level meter (need to make it less flashy though) 2021-11-11 07:18:40 -05:00
zetaPRIME ec94dce150 file dialog improvements (bigger starting size) 2021-11-11 04:44:28 -05:00
zetaPRIME eb40b74234 fix accidental level wrap; compensate for amplitude loss 2021-11-11 02:09:52 -05:00
zetaPRIME 651daf5e4a capaxitor doesn't need the 97% filter 2021-11-11 01:53:09 -05:00
zetaPRIME 187b51d524 resampling early-out; capaxitor note kill on end; lut improvements 2021-11-11 01:51:42 -05:00
zetaPRIME d4aa622fa6 sample base note support 2021-11-11 00:47:37 -05:00
zetaPRIME 14af2ffeb7 16-level LUT, now sounds better than modplug at high frequencies 2021-11-10 23:26:54 -05:00
zetaPRIME 60df49db69 innovative™️ dual-LUT system. because it sounds better for some reason. 2021-11-10 22:46:52 -05:00
zetaPRIME a78d41b134 old one was less artifacty; convert beatpad to new sample func 2021-11-10 18:38:33 -05:00
zetaPRIME 4c6c135617 rewrite lut generation to be a bit more correct; fix looping 2021-11-10 16:57:00 -05:00
zetaPRIME 72b5eb3b53 fix sample info overwrite; show loop points on preview 2021-11-10 02:42:13 -05:00
zetaPRIME 39f5966c0f sample looping! 2021-11-10 02:13:51 -05:00
zetaPRIME b57974e066 adsr; fix dumb aliasing 2021-11-09 20:36:18 -05:00
zetaPRIME 26a2bf4e82 beginning of Capaxitor, simple lead sampler 2021-11-09 19:56:01 -05:00
zetaPRIME 82bb4e48e1 switch cyl_bessel_i defines to using statements 2021-11-09 16:27:27 -05:00
zetaPRIME 84a2f2441d fix build 2020-07-31 17:16:48 -04:00
zetaPRIME d101975d5d clean up deprecated flags 2020-02-20 04:09:49 -05:00
zetaPRIME ac2e81ab10 Merge branch 'master' of https://git.foxiepa.ws/foxiepaws/xybrid 2020-02-20 03:58:36 -05:00
zetaPRIME 1b8eeffbcf default pan command for InstrumentCore 2019-07-23 07:52:56 -04:00
109 changed files with 3795 additions and 743 deletions

61
notes
View File

@ -31,31 +31,57 @@ parameters {
}
TODO {
immediate frontburner {
distortion effect
single-selection sampler
global (default) pan (PXX) for InstrumentCore
add ,XX support to global tempo
settings dialog {
about-license info
}
> add common oscillators to a nodelib header
revert-to-saved menu action
automation node {
listens for one specific param
supports tweening
value bounds, scaling exponent
passthrough command port with option to consume marked param
how to UI?
I guess some sort of text box/spinner to enter bounds
dial for exponent
focusable control to set param
}
editing song info should probably be an UndoStack action
editing song *tempo* ABSOLUTELY should
maybe retool rendering to feed f32 (or even f64) to ffmpeg
figure out what to actually do with directory config
buffer helper akin to what quicklevel does {
keeps a buffer length, running averages, etc.
can lerp across tick for speed
useful for level reading, waveform output, compression/sidechaining etc.
}
solo gadget {
interprets incoming commands as monophonic with portamento
probably not super useful for tracked things but good for playing live
}
- actual config file loading/saving
color scheme load/save
- indexer abstraction for audioports (assign/add std::pair<float, float>)
maybe a similar abstraction for processing notes to what commandreader does
maybe interpolate between resampler LUT levels
bugs to fix {
playback after stopping immediately after a note in the first pattern played sometimes skips that note
things can apparently be hooked up cyclically, which completely breaks the queue
pattern switching is slow when changing (especially increasing) number of rows; set fixed page size to avoid reallocation?
}
misc features needed before proper release {
ABOUT BOX WITH INCLIB LICENSE NOTICES
expand/compact pattern 2x/3x, keeping fold interval
at *least* js plugin support, with lua+lv2 highly preferable
@ -69,22 +95,21 @@ TODO {
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
make the save routine displace the old file and write a new one
open file from command line argument
^ multi-document, single-instance (QLocalServer etc.)
}
gadgets and bundled things {
(the simple things:)
- gain and panning gadget
- note transpose
volume meter
- volume meter
"wrap clipper" (gain up, then wrap around +-1.0, then gain down)
Polyplexer (splits a single command input into several monophonic outputs and keeps track of individual notes between them)
probably three sorts of sampler (quick drum sequencer, quick single-sample "wavetable", then the full-on tracker sampler later on)
- quick drum sequencer (BeatPad)
- quick single-sample "wavetable" (Capaxitor)
full-fat tracker sampler at some point
}
}

5
xybrid/audio/audio.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
namespace Xybrid::Audio {
typedef double bufferType;
}

View File

@ -5,6 +5,9 @@ using namespace Xybrid::Data;
#include "data/graph.h"
#include "data/porttypes.h"
#include "config/audioconfig.h"
using namespace Xybrid::Config;
#include "mainwindow.h"
#include "uisocket.h"
@ -13,11 +16,16 @@ using namespace Xybrid::Data;
#include <algorithm>
#include <cmath>
#include <iostream>
#include <QDebug>
#include <QThread>
#include <QMutex>
#include <QTimer>
#include <QProcess>
#include <QFileInfo>
#include <QCoreApplication>
#include <QElapsedTimer>
#ifdef Q_OS_MAC
#define FFMPEG "/usr/local/bin/ffmpeg"
@ -41,8 +49,7 @@ void AudioEngine::init() {
// ...
// and off to the races
thread->start();
thread->setPriority(QThread::TimeCriticalPriority);
thread->start(QThread::TimeCriticalPriority);
QMetaObject::invokeMethod(audioEngine, &AudioEngine::postInit, Qt::QueuedConnection);
}
void AudioEngine::postInit() {
@ -87,7 +94,7 @@ void AudioEngine::initAudio(bool startNow) {
const QAudioDeviceInfo& deviceInfo = QAudioDeviceInfo::defaultOutputDevice();
QAudioFormat format;
format.setSampleRate(48000);
format.setSampleRate(sampleRate);
format.setChannelCount(2);
format.setSampleSize(16);
format.setCodec("audio/pcm");
@ -103,15 +110,17 @@ void AudioEngine::initAudio(bool startNow) {
output.reset(new QAudioOutput(deviceInfo, format));
output->setCategory("Xybrid");
output->setObjectName("Xybrid"); // if Qt ever implements naming the stream this way, WE'LL BE READY
output->setBufferSize(static_cast<int>(sampleRate*4*( 64.0 )/1000.0)); // 64ms seems to be a sweet spot now
//output->setBufferSize(static_cast<int>(sampleRate*4*( 64.0 )/1000.0)); // 64ms seems to be a sweet spot now
//
}
if (startNow) output->start();
if (startNow) startOutput();
}
void AudioEngine::deinitAudio() {
if (output) {
QTimer::singleShot(20, [this] { // delay to flush buffers with silence, else we get leftovers on next playback
QTimer::singleShot(20, this, [this] { // delay to flush buffers with silence, else we get leftovers on next playback
if (output && mode == Stopped) {
output->stop();
output.reset();
@ -120,11 +129,27 @@ void AudioEngine::deinitAudio() {
}
}
void AudioEngine::startOutput() {
if (!output) return;
//outBuf.resize(0);
output->setNotifyInterval(0); // don't need this here
output->setBufferSize(sampleRate*4*bufferMs/1000); // set canonical buffer size
output->start(this); // set output to take the active role
}
void AudioEngine::play(std::shared_ptr<Project> p, int fromPos) {
QMetaObject::invokeMethod(this, [this, p, fromPos] {
if (!p) return; // nope
project = p;
if (output) output->stop();
output.reset();
// load audio settings
sampleRate = AudioConfig::playbackSampleRate;
bufferMs = AudioConfig::playbackBufferMs;
// stop and reset, then init playback
queueValid = false;
queue.clear();
@ -143,7 +168,11 @@ void AudioEngine::play(std::shared_ptr<Project> p, int fromPos) {
tempo = project->tempo;
tickAcc = 0;
output->start(this);
// properly initialize note tracking to prevent
chTrack.clear(); // overwritten starting notes
chTrack.resize(findPattern()->channels.size());
startOutput();
mode = Playing;
emit this->playbackModeChanged();
@ -176,6 +205,11 @@ uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t
deinitAudio();
project = p;
// load audio settings
sampleRate = AudioConfig::previewSampleRate;
bufferMs = AudioConfig::previewBufferMs;
// reset state
queueValid = false;
queue.clear();
buf.clear();
@ -189,7 +223,7 @@ uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t
}
tempo = project->tempo;
output->start(this);
startOutput();
mode = Previewing;
emit this->playbackModeChanged();
}
@ -210,7 +244,7 @@ uint16_t AudioEngine::preview(std::shared_ptr<Project> p, int16_t port, int16_t
return nId;
}
void AudioEngine::render(std::shared_ptr<Project> p, QString filename) {
void AudioEngine::render(std::shared_ptr<Project> p, QString fileName) {
if (!p) return; // yeah, no
if (mode != Stopped) {
@ -219,48 +253,70 @@ void AudioEngine::render(std::shared_ptr<Project> p, QString filename) {
}
project = p;
queueValid = false;
queue.clear();
portLastNoteId.fill(0);
project->rootGraph->reset();
for (auto& b : buffer) {
b.clear();
b.reserve(static_cast<size_t>(sampleRate/4));
}
seqPos = -1;
curRow = -1;
curTick = -2;
tempo = project->tempo;
tickAcc = 0;
initAudio(); // we actually need the period size. whoops.
QProcess enc;
QStringList param;
param << "-y" << "-f" << "s16le" << "-ac" << "2" << "-ar" << QString::number(sampleRate) << "-i" << "pipe:";
if (!project->title.isEmpty()) param << "-metadata" << qs("title=%1").arg(project->title);
if (!project->artist.isEmpty()) param << "-metadata" << qs("artist=%1").arg(project->artist);
param << "-f" << "mp3" << "-codec:a" << "libmp3lame"<< "-q:a" << "0"; // specify mp3, vbr v0
param << filename;
enc.start(FFMPEG, param);
enc.waitForStarted();
std::vector<char> dat;
dat.resize(1024);
mode = Rendering;
while (mode == Rendering) {
enc.write(&dat[0], readData(&dat[0], 1024));
//enc.write(read(1024));
}
enc.closeWriteChannel();
enc.waitForFinished();
QMetaObject::invokeMethod(this, [this, fileName] {
// load sample rate
sampleRate = AudioConfig::renderSampleRate;
stop();
// reset state
queueValid = false;
queue.clear();
portLastNoteId.fill(0);
project->rootGraph->reset();
for (auto& b : buffer) {
b.clear();
b.reserve(static_cast<size_t>(sampleRate/4));
}
seqPos = -1;
curRow = -1;
curTick = -2;
tempo = project->tempo;
tickAcc = 0;
// properly initialize note tracking to prevent
chTrack.clear(); // overwritten starting notes
chTrack.resize(findPattern()->channels.size());
initAudio(); // we actually need the period size. whoops.
QFileInfo fi(fileName);
auto ext = fi.suffix().toLower();
QProcess enc;
QStringList param;
param << "-y" << "-f" << "s16le" << "-ac" << "2" << "-ar" << QString::number(sampleRate) << "-i" << "pipe:";
if (!project->title.isEmpty()) param << "-metadata" << qs("title=%1").arg(project->title);
if (!project->artist.isEmpty()) param << "-metadata" << qs("artist=%1").arg(project->artist);
// flac out is pretty simple, as it turns out
if (ext == "flac") param << "-c:a" << "flac" << "-compression_level" << "8";
// else specify mp3, vbr v0
else param << "-f" << "mp3" << "-codec:a" << "libmp3lame"<< "-q:a" << "0";
param << fileName;
enc.start(FFMPEG, param);
enc.waitForStarted();
std::vector<char> dat;
dat.resize(1024);
//QElapsedTimer timer;
//timer.start();
//mode = Rendering;
while (mode == Rendering) {
enc.write(&dat[0], readData(&dat[0], 1024));
}
//std::cout << "Render finished in " << static_cast<float>(timer.elapsed())/1000 << " seconds." << std::endl;
enc.closeWriteChannel();
enc.waitForFinished();
stop();
});
while (mode == Rendering && project == p) QCoreApplication::processEvents(); // hold modality but allow UI updates
//qDebug() << enc.readAllStandardOutput();
//qDebug() << enc.readAllStandardError();
@ -270,7 +326,7 @@ void AudioEngine::buildQueue() {
// keep track of what was there before
std::unordered_set<Node*> prev;
prev.reserve(queue.size() + 1);
for (auto n : queue) prev.insert(n.get());
for (auto& n : queue) prev.insert(n.get());
queue.clear();
// stuff
@ -285,11 +341,11 @@ void AudioEngine::buildQueue() {
while (!qCurrent->empty()) {
// ... this could be made more efficient with some redundancy checking, but whatever
for (auto n : *qCurrent) {
for (auto& n : *qCurrent) {
qf.push_front(n); // add to actual queue
for (auto p1 : n->inputs) { // data types...
for (auto p2 : p1.second) { // ports...
for (auto p3 : p2.second->connections) { // connected ports!
for (auto& p1 : n->inputs) { // data types...
for (auto& p2 : p1.second) { // ports...
for (auto& p3 : p2.second->connections) { // connected ports!
auto pc = p3.lock();
if (!pc) continue;
auto pcn = pc->owner.lock();
@ -311,7 +367,7 @@ void AudioEngine::buildQueue() {
// assemble final deduplicated queue
std::unordered_set<Node*> dd;
for (auto n : qf) {
for (auto& n : qf) {
if (dd.find(n.get()) == dd.end()) {
queue.push_back(n);
dd.insert(n.get());
@ -350,8 +406,8 @@ qint64 AudioEngine::readData(char *data, qint64 maxlen) {
// convert non-interleaved floating point into interleaved int16
int16_t* l = reinterpret_cast<int16_t*>(data);
int16_t* r = reinterpret_cast<int16_t*>(data+smp);
*l = static_cast<int16_t>(std::clamp(buffer[0][bufPos], -1.0f, 1.0f) * 32767);
*r = static_cast<int16_t>(std::clamp(buffer[1][bufPos], -1.0f, 1.0f) * 32767);
*l = static_cast<int16_t>(std::clamp(buffer[0][bufPos] * 32768.0, -32767.0, 32767.0));
*r = static_cast<int16_t>(std::clamp(buffer[1][bufPos] * 32768.0, -32767.0, 32767.0));
bufPos++;
data += stride;
@ -414,7 +470,7 @@ void AudioEngine::nextTick() {
processNodes();
if (auto p = std::static_pointer_cast<AudioPort>(project->rootGraph->port(Port::Output, Port::Audio, 0)); p) {
p->pull();
size_t bufs = ts * sizeof(float);
size_t bufs = ts * sizeof(bufferType);
memcpy(buffer[0].data(), p->bufL, bufs);
memcpy(buffer[1].data(), p->bufR, bufs);
}
@ -458,8 +514,20 @@ void AudioEngine::nextTick() {
// process global commands first
for (int c = 0; c < static_cast<int>(p->numChannels()); c++) {
if (auto& row = p->rowAt(c, curRow); row.port == -2 && row.params) {
for (auto p : *row.params) {
if (p[0] == 't' && p[1] > 0) tempo = p[1];
auto& p = *row.params;
auto n = p.size();
for (size_t i = 0; i < n; i++) {
if (p[i][0] == 't') { // tempo
auto ot = tempo;
tempo = p[i][1];
double m = 1.0;
while (i < n-1 && p[i+1][0] == ',') { // param notation: little-endian
m *= 256.0;
tempo += m*p[i+1][1];
i++;
}
if (tempo <= 1) tempo = ot; // reject tempo changes below 1bpm for safety
}
}
}
}
@ -509,7 +577,7 @@ void AudioEngine::nextTick() {
}
auto& cpm = project->rootGraph->inputs[Port::Command];
for (auto p_ : cpm) {
for (auto& p_ : cpm) {
auto* pt = static_cast<CommandPort*>(p_.second.get());
//if (pt->passthroughTo.lock()->connections.empty()) continue; // port isn't hooked up to anything
uint8_t idx = pt->index;
@ -586,7 +654,7 @@ void AudioEngine::nextTick() {
processNodes();
if (auto p = std::static_pointer_cast<AudioPort>(project->rootGraph->port(Port::Output, Port::Audio, 0)); p) {
p->pull();
size_t bufs = ts * sizeof(float);
size_t bufs = ts * sizeof(bufferType);
memcpy(buffer[0].data(), p->bufL, bufs);
memcpy(buffer[1].data(), p->bufR, bufs);
}

View File

@ -12,6 +12,8 @@
#include <QSemaphore>
#include <QWaitCondition>
#include "audio/audio.h"
class QThread;
namespace Xybrid::Data {
class Project;
@ -53,12 +55,16 @@ namespace Xybrid::Audio {
private:
QThread* thread;
std::unique_ptr<QAudioOutput> output;
QIODevice* outStream;
int sampleRate = 48000;
int bufferMs = 64;
std::vector<float> buffer[2];
std::vector<bufferType> buffer[2];
size_t bufPos = 0;
//std::vector<char> outBuf;
static const constexpr size_t tickBufSize = (1024*1024*5); // 5mb should be enough
// 32MiB really isn't much to take up for being a far higher ceiling than we should ever need
static const constexpr size_t tickBufSize = (1024*1024*32);
std::unique_ptr<size_t[]> tickBuf;
std::atomic<size_t*> tickBufPtr;
size_t* tickBufEnd;
@ -95,9 +101,11 @@ namespace Xybrid::Audio {
void postInit();
void initAudio(bool startNow = false);
void deinitAudio();
void startOutput();
Data::Pattern* findPattern(int = 0);
void nextTick();
void processNodes();
public:
static void init();
inline constexpr PlaybackMode playbackMode() const { return mode; }

View File

@ -0,0 +1,13 @@
#pragma once
namespace Xybrid::Config {
namespace AudioConfig {
extern int playbackSampleRate;
extern int playbackBufferMs;
extern int previewSampleRate;
extern int previewBufferMs;
extern int renderSampleRate;
}
}

View File

@ -1,4 +0,0 @@
#include "colorscheme.h"
using Xybrid::Config::ColorScheme;
ColorScheme Xybrid::Config::colorScheme;

View File

@ -25,6 +25,7 @@ namespace Xybrid::Config {
QColor waveformBg = {23, 23, 23};
QColor waveformBgHighlight = {31, 31, 47};
QColor waveformFgPrimary = {191, 163, 255};
QColor waveformLoopPoints = {255, 127, 127};
};
extern ColorScheme colorScheme;
}

View File

@ -0,0 +1,33 @@
#include "audioconfig.h"
#include "uiconfig.h"
#include "colorscheme.h"
#include "directories.h"
#include <QStandardPaths>
using namespace Xybrid::Config;
// Audio defaults
int AudioConfig::playbackSampleRate = 48000;
int AudioConfig::playbackBufferMs = 64;
int AudioConfig::previewSampleRate = 48000;
int AudioConfig::previewBufferMs = 64;
int AudioConfig::renderSampleRate = 48000;
// UIConfig defaults
bool UIConfig::verticalKnobs = false;
bool UIConfig::invertScrollWheel = false;
// instantiate color scheme
ColorScheme Xybrid::Config::colorScheme;
// Directories
const QString Directories::configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/config.dat");
const QString Directories::stateFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/state.dat");
QString Directories::projects = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/projects");
QString Directories::presets = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/nodes");
QString Directories::userDefaultTemplate = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/default.xyp");

View File

@ -1,9 +0,0 @@
#include "directories.h"
using namespace Xybrid::Config;
#include <QStandardPaths>
const QString Directories::configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation).append("/xybrid/config.json");
QString Directories::projects = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/projects");
QString Directories::presets = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation).append("/xybrid/nodes");

View File

@ -5,8 +5,10 @@
namespace Xybrid::Config {
namespace Directories {
const extern QString configFile;
const extern QString stateFile;
extern QString projects;
extern QString presets;
extern QString userDefaultTemplate;
}
}

View File

@ -16,7 +16,7 @@ using Xybrid::Gadgets::IOPort;
#include "util/strings.h"
namespace {
namespace { // clazy:excludeall=non-pod-global-static
typedef std::list<std::function<void()>> fqueue; // typedef so QtCreator's auto indent doesn't completely break :|
fqueue& regQueue() {
static fqueue q;
@ -27,15 +27,15 @@ namespace {
QHash<QString, std::shared_ptr<PluginInfo>> plugins;
QString priorityCategories[] {
"Gadget", "Instrument", "Sampler", "Effect"
"Gadget", "Effect", "Instrument", "Sampler"
};
}
bool PluginRegistry::enqueueRegistration(std::function<void ()> f) {
std::shared_ptr<PluginInfo> PluginRegistry::enqueueRegistration(std::function<void ()> f) {
auto& queue = regQueue();
queue.push_back(f);
if (initialized()) f();
return true;
return nullptr;
}
void PluginRegistry::init() {
@ -65,7 +65,7 @@ std::shared_ptr<Node> PluginRegistry::createInstance(const QString& id) {
void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::shared_ptr<Node>)> f, Graph* g) {
std::map<QString, std::map<QString, std::shared_ptr<PluginInfo>>> cm; // category map
cm.try_emplace(""); // force empty category
for (auto i : plugins) {
for (auto& i : qAsConst(plugins)) {
if (i->hidden) continue;
cm.try_emplace(i->category);
cm[i->category][i->displayName] = i;
@ -76,27 +76,27 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
auto* mio = m->addMenu("I/O Port");
//auto* mi = mio->addMenu("Input");
//auto* mo = mio->addMenu("Output");
Port::DataType d[] {Port::Audio, Port::Command};
Port::DataType d[] {Port::Command, Port::Audio};
for (auto dt : d) {
auto* mi = mio->addMenu(QString("&%1 In").arg(Util::enumName(dt)));
auto* mo = mio->addMenu(QString("&%1 Out").arg(Util::enumName(dt)));
auto* mi = mio->addMenu(qs("&%1 In").arg(Util::enumName(dt)));
auto* mo = mio->addMenu(qs("&%1 Out").arg(Util::enumName(dt)));
//mi->setStyleSheet("QMenu { menu-scrollable: 1; }");
//mi->setFixedHeight(256);
for (int ih = 0; ih < 16; ih++) {
QString n = QString::number(ih, 16).toUpper();
auto* mis = mi->addMenu(QString(u8"%10-%1F").arg(n));
auto* mos = mo->addMenu(QString(u8"%10-%1F").arg(n));
auto* mis = mi->addMenu(qs("%10-%1F").arg(n));
auto* mos = mo->addMenu(qs("%10-%1F").arg(n));
for (int il = 0; il < 16; il++) {
int i = ih*16+il;
QString nn = Util::hex(i);
mis->addAction(nn, [f, dt, i] {
mis->addAction(nn, m, [f, dt, i] {
auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
n->setPort(Port::Input, dt, static_cast<uint8_t>(i));
f(n);
})->setEnabled(!g->port(Port::Input, dt, static_cast<uint8_t>(i)));
mos->addAction(nn, [f, dt, i] {
mos->addAction(nn, m, [f, dt, i] {
auto n = std::static_pointer_cast<IOPort>(createInstance("ioport"));
n->setPort(Port::Output, dt, static_cast<uint8_t>(i));
f(n);
@ -115,7 +115,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
if (auto c = cm.find(pc); c != cm.end()) {
auto* ccm = m->addMenu(c->first);
for (auto& i : c->second) {
ccm->addAction(i.second->displayName, [f, pi = i.second] {
ccm->addAction(i.second->displayName, m, [f, pi = i.second] {
auto n = pi->createInstance();
n->plugin = pi;
n->init();
@ -131,7 +131,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
if (c.first.isEmpty() || c.second.empty()) continue;
auto* ccm = m->addMenu(c.first);
for (auto& i : c.second) {
ccm->addAction(i.second->displayName, [f, pi = i.second] {
ccm->addAction(i.second->displayName, m, [f, pi = i.second] {
auto n = pi->createInstance();
n->plugin = pi;
n->init();
@ -142,7 +142,7 @@ void PluginRegistry::populatePluginMenu(QMenu* m, std::function<void (std::share
m->addSeparator();
for (auto& i : cm[""]) m->addAction(i.second->displayName, [f, pi = i.second] {
for (auto& i : cm[""]) m->addAction(i.second->displayName, m, [f, pi = i.second] {
auto n = pi->createInstance();
n->plugin = pi;
n->init();

View File

@ -29,7 +29,7 @@ namespace Xybrid::Config {
};
namespace PluginRegistry {
bool enqueueRegistration(std::function<void()>);
std::shared_ptr<PluginInfo> enqueueRegistration(std::function<void()>);
void registerPlugin(std::shared_ptr<PluginInfo>);
void init();
@ -37,3 +37,10 @@ namespace Xybrid::Config {
void populatePluginMenu(QMenu*, std::function<void(std::shared_ptr<Data::Node>)>, Data::Graph* = nullptr);
}
}
#define RegisterPlugin(NAME, ...) \
namespace { std::shared_ptr<Xybrid::Config::PluginInfo> _regInfo_##NAME = Xybrid::Config::PluginRegistry::enqueueRegistration([] { \
auto i = std::make_shared<Xybrid::Config::PluginInfo>();\
i->createInstance = []{ return std::make_shared<NAME>(); };\
__VA_ARGS__ \
Xybrid::Config::PluginRegistry::registerPlugin(i); }); }

11
xybrid/config/uiconfig.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
namespace Xybrid::Config {
namespace UIConfig {
/// Determines if KnobGadgets turn with vertical mouse movement instead of horizontal.
extern bool verticalKnobs;
/// Controls if scroll wheel function is inverted for knobs, etc.
extern bool invertScrollWheel;
}
}

20
xybrid/config/uistate.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "uistate.h"
#include <algorithm>
#include "fileops.h"
using namespace Xybrid::Config;
std::list<QString> UIState::recentFiles;
void UIState::save() {
FileOps::saveUIState();
}
void UIState::addRecentFile(const QString &f) {
if (!recentFiles.empty() && recentFiles.front() == f) return; // if it's already the most recent file, skip
recentFiles.remove(f); // remove any existing instance from later in the list
recentFiles.push_front(f);
while (recentFiles.size() > MAX_RECENTS) recentFiles.pop_back(); // trim to max size
save(); // and save changes
}

15
xybrid/config/uistate.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include <list>
#include <QString>
namespace Xybrid::Config {
namespace UIState {
const constexpr size_t MAX_RECENTS = 10;
extern std::list<QString> recentFiles;
void save();
void addRecentFile(const QString& f);
}
}

View File

@ -1,10 +1,12 @@
#pragma once
#include <algorithm>
namespace Xybrid::Data {
struct AudioFrame {
// stored as double here for operational precision
double l = 0;
double r = 0;
double l = 0.0;
double r = 0.0;
inline AudioFrame() = default;
inline AudioFrame(double v) : l(v), r(v) { }
@ -45,6 +47,11 @@ namespace Xybrid::Data {
r *= m;
}
inline AudioFrame flip() { return {r, l}; }
inline AudioFrame clamp(double m = 1.0) { return { std::clamp(l, -m, m), std::clamp(r, -m, m) }; }
static inline AudioFrame lerp(AudioFrame a, AudioFrame b, double r) { return b * r + a * (1.0 - r); }
static AudioFrame gainBalanceMult(double gain, double balance = 0.0);
inline AudioFrame gainBalance(double gain, double balance = 0.0) const { return *this*gainBalanceMult(gain, balance); }
};

View File

@ -15,24 +15,16 @@ using namespace Xybrid::Config;
#include <QMetaType>
#include <QMetaEnum>
#define qs QStringLiteral
namespace {
std::shared_ptr<PluginInfo> inf;
bool c = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "graph";
i->displayName = "Subgraph";
i->createInstance = []{ return std::make_shared<Graph>(); };
PluginRegistry::registerPlugin(i);
inf = i;
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Graph, {
i->id = "graph";
i->displayName = "Subgraph";
})
//std::string Graph::pluginName() const { return "Subgraph"; }
Graph::Graph() {
plugin = inf; // harder bind
plugin = _regInfo_Graph;//inf; // harder bind
}
// propagate
@ -48,7 +40,7 @@ void Graph::saveData(QCborMap& m) const {
QCborArray c;
int idx = 0;
for (auto ch : children) {
for (auto& ch : children) {
if (!ch->plugin) continue;
indices[ch.get()] = idx++;
c << ch->toCbor();
@ -62,14 +54,14 @@ void Graph::saveData(QCborMap& m) const {
// array { oIdx, dataType, pIdx, iIdx, dataType, pIdx }
QCborArray cn;
for (auto ch : children) {
for (auto& ch : children) {
if (!ch->plugin) continue; // already skipped over
int idx = indices[ch.get()];
for (auto dt : ch->outputs) {
for (auto op : dt.second) {
for (auto& dt : ch->outputs) {
for (auto& op : dt.second) {
auto o = op.second;
o->cleanConnections(); // let's just do some groundskeeping here
for (auto iw : o->connections) {
for (auto& iw : o->connections) {
auto i = iw.lock();
QCborArray c;
c << idx;
@ -123,7 +115,7 @@ void Graph::onParent(std::shared_ptr<Graph>) {
for (auto c : children) {
c->project = project;
// let this handle the recursion for us, since this is all this function does
if (c->plugin == inf) c->onParent(c->parent.lock());
if (c->plugin == _regInfo_Graph) c->onParent(c->parent.lock());
}
}

View File

@ -48,7 +48,7 @@ bool Port::connect(std::shared_ptr<Port> p) {
// actual processing is always done on the input port, since that's where any limits are
if (type == Output) return p->type == Input && p->connect(shared_from_this());
if (!canConnectTo(p->dataType())) return false; // can't hook up to an incompatible data type
for (auto c : connections) if (c.lock() == p) return true; // I guess report success if already connected?
for (auto& c : connections) if (c.lock() == p) return true; // I guess report success if already connected?
if (singleInput() && connections.size() > 0) return false; // reject multiple connections on single-input ports
if (auto o = owner.lock(), po = p->owner.lock(); !o || !po || po->dependsOn(o)) return false; // no dependency loops!
// actually hook up
@ -81,6 +81,7 @@ void Port::cleanConnections() {
std::shared_ptr<Port> Port::makePort(DataType dt) {
if (dt == Audio) return std::make_shared<AudioPort>();
if (dt == Command) return std::make_shared<CommandPort>();
if (dt == Parameter) return std::make_shared<ParameterPort>();
// fallback
return std::make_shared<Port>();
}
@ -117,7 +118,7 @@ QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
{ /* nodes */ } {
QCborArray nm;
int idx = 0;
for (auto n : v) {
for (auto& n : v) {
if (n->isVolatile()) continue; // skip things with volatile locality (i/o ports etc.)
indices[n.get()] = idx++;
nm << n->toCbor();
@ -128,21 +129,21 @@ QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
// exported samples
if (auto v = Sample::finishExport(); !v.empty()) {
QCborMap smp;
for (auto s : v) smp[QCborValue(s->uuid)] = s->toCbor();
for (auto& s : v) smp[QCborValue(s->uuid)] = s->toCbor();
m[qs("samples")] = smp;
}
{ /* connections */ } {
QCborArray cm;
for (auto n : v) {
for (auto& n : v) {
if (n->isVolatile()) continue; // already skipped
int idx = indices[n.get()];
for (auto dt : n->outputs) {
for (auto op : dt.second) {
for (auto& dt : n->outputs) {
for (auto& op : dt.second) {
auto o = op.second;
o->cleanConnections(); // let's just do some groundskeeping here
for (auto iw : o->connections) {
for (auto& iw : o->connections) {
auto i = iw.lock();
if (auto in = indices.find(i->owner.lock().get()); in != indices.end()) { // only connections within the collection
QCborArray c;
@ -165,7 +166,7 @@ QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
{ /* center */ } {
int count = 0;
QPoint center;
for (auto n : v) {
for (auto& n : v) {
if (n->isVolatile()) continue;
center += QPoint(n->x, n->y);
count++;
@ -226,7 +227,7 @@ std::vector<std::shared_ptr<Node>> Node::multiFromCbor(const QCborMap& m, std::s
if (!cp.isNull()) { // offset and such
QPoint off = cp - center;
for (auto n : v) {
for (auto& n : v) {
n->x += off.x();
n->y += off.y();
}
@ -304,7 +305,7 @@ void Node::collapsePorts(Port::Type t, Port::DataType dt) {
if (mdt == m.end()) return; // nothing there
auto& mm = mdt->second;
uint8_t maxIdx = 0;
for (auto p : mm) maxIdx = std::max(maxIdx, p.first);
for (auto& p : mm) maxIdx = std::max(maxIdx, p.first);
uint8_t firstUnused = 255;
for (uint8_t i = 0; i <= maxIdx; i++) {
if (auto pi = mm.find(i); pi != mm.end()) {
@ -346,7 +347,7 @@ bool Node::try_process(bool checkDependencies) {
if (checkDependencies) { // check if dependencies are done
auto checkInput = Util::yCombinator([tick_this](auto checkInput, std::shared_ptr<Port> p) -> bool {
for (auto c : p->connections) { // check each connection; if node valid...
for (auto& c : p->connections) { // check each connection; if node valid...
if (auto cp = c.lock(); cp) {
if (auto n = cp->owner.lock(); n) {
if (n->tick_last != tick_this) return false; // if node itself not yet processed, check failed
@ -362,7 +363,7 @@ bool Node::try_process(bool checkDependencies) {
return true;
});
for (auto t : inputs) for (auto p : t.second) if (!checkInput(p.second)) return false;
for (auto& t : inputs) for (auto& p : t.second) if (!checkInput(p.second)) return false;
/*for (auto& t : inputs) {
for (auto& p : t.second) {

View File

@ -71,6 +71,7 @@ namespace Xybrid::Data {
virtual DataType dataType() const { return static_cast<DataType>(-1); }
virtual bool singleInput() const { return false; }
virtual bool canConnectTo(DataType) const;
inline bool isConnected() const { return connections.size() > 0; }
/*virtual*/ bool connect(std::shared_ptr<Port>);
/*virtual*/ void disconnect(std::shared_ptr<Port>);
void cleanConnections();

View File

@ -14,7 +14,7 @@ void AudioPort::pull() {
size_t ts = audioEngine->curTickSize();
size = ts;
size_t s = sizeof(float) * ts;
size_t s = sizeof(bufferType) * ts;
if (type == Input) {
if (connections.size() == 1) {
@ -26,11 +26,11 @@ void AudioPort::pull() {
goto done;//return;
}
}
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
bufL = static_cast<bufferType*>(audioEngine->tickAlloc(s*2));
bufR = &bufL[ts]; // for some reason just adding the size wonks out
memset(bufL, 0, s*2); // clear buffers
for (auto c : connections) { // mix
for (auto& c : connections) { // mix
if (auto p = std::static_pointer_cast<AudioPort>(c.lock()); p && p->dataType() == Audio) {
p->pull();
for (size_t i = 0; i < ts; i++) {
@ -45,7 +45,7 @@ void AudioPort::pull() {
bufL = pt->bufL;
bufR = pt->bufR;
} else { // output without valid passthrough, just clear and prepare a blank buffer
bufL = static_cast<float*>(audioEngine->tickAlloc(s*2));
bufL = static_cast<bufferType*>(audioEngine->tickAlloc(s*2));
bufR = &bufL[ts];
memset(bufL, 0, s*2); // clear buffers
}
@ -60,13 +60,13 @@ void CommandPort::pull() {
lock.lock();
if (tickUpdatedOn == t) { lock.unlock(); return; } // someone else got here before us
dataSize = 0;
size = 0;
if (type == Input) {
for (auto c : connections) {
for (auto& c : connections) {
if (auto p = std::static_pointer_cast<CommandPort>(c.lock()); p && p->dataType() == Command) {
p->pull();
data = p->data; // just repoint to input's buffer
dataSize = p->dataSize;
size = p->size;
break;
}
}
@ -74,7 +74,7 @@ void CommandPort::pull() {
// valid passthrough
pt->pull();
data = pt->data; // again, just repoint
dataSize = pt->dataSize;
size = pt->size;
} // don't need an else case, size is already zero
tickUpdatedOn = t;
@ -83,7 +83,37 @@ void CommandPort::pull() {
void CommandPort::push(std::vector<uint8_t> v) {
tickUpdatedOn = audioEngine->curTickId();
dataSize = v.size();
data = static_cast<uint8_t*>(audioEngine->tickAlloc(dataSize));
memcpy(data, v.data(), dataSize);
size = v.size();
data = static_cast<uint8_t*>(audioEngine->tickAlloc(size));
memcpy(data, v.data(), size);
}
void ParameterPort::pull() {
auto t = audioEngine->curTickId();
if (tickUpdatedOn == t) return;
lock.lock();
if (tickUpdatedOn == t) { lock.unlock(); return; } // someone else got here before us
data = nullptr;
if (type == Input) {
if (isConnected()) {
if (auto p = std::static_pointer_cast<ParameterPort>(connections[0].lock()); p) {
p->pull();
data = p->data;
size = p->size;
}
}
} else if (auto pt = std::static_pointer_cast<ParameterPort>(passthroughTo.lock()); pt && pt->dataType() == Parameter) {
pt->pull();
data = pt->data;
size = pt->size;
}
if (!data) { // no buffer pulled from input or passthrough; create a new one
size = audioEngine->curTickSize();
data = static_cast<double*>(audioEngine->tickAlloc(size * sizeof(double)));
std::fill_n(data, size, std::numeric_limits<double>::quiet_NaN());
}
tickUpdatedOn = t;
lock.unlock();
}

View File

@ -2,6 +2,7 @@
#include "data/node.h"
#include "data/audioframe.h"
#include "audio/audio.h"
namespace Xybrid::Data {
class AudioPort : public Port {
@ -13,21 +14,21 @@ namespace Xybrid::Data {
FrameRef(AudioPort* port, size_t at) : port(port), at(at) { }
public:
FrameRef& operator=(AudioFrame f) {
port->bufL[at] = static_cast<float>(f.l);
port->bufR[at] = static_cast<float>(f.r);
port->bufL[at] = static_cast<Audio::bufferType>(f.l);
port->bufR[at] = static_cast<Audio::bufferType>(f.r);
return *this;
}
FrameRef& operator+=(AudioFrame f) {
port->bufL[at] += static_cast<float>(f.l);
port->bufR[at] += static_cast<float>(f.r);
port->bufL[at] += static_cast<Audio::bufferType>(f.l);
port->bufR[at] += static_cast<Audio::bufferType>(f.r);
return *this;
}
operator AudioFrame() const { return { port->bufL[at], port->bufR[at] }; }
AudioFrame operator*(AudioFrame o) const { return static_cast<AudioFrame>(*this) * o; }
};
float* bufL;
float* bufR;
Audio::bufferType* bufL;
Audio::bufferType* bufR;
size_t size;
AudioPort() = default;
@ -44,7 +45,7 @@ namespace Xybrid::Data {
class CommandPort : public Port {
public:
uint8_t* data;
size_t dataSize;
size_t size;
CommandPort() = default;
~CommandPort() override = default;
@ -57,4 +58,20 @@ namespace Xybrid::Data {
/// Push a data buffer
void push(std::vector<uint8_t>);
};
class ParameterPort : public Port {
public:
double* data;
size_t size;
ParameterPort() = default;
~ParameterPort() override = default;
double& operator[](size_t at) { return data[at]; }
double& at(size_t at) { return data[at]; }
Port::DataType dataType() const override { return Port::Parameter; }
void pull() override;
};
}

View File

@ -35,7 +35,7 @@ using namespace Xybrid::Data;
std::array<float, 2> Sample::plotBetween(size_t ch, size_t start, size_t end) const {
if (end < start) end = start;
if (ch >= 2 || start >= data[ch].size()) return {0, 0};
end = std::min(end, data[ch].size());
end = std::min(end, data[ch].size()-1);
float mx = -100;
float mn = 100;
@ -48,22 +48,48 @@ std::array<float, 2> Sample::plotBetween(size_t ch, size_t start, size_t end) co
return {mn, mx};
}
// threshold in MiB-stored-as-float for saving long samples as s16 instead
const constexpr double PCM_MiB_THRESHOLD = 5;
const constexpr int PCM_THRESHOLD = static_cast<int>(PCM_MiB_THRESHOLD * (1024*1024) / sizeof(float));
QCborMap Sample::toCbor() const {
QCborMap m;
m[qs("name")] = name;
m[qs("rate")] = sampleRate;
QString fmt = qs("f32");
if (numChannels() * length() > PCM_THRESHOLD) fmt = qs("s16");
m[qs("fmt")] = fmt;
{
QCborArray ch;
auto n = static_cast<size_t>(numChannels());
for (size_t i = 0; i < n; i++) {
ch[static_cast<qsizetype>(i)] = QByteArray(reinterpret_cast<const char*>(data[i].data()), static_cast<int>(data[i].size() * sizeof(data[i][0])));
if (fmt == qs("f32")) {
for (size_t i = 0; i < n; i++) {
ch[static_cast<qsizetype>(i)] = QByteArray(reinterpret_cast<const char*>(data[i].data()), static_cast<int>(data[i].size() * sizeof(data[i][0])));
}
} else if (fmt == qs("s16")) {
for (size_t i = 0; i < n; i++) {
auto sz = data[i].size();
QByteArray dat(static_cast<int>(sz * 2), static_cast<char>(0));
for (size_t j = 0; j < sz; j++) *reinterpret_cast<int16_t*>(dat.data() + j*2) = static_cast<int16_t>(std::clamp(static_cast<double>(data[i][j]) * 32768.0, -32767.0, 32767.0));
ch[static_cast<qsizetype>(i)] = dat;
}
}
m[qs("channels")] = ch;
}
if (loopStart >= 0) { // only store if there is a loop point
m[qs("loopStart")] = loopStart;
m[qs("loopEnd")] = loopEnd;
}
m[qs("note")] = baseNote;
m[qs("subNote")] = subNote;
return m;
}
@ -74,16 +100,37 @@ std::shared_ptr<Sample> Sample::fromCbor(const QCborMap& m, QUuid uuid) {
smp->sampleRate = static_cast<int>(m.value("rate").toInteger(48000));
auto fmt = m.value("fmt").toString(qs("f32"));
auto ch = m.value("channels").toArray();
auto s = static_cast<size_t>(ch.size());
for (size_t i = 0; i < s; i++) {
auto c = ch[static_cast<qint64>(i)].toByteArray();
auto bs = static_cast<size_t>(c.size());
smp->data[i].resize(bs / sizeof(*smp->data[i].begin()));
memcpy(smp->data[i].data(), c.constData(), bs);
if (fmt == qs("f32")) {
for (size_t i = 0; i < s; i++) {
auto c = ch[static_cast<qint64>(i)].toByteArray();
auto bs = static_cast<size_t>(c.size());
smp->data[i].resize(bs / sizeof(*smp->data[i].begin()));
memcpy(smp->data[i].data(), c.constData(), bs);
}
} else if (fmt == qs("s16")) {
for (size_t i = 0; i < s; i++) {
auto c = ch[static_cast<qint64>(i)].toByteArray();
auto bs = static_cast<size_t>(c.size());
auto sz = bs / 2;
smp->data[i].resize(sz);
for (size_t j = 0; j < sz; j++) {
smp->data[i][j] = static_cast<float>(static_cast<double>(*reinterpret_cast<int16_t*>(c.data()+j*2))/32768.0);
}
//memcpy(smp->data[i].data(), c.constData(), bs);
}
}
smp->loopStart = static_cast<int>(m.value("loopStart").toInteger(-1));
smp->loopEnd = static_cast<int>(m.value("loopEnd").toInteger(-1));
smp->baseNote = static_cast<int>(m.value("note").toInteger(60));
smp->subNote = m.value("subNote").toDouble(0.0);
return smp;
}
std::shared_ptr<Sample> Sample::fromCbor(const QCborValue& m, QUuid uuid) { return fromCbor(m.toMap(), uuid); }
@ -188,7 +235,7 @@ std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
qCritical() << (probe.errorString());
}
auto mystdout = probe.readAllStandardOutput();
auto mystderr = probe.readAllStandardError();
// auto mystderr = probe.readAllStandardError();
auto doc = QJsonDocument::fromJson(mystdout);
info = doc.object()["streams"].toArray().first().toObject();
}
@ -236,7 +283,7 @@ std::shared_ptr<Sample> Sample::fromFile(QString fileName) {
#endif
namespace {
namespace { // clazy:excludeall=non-pod-global-static
bool exporting = false;
std::unordered_map<Sample*, bool> exportMap;
}

View File

@ -26,6 +26,12 @@ namespace Xybrid::Data {
std::array<std::vector<float>, 2> data;
int loopStart = -1;
int loopEnd = -1;
int baseNote = 60;
double subNote = 0.0;
inline AudioFrame operator[] (size_t at) const {
if (data[1].empty()) return {data[0][at]};
return {data[0][at], data[1][at]};
@ -40,6 +46,8 @@ namespace Xybrid::Data {
inline int length() const { return static_cast<int>(data[0].size()); }
std::array<float, 2> plotBetween(size_t ch, size_t start, size_t end) const;
inline double getNote() const { return static_cast<double>(baseNote) + subNote; }
QCborMap toCbor() const;
static std::shared_ptr<Sample> fromCbor(const QCborMap&, QUuid);
static std::shared_ptr<Sample> fromCbor(const QCborValue&, QUuid);

157
xybrid/fileops-config.cpp Normal file
View File

@ -0,0 +1,157 @@
#include "fileops.h"
#include "uisocket.h"
#include "config/audioconfig.h"
#include "config/uistate.h"
#include "config/uiconfig.h"
#include <QDebug>
#include <QFile>
#include <QCborMap>
#include <QCborArray>
#include <QCborStreamReader>
#include <QCborStreamWriter>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QUndoStack>
#include <QFileDialog>
#define qs QStringLiteral
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
using Xybrid::Data::Graph;
using Xybrid::Data::Node;
namespace FileOps = Xybrid::FileOps;
using namespace Xybrid::Config;
namespace { // utilities
struct _MapSaver {
QCborMap& root;
QString name;
QCborMap map;
_MapSaver(QCborMap& root, const QString& name) : root(root), name(name) { }
~_MapSaver() {
root[name] = map;
}
inline auto operator[](const QString& s) { return map[s]; }
};
inline void load(QCborValueRef m, QString& v) { v = m.toString(v); }
inline void load(QCborValueRef m, bool& v) { v = m.toBool(v); }
inline void load(QCborValueRef m, int& v) { v = static_cast<int>(m.toInteger(v)); }
}
#define lsection(NAME) if (auto _sec = root[qs(#NAME)].toMap(); !_sec.isEmpty())
#define lvar(NS, NAME) load(_sec[qs(#NAME)], NS::NAME)
#define ssection(NAME) if (_MapSaver _sec(root, qs(#NAME)) ; true)
#define svar(NS, NAME) _sec[qs(#NAME)] = NS::NAME
void FileOps::loadConfig() {
QFile file(Config::Directories::configFile);
if (file.open({QFile::ReadOnly})) { // file exists! read in
QCborStreamReader read(&file);
auto root = QCborValue::fromCbor(read).toMap();
file.close();
lsection(directories) {
lvar(Directories, projects);
lvar(Directories, presets);
}
lsection(ui) {
lvar(UIConfig, verticalKnobs);
lvar(UIConfig, invertScrollWheel);
}
lsection(audio) {
lvar(AudioConfig, playbackSampleRate);
lvar(AudioConfig, playbackBufferMs);
lvar(AudioConfig, previewSampleRate);
lvar(AudioConfig, previewBufferMs);
lvar(AudioConfig, renderSampleRate);
}
}
// make sure directories exist
if (auto d = QDir(Directories::projects); !d.exists()) d.mkpath(".");
if (auto d = QDir(Directories::presets); !d.exists()) d.mkpath(".");
}
void FileOps::saveConfig() {
QFileInfo fi(Directories::configFile);
fi.dir().mkpath("."); // make sure directory exists
QFile file(fi.filePath());
if (!file.open({QFile::WriteOnly})) return;
QCborMap root;
ssection(directories) {
svar(Directories, projects);
svar(Directories, presets);
}
ssection(ui) {
svar(UIConfig, verticalKnobs);
svar(UIConfig, invertScrollWheel);
}
ssection(audio) {
svar(AudioConfig, playbackSampleRate);
svar(AudioConfig, playbackBufferMs);
svar(AudioConfig, previewSampleRate);
svar(AudioConfig, previewBufferMs);
svar(AudioConfig, renderSampleRate);
}
// write out
QCborStreamWriter w(&file);
root.toCborValue().toCbor(w);
file.close();
}
void FileOps::loadUIState() {
QFile file(Directories::stateFile);
if (file.open({QFile::ReadOnly})) { // file exists! read in
QCborStreamReader read(&file);
auto root = QCborValue::fromCbor(read).toMap();
file.close();
if (auto recent = root[qs("recent")].toArray(); !recent.isEmpty()) {
UIState::recentFiles.clear();
for (auto r : recent) UIState::recentFiles.push_back(r.toString());
}
}
}
void FileOps::saveUIState() {
QFileInfo fi(Directories::stateFile);
fi.dir().mkpath("."); // make sure directory exists
QFile file(fi.filePath());
if (!file.open({QFile::WriteOnly})) return;
QCborMap root;
{
QCborArray recent;
for (auto& r : UIState::recentFiles) recent.append(r);
root[qs("recent")] = recent;
}
// write out
QCborStreamWriter w(&file);
root.toCborValue().toCbor(w);
file.close();
}

View File

@ -35,26 +35,36 @@ namespace {
+ (static_cast<uint32_t>(minor)<<16)
+ (static_cast<uint32_t>(major)<<24);
}
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,1);
constexpr const uint32_t XYBRID_VERSION = packedVersion(0,0,0,2);
constexpr const QSize dlgSize(700, 500);
}
const QString FileOps::Filter::project = qs("Xybrid project (*.xyp);;All files (*)");
const QString FileOps::Filter::node = qs("Xybrid node (*.xyn);;All files (*)");
const QString FileOps::Filter::audioIn = qs("Audio files (*.mp3, *.ogg, *.flac, *.wav);;MPEG Layer 3 (*.mp3);;All files (*)");
const QString FileOps::Filter::audioOut = qs("MPEG Layer 3 (*.mp3)"); // only supported formats
const QString FileOps::Filter::audioIn = qs("Audio files (*.mp3 *.ogg *.flac *.wav);;MPEG Layer 3 (*.mp3);;All files (*)");
const QString FileOps::Filter::audioOut = qs("Audio files (*.mp3 *.flac);;MPEG Layer 3 (*.mp3);;FLAC (*.flac)"); // only supported formats
QString FileOps::showOpenDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter) {
return QFileDialog::getOpenFileName(parent, caption, directory, filter); // just a wrapper for now
QFileDialog dlg(parent, caption, directory, filter);
dlg.resize(dlgSize);
dlg.setFileMode(QFileDialog::ExistingFile);
dlg.setAcceptMode(QFileDialog::AcceptOpen);
if (!dlg.exec()) return QString(); // canceled
auto sf = dlg.selectedFiles().at(0);
return sf;
}
QString FileOps::showSaveAsDialog(QWidget* parent, const QString& caption, const QString& directory, const QString& filter, const QString& suffix) {
QFileDialog dlg(parent, caption, directory, filter);
dlg.resize(dlgSize);
dlg.setDefaultSuffix(suffix);
dlg.setFileMode(QFileDialog::AnyFile);
dlg.setAcceptMode(QFileDialog::AcceptSave);
if (!dlg.exec()) return QString(); // canceled
return dlg.selectedFiles()[0];
auto sf = dlg.selectedFiles().at(0);
return sf;
}
bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
@ -63,7 +73,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
if (fileName.isEmpty()) return false; // fail
QFile file(fileName);
if (!file.open(QFile::WriteOnly)) return false;
if (!file.open({QFile::WriteOnly})) return false;
// header
QCborArray root;
@ -90,7 +100,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
{ /* Patterns */ } {
QCborArray ptns;
for (auto p : project->patterns) {
for (auto& p : project->patterns) {
QCborMap pm;
pm[qs("name")] = p->name;
pm[qs("fold")] = p->fold;
@ -135,7 +145,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
{ /* Samples */ } {
QCborMap smp;
for (auto s : project->samples) smp[QCborValue(s->uuid)] = s->toCbor();
for (auto& s : qAsConst(project->samples)) smp[QCborValue(s->uuid)] = s->toCbor();
main[qs("samples")] = smp;
}
@ -158,7 +168,7 @@ bool FileOps::saveProject(std::shared_ptr<Project> project, QString fileName) {
return true;
}
std::shared_ptr<Project> FileOps::loadProject(QString fileName) {
std::shared_ptr<Project> FileOps::loadProject(QString fileName, bool asTemplate) {
QCborArray root;
{
QFile file(fileName);
@ -171,13 +181,12 @@ std::shared_ptr<Project> FileOps::loadProject(QString fileName) {
}
// header and sanity checks
if (root.at(0) != QString("xybrid:project")) return nullptr; // not a project
if (root.at(0) != qs("xybrid:project")) return nullptr; // not a project
if (auto v = root.at(1); !v.isInteger() || v.toInteger() > XYBRID_VERSION) return nullptr; // invalid version or too new
if (!root.at(2).isMap()) return nullptr; // so close, but... nope
// intentionally allocate project and control block separately
std::shared_ptr<Project> project(new Project());
project->fileName = fileName;
auto project = std::make_shared<Project>();
if (!asTemplate) project->fileName = fileName;
QCborMap main = root.at(2).toMap();
{ /* Project metadata */ } {
@ -259,9 +268,23 @@ std::shared_ptr<Project> FileOps::loadProject(QString fileName) {
return project;
}
std::shared_ptr<Project> FileOps::newProject(bool useTemplate) {
std::shared_ptr<Project> project;
if (useTemplate) {
project = loadProject(Config::Directories::userDefaultTemplate, true);
if (!project) project = loadProject(":/template/default.xyp", true);
}
if (!project) {
project = std::make_shared<Project>();
project->sequence.emplace_back(project->newPattern());
}
return project;
}
bool FileOps::saveNode(std::shared_ptr<Node> node, QString fileName) {
QFile file(fileName);
if (!file.open(QFile::WriteOnly)) return false;
if (!file.open({QFile::WriteOnly})) return false;
Sample::startExport();
@ -272,7 +295,7 @@ bool FileOps::saveNode(std::shared_ptr<Node> node, QString fileName) {
// and write in any exported samples
if (auto v = Sample::finishExport(); !v.empty()) {
QCborMap smp;
for (auto s : v) smp[QCborValue(s->uuid)] = s->toCbor();
for (auto& s : v) smp[QCborValue(s->uuid)] = s->toCbor();
root << smp;
}
@ -317,45 +340,4 @@ std::shared_ptr<Node> FileOps::loadNode(QString fileName, std::shared_ptr<Graph>
return Node::fromCbor(root.at(2), parent); // let Node handle the rest
}
void FileOps::loadConfig() {
QFile file(Config::Directories::configFile);
if (file.open(QFile::ReadOnly)) { // file exists! read in
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
auto root = doc.object();
file.close();
if (auto dirs = root["directories"].toObject(); !dirs.isEmpty()) {
if (auto s = dirs["projects"].toString(); !s.isNull()) Config::Directories::projects = s;
if (auto s = dirs["presets"].toString(); !s.isNull()) Config::Directories::presets = s;
}
}
// make sure directories exist
if (auto d = QDir(Config::Directories::projects); !d.exists()) d.mkpath(".");
if (auto d = QDir(Config::Directories::presets); !d.exists()) d.mkpath(".");
}
void FileOps::saveConfig() {
QFileInfo fi(Config::Directories::configFile);
fi.dir().mkpath("."); // make sure directory exists
QFile file(fi.filePath());
if (!file.open(QFile::WriteOnly)) return;
QJsonDocument doc;
QJsonObject root;
{
QJsonObject dirs;
dirs["projects"] = Config::Directories::projects;
dirs["presets"] = Config::Directories::presets;
root["directories"] = dirs;
}
doc.setObject(root);
file.write(doc.toJson(QJsonDocument::Indented));
file.close();
}

View File

@ -24,7 +24,8 @@ namespace Xybrid::FileOps {
QString showSaveAsDialog(QWidget* parent = nullptr, const QString& caption = QString(), const QString& directory = QString(), const QString& filter = QString(), const QString& suffix = QString());
bool saveProject(std::shared_ptr<Data::Project> project, QString fileName = QString());
std::shared_ptr<Data::Project> loadProject(QString fileName);
std::shared_ptr<Data::Project> loadProject(QString fileName, bool asTemplate = false);
std::shared_ptr<Data::Project> newProject(bool useTemplate = true);
bool saveNode(std::shared_ptr<Data::Node> node, QString fileName);
std::shared_ptr<Data::Node> loadNode(QString fileName, std::shared_ptr<Data::Graph> parent = nullptr);
@ -32,4 +33,7 @@ namespace Xybrid::FileOps {
void loadConfig();
void saveConfig();
void loadUIState();
void saveUIState();
}

View File

@ -4,15 +4,28 @@
#include "data/graph.h"
#include "fileops.h"
#include "util/mem.h"
#include <vector>
#include <QDebug>
#include <QFontDatabase>
#include <QApplication>
#include <QCommandLineParser>
#include <QLocalServer>
#include <QLocalSocket>
#include <QSurfaceFormat>
#include <QFontDatabase>
#include <QCborMap>
#include <QCborArray>
#include <QCborStreamReader>
#include <QCborStreamWriter>
#define qs QStringLiteral
int main(int argc, char *argv[]) {
qRegisterMetaType<Xybrid::Data::Port>();
// enable antialiasing on accelerated graphicsview
QSurfaceFormat fmt;
fmt.setSamples(10);
@ -20,20 +33,103 @@ int main(int argc, char *argv[]) {
QSurfaceFormat::setDefaultFormat(fmt);
QApplication a(argc, argv);
QCommandLineParser cl;
cl.addHelpOption();
cl.addVersionOption();
cl.addPositionalArgument("[project...]", QApplication::translate("main", "Project file(s) to open."));
cl.process(a);
auto args = cl.positionalArguments();
QString userName = qEnvironmentVariable("USER");
if (userName.isEmpty()) userName = qEnvironmentVariable("USERNAME");
QString socketName = qs("xybrid-ipc-%1").arg(userName);
QLocalSocket tryc;
tryc.connectToServer(socketName);
tryc.waitForConnected(1000); // wait for connection attempt (can't hang on local)
if (tryc.isOpen()) { // if server already exists, give it the signal and exit
QCborArray root;
root << "open";
QCborArray lst;
for (auto& fn : args) {
QFileInfo fi(fn);
if (!fi.exists()) continue;
lst << fi.absoluteFilePath();
}
root << lst;
QCborStreamWriter csw(&tryc);
root.toCborValue().toCbor(csw);
tryc.waitForBytesWritten();
tryc.close();
return 0;
}
QLocalServer srv;
srv.setSocketOptions(QLocalServer::UserAccessOption);
srv.removeServer(socketName); // if it exists and we're here, previous instance probably crashed or was killed
srv.listen(socketName);
QObject::connect(&srv, &QLocalServer::newConnection, &srv, [&]() {
auto s = srv.nextPendingConnection();
s->waitForDisconnected();
QCborStreamReader csr(s);
auto root = QCborValue::fromCbor(csr).toArray();
s->deleteLater();
auto cmd = root.at(0).toString();
if (cmd == "open") {
auto lst = root.at(1).toArray();
if (lst.isEmpty()) {
auto w = new Xybrid::MainWindow(nullptr);
w->show();
}
for (auto e : lst) {
QFileInfo fi(e.toString());
if (!fi.exists()) continue;
auto fileName = fi.absoluteFilePath();
if (auto w = Xybrid::MainWindow::projectWindow(fileName); w) w->tryFocus();
else {
w = new Xybrid::MainWindow(nullptr, fileName);
if (w->getProject()) w->show();
else (w->deleteLater());
}
}
}
});
// make sure bundled fonts are loaded
QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf");
QFontDatabase::addApplicationFont(":/fonts/Arcon-Rounded-Regular.otf");
Xybrid::FileOps::loadConfig();
Xybrid::FileOps::loadUIState();
Xybrid::Config::PluginRegistry::init();
Xybrid::Audio::AudioEngine::init();
auto* w = new Xybrid::MainWindow();
w->show();
Xybrid::Util::reserveInitialPool(); // reserve arena pool ahead of time
bool opn = false;
for (auto& fn : args) {
QFileInfo fi(fn);
if (!fi.exists()) continue;
auto fileName = fi.absoluteFilePath();
auto w = new Xybrid::MainWindow(nullptr, fileName);
if (w->getProject()) { w->show(); opn = true; }
else (w->deleteLater());
}
if (!opn) { // always show one window on launch
auto w = new Xybrid::MainWindow(nullptr);
w->show();
}
// hook up exit event
QObject::connect(&a, &QCoreApplication::aboutToQuit, [] {

View File

@ -1,5 +1,6 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "settingsdialog.h"
using Xybrid::MainWindow;
#include <QDebug>
@ -15,7 +16,6 @@ using Xybrid::MainWindow;
#include <QUndoStack>
#include <QTimer>
#include <QOpenGLWidget>
#include <QGLWidget>
#include <QScroller>
#include <QGraphicsTextItem>
@ -41,6 +41,7 @@ using Xybrid::MainWindow;
#include "editing/projectcommands.h"
#include "editing/patterncommands.h"
#include "config/uistate.h"
#include "config/pluginregistry.h"
#include "audio/audioengine.h"
@ -69,14 +70,17 @@ namespace {
//
}
MainWindow::MainWindow(QWidget *parent) :
std::unordered_set<MainWindow*> MainWindow::openWindows;
MainWindow::MainWindow(QWidget *parent, const QString& fileName) :
QMainWindow(parent),
ui(new Ui::MainWindow) {
socket = new UISocket(); // create this first
ui->setupUi(this);
// remove tab containing system widgets
// remove tabs containing system widgets
ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_));
ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_2));
undoStack = new QUndoStack(this);
//undoStack->setUndoLimit(256);
@ -84,13 +88,14 @@ MainWindow::MainWindow(QWidget *parent) :
updateTitle();
});
auto efa = ui->menuEdit->actions().at(0);
auto* undoAction = undoStack->createUndoAction(this, tr("&Undo"));
undoAction->setShortcuts(QKeySequence::Undo);
ui->menuEdit->addAction(undoAction);
ui->menuEdit->insertAction(efa, undoAction);
auto* redoAction = undoStack->createRedoAction(this, tr("&Redo"));
redoAction->setShortcuts(QKeySequence::Redo);
ui->menuEdit->addAction(redoAction);
ui->menuEdit->insertAction(efa, redoAction);
// prevent right pane of pattern view from being collapsed
ui->patternViewSplitter->setCollapsible(1, false);
@ -124,6 +129,55 @@ MainWindow::MainWindow(QWidget *parent) :
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
}
{ /* Set up floater container */ } {
auto fc = ui->floaterContainer;
fc->setParent(this);
fc->setAttribute(Qt::WA_TransparentForMouseEvents); // click through
fc->setFocusPolicy(Qt::NoFocus); // can't be focused
fc->move(0, 0);
fc->setFixedSize(this->size());
setFloater();
}
{ /* Set up recent file entries */ } {
auto fm = ui->menuFile;
auto bfr = ui->actionNew_Window;
for (size_t i = 0; i < UIState::MAX_RECENTS; i++) {
auto ac = new QAction(fm);
ac->setVisible(false);
fm->insertAction(bfr, ac);
recentFileActions.push_back(ac);
QObject::connect(ac, &QAction::triggered, ac, [this, i]() { openRecentProject(i); });
}
fm->insertSeparator(bfr);
// update list every time we show this menu
QObject::connect(fm, &QMenu::aboutToShow, fm, [this]() {
auto ri = UIState::recentFiles.begin();
auto sz = UIState::recentFiles.size();
for (size_t i = 0; i < UIState::MAX_RECENTS; i++) {
auto ac = recentFileActions[i];
if (i<sz) {
QFileInfo fi(*ri);
QString ix = i == 9 ? qs("1&0") : qs("&%1").arg(i+1);
ac->setText(qs("%1 %2").arg(ix, fi.fileName()));
ac->setVisible(true);
ri++;
} else {
ac->setVisible(false);
ac->setText(QString());
}
}
});
}
{ /* Set up pattern list */ } {
// model
ui->patternList->setModel(new PatternListModel(ui->patternList, this));
@ -319,12 +373,12 @@ MainWindow::MainWindow(QWidget *parent) :
auto* vp = new QOpenGLWidget();
view->setViewport(vp); // enable hardware acceleration
}
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
// Under OSX these cause Xybrid to crash.
#ifndef __APPLE__
#ifndef __APPLE__
glEnable(GL_MULTISAMPLE);
glEnable(GL_LINE_SMOOTH);
#endif
#endif
//QGL::FormatOption::Rgba
@ -352,7 +406,7 @@ MainWindow::MainWindow(QWidget *parent) :
view->setDragMode(QGraphicsView::RubberBandDrag);
}
} else if (e->type() == QEvent::MouseButtonRelease) { // disable drag after end
QTimer::singleShot(1, [view] {
QTimer::singleShot(1, view, [view] {
view->setDragMode(QGraphicsView::NoDrag);
});
}
@ -371,8 +425,52 @@ MainWindow::MainWindow(QWidget *parent) :
connect(ui->sampleList->selectionModel(), &QItemSelectionModel::currentChanged, this, [this, mdl](const QModelIndex& ind, const QModelIndex& old [[maybe_unused]]) {
selectSampleForEditing(mdl->itemAt(ind));
});
// edit pane
connect(ui->groupSampleLoop, &QGroupBox::toggled, this, [this](bool on) {
if (editingSample) {
if (on) {
editingSample->loopStart = ui->spinSampleLoopStart->value();
editingSample->loopEnd = ui->spinSampleLoopEnd->value();
} else {
editingSample->loopStart = -1;
editingSample->loopEnd = -1;
}
ui->waveformPreview->update();
}
});
connect(ui->spinSampleLoopStart, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
if (editingSample && ui->groupSampleLoop->isChecked()) {
editingSample->loopStart = v;
ui->waveformPreview->update();
}
});
connect(ui->spinSampleLoopEnd, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
if (editingSample && ui->groupSampleLoop->isChecked()) {
editingSample->loopEnd = v;
ui->waveformPreview->update();
}
});
connect(ui->spinSampleNote, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
auto sp = ui->spinSampleNote;
sp->setSuffix(")");
sp->setPrefix(qs("%1 (").arg(Util::noteName(static_cast<int16_t>(v))));
if (editingSample) editingSample->baseNote = v;
});
connect(ui->spinSampleNoteSub, qOverload<double>(&QDoubleSpinBox::valueChanged), this, [this](double v) {
auto sp = ui->spinSampleNoteSub;
sp->setPrefix(v >= 0.0 ? qs("+") : QString());
if (editingSample) editingSample->subNote = v;
});
emit ui->spinSampleNoteSub->valueChanged(0.0); // force refresh
}
// force fonts to display properly
updateFont();
// Set up signaling from project to UI
socket->setParent(this);
socket->window = this;
@ -421,8 +519,19 @@ MainWindow::MainWindow(QWidget *parent) :
selectSampleForEditing(nullptr); // init blank
// and start with a new project
menuFileNew();
bool isFirst = false;//openWindows.empty();
openWindows.insert(this);
if (fileName.isEmpty()) {
// start with a new project
menuFileNew();
} else {
openProject(fileName, isFirst);
if (!project) {
if (!isFirst) close();
else menuFileNew();
}
}
}
MainWindow::~MainWindow() {
@ -430,9 +539,27 @@ MainWindow::~MainWindow() {
delete ui;
}
void MainWindow::closeEvent(QCloseEvent*) {
MainWindow* MainWindow::projectWindow(const QString &fileName) {
if (fileName.isEmpty()) return nullptr;
for (auto w : openWindows) if (w->project && w->project->fileName == fileName) return w;
return nullptr;
}
void MainWindow::resizeEvent(QResizeEvent* e) {
this->QMainWindow::resizeEvent(e);
ui->floaterContainer->setFixedSize(this->size());
}
void MainWindow::closeEvent(QCloseEvent* e) {
if (promptSave()) {
e->ignore();
return;
}
undoStack->clear();
setAttribute(Qt::WA_DeleteOnClose); // delete when done
openWindows.erase(this); // and remove from list now
if (openWindows.size() == 0 && SettingsDialog::instance) SettingsDialog::instance->reject();
}
bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
@ -450,25 +577,66 @@ bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
return false;
}
void MainWindow::tryFocus() {
raise();
activateWindow();
}
void MainWindow::openProject(const QString& fileName, bool failSilent) {
auto pw = projectWindow(fileName);
if (pw && pw != this) {
pw->tryFocus();
return;
}
auto np = FileOps::loadProject(fileName);
if (!np) {
if (!failSilent) QMessageBox::critical(this, qs("Error"), qs("Error loading project \"%1\".").arg(QFileInfo(fileName).fileName()));
return;
}
if (audioEngine->playingProject() == project) audioEngine->stop();
project = np;
UIState::addRecentFile(fileName);
onNewProjectLoaded();
}
void MainWindow::openRecentProject(size_t idx) {
if (promptSave()) return;
if (idx > UIState::recentFiles.size()) return;
auto it = UIState::recentFiles.begin();
for (size_t i = 0; i < idx; i++) it++;
openProject(QString(*it)); // need to copy string before opening
}
bool MainWindow::promptSave() {
if (!project) return false; // window closing on open
if (!undoStack->isClean()) {
auto ftxt = project->fileName.isEmpty() ? qs("unsaved project") : qs("project \"%1\"").arg(QFileInfo(project->fileName).fileName());
auto r = QMessageBox::warning(this, qs("Unsaved changes"), qs("Save changes to %1?").arg(ftxt),
static_cast<QMessageBox::StandardButtons>(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel));
if (r == QMessageBox::Cancel || r == QMessageBox::Escape) return true; // signal abort
if (r == QMessageBox::Yes) {
menuFileSave();
if (project->fileName.isEmpty()) return true; // save-as-new canceled
}
}
return false;
}
void MainWindow::menuFileNew() {
if (promptSave()) return;
auto hold = project; // keep alive until done
if (audioEngine->playingProject() == project) audioEngine->stop();
project = std::make_shared<Project>();
project->sequence.emplace_back(project->newPattern());
project = FileOps::newProject();
onNewProjectLoaded();
}
void MainWindow::menuFileOpen() {
if (promptSave()) return;
if (auto fileName = FileOps::showOpenDialog(this, "Open project...", Config::Directories::projects, FileOps::Filter::project); !fileName.isEmpty()) {
auto np = FileOps::loadProject(fileName);
if (!np) {
QMessageBox::critical(this, "Error", "Error loading project");
return;
}
if (audioEngine->playingProject() == project) audioEngine->stop();
project = np;
onNewProjectLoaded();
openProject(fileName);
}
}
@ -488,6 +656,7 @@ void MainWindow::menuFileSaveAs() {
}
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) {
FileOps::saveProject(project, fileName);
UIState::addRecentFile(fileName);
undoStack->setClean();
updateTitle();
}
@ -495,28 +664,63 @@ void MainWindow::menuFileSaveAs() {
void MainWindow::menuFileExport() {
if (project->exportFileName.isEmpty()) menuFileExportAs();
else {
audioEngine->render(project, project->exportFileName);
}
else render();
}
void MainWindow::menuFileExportAs() {
QString saveDir = Config::Directories::projects;
if (!project->fileName.isEmpty()) {
QFileInfo f(project->fileName);
saveDir = f.dir().filePath(f.baseName());
}
saveDir = f.dir().filePath(f.baseName()).append(".mp3");
} else saveDir = saveDir.append("/untitled.mp3");
if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::audioOut, "mp3"); !fileName.isEmpty()) {
project->exportFileName = fileName;
audioEngine->render(project, project->exportFileName);
render();
}
}
void MainWindow::render() {
std::vector<QWidget*> dis {
ui->pattern, ui->patchboard, ui->samples,
ui->menuBar, ui->playButton
};
for (auto w : dis) w->setEnabled(false);
setFloater(ui->floaterRendering);
audioEngine->render(project, project->exportFileName);
if (openWindows.find(this) == openWindows.end()) return; // don't try to update UI if the window has been disposed
setFloater();
for (auto w : dis) w->setEnabled(true);
}
void MainWindow::menuFileNewWindow() {
auto w = new MainWindow();
w->show();
}
void MainWindow::menuSettings() {
SettingsDialog::tryOpen();
}
void MainWindow::menuQuit() {
auto c = openWindows.size();
if (c > 1) { // prompt if more than just this window
auto r = QMessageBox::warning(this, qs("Quit"), qs("Close %1 open projects?").arg(c),
static_cast<QMessageBox::StandardButtons>(QMessageBox::Yes | QMessageBox::No));
if (r == QMessageBox::No || r == QMessageBox::Escape) return;
}
// assemble list
std::vector<MainWindow*> cl;
cl.push_back(this);
for (auto w : openWindows) if (w != this) cl.push_back(w);
// and close
for (auto w : cl) w->close();
}
void MainWindow::onNewProjectLoaded() {
undoStack->clear();
@ -545,6 +749,8 @@ void MainWindow::onNewProjectLoaded() {
updateTitle();
setSongInfoPaneExpanded(false);
if (ui->tabWidget->currentWidget() == ui->patchboard) ui->patchboardView->setFocus();
emit projectLoaded();
}
@ -588,7 +794,7 @@ void MainWindow::updateTitle() {
if (project->fileName.isEmpty()) songTitle = qs("(new project)");
else songTitle = QFileInfo(project->fileName).baseName();
} else {
if (!project->artist.isEmpty()) songTitle = qs("%1 - %2").arg(project->artist).arg(project->title);
if (!project->artist.isEmpty()) songTitle = qs("%1 - %2").arg(project->artist, project->title);
else songTitle = project->title;
}
@ -597,6 +803,19 @@ void MainWindow::updateTitle() {
setWindowTitle(qs("Xybrid - ") % songTitle);
}
void MainWindow::updateFont() {
QString font = qs("Iosevka Term Light");
double pt = 10.0;
QString fstr = qs("font: %2pt \"%1\";").arg(font).arg(pt);
QString tfstr = qs("QTableView { %1 }").arg(fstr);
QString hfstr = qs("QHeaderView { %1 }").arg(fstr);
ui->patternSequencer->setStyleSheet(tfstr);
ui->patternEditor->setStyleSheet(tfstr);
ui->patternEditor->verticalHeader()->setStyleSheet(hfstr);
}
void MainWindow::setSongInfoPaneExpanded(bool open) {
if (open) {
ui->songInfoPane->setCurrentIndex(1);
@ -609,6 +828,12 @@ void MainWindow::setSongInfoPaneExpanded(bool open) {
ui->songInfoPane->setMaximumHeight(s);
}
void MainWindow::setFloater(QWidget* w) {
auto idx = ui->floaterContainer->indexOf(w);
if (idx < 0) idx = 0;
ui->floaterContainer->setCurrentIndex(idx);
}
bool MainWindow::selectPatternForEditing(Pattern* pattern) {
if (!pattern || pattern == editingPattern.get()) return false; // no u
if (pattern->project != project.get()) return false; // wrong project
@ -626,20 +851,48 @@ bool MainWindow::selectPatternForEditing(Pattern* pattern) {
void MainWindow::selectSampleForEditing(std::shared_ptr<Xybrid::Data::Sample> smp) {
if (!smp || smp->project != project.get()) { // no valid sample selected
editingSample = nullptr;
ui->sampleViewPane->setEnabled(false);
ui->sampleInfo->setText(qs("(no sample selected)"));
ui->groupSampleLoop->setChecked(false);
ui->spinSampleLoopStart->setValue(0);
ui->spinSampleLoopEnd->setValue(0);
ui->spinSampleNote->setValue(60);
ui->spinSampleNoteSub->setValue(0.0);
} else {
editingSample = nullptr;
ui->sampleViewPane->setEnabled(true);
ui->sampleInfo->setText(
qs("%1 // %2\n%3 %4Hz, %5 frames (%6)")
.arg(smp->name.section('/', -1, -1))
.arg(smp->uuid.toString())
.arg(smp->numChannels() == 2 ? qs("Stereo") : qs("Mono"))
.arg(smp->name.section('/', -1, -1),
smp->uuid.toString(),
smp->numChannels() == 2 ? qs("Stereo") : qs("Mono"))
.arg(smp->sampleRate)
.arg(smp->length())
.arg(Util::sampleLength(smp->sampleRate, smp->length()))
);
ui->spinSampleLoopStart->setRange(0, smp->length());
ui->spinSampleLoopEnd->setRange(0, smp->length());
if (smp->loopStart < 0) { // loop disabled
ui->groupSampleLoop->setChecked(false);
ui->spinSampleLoopStart->setValue(0);
ui->spinSampleLoopEnd->setValue(smp->length());
} else {
ui->groupSampleLoop->setChecked(true);
ui->spinSampleLoopStart->setValue(smp->loopStart);
ui->spinSampleLoopEnd->setValue(smp->loopEnd);
}
ui->spinSampleNote->setValue(smp->baseNote);
ui->spinSampleNoteSub->setValue(smp->subNote);
editingSample = smp;
}
ui->waveformPreview->setSample(smp);
}

View File

@ -1,5 +1,7 @@
#pragma once
#include <unordered_set>
#include <QMainWindow>
#include "uisocket.h"
@ -19,16 +21,25 @@ namespace Xybrid {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
explicit MainWindow(QWidget *parent = nullptr, const QString& fileName = QString());
~MainWindow() override;
static std::unordered_set<MainWindow*> openWindows;
static MainWindow* projectWindow(const QString& fileName);
private:
Ui::MainWindow* ui;
UISocket* socket;
std::shared_ptr<Data::Project> project;
std::shared_ptr<Data::Pattern> editingPattern;
std::shared_ptr<Data::Sample> editingSample;
QUndoStack* undoStack;
std::vector<QAction*> recentFileActions;
void openProject(const QString& fileName, bool failSilent = false);
void openRecentProject(size_t idx);
bool promptSave();
void onNewProjectLoaded();
void updatePatternLists();
@ -42,8 +53,12 @@ namespace Xybrid {
void openPatternProperties(const std::shared_ptr<Data::Pattern>&);
void updateTitle();
void updateFont();
void setSongInfoPaneExpanded(bool);
void setFloater(QWidget* = nullptr);
void render();
public:
const std::shared_ptr<Data::Project>& getProject() const { return project; }
@ -56,10 +71,13 @@ namespace Xybrid {
inline UISocket* uiSocket() { return socket; }
protected:
void resizeEvent(QResizeEvent*) override;
void closeEvent(QCloseEvent*) override;
bool eventFilter(QObject *obj, QEvent *event) override;
public slots:
void tryFocus();
void menuFileNew();
void menuFileOpen();
void menuFileSave();
@ -69,6 +87,8 @@ namespace Xybrid {
void menuFileExportAs();
void menuFileNewWindow();
void menuSettings();
void menuQuit();
signals:
void projectLoaded();

View File

@ -452,7 +452,9 @@
<property name="font">
<font>
<family>Iosevka Term Light</family>
<pointsize>9</pointsize>
<pointsize>10</pointsize>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="focusPolicy">
@ -504,7 +506,9 @@
<property name="font">
<font>
<family>Iosevka Term Light</family>
<pointsize>9</pointsize>
<pointsize>10</pointsize>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="editTriggers">
@ -687,6 +691,248 @@
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="sampleEditRow" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupSampleLoop">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Loop</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="labelSampleLoopStart">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinSampleLoopStart">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>4</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="labelSampleLoopEnd">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>End</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinSampleLoopEnd">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>4</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupSampleNote">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Base Note</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QWidget" name="widget_3" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_11">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QSpinBox" name="spinSampleNote">
<property name="maximum">
<number>119</number>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="spinSampleNoteSub">
<property name="minimum">
<double>-1.000000000000000</double>
</property>
<property name="maximum">
<double>1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@ -801,6 +1047,109 @@
</layout>
</widget>
</widget>
<widget class="QWidget" name="extra_2">
<attribute name="title">
<string>floater</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QStackedWidget" name="floaterContainer">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="floaterNone"/>
<widget class="QWidget" name="floaterRendering">
<layout class="QHBoxLayout" name="horizontalLayout_13">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>388</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Xybrid::UI::FloaterBG" name="floaterRenderingBg" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>48</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_14">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="labelRendering">
<property name="text">
<string>Rendering...</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@ -840,11 +1189,15 @@
<addaction name="separator"/>
<addaction name="actionNew_Window"/>
<addaction name="actionClose_Window"/>
<addaction name="separator"/>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>Edit</string>
</property>
<addaction name="separator"/>
<addaction name="actionSettings"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
@ -890,7 +1243,7 @@
</action>
<action name="actionSave_As">
<property name="text">
<string>Sa&amp;ve As...</string>
<string>Save &amp;As...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
@ -925,12 +1278,28 @@
</action>
<action name="actionExport_As">
<property name="text">
<string>E&amp;xport As...</string>
<string>Export As...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+E</string>
</property>
</action>
<action name="actionQuit">
<property name="text">
<string>&amp;Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionSettings">
<property name="text">
<string>Settings</string>
</property>
<property name="shortcut">
<string>Ctrl+F1</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
@ -950,6 +1319,12 @@
<header>ui/waveformpreviewwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>Xybrid::UI::FloaterBG</class>
<extends>QWidget</extends>
<header>ui/floaterbg.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="res/resources.qrc"/>
@ -1083,6 +1458,38 @@
</hint>
</hints>
</connection>
<connection>
<sender>actionQuit</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>menuQuit()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>459</x>
<y>305</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionSettings</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>menuSettings()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>459</x>
<y>305</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>menuFileNew()</slot>
@ -1092,5 +1499,7 @@
<slot>menuFileNewWindow()</slot>
<slot>menuFileExport()</slot>
<slot>menuFileExportAs()</slot>
<slot>menuQuit()</slot>
<slot>menuSettings()</slot>
</slots>
</ui>

View File

@ -8,14 +8,14 @@ class QCborValue;
namespace Xybrid::NodeLib {
// more precision than probably fits in a double, but it certainly shouldn't hurt
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const constexpr double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
const inline constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const inline constexpr double SEMI = 1.059463094359295264561825294946341700779204317494185628559;
/// Multiplier to compensate for the balance equation
// (1.0 / cos(PI*0.25))
const constexpr double PAN_MULT = 1.414213562373095048801688724209698078569671875376948073176;
/// (1.0 / cos(PI*0.25))
const inline constexpr double PAN_MULT = 1.414213562373095048801688724209698078569671875376948073176;
/// Sane mimimum transition time to avoid clip artifacts
const constexpr double shortStep = 0.0025;
const inline constexpr double shortStep = 0.0025;
struct ADSR {
double a = 0.0, d = 0.0, s = 1.0, r = 0.0;

View File

@ -8,7 +8,7 @@ using namespace Xybrid::Data;
CommandReader::CommandReader(CommandPort* p) {
p->pull();
data = p->data;
dataSize = p->dataSize;
dataSize = p->size;
}
CommandReader::operator bool() const { return dataSize >= cur+5; }

View File

@ -37,6 +37,7 @@ void InstrumentCore::reset() {
time = 0;
volume = 1.0;
pan = 0.0;
}
void InstrumentCore::process(Node* n) {
@ -102,6 +103,7 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
forceRetrigger:
note.note = n;
note.time = note.adsrTime = -smpTime; // compensate for first-advance
note.pan = pan;
if (onNoteOn) onNoteOn(note);
}
} else { // existing note
@ -194,6 +196,10 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
volume = (1.0*v) / 255.0;
continue;
}
case 'P': {
pan = std::clamp((1.0*static_cast<int8_t>(v)) / 127.0, -1.0, 1.0);
continue;
}
default:
break;
}
@ -203,7 +209,7 @@ void InstrumentCore::process(CommandPort* i, AudioPort* o) {
// then do the thing
if (o) o->pull();
double tickTime = smpTime * audioEngine->curTickSize();
double tickTime = smpTime * static_cast<double>(audioEngine->curTickSize());
if (processNote) {
for (auto p = activeNotes.begin(); p != activeNotes.end(); ) {

View File

@ -3,10 +3,27 @@
#include <memory>
#include <functional>
#include <unordered_map>
#ifdef WITH_BOOST
#include <boost/container/pmr/memory_resource.hpp>
#include <boost/container/pmr/polymorphic_allocator.hpp>
using boost::container::pmr::polymorphic_allocator;
template <class Key,
class T,
class Hash = std::hash<Key>,
class Pred = std::equal_to<Key>>
using unordered_map = std::unordered_map<Key, T, Hash, Pred, polymorphic_allocator<std::pair<const Key,T>>>;
template <class Key, class T,
class Hash = std::hash<Key>,
class Pred = std::equal_to<Key>>
using unordered_multimap = std::unordered_multimap<Key, T, Hash, Pred, polymorphic_allocator<std::pair<const Key,T>>>;
#else
using std::pmr::unordered_map;
using std::pmr::unordered_multimap;
#endif
#include <array>
#include "nodelib/basics.h"
#include "data/node.h"
#include "util/mem.h"
namespace Xybrid::Data {
class CommandPort;
@ -23,9 +40,6 @@ namespace Xybrid::NodeLib {
* Not mandatory by any means, but handles all the "standard" commands for you.
*/
class InstrumentCore {
double time;
double smpTime;
public:
class Note {
friend class InstrumentCore;
@ -33,7 +47,7 @@ namespace Xybrid::NodeLib {
void* intern = nullptr;
public:
uint16_t id;
double note; // floating point to allow smooth pitch bends
double note = 64.0; // floating point to allow smooth pitch bends
double noteAdd = 0.0;
double time = 0.0;
@ -45,7 +59,12 @@ namespace Xybrid::NodeLib {
double adsrTime = 0;
uint8_t adsrPhase = 0;
std::array<double, 5> scratch{0.0};
union {
std::array<double, 5> scratch {0.0};
std::array<void*, 5> ptr;
};
template<typename T> inline T& scratchAs() { return *(reinterpret_cast<T*>(reinterpret_cast<void*>(&scratch))); }
Note() = default;
Note(InstrumentCore*, uint16_t id);
@ -76,11 +95,19 @@ namespace Xybrid::NodeLib {
void startTick(Note&, double tickTime);
void process(Note&, double smpTime);
};
private:
//
double time;
double smpTime;
public:
double volume = 1.0;
double pan = 0.0;
std::unordered_map<uint16_t, Note> activeNotes;
std::unordered_multimap<uint16_t, Tween> activeTweens;
unordered_map<uint16_t, Note> activeNotes = {16, Util::ralloc};
unordered_multimap<uint16_t, Tween> activeTweens = {16, Util::ralloc};
std::function<bool(Note*, const ParamReader&)> paramFilter;
std::unordered_map<uint8_t, std::function<bool(const ParamReader&)>> globalParam;

10
xybrid/nodelib/osc.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include "util/ext.h"
namespace Xybrid::NodeLib {
namespace Oscillator {
//
}
namespace Osc = Oscillator;
}

22
xybrid/nodelib/param.cpp Normal file
View File

@ -0,0 +1,22 @@
#include "param.h"
using Xybrid::NodeLib::Param;
using namespace Xybrid::Data;
ParameterPort* Param::makePort(Data::Node* node, const QString& name) {
if (port) return port;
node->inputs.try_emplace(Port::Parameter);
auto& inp = node->inputs.find(Port::Parameter)->second;
uint8_t id = 0;
auto it = inp.begin();
while (it != inp.end() && it->first == id) { ++id; ++it; } // scan for first unused id
auto n = name;
if (n.isEmpty()) n = this->name.toLower();
port = static_cast<ParameterPort*>(node->addPort(Port::Input, Port::Parameter, id).get());
port->name = n;
return port;
}

88
xybrid/nodelib/param.h Normal file
View File

@ -0,0 +1,88 @@
#pragma once
#include <cmath>
#include <limits>
#include <QCborMap>
#include "data/node.h"
#include "data/porttypes.h"
//namespace Xybrid::Data { class ParameterPort; }
namespace Xybrid::NodeLib {
/// Plugin parameter with associated metadata and automation faculties
class Param {
//
Param() = default;
public:
class Reader {
friend class Param;
Param* p;
size_t pos = 0;
bool r;
Reader(Param* p, bool r) : p(p), r(r) { }
public:
double next() {
if (r) {
auto v = p->port->data[pos++];
if (!std::isnan(v)) {
if (p->min == -p->max) // "signed" logic if symmetrical around zero
v = std::clamp(v, -1.0, 1.0) * p->max;
else // extents 0.0 .. 1.0 scaled across parameter range
v = std::clamp(p->min + v * (p->max - p->min), p->min, p->max);
p->vt = v;
}
}
if (!std::isnan(p->vt)) return p->vt;
return p->value;
}
};
enum Flags : uint8_t {
ResetOnTick = 0b00000001,
};
QString name;
double min, max, def;
double value;
double vt = std::numeric_limits<double>::quiet_NaN();
Flags flags;
Data::ParameterPort* port = nullptr;
Param(const QString& name, double min, double max, double def, Flags flags = {}) : Param() {
this->name = name;
this->min = min;
this->max = max;
this->def = def;
this->value = def;
this->flags = flags;
}
Data::ParameterPort* makePort(Data::Node*, const QString& name = { });
Reader start() {
bool r = port && port->isConnected();
if (r) port->pull();
if (flags & ResetOnTick) vt = std::numeric_limits<double>::quiet_NaN();
return Reader(this, r);
}
inline QString saveName() const {
return name.toLower().remove(' ');
}
inline void save(QCborMap& m, QString id = { }) const {
if (id.isEmpty()) id = saveName();
m[id] = value;
}
inline void load(const QCborMap& m, QString id = { }) {
if (id.isEmpty()) id = saveName();
value = m.value(id).toDouble(value);
vt = std::numeric_limits<double>::quiet_NaN();
}
};
}

View File

@ -1,49 +1,73 @@
#include "resampler.h"
using namespace Xybrid::NodeLib;
#include <QDebug>
#include <iostream>
#include <array>
#include <cmath>
#ifdef WITH_BOOST
#include <boost/math/special_functions/bessel.hpp>
#define cyl_bessel_i boost::math::cyl_bessel_i
using boost::math::cyl_bessel_i;
#else
#include <cmath>
#define cyl_bessel_i std::cyl_bessel_i
using std::cyl_bessel_i;
#endif
namespace {
const constexpr double PI = 3.141592653589793238462643383279502884197169399375105820974;
const constexpr double KAISER_ALPHA = 7.5;
// 5.5 for downrate; example gave 7.5
const constexpr double KAISER_ALPHA = 5.5;
const constexpr double KAISER_BETA = PI * KAISER_ALPHA;
inline constexpr double sinc(double x) {
if (x == 0) return 1;
double px = x * PI;
double px = x * PI/1;
return std::sin(px) / px;
}
#if __cplusplus >= 202002L
using std::lerp;
#else
inline constexpr double lerp(double a, double b, double t) { return (1.0 - t) * a + t * b; }
#endif
}
// generate
const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> Xybrid::NodeLib::resamplerLUT = [] {
const std::array<std::array<std::array<double, LUT_TAPS>, LUT_LEVELS>, LUT_STEPS> Xybrid::NodeLib::resamplerLUT = [] {
double denom = cyl_bessel_i(0, KAISER_BETA);
std::array<std::array<double, LUT_TAPS>, LUT_STEPS> t;
t[0] = {0, 0, 0, 1, 0, 0, 0, 0}; // we already know the ideal integer step
for (size_t step = 1; step < LUT_STEPS; step++) {
double sv = static_cast<double>(step) / LUT_STEPS;
std::array<std::array<std::array<double, LUT_TAPS>, LUT_LEVELS>, LUT_STEPS> t;
//t[0] = {0, 0, 0, 1, 0, 0, 0, 0}; // we already know the ideal integer step
for (size_t step = 0; step < LUT_STEPS; step++) {
double sv = static_cast<double>(step) / LUT_STEPS; // subvalue (offset of tap position)
for (size_t tap = 0; tap < LUT_TAPS; tap++) {
double x = static_cast<double>(tap) - sv;
t[step][tap] = sinc(x-(LUT_TAPS/2-1)) * (cyl_bessel_i(0, KAISER_BETA * std::sqrt(1 - std::pow(((2 * (x+1)) / (LUT_TAPS)) - 1, 2))) / denom);
if (t[step][tap] != t[step][tap]) t[step][tap] = 0; // NaN guard
//std::cout << "tap " << tap << ": " << t[step][tap] << " ";
double x = static_cast<double>(tap) - sv; // x position of tap;
double sx = x-LUT_HTAPS;
double kaiser = cyl_bessel_i(0, KAISER_BETA * std::sqrt(1.0 - std::pow( ( (2.0*(x+1))/(LUT_TAPS) ) - 1.0, 2 ) ) ) / denom; // original kaiser window generation
//double kaiser = cyl_bessel_i(0, KAISER_BETA * std::sqrt(1.0 - std::pow( (2.0*x)/(LUT_TAPS-2) - 1.0, 2 ) ) ) / denom; // by-the-book kaiser window of length LUT_TAPS-1
//double idl = (2.0*PI)/(LUT_TAPS-1);
//double kaiser = 0.40243 - 0.49804 * std::cos(idl * x) + 0.09831 * std::cos(2.0 * idl * x) - 0.00122 * std::cos(3.0 * idl * x); // approximate
//kaiser = std::max(kaiser, 0.0);
for (size_t i = 0; i < LUT_LEVELS; i++) {
double m = 1.0/std::max(static_cast<double>(i), 1.0); // sinc function "expands" the higher we pitch things
double om = lerp(0.2, 1.0, m) // we need to compensate slightly for amplitude loss at higher levels
/* */ * lerp(1.0, kaiser, std::pow(m, 0.333)); // apply kaiser proportionally; we want it less the higher we go
t[step][i][tap] = sinc(sx*m) * om;
if (t[step][i][tap] != t[step][i][tap]) t[step][i][tap] = 0; // NaN guard
}
if (t[step][0][tap] != t[step][0][tap]) t[step][0][tap] = 0; // NaN guard
}
//std::cout << "\n";
}
/*t[0] = {};
t[0][LUT_HTAPS] = 1;*/
/*for (auto v : t[0]) std::cout << v << ", ";
std::cout << std::endl;*/
return t;
}();

View File

@ -1,9 +1,39 @@
#pragma once
#include <cmath>
#include <cstddef>
#include <array>
#include "data/audioframe.h"
#include "data/sample.h"
namespace Xybrid::NodeLib {
const constexpr size_t LUT_LEVELS = 16;
const constexpr size_t LUT_TAPS = 8;
const constexpr ptrdiff_t LUT_HTAPS = LUT_TAPS/2-1;//static_cast<ptrdiff_t>(LUT_TAPS - (LUT_TAPS+0.5)/2);
const constexpr size_t LUT_STEPS = 1024;
extern const std::array<std::array<double, LUT_TAPS>, LUT_STEPS> resamplerLUT;
extern const std::array<std::array<std::array<double, LUT_TAPS>, LUT_LEVELS>, LUT_STEPS> resamplerLUT;
inline Data::AudioFrame resamp(Data::Sample* smp, double pos, double rate [[maybe_unused]]) {
auto loop = smp->loopStart >= 0;
auto len = static_cast<ptrdiff_t>(smp->length());
auto ls = static_cast<ptrdiff_t>(smp->loopStart);
auto le = static_cast<ptrdiff_t>(smp->loopEnd);
auto ll = le - ls;
double ip = std::floor(pos);
auto& pt = NodeLib::resamplerLUT[static_cast<size_t>((pos - ip)*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS][static_cast<size_t>(std::clamp(std::floor(rate - 0.00001), 0.0, (LUT_LEVELS-1)*1.0))];
Data::AudioFrame out(0.0);
auto ii = static_cast<ptrdiff_t>(ip) - LUT_HTAPS;
for (size_t i = 0; i < 8; i++) {
if (loop && ii >= le) ii = ((ii - ls) % ll) + ls;
else if (ii >= len) return out; // we can early-out here
if (ii >= 0) out += (*smp)[static_cast<size_t>(ii)] * pt[i];
ii++;
}
return out;
}
}

View File

@ -0,0 +1,31 @@
#include "svfilter.h"
#include "nodelib/basics.h"
#include "audio/audioengine.h"
using Xybrid::NodeLib::SVFilter;
using Xybrid::NodeLib::GenericSVFilter;
using Xybrid::Data::AudioFrame;
using namespace Xybrid::Audio;
template class Xybrid::NodeLib::GenericSVFilter<AudioFrame>;
template class Xybrid::NodeLib::GenericSVFilter<double>;
/*
template<typename DT>
void GenericSVFilter<DT>::process(DT in, double cutoff, double resonance, int ovs) {
if (ovs <= 0) return;
cutoff = std::max(cutoff, 1.0);
resonance = std::max(resonance, 0.01);
double f = 2.0 * std::sin(PI * cutoff / (audioEngine->curSampleRate() * ovs));
double q = std::sqrt(1.0 - std::atan(std::sqrt(resonance)) * 2.0 / PI);
double damp = std::sqrt(q);
for (int i = 0; i < ovs; i++) {
low += band*f;
high = in*damp - low - band*q;
band += high*f;
}
notch = high+low;
}
*/

71
xybrid/nodelib/svfilter.h Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include <cstring>
#include <cmath>
#include "data/audioframe.h"
#include "nodelib/basics.h"
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
namespace Xybrid::NodeLib {
/// 12db Chamberlin State Variable Filter
template<typename DT> class GenericSVFilter {
public:
/// Default oversampling level. Enough to mostly eliminate artifacting at high cutoff.
static const constexpr int DEFAULT_OVERSAMP = 3;
DT low = 0.0;
DT high = 0.0;
DT band = 0.0;
DT notch = 0.0;
GenericSVFilter() = default;
~GenericSVFilter() = default;
// nothing used here should care about taking the raw approach
inline GenericSVFilter<DT>(const GenericSVFilter<DT>& o) { std::memcpy(static_cast<void*>(this), static_cast<const void*>(&o), sizeof(o)); }
inline GenericSVFilter<DT>& operator=(const GenericSVFilter<DT>& o) { std::memcpy(static_cast<void*>(this), static_cast<const void*>(&o), sizeof(o)); return *this; }
void process(DT in, double cutoff, double resonance, int oversamp = DEFAULT_OVERSAMP) {
if (oversamp <= 0) return;
cutoff = std::max(cutoff, 1.0);
resonance = std::max(resonance, 0.01);
double f = 2.0 * std::sin(PI * cutoff / (audioEngine->curSampleRate() * oversamp));
double q = std::sqrt(1.0 - std::atan(std::sqrt(resonance)) * 2.0 / PI);
double damp = std::sqrt(q);
for (int i = 0; i < oversamp; i++) {
low += band*f;
high = in*damp - low - band*q;
band += high*f;
}
notch = high+low;
}
inline void reset() { low = 0.0; high = 0.0; band = 0.0; notch = 0.0; }
inline void normalize(double m) {
if constexpr (std::is_arithmetic_v<DT>) {
low = std::clamp(low, -m, m);
high = std::clamp(high, -m, m);
band = std::clamp(band, -m, m);
notch = std::clamp(notch, -m, m);
} else {
low = low.clamp(m);
high = high.clamp(m);
band = band.clamp(m);
notch = notch.clamp(m);
}
}
static inline double scaledResonance(double r) { return std::pow(10, r*5); }
};
// explicit instantiation declarations to eliminate warnings
extern template void Xybrid::NodeLib::GenericSVFilter<Data::AudioFrame>::process(Data::AudioFrame, double, double, int);
extern template void Xybrid::NodeLib::GenericSVFilter<double>::process(double, double, double, int);
/// 12db Chamberlin State Variable Filter
typedef GenericSVFilter<Xybrid::Data::AudioFrame> SVFilter;
/// 12db Chamberlin State Variable Filter (mono version)
typedef GenericSVFilter<double> SVFilterM;
}

View File

@ -0,0 +1,95 @@
#include "autopan.h"
using Xybrid::Effects::AutoPan;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
#include "data/audioframe.h"
#include "data/porttypes.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/layoutgadget.h"
#include "ui/gadgets/togglegadget.h"
#include "ui/gadgets/knobgadget.h"
using namespace Xybrid::UI;
#include <cmath>
#include <QCborMap>
// clazy:excludeall=non-pod-global-static
RegisterPlugin(AutoPan, {
i->id = "fx:autopan";
i->displayName = "Auto Pan";
i->category = "Effect";
})
AutoPan::AutoPan() { }
void AutoPan::init() {
addPort(Port::Input, Port::Audio, 0);
addPort(Port::Output, Port::Audio, 0);
}
void AutoPan::reset() {
cyc = 0.0;
}
void AutoPan::process() {
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
in->pull();
out->pull();
auto sr = audioEngine->curSampleRate();
auto ts = audioEngine->curTickSize();
auto eRate = rate;
if (bpmRelative) eRate *= audioEngine->curTempo() / 60.0;
auto tl = static_cast<double>(ts) / static_cast<double>(sr); // tick length
for (size_t f = 0; f < ts; f++) {
AudioFrame fCurrent = (*in)[f];
auto cc = cyc+phase + (static_cast<double>(f) / static_cast<double>(sr)) * eRate;
(*out)[f] = fCurrent.gainBalance(0.0, level * std::sin(cc * 2*PI));
}
cyc = std::fmod(cyc + tl*eRate, 1.0);
}
void AutoPan::saveData(QCborMap& m) const {
m[qs("level")] = level;
m[qs("rate")] = rate;
m[qs("phase")] = phase;
m[qs("bpmRelative")] = bpmRelative;
}
void AutoPan::loadData(const QCborMap& m) {
level = m.value("level").toDouble(level);
rate = m.value("rate").toDouble(rate);
phase = m.value("phase").toDouble(phase);
bpmRelative = m.value("bpmRelative").toBool(bpmRelative);
}
void AutoPan::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
l->setMetrics(-1, 2);
(new KnobGadget(l))->bind(rate)->setLabel(qs("Rate"))->setRange(0.0, 8.0, 0.01, -1, 0.001)->setDefault(1.0);
auto l2 = (new LayoutGadget(l, true))->setMetrics(0, 2);
(new ToggleGadget(l2))->bind(bpmRelative)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
KnobGadget::autoPercent(l2, phase)->setLabel(qs("Phase"))->setSize(22);
KnobGadget::autoBalance(l, level)->setLabel(qs("Level"));
}

View File

@ -0,0 +1,28 @@
#pragma once
#include "data/node.h"
namespace Xybrid::Effects {
class AutoPan : public Data::Node {
double level;
double rate = 1.0;
double phase = 0.0;
bool bpmRelative = false;
double cyc; // current cycle tracking
public:
AutoPan();
~AutoPan() override = default;
void init() override;
void reset() override;
//void release() override;
void process() override;
void saveData(QCborMap&) const override;
void loadData(const QCborMap&) override;
void onGadgetCreated() override;
};
}

View File

@ -2,6 +2,8 @@
using Xybrid::Effects::Delay;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
@ -24,24 +26,20 @@ using namespace Xybrid::UI;
#include <QCborMap>
#define qs QStringLiteral
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "gadget:delay";
i->displayName = "Delay";
i->category = "Effect";
i->createInstance = []{ return std::make_shared<Delay>(); };
PluginRegistry::registerPlugin(i);
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Delay, {
i->id = "fx:delay";
i->oldIds = {"gadget:delay"};
i->displayName = "Delay";
i->category = "Effect";
})
Delay::Delay() { }
void Delay::init() {
addPort(Port::Input, Port::Audio, 0);
addPort(Port::Output, Port::Audio, 0);
addPort(Port::Output, Port::Audio, 1)->name = "wet only";
}
void Delay::reset() {
@ -61,7 +59,7 @@ void Delay::process() {
auto fbMult = std::pow(feedback, 2);
// calculate number of frames ahead to write to the buffer
int frames = static_cast<int>(delayTime * static_cast<double>(audioEngine->curSampleRate()) * (timeInBeats ? (60.0 / audioEngine->curTempo()) : 1.0));
int frames = static_cast<int>(delayTime * static_cast<double>(audioEngine->curSampleRate()) * (bpmRelative ? (60.0 / audioEngine->curTempo()) : 1.0));
// enlarge buffer if too small
if (auto mc = frames+1; buf.capacity() < mc) buf.setCapacity(mc);
@ -69,8 +67,12 @@ void Delay::process() {
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
auto wout = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 1));
in->pull();
out->pull();
bool oc = out->isConnected();
bool wc = wout->isConnected();
if (oc) out->pull();
if (wc) wout->pull();
size_t ts = audioEngine->curTickSize();
for (size_t f = 0; f < ts; f++) {
@ -79,21 +81,25 @@ void Delay::process() {
if (!buf.areIndexesValid()) buf.normalizeIndexes(); // make sure we can actually reach the point we need
int i = frames + buf.firstIndex();
buf[i] += (fCurrent * delayMult) + (fOut * fbMult);
if (pingPong) buf[i] = buf[i].flip();
(*out)[f] = fCurrent + fOut;
if (oc) (*out)[f] = fCurrent + fOut;
if (wc) (*wout)[f] = fOut;
}
}
void Delay::saveData(QCborMap& m) const {
m[qs("time")] = QCborValue(delayTime);
m[qs("inBeats")] = QCborValue(timeInBeats);
m[qs("amount")] = QCborValue(amount);
m[qs("feedback")] = QCborValue(feedback);
m[qs("time")] = delayTime;
m[qs("bpmRelative")] = bpmRelative;
m[qs("pingPong")] = pingPong;
m[qs("amount")] = amount;
m[qs("feedback")] = feedback;
}
void Delay::loadData(const QCborMap& m) {
delayTime = m.value("time").toDouble(delayTime);
timeInBeats = m.value("inBeats").toBool(timeInBeats);
bpmRelative = m.value("bpmRelative").toBool(m.value("inBeats").toBool(bpmRelative));
pingPong = m.value("pingPong").toBool(pingPong);
amount = m.value("amount").toDouble(amount);
feedback = m.value("feedback").toDouble(feedback);
}
@ -102,10 +108,12 @@ void Delay::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
(new KnobGadget(l))->bind(delayTime)->setLabel(qs("Time"))->setRange(0.0, 5.0, 0.001)->setDefault(0.5);
(new ToggleGadget(l))->bind(timeInBeats)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
l->addSpacer();
(new KnobGadget(l))->bind(amount)->setLabel(qs("Amount"))->setDefault(0.5);
l->addSpacer();
(new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setDefault(0.0);
(new KnobGadget(l))->bind(delayTime)->setLabel(qs("Time"))->setRange(0.0, 5.0, 0.001, -1, 0.01)->setDefault(0.5);
auto l2 = (new LayoutGadget(l, true))->setMetrics(0, 4);
(new ToggleGadget(l2))->bind(bpmRelative)->setColor({191, 127, 255})->setToolTip(qs("BPM-relative"));
(new ToggleGadget(l2))->bind(pingPong)->setColor({127, 191, 255})->setToolTip(qs("Stereo ping-pong"));
//l->addSpacer();
(new KnobGadget(l))->bind(amount)->setLabel(qs("Level"))->setTextFunc(KnobGadget::textPercent)->setDefault(0.5);
//l->addSpacer();
(new KnobGadget(l))->bind(feedback)->setLabel(qs("Feedback"))->setTextFunc(KnobGadget::textPercent)->setDefault(0.0);
}

View File

@ -10,7 +10,9 @@ namespace Xybrid::Effects {
QContiguousCache<Data::AudioFrame> buf;
double delayTime = 0.5;
bool timeInBeats = false;
bool bpmRelative = false;
bool pingPong = false;
double amount = 0.5;
double feedback = 0.0;

View File

@ -0,0 +1,99 @@
#include "distortion.h"
using Xybrid::Effects::Distortion;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
#include "data/audioframe.h"
#include "data/porttypes.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/layoutgadget.h"
#include "ui/gadgets/togglegadget.h"
#include "ui/gadgets/knobgadget.h"
using namespace Xybrid::UI;
#include <cmath>
#include <QCborMap>
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Distortion, {
i->id = "fx:distortion";
i->displayName = "Distortion";
i->category = "Effect";
})
Distortion::Distortion() = default;
void Distortion::init() {
addPort(Port::Input, Port::Audio, 0);
addPort(Port::Output, Port::Audio, 0);
}
void Distortion::reset() {
//
}
namespace {
inline double sxp(double v, double e) {
double s = v < 0 ? -1.0 : 1.0;
return std::pow(std::abs(v), e) * s;
}
inline AudioFrame sxp(AudioFrame v, double e) { return {sxp(v.l, e), sxp(v.r, e)}; }
}
void Distortion::process() {
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
in->pull();
out->pull();
auto ts = audioEngine->curTickSize();
auto d = drive.start(), s = shape.start(), m = mix.start(), o = output.start();
for (size_t f = 0; f < ts; f++) {
AudioFrame inp = (*in)[f];
auto g = inp.gainBalance(d.next());
auto pv = s.next();
auto exp = pv > 0.0 ? 1.0 / (1.0 + pv) : -pv + 1.0;
(*out)[f] = AudioFrame::lerp(inp, sxp(g.clamp(), exp), m.next()).gainBalance(o.next());
}
}
void Distortion::saveData(QCborMap& m) const {
drive.save(m);
shape.save(m);
mix.save(m);
output.save(m);
}
void Distortion::loadData(const QCborMap& m) {
drive.load(m);
shape.load(m);
mix.load(m);
output.load(m);
}
void Distortion::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
KnobGadget::autoGain(l, drive);
(new KnobGadget(l))->setRange(0, 0, 0.1)->bind(shape)->setTextFunc(KnobGadget::textOffset);
KnobGadget::autoPercent(l, mix);
KnobGadget::autoGain(l, output);
}

View File

@ -0,0 +1,27 @@
#pragma once
#include "data/node.h"
#include "nodelib/param.h"
namespace Xybrid::Effects {
class Distortion : public Data::Node {
NodeLib::Param drive = {"Drive", 0.0, 24.0, 0.0};
NodeLib::Param shape = {"Shape", -10.0, 10.0, 0.0};
NodeLib::Param mix = {"Mix", 0.0, 1.0, 1.0};
NodeLib::Param output = {"Output", -12.0, 12.0, 0.0};
public:
Distortion();
~Distortion() override = default;
void init() override;
void reset() override;
//void release() override;
void process() override;
void saveData(QCborMap&) const override;
void loadData(const QCborMap&) override;
void onGadgetCreated() override;
};
}

View File

@ -0,0 +1,85 @@
#include "ringmod.h"
using Xybrid::Effects::RingMod;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
#include "data/audioframe.h"
#include "data/porttypes.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/layoutgadget.h"
#include "ui/gadgets/togglegadget.h"
#include "ui/gadgets/knobgadget.h"
using namespace Xybrid::UI;
#include <cmath>
#include <QCborMap>
// clazy:excludeall=non-pod-global-static
RegisterPlugin(RingMod, {
i->id = "fx:ringmod";
i->displayName = "Ring Mod";
i->category = "Effect";
})
RingMod::RingMod() { }
void RingMod::init() {
addPort(Port::Input, Port::Audio, 0)->name = "carrier";
addPort(Port::Input, Port::Audio, 1)->name = "modulator";
addPort(Port::Output, Port::Audio, 0);
}
void RingMod::reset() {
//
}
void RingMod::process() {
auto c = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
auto m = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 1));
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
c->pull();
m->pull();
out->pull();
auto ts = audioEngine->curTickSize();
for (size_t f = 0; f < ts; f++) {
AudioFrame fc = (*c)[f];
AudioFrame fm = (*m)[f];
if (am) fm = {std::abs(fm.l), std::abs(fm.r)};
(*out)[f] = AudioFrame::lerp(fc, fc*fm, mix);
}
}
void RingMod::saveData(QCborMap& m) const {
m[qs("mix")] = mix;
m[qs("am")] = am;
}
void RingMod::loadData(const QCborMap& m) {
mix = m.value("mix").toDouble(mix);
am = m.value("am").toBool(am);
}
void RingMod::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
l->setMetrics(3, 4);
KnobGadget::autoPercent(l, mix)->setLabel(qs("Mix"))->setDefault(1.0);
(new ToggleGadget(l))->bind(am)->setToolTip("AM mode", {1.0, 0.0})->setColor({127, 255, 127});
}

View File

@ -0,0 +1,24 @@
#pragma once
#include "data/node.h"
namespace Xybrid::Effects {
class RingMod : public Data::Node {
double mix = 1.0;
bool am = false;
public:
RingMod();
~RingMod() override = default;
void init() override;
void reset() override;
//void release() override;
void process() override;
void saveData(QCborMap&) const override;
void loadData(const QCborMap&) override;
void onGadgetCreated() override;
};
}

View File

@ -4,7 +4,7 @@
* Description: State Variable Filter
*
*
* Version:
* Version:
* Created: Fri Nov 1 23:36:50 2019
* Revision: None
* Author: Rachel Fae Fox (foxiepaws),fox@foxiepa.ws
@ -16,6 +16,8 @@
using Xybrid::Effects::SVF;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
@ -38,35 +40,27 @@ using namespace Xybrid::UI;
#include <QCborMap>
#define qs QStringLiteral
#define MIN(a,b) (a<b?a:b)
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "gadget:svf";
i->displayName = "Filter";
i->category = "Effect";
i->createInstance = []{ return std::make_shared<SVF>(); };
PluginRegistry::registerPlugin(i);
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(SVF, {
i->id = "fx:svf";
i->oldIds = {"gadget:svf"};
i->displayName = "Filter";
i->category = "Effect";
})
SVF::SVF() { }
void SVF::init() {
auto sr = audioEngine->curSampleRate();
this->max_freq = ((float)sr / 4.0);
this->frequency = 12000;
this->resonance = 65;
addPort(Port::Input, Port::Audio, 0);
addPort(Port::Output, Port::Audio, 0);
//addPort(Port::Input, Port::Parameter, 0);
//auto p = Param("Cutoff", 0.0, 16000.0, 0.0);
cutoff.makePort(this);
}
void SVF::reset() {
release();
auto sr = audioEngine->curSampleRate();
filter.reset();
}
void SVF::release() {
@ -74,107 +68,76 @@ void SVF::release() {
}
void SVF::process() {
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
auto out = std::static_pointer_cast<AudioPort>(port(Port::Output, Port::Audio, 0));
in->pull();
out->pull();
if (this->fm != _off)
if (this->frequency > 0) { this->frequency-=0.1; }
auto r = filter.scaledResonance(resonance);
auto c = cutoff.start();
size_t ts = audioEngine->curTickSize();
for (size_t f = 0; f < ts; f++) {
AudioFrame fCurrent = (*in)[f];
AudioFrame fOut;
if (this->fm == _off) {
(*out)[f] = fCurrent;
continue;
}
double res = this->resonance;
//double damp = MIN(2.0*(1.0 - pow(res, 0.25)), MIN(2.0, 2.0/freq - freq*0.5));
double q = sqrt(1.0 - atan(sqrt(res)) * 2.0 / PI);
double damp = sqrt(q);
AudioFrame inp = (*in)[f];
//double freq = 2.0*sin(M_PI * MIN(0.25, this->frequency/audioEngine->curSampleRate()));
double freq = this->frequency / (audioEngine->curSampleRate() * 2 );
double in_l, in_r;
double low_l, low_r;
double band_l, band_r;
double high_l, high_r;
double notch_l, notch_r;
filter.process(inp, c.next(), r);
in_l = fCurrent.l;
in_r = fCurrent.r;
low_l=low.l;
low_r=low.r;
band_l=band.l;
band_r=band.r;
high_l=high.l;
high_r=high.r;
notch_l=notch.l;
notch_r=notch.r;
low_l = low_l + freq * band_l;
high_l = damp * in_l - low_l - q * band_l;
band_l = freq * high_l + band_l;
notch_l = high_l + low_l;
low_r = low_r + freq * band_r;
high_r = damp * in_r - low_r - q * band_r;
band_r = freq * high_r + band_r;
notch_r = high_r + low_r;
low_l = low_l + freq * band_l;
high_l = damp * in_l - low_l - q * band_l;
band_l = freq * high_l + band_l;
notch_l = high_l + low_l;
low_r = low_r + freq * band_r;
high_r = damp * in_r - low_r - q * band_r;
band_r = freq * high_r + band_r;
notch_r = high_r + low_r;
low = {low_l, low_r};
band = {band_l, band_r};
high = {high_l, high_r};
notch = {notch_l, notch_r};
switch (fm) {
case _low:
(*out)[f] = this->low;
break;
case _band:
(*out)[f] = this->band;
break;
case _high:
(*out)[f] = this->high;
break;
case _notch:
(*out)[f] = this->notch;
break;
}
switch (mode) {
case Low:
(*out)[f] = filter.low;
break;
case High:
(*out)[f] = filter.high;
break;
case Band:
(*out)[f] = filter.band;
break;
case Notch:
(*out)[f] = filter.notch;
break;
default:
(*out)[f] = inp;
}
}
}
void SVF::saveData(QCborMap& m) const {
//m[qs("frequency")] = QCborValue(frequency);
//m[qs("resonance")] = QCborValue(resonance);
//m[qs("cutoff")] = cutoff;
cutoff.save(m);
m[qs("resonance")] = resonance;
m[qs("mode")] = mode;
}
void SVF::loadData(const QCborMap& m) {
//frequency = m.value("frequency").toDouble(frequency);
//resonance = m.value("resonance").toDouble(resonance);
cutoff.load(m, "frequency");
cutoff.load(m);
//cutoff = m.value("cutoff").toDouble(m.value("frequency").toDouble(cutoff));
resonance = m.value("resonance").toDouble(resonance);
mode = static_cast<FilterMode>(m.value("mode").toInteger(mode));
}
namespace {
std::unordered_map<SVF::FilterMode, QString> modeNames = [] {
std::unordered_map<SVF::FilterMode, QString> m;
m[SVF::Off] = "off";
m[SVF::Low] = "low";
m[SVF::High] = "high";
m[SVF::Band] = "band";
m[SVF::Notch] = "notch";
return m;
}();
}
void SVF::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
//(new KnobGadget(l))->bind(freq)->setLabel(qs("Frequency Step"))->setRange(0.0, 10, 0.5)->setDefault(1.0);
(new KnobGadget(l))->bind(frequency)->setLabel(qs("Frequency"))->setRange(0.0, this->max_freq, 10.0)->setDefault(6440.0);
l->addSpacer();
(new KnobGadget(l))->bind(resonance)->setLabel(qs("Resonance"))->setRange(0.0, 100.0, 0.1)->setDefault(0.0);
l->addSpacer();
(new KnobGadget(l))->bind(fm)->setLabel(qs("Filter Mode"))->setRange(0,4,1)->setDefault(0);
auto modetxt = [](double inp) {
if (auto f = modeNames.find(static_cast<FilterMode>(inp)); f != modeNames.end()) return f->second;
return qs("?");
};
KnobGadget::autoCutoff(l, cutoff);
(new KnobGadget(l))->bind(resonance)->setLabel(qs("Res"))->setTextFunc(KnobGadget::textPercent)->setRange(0.0, 1.0, 0.01)->setDefault(0.0);
(new KnobGadget(l))->bind(mode)->setLabel(qs("Mode"))->setTextFunc(modetxt)->setRange(0, Notch, 1, KnobGadget::BigStep)->setDefault(0);
}

View File

@ -4,7 +4,7 @@
* Description:
*
*
* Version:
* Version:
* Created: Fri Nov 1 23:34:34 2019
* Revision: None
* Author: Rachel Fae Fox (foxiepaws),fox@foxiepa.ws
@ -14,29 +14,24 @@
#pragma once
#include <QContiguousCache>
#include "data/node.h"
#include "data/audioframe.h"
#include "nodelib/svfilter.h"
#include "nodelib/param.h"
namespace Xybrid::Effects {
class SVF : public Data::Node {
enum FilterMode {_off, _low, _band, _high, _notch };
double frequency= 0.5;
NodeLib::SVFilter filter;
//double cutoff = 6440.0;
NodeLib::Param cutoff = {"Cutoff", 0, 16000, 0};
double resonance = 0.0;
Xybrid::Data::AudioFrame low = 0.0;
Xybrid::Data::AudioFrame band = 0.0;
Xybrid::Data::AudioFrame high = 0.0;
Xybrid::Data::AudioFrame notch = 0.0;
FilterMode fm = _off;
// solve these in cons.
double max_freq;
double freq;
double q;
public:
enum FilterMode : uchar { Off, Low, High, Band, Notch };
FilterMode mode = Low;
SVF();
~SVF() override = default;

View File

@ -2,6 +2,8 @@
using Xybrid::Gadgets::GainBalance;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
@ -23,20 +25,13 @@ using namespace Xybrid::UI;
#include <QCborMap>
#define qs QStringLiteral
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "gadget:gainbalance";
i->displayName = "Gain/Balance";
i->category = "Gadget";
//i->hidden = true;
i->createInstance = []{ return std::make_shared<GainBalance>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(GainBalance, {
i->id = "gadget:gainbalance";
i->displayName = "Gain/Balance";
i->category = "Gadget";
//i->hidden = true;
})
GainBalance::GainBalance() {
@ -74,6 +69,6 @@ void GainBalance::onGadgetCreated() {
obj->showPluginName = false;
auto l = new LayoutGadget(obj);
(new KnobGadget(l))->bind(gain)->setRange(-60, 6, .1)->setLabel("Gain")->setTextFunc([](double d) { return QString("%1dB").arg(d); });
(new KnobGadget(l))->bind(balance)->setRange(-1.0, 1.0)->setLabel("Balance");
KnobGadget::autoGain(l, gain);
KnobGadget::autoBalance(l, balance);
}

View File

@ -26,17 +26,14 @@ using namespace Xybrid::Audio;
#include "util/strings.h"
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "ioport";
i->displayName = "I/O Port";
i->hidden = true;
i->createInstance = []{ return std::make_shared<IOPort>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
// clazy:excludeall=non-pod-global-static
RegisterPlugin(IOPort, {
i->id = "ioport";
i->displayName = "I/O Port";
i->hidden = true;
})
namespace {
Port::Type opposite(Port::Type t) {
if (t == Port::Input) return Port::Output;
return Port::Input;

View File

@ -2,6 +2,8 @@
using Xybrid::Gadgets::MixBoard;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "nodelib/basics.h"
using namespace Xybrid::NodeLib;
@ -27,18 +29,12 @@ using namespace Xybrid::UI;
#include <QCborMap>
#include <QCborArray>
#define qs QStringLiteral
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "gadget:mixboard";
i->displayName = "Mixer Board";
i->category = "Gadget";
i->createInstance = []{ return std::make_shared<MixBoard>(); };
PluginRegistry::registerPlugin(i);
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(MixBoard, {
i->id = "gadget:mixboard";
i->displayName = "Mixer Board";
i->category = "Gadget";
})
MixBoard::MixBoard() {
@ -137,7 +133,8 @@ void MixBoard::onGadgetCreated() {
if (!obj) return;
{
auto k = new Gadget();
for (auto c : obj->contents->childItems()) c->setParentItem(k);
auto ch = obj->contents->childItems(); // avoid detach warnings
for (auto c : ch) c->setParentItem(k);
k->deleteLater();
}
qDeleteAll(obj->contents->childItems()); // clear out anything already there
@ -159,13 +156,13 @@ void MixBoard::onGadgetCreated() {
/*auto mute = */(new ToggleGadget(tl))->bind(sections[i].mute)->setColor({255, 0, 0})->setToolTip("Mute", {-1, 0});
/*auto solo = */(new ToggleGadget(tl))->bind(sections[i].solo)->setColor({191, 191, 0})->setToolTip("Solo", {-1, 0});
/*auto gain = */(new KnobGadget(ln))->bind(sections[i].gain)->setRange(-60, 6, .1)->setLabel("Gain")->setTextFunc([](double d) { return QString("%1dB").arg(d); });
/*auto gain = */KnobGadget::autoGain(ln, sections[i].gain);
auto end = (new LayoutGadget(ln, true))->setMetrics(-1, spc);
auto bIns = (new ButtonGadget(end))->setSize(16, 16)->setText("+");
auto bDel = (new ButtonGadget(end))->setSize(16, 16)->setText("-");
QObject::connect(bIns, &ButtonGadget::clicked, [this, i] { insertSection(static_cast<uint8_t>(i)); });
QObject::connect(bDel, &ButtonGadget::clicked, [this, i] { removeSection(static_cast<uint8_t>(i)); });
QObject::connect(bIns, &ButtonGadget::clicked, obj, [this, i] { insertSection(static_cast<uint8_t>(i)); });
QObject::connect(bDel, &ButtonGadget::clicked, obj, [this, i] { removeSection(static_cast<uint8_t>(i)); });
if (count <= 1) delete bDel; // no dropping to zero
}
@ -175,5 +172,5 @@ void MixBoard::onGadgetCreated() {
for (auto i = 0; i < static_cast<int>(count); i++) c[i]->setY(lc[i]->y() + lc[i]->boundingRect().center().y());
auto btn = (new ButtonGadget(l))->setSize(16, 16)->setText("+");
QObject::connect(btn, &ButtonGadget::clicked, [this, count] { insertSection(static_cast<uint8_t>(count)); });
QObject::connect(btn, &ButtonGadget::clicked, obj, [this, count] { insertSection(static_cast<uint8_t>(count)); });
}

View File

@ -0,0 +1,160 @@
#include "quicklevel.h"
using Xybrid::Gadgets::QuickLevel;
using namespace Xybrid::Data;
#include <cmath>
#include <iostream>
#include <QPainter>
#include <QGraphicsScene>
#include <QStyleOptionGraphicsItem>
#include "util/strings.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "ui/patchboard/nodeobject.h"
using namespace Xybrid::UI;
#include "data/audioframe.h"
#include "data/porttypes.h"
// clazy:excludeall=non-pod-global-static
RegisterPlugin(QuickLevel, {
i->id = "gadget:quicklevel";
i->displayName = "Quick Level";
i->category = "Gadget";
})
QuickLevel::QuickLevel() {
}
void QuickLevel::init() {
auto in = addPort(Port::Input, Port::Audio, 0);
auto out = addPort(Port::Output, Port::Audio, 0);
out->passthroughTo = in;
lv = { };
}
void QuickLevel::reset() {
release();
auto sr = audioEngine->curSampleRate();
buf.setCapacity(static_cast<int>(sr * SPAN_TIME)); // fixed timestep
}
void QuickLevel::release() {
buf.clear();
buf.setCapacity(0);
lv = { };
if (obj) QMetaObject::invokeMethod(obj->scene(), "update", Qt::QueuedConnection);
}
void QuickLevel::process() {
if (!obj) return;
auto in = std::static_pointer_cast<AudioPort>(port(Port::Input, Port::Audio, 0));
in->pull();
size_t ts = audioEngine->curTickSize();
decltype(lv) clv; // compute on local copy to avoid artifacting on draw thread
for (size_t c = 0; c < 2; c++) {
clv[c][0] = std::numeric_limits<double>::max();
clv[c][1] = std::numeric_limits<double>::min();
}
// push entire tick; we don't need to clear anything as the buffer's maximum size is exactly how much we want
for (size_t s = 0; s < ts; s++) buf.append(static_cast<AudioFrame>((*in)[s]));
if (!buf.areIndexesValid()) buf.normalizeIndexes();
auto fst = buf.firstIndex(), lst = buf.lastIndex();
for (int i = fst; i <= lst; i++) {
auto f = buf.at(i);
clv[0][0] = std::min(clv[0][0], f.l);
clv[0][1] = std::max(clv[0][1], f.l);
clv[1][0] = std::min(clv[1][0], f.r);
clv[1][1] = std::max(clv[1][1], f.r);
}
lv = clv;
if (obj) QMetaObject::invokeMethod(obj, [obj = obj] {
obj->scene()->update(obj->sceneBoundingRect());
}, Qt::QueuedConnection);
}
// clear levels on port disconnect
void QuickLevel::onPortDisconnected(Data::Port::Type, Data::Port::DataType, uint8_t, std::weak_ptr<Data::Port>) {
lv = { };
if (obj) QMetaObject::invokeMethod(obj->scene(), "update", Qt::QueuedConnection);
}
void QuickLevel::onGadgetCreated() {
if (!obj) return;
obj->customChrome = true;
obj->autoPositionPorts = false;
obj->setGadgetSize(QPointF(43, 89));
auto r = obj->boundingRect();
auto pm = PortObject::portSize * .5 + PortObject::portSpacing;
auto offs = QPointF(r.width() / 2 + pm, 0);
obj->inputPortContainer->setPos(r.center() - offs);
obj->outputPortContainer->setPos(r.center() + offs);
}
namespace {
inline constexpr double dval(double in) {
in = std::clamp(in, -1.0, 1.0);
double n = in < 0.0 ? -1.0 : 1.0;
return std::pow(std::abs(in), QuickLevel::DISPLAY_EXPONENT) * n;
}
}
void QuickLevel::drawCustomChrome(QPainter* painter, const QStyleOptionGraphicsItem* opt) {
auto r = obj->boundingRect();
NodeObject::drawPanel(painter, opt, r, 4);
// set up bar geometry
QSizeF barSize(16, 81);
QRectF barL(QPointF(), barSize);
QRectF barR(QPointF(), barSize);
barL.moveCenter(r.center() + QPointF(-9.5, 0));
barR.moveCenter(r.center() + QPointF(9.5, 0));
double oh = barSize.height() / 2;
// draw bars
for (uint8_t i = 0; i < 2; i++) {
auto b = i == 0 ? barL : barR;
// bg
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0, 0, 0, 127));
painter->drawRect(b);
// level
QRectF bc(QPoint(), QSizeF(b.width(), 0));
bc.moveCenter(b.center());
bc.adjust(0, dval(-lv[i][1]) * oh, 0, dval(-lv[i][0]) * oh);
if (std::abs(lv[i][0]) > 1.0 || std::abs(lv[i][1]) > 1.0) painter->setBrush(QColor(255, 255, 63));
else painter->setBrush(QColor(63, 255, 63));
painter->drawRect(bc);
// center tick
QRectF ln(QPoint(), QSizeF(b.width(), 1));
ln.moveCenter(b.center());
painter->setBrush(QColor(0, 0, 0, 127));
painter->drawRect(ln.adjusted(0, -1, 0, 1));
painter->setBrush(QColor(255, 255, 255, 191));
painter->drawRect(ln);
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <QContiguousCache>
#include "data/node.h"
#include "data/audioframe.h"
#include <array>
namespace Xybrid::Gadgets {
class QuickLevel : public Data::Node {
QContiguousCache<Data::AudioFrame> buf;
std::array<std::array<double, 2>, 2> lv;
public:
// time across which the displayed levels are calculated
static const constexpr double SPAN_TIME = 1.0/30;
static const constexpr double DISPLAY_EXPONENT = 1;
QuickLevel();
~QuickLevel() override = default;
void init() override;
void reset() override;
void release() override;
void process() override;
void onPortDisconnected(Data::Port::Type, Data::Port::DataType, uint8_t, std::weak_ptr<Data::Port>) override;
void onGadgetCreated() override;
void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
};
}

View File

@ -2,6 +2,8 @@
using Xybrid::Gadgets::Transpose;
using namespace Xybrid::Data;
#include "util/strings.h"
#include "data/porttypes.h"
#include "config/pluginregistry.h"
@ -19,20 +21,12 @@ using namespace Xybrid::UI;
#include <QCborMap>
#define qs QStringLiteral
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "gadget:transpose";
i->displayName = "Transpose";
i->category = "Gadget";
//i->hidden = true;
i->createInstance = []{ return std::make_shared<Transpose>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Transpose, {
i->id = "gadget:transpose";
i->displayName = "Transpose";
i->category = "Gadget";
})
Transpose::Transpose() {
@ -44,19 +38,24 @@ void Transpose::init() {
}
void Transpose::process() {
int off = amount.load();
int off = octave.load()*12 + amount.load();
auto in = std::static_pointer_cast<CommandPort>(port(Port::Input, Port::Command, 0));
auto out = std::static_pointer_cast<CommandPort>(port(Port::Output, Port::Command, 0));
if (off == 0) { // a
if (out->passthroughTo.expired()) out->passthroughTo = in;
return;
}
if (!out->passthroughTo.expired()) out->passthroughTo.reset();
in->pull();
out->pull();
out->data = reinterpret_cast<uint8_t*>(audioEngine->tickAlloc(in->dataSize));
out->dataSize = in->dataSize;
memcpy(out->data, in->data, in->dataSize); // precopy
out->data = reinterpret_cast<uint8_t*>(audioEngine->tickAlloc(in->size));
out->size = in->size;
memcpy(out->data, in->data, in->size); // precopy
size_t mi = 0;
while (out->dataSize >= mi+5) {
while (out->size >= mi+5) {
int16_t& n = reinterpret_cast<int16_t&>(out->data[mi+2]);
if (n > -1) {
int nn = n;
@ -70,16 +69,28 @@ void Transpose::process() {
void Transpose::saveData(QCborMap& m) const {
m[qs("amount")] = QCborValue(amount);
m[qs("octave")] = QCborValue(octave);
}
void Transpose::loadData(const QCborMap& m) {
auto oct = m.value("octave");
if (oct.isUndefined()) { // convert from single value
int a = static_cast<int>(m.value("amount").toInteger(0));
int s = a < 0 ? -1 : 1;
a = std::abs(a);
octave = (a-(a%12))/12*s;
amount = a%12 * s;
return;
}
octave = static_cast<int>(oct.toInteger(0));
amount = static_cast<int>(m.value("amount").toInteger(0));
}
void Transpose::onGadgetCreated() {
if (!obj) return;
obj->showPluginName = false;
auto l = (new LayoutGadget(obj))->setMetrics(12);
auto l = (new LayoutGadget(obj))->setMetrics(8, 10);
(new KnobGadget(l))->bind(amount)->setLabel("Transpose")->setRange(-24, 24, 1);
(new KnobGadget(l))->bind(amount)->setLabel("Transpose")->setTextFunc(KnobGadget::textOffset)->setRange(-12, 12, 1, KnobGadget::MedStep);
(new KnobGadget(l))->bind(octave)->setLabel("Octave")->setTextFunc(KnobGadget::textOffset)->setRange(-5, 5, 1, KnobGadget::BigStep);
}

View File

@ -7,6 +7,7 @@
namespace Xybrid::Gadgets {
class Transpose : public Data::Node {
std::atomic<int> amount = 0;
std::atomic<int> octave = 0;
public:
Transpose();
~Transpose() override = default;

View File

@ -18,6 +18,7 @@ using namespace Xybrid::Audio;
using namespace Xybrid::UI;
#include "util/strings.h"
#include "util/ext.h"
#include <cmath>
@ -26,18 +27,14 @@ using namespace Xybrid::UI;
#include <QCborValue>
#include <QCborArray>
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "plug:2x03";
i->displayName = "2x03";
i->category = "Instrument";
//i->hidden = true;
i->createInstance = []{ return std::make_shared<I2x03>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
// clazy:excludeall=non-pod-global-static
RegisterPlugin(I2x03, {
i->id = "plug:2x03";
i->displayName = "2x03";
i->category = "Instrument";
})
namespace {
std::unordered_map<int8_t, QString> waveNames = [] {
std::unordered_map<int8_t, QString> m;
@ -53,12 +50,8 @@ namespace {
return m;
}();
// silence qtcreator warnings about gcc optimize attributes
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattributes"
// polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
[[gnu::optimize("O3")]] double polyblep(double t, double dt) {
force_opt double polyblep(double t, double dt) {
// 0 <= t < 1
if (t < dt) {
t /= dt;
@ -73,22 +66,22 @@ namespace {
return 0.0;
}
[[gnu::optimize("O3")]] double oscPulse(double phase, double delta, double duty = 0.5) {
force_opt double oscPulse(double phase, double delta, double duty = 0.5) {
//double duty = 0.5 + std::cos(time * 2.5) * (1 - 0.125*2) * 0.5;
double d = 1.0;
if (std::fmod(phase, 1.0) >= duty) d = -1.0;
d += polyblep(std::fmod(phase, 1.0), delta);
d -= polyblep(std::fmod(phase + (1.0 - duty), 1.0), delta);
return d;
return d - (duty-0.5)*2; // DC offset compensation
}
[[gnu::optimize("O3")]] double oscTri(double phase) {
force_opt double oscTri(double phase) {
phase = std::fmod(phase + 0.75, 1.0);
phase = phase * 0.2 + (std::floor(phase*32.0) / 32.0) * 0.8;
return std::abs(phase*2.0 - 1.0)*2.0 - 1.0;
}
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta) {
force_opt double oscSaw(double phase, double delta) {
phase = std::fmod(phase + 0.5, 1.0);
double d = phase * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8;
d = d * 2.0 - 1.0;
@ -96,7 +89,6 @@ namespace {
return d;
}
#pragma GCC diagnostic pop
}
I2x03::I2x03() {
@ -207,10 +199,7 @@ void I2x03::onGadgetCreated() {
auto wavetxt = [](double inp) {
if (auto f = waveNames.find(static_cast<int8_t>(inp)); f != waveNames.end()) return f->second;
return QString("?");
};
auto percenttxt = [](double d) {
return QString("%1%").arg(d*100, 0);
return qs("?");
};
auto ol = new LayoutGadget(obj, true);
@ -226,14 +215,14 @@ void I2x03::onGadgetCreated() {
// blip group
(new KnobGadget(l2))->bind(blipTime)->setLabel(qs("Blip"))->setRange(0.0, 0.1, 0.001);
(new KnobGadget(l2))->bind(blipWave)->setLabel(qs("Wave"))->setTextFunc(wavetxt)->setRange(-1, 4, 1, KnobGadget::BigStep)->setDefault(-1);
(new KnobGadget(l2))->bind(blipNote)->setLabel(qs("Note"))->setRange(-12, 12, 1);
(new KnobGadget(l2))->bind(blipNote)->setLabel(qs("Note"))->setTextFunc(KnobGadget::textOffset)->setRange(-12, 12, 1);
l2->addSpacer();
// pwm group
(new KnobGadget(l2))->bind(pwmDepth)->setLabel(qs("PWM"))->setTextFunc(percenttxt)->setRange(0.0, 1.0, 0.01)->setDefault(0.75);
KnobGadget::autoPercent(l2, pwmDepth)->setLabel(qs("PWM"))->setDefault(0.75);
(new KnobGadget(l2))->bind(pwmTime)->setLabel(qs("Time"))->setRange(0.01, 5.0, 0.01)->setDefault(3.0);
(new KnobGadget(l2))->bind(pwmPhase)->setLabel(qs("Phase"))->setRange(0.0, 1.0, 0.01);
KnobGadget::autoPercent(l2, pwmPhase)->setLabel(qs("Phase"));
//
}

View File

@ -41,4 +41,3 @@ namespace Xybrid::Instruments {
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
};
}

View File

@ -23,22 +23,19 @@ using namespace Xybrid::UI;
#include "nodelib/resampler.h"
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "plug:testsynth";
i->displayName = "The Testron";
i->category = "Instrument";
//i->hidden = true;
i->createInstance = []{ return std::make_shared<TestSynth>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
// clazy:excludeall=non-pod-global-static
RegisterPlugin(TestSynth, {
i->id = "plug:testsynth";
i->displayName = "The Testron";
i->category = "Instrument";
i->hidden = true;
})
namespace {
const double PI = std::atan(1)*4;
const double SEMI = std::pow(2.0, 1.0/12.0);
double fOsc(double& time) {
[[maybe_unused]] double fOsc(double& time) {
time = std::fmod(time, 2.0);
auto a = 0.0;
@ -78,7 +75,7 @@ void TestSynth::process() {
auto smp = *(project->samples.begin());
size_t mi = 0;
while (cp->dataSize >= mi+5) {
while (cp->size >= mi+5) {
uint16_t id = reinterpret_cast<uint16_t&>(cp->data[mi]);
int16_t n = reinterpret_cast<int16_t&>(cp->data[mi+2]);
if (n > -1) {
@ -100,7 +97,7 @@ void TestSynth::process() {
//qDebug() << "rate" << rate << "note" << note;
for (size_t s = 0; s < ts; s++) {
double ip = std::floor(osc);
/*double ip = std::floor(osc);
double fp = osc - ip;
size_t lutIndex = static_cast<size_t>(fp*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS;
auto& pt = NodeLib::resamplerLUT[lutIndex];
@ -108,9 +105,9 @@ void TestSynth::process() {
auto ii = static_cast<ptrdiff_t>(ip);
for (size_t i = 0; i < 8; i++) {
auto si = ii+static_cast<ptrdiff_t>(i);
if (si >= 0 && si < static_cast<ptrdiff_t>(smp->length())) out += (*smp)[static_cast<size_t>(si)] * pt[i];
//if (si >= 0 && si < static_cast<ptrdiff_t>(smp->length())) out += (*smp)[static_cast<size_t>(si)] * pt[i];
}
(*p)[s] = out;
(*p)[s] = out;*/
osc += rate;

View File

@ -18,6 +18,7 @@ using namespace Xybrid::Audio;
using namespace Xybrid::UI;
#include "util/strings.h"
#include "util/ext.h"
#include <cmath>
#include <array>
@ -28,16 +29,14 @@ using namespace Xybrid::UI;
#include <QCborValue>
#include <QCborArray>
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "plug:thicc";
i->displayName = "THiCC";
i->category = "Instrument";
i->createInstance = []{ return std::make_shared<Thicc>(); };
PluginRegistry::registerPlugin(i);
});
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Thicc, {
i->id = "plug:thicc";
i->displayName = "THiCC";
i->category = "Instrument";
})
namespace {
[[maybe_unused]] inline double wrap(double d) {
while (true) {
if (d > 1.0) d = (d - 2.0) * -1; //d-=2.0;
@ -49,12 +48,8 @@ namespace {
return b * p + a * (1.0 - p);
}
// silence qtcreator warnings about gcc optimize attributes
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattributes"
// polyBLEP algorithm (pulse antialiasing), slightly modified from https://www.kvraudio.com/forum/viewtopic.php?t=375517
[[gnu::optimize("O3")]] double polyblep(double t, double dt) {
force_opt double polyblep(double t, double dt) {
// 0 <= t < 1
if (t < dt) {
t /= dt;
@ -69,7 +64,7 @@ namespace {
return 0.0;
}
[[gnu::optimize("O3")]] double push(double in, double mod, double factor) {
force_opt inline double push(double in, double mod, double factor) {
double s = in < 0 ? -1 : 1;
in *= s;
//if (mod < 0) mod = 1.0/-mod;
@ -78,7 +73,7 @@ namespace {
return std::pow(in, mod)*s;
}
[[gnu::optimize("O3")]] double oscSaw(double phase, double delta, double mod) {
force_opt double oscSaw(double phase, double delta, double mod) {
phase = std::fmod(phase + 0.5, 1.0);
double d = phase;// * 0.2 + (std::floor(phase*7.0) / 7.0 + (0.5/7.0)) * 0.8;
d = d * 2.0 - 1.0;
@ -87,9 +82,9 @@ namespace {
return d;
}
[[gnu::optimize("O3")]] double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); }
force_opt double oscSine(double phase, double, double mod) { return push(std::sin(phase*PI*2), -mod, 5); }
[[gnu::optimize("O3")]] double oscPulse(double phase, double delta, double mod) {
force_opt double oscPulse(double phase, double delta, double mod) {
double duty = (mod+1.0)/2.0;
double d = 1.0;
if (std::fmod(phase, 1.0) >= duty) d = -1.0;
@ -98,9 +93,6 @@ namespace {
return d;
}
#pragma GCC diagnostic pop
// for clang on freebsd (and possibly other non-apple llvm sources) it seems we need to specify more.
// wave function list(s)
const constexpr std::array<double(*)(double,double,double),3> waveFunc = {
@ -203,7 +195,7 @@ void Thicc::onGadgetCreated() {
(new KnobGadget(l))->bind(mod)->setLabel(qs("W. Mod"))->setRange(-1.0, 1.0, 0.01);
l->addSpacer();
(new KnobGadget(l))->bind(voices)->setLabel(qs("Voices"))->setRange(1, 16, 1, KnobGadget::BigStep)->setDefault(1);
(new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setRange(0.0, 1.0, 0.001);
(new KnobGadget(l))->bind(detune)->setLabel(qs("Detune"))->setTextFunc(KnobGadget::textPercent)->setRange(0.0, 1.0, 0.001);
l->addSpacer();
KnobGadget::autoCreate(l, adsr);
}

View File

@ -29,4 +29,3 @@ namespace Xybrid::Instruments {
void onGadgetCreated() override;
};
}

View File

@ -26,16 +26,14 @@ using namespace Xybrid::UI;
#include <QCborValue>
#include <QCborArray>
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "plug:xriek";
i->displayName = "Xriek";
i->category = "Instrument";
i->createInstance = []{ return std::make_shared<Xriek>(); };
PluginRegistry::registerPlugin(i);
});
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Xriek, {
i->id = "plug:xriek";
i->displayName = "Xriek";
i->category = "Instrument";
})
namespace {
[[maybe_unused]] inline double wrap(double d) {
while (true) {
if (d > 1.0) d = (d - 2.0) * -1; //d-=2.0;
@ -134,6 +132,7 @@ void Xriek::onGadgetCreated() {
k->step = .01;
k->bind(drive);
k->setLabel("Drive");
k->setTextFunc(KnobGadget::textPercent);
}
{
@ -143,6 +142,7 @@ void Xriek::onGadgetCreated() {
k->step = .01;
k->bind(saturation);
k->setLabel("Saturate");
k->setTextFunc(KnobGadget::textPercent);
}
l->addSpacer();

View File

@ -25,4 +25,3 @@ namespace Xybrid::Instruments {
void onGadgetCreated() override;
};
}

View File

@ -19,6 +19,7 @@ using namespace Xybrid::Audio;
#include "ui/waveformpreviewwidget.h"
#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/layoutgadget.h"
#include "ui/gadgets/knobgadget.h"
#include "ui/gadgets/selectorgadget.h"
#include "ui/gadgets/sampleselectorgadget.h"
@ -27,9 +28,11 @@ using namespace Xybrid::UI;
#include "ui/patchboard/nodeuiscene.h"
#include "uisocket.h"
#include "util/ext.h"
#include "util/strings.h"
#include <cmath>
#include <iostream>
#include <QDebug>
#include <QCborMap>
@ -39,21 +42,12 @@ using namespace Xybrid::UI;
#include <QMenu>
#include <QGraphicsProxyWidget>
#define qs QStringLiteral
namespace {
bool _ = PluginRegistry::enqueueRegistration([] {
auto i = std::make_shared<PluginInfo>();
i->id = "plug:beatpad";
i->displayName = "BeatPad";
i->category = "Sampler";
//i->hidden = true;
i->createInstance = []{ return std::make_shared<BeatPad>(); };
PluginRegistry::registerPlugin(i);
//inf = i;
});
}
// clazy:excludeall=non-pod-global-static
RegisterPlugin(BeatPad, {
i->id = "plug:beatpad";
i->displayName = "BeatPad";
i->category = "Sampler";
})
BeatPad::BeatPad() {
@ -64,7 +58,7 @@ void BeatPad::init() {
addPort(Port::Output, Port::Audio, 0);
core.onNoteOn = [this](Note& note) {
auto& data = *reinterpret_cast<NoteData*>(&note.scratch);
auto& data = *hard_cast<NoteData*>(&note.scratch);
new (&data) NoteData(); // construct in-place
// look up config for note
@ -77,7 +71,7 @@ void BeatPad::init() {
};
core.onDeleteNote = [](Note& note) {
auto& data = *reinterpret_cast<NoteData*>(&note.scratch);
auto& data = *hard_cast<NoteData*>(&note.scratch);
data.~NoteData(); // destroy
};
@ -87,12 +81,13 @@ void BeatPad::init() {
};*/
core.processNote = [this](Note& note, AudioPort* p) {
auto& data = *reinterpret_cast<NoteData*>(&note.scratch);
auto& data = *hard_cast<NoteData*>(&note.scratch);
if (!data.config) return core.deleteNote(note);
auto smp = data.config->smp.lock();
if (!smp) return core.deleteNote(note);
double rate = static_cast<double>(smp->sampleRate) / static_cast<double>(audioEngine->curSampleRate());
//std::cout << "rate: " << rate << std::endl;
auto start = data.config->start;
if (start < 0) start = 0;
auto end = data.config->end;
@ -105,21 +100,11 @@ void BeatPad::init() {
// actual sample pos
double sp = static_cast<double>(start) + data.sampleTime * rate;
if (sp >= static_cast<double>(end)) return core.deleteNote(note);
double ip = std::floor(sp);
double fp = sp - ip;
size_t lutIndex = static_cast<size_t>(fp*NodeLib::LUT_STEPS) % NodeLib::LUT_STEPS;
auto& pt = NodeLib::resamplerLUT[lutIndex];
AudioFrame out(0.0);
auto ii = static_cast<ptrdiff_t>(ip) - 3;
for (size_t i = 0; i < 8; i++) {
auto si = ii+static_cast<ptrdiff_t>(i);
if (si >= start && si < static_cast<ptrdiff_t>(end)) out += (*smp)[static_cast<size_t>(si)] * pt[i];
}
auto out = NodeLib::resamp(smp.get(), sp, rate);
// stuff
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
(*p)[i] += out.gainBalance(data.config->gain, note.pan) * note.ampMult();
data.sampleTime += 1;
}
};
@ -131,12 +116,13 @@ void BeatPad::process() { core.process(this); }
void BeatPad::saveData(QCborMap& m) const {
QCborMap cm;
for (auto c : cfg) {
for (auto& c : cfg) {
if (auto smp = c.second->smp.lock(); smp) {
QCborMap e;
e[qs("sample")] = QCborValue(smp->uuid);
e[qs("start")] = static_cast<qint64>(c.second->start);
e[qs("end")] = static_cast<qint64>(c.second->end);
e[qs("gain")] = c.second->gain;
cm[c.first] = e;
smp->markForExport();
}
@ -153,6 +139,7 @@ void BeatPad::loadData(const QCborMap& m) {
c->smp = f.value();
c->start = cm.value("start").toInteger(-1);
c->end = cm.value("end").toInteger(-1);
c->gain = cm.value("gain").toDouble(0.0);
cfg[static_cast<int16_t>(ce.first.toInteger())] = c;
}
}
@ -180,10 +167,11 @@ void BeatPad::initUI(NodeUIScene* scene) {
};
auto state = scene->makeStateObject<UIState>();
// init layout
auto ol = (new LayoutGadget(scene, true))->setPanel(true);
// set up gadgets
auto noteSelector = new SelectorGadget();
scene->addItem(noteSelector);
noteSelector->setPos(0, 0);
auto noteSelector = new SelectorGadget(ol);
noteSelector->setWidth(320);
noteSelector->fGetList = [=] {
@ -194,7 +182,7 @@ void BeatPad::initUI(NodeUIScene* scene) {
auto c = f->second;
QString n;
if (auto smp = c->smp.lock(); smp) n = smp->name.section('/', -1, -1);
v.push_back({ f->first, qs("%1 %2").arg(Util::noteName(i)).arg(n) });
v.push_back({ f->first, qs("%1 %2").arg(Util::noteName(i), n) });
}
}
return v;
@ -209,16 +197,17 @@ void BeatPad::initUI(NodeUIScene* scene) {
for (int16_t i = 0; i < 12; i++) {
int16_t n = oct*12+i;
if (auto f = cfg.find(n); f != cfg.end()) mo->addAction(Util::noteName(n))->setDisabled(true);
else mo->addAction(Util::noteName(n), [=] { state->selectNote(n); });
else mo->addAction(Util::noteName(n), m, [=] { state->selectNote(n); });
}
}
};
auto sampleSelector = new SampleSelectorGadget(project);
scene->addItem(sampleSelector);
sampleSelector->setPos(0, 28);
auto sampleSelector = new SampleSelectorGadget(project, ol);
sampleSelector->setSize(320, 96);
auto r1 = (new LayoutGadget(ol))->setMetrics(0, -1, 0.0);
auto gain = KnobGadget::autoGain(r1);
// create functions now that all UI elements exist to be referenced
state->selectNote = [=](int16_t n) {
if (auto f = cfg.find(n); f != cfg.end()) state->cfg = f->second;
@ -226,8 +215,9 @@ void BeatPad::initUI(NodeUIScene* scene) {
state->note = n;
auto smp = state->cfg->smp.lock();
sampleSelector->setSample(smp);
gain->bind(state->cfg->gain);
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name.section('/', -1, -1) : "")}, false);
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n), smp ? smp->name.section('/', -1, -1) : "")}, false);
};
state->setSample = [=](std::shared_ptr<Sample> smp) {
state->cfg->smp = smp;
@ -235,9 +225,11 @@ void BeatPad::initUI(NodeUIScene* scene) {
if (smp) cfg[n] = state->cfg;
else if (auto f = cfg.find(n); f != cfg.end()) cfg.erase(f);
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n)).arg(smp ? smp->name : "")}, false);
noteSelector->setEntry({n, qs("%1 %2").arg(Util::noteName(n), smp ? smp->name : "")}, false);
};
//ol->updateGeometry();
// hook up relevantsignals
QObject::connect(scene, &NodeUIScene::notePreview, [=](int16_t note) { state->selectNote(note); });
QObject::connect(noteSelector, &SelectorGadget::onSelect, [=](auto e) { state->selectNote(e.first); });

View File

@ -11,6 +11,8 @@ namespace Xybrid::Instruments {
std::weak_ptr<Data::Sample> smp = std::weak_ptr<Data::Sample>();
ptrdiff_t start = -1;
ptrdiff_t end = -1;
double gain = 0.0;
};
struct NoteData {
@ -49,4 +51,3 @@ namespace Xybrid::Instruments {
void initUI(UI::NodeUIScene*) override;
};
}

View File

@ -0,0 +1,134 @@
#include "capaxitor.h"
using Xybrid::Instruments::Capaxitor;
using namespace Xybrid::NodeLib;
using Note = InstrumentCore::Note;
using namespace Xybrid::Data;
#include "nodelib/commandreader.h"
#include "nodelib/resampler.h"
#include "data/project.h"
#include "data/sample.h"
#include "data/porttypes.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "ui/waveformpreviewwidget.h"
#include "ui/patchboard/nodeobject.h"
#include "ui/gadgets/layoutgadget.h"
#include "ui/gadgets/knobgadget.h"
#include "ui/gadgets/selectorgadget.h"
#include "ui/gadgets/sampleselectorgadget.h"
using namespace Xybrid::UI;
#include "ui/patchboard/nodeuiscene.h"
#include "uisocket.h"
#include "util/ext.h"
#include "util/strings.h"
#include <cmath>
#include <QDebug>
#include <QCborMap>
#include <QCborValue>
#include <QCborArray>
#include <QMenu>
#include <QGraphicsProxyWidget>
// clazy:excludeall=non-pod-global-static
RegisterPlugin(Capaxitor, {
i->id = "plug:capaxitor";
i->displayName = "CapaXitor";
i->category = "Sampler";
})
Capaxitor::Capaxitor() {
}
void Capaxitor::init() {
addPort(Port::Input, Port::Command, 0);
addPort(Port::Output, Port::Audio, 0);
core.onNoteOn = [this](Note& note) {
auto& data = *hard_cast<NoteData*>(&note.scratch);
new (&data) NoteData(); // construct in-place
note.adsr = adsr;
};
core.onDeleteNote = [](Note& note) {
auto& data = *hard_cast<NoteData*>(&note.scratch);
data.~NoteData(); // destroy
};
core.processNote = [this](Note& note, AudioPort* p) {
auto& data = *hard_cast<NoteData*>(&note.scratch);
auto smp = this->smp.lock();
if (!smp) return core.deleteNote(note);
double rate = static_cast<double>(smp->sampleRate) / static_cast<double>(audioEngine->curSampleRate());
double baseNote = smp->getNote();
bool loop = smp->loopStart >= 0;
auto len = static_cast<double>(smp->length());
size_t ts = p->size;
for (size_t i = 0; i < ts; i++) {
core.advanceNote(note);
double n = note.effectiveNote();
double fr = std::pow(SEMI, n - baseNote);
// actual sample pos
double sp = data.sampleTime * rate;
if (!loop && sp >= len) return core.deleteNote(note);
auto out = NodeLib::resamp(smp.get(), sp, rate*fr);
(*p)[i] += out.gainBalance(0, note.pan) * note.ampMult();
data.sampleTime += fr;
}
};
}
void Capaxitor::reset() { core.reset(); }
void Capaxitor::release() { core.release(); }
void Capaxitor::process() { core.process(this); }
void Capaxitor::saveData(QCborMap& m) const {
if (auto smp = this->smp.lock(); smp) {
m[qs("sample")] = QCborValue(smp->uuid);
smp->markForExport();
}
m[qs("adsr")] = adsr;
}
void Capaxitor::loadData(const QCborMap& m) {
auto id = m.value("sample").toUuid();
if (auto f = project->samples.find(id); f != project->samples.end()) {
smp = f.value();
}
adsr = m.value("adsr");
}
void Capaxitor::onGadgetCreated() {
if (!obj) return;
auto l = new LayoutGadget(obj);
auto sampleSelector = new SampleSelectorGadget(project, l);
sampleSelector->setSize(128, 48);
sampleSelector->setSample(smp.lock(), false);
KnobGadget::autoCreate(l, adsr);
QObject::connect(sampleSelector, &SampleSelectorGadget::sampleSelected, sampleSelector, [=](auto smp) { this->smp = smp; });
}
void Capaxitor::onDoubleClick() { emit project->socket->openNodeUI(this); }

View File

@ -0,0 +1,49 @@
#pragma once
#include "nodelib/instrumentcore.h"
#include "data/audioframe.h"
namespace Xybrid::Data { class Sample; }
namespace Xybrid::Instruments {
class Capaxitor : public Data::Node {
NodeLib::InstrumentCore core;
struct NoteData {
double sampleTime = 0;
NoteData() = default;
~NoteData() = default;
};
static_assert (sizeof(NoteData) <= sizeof(NodeLib::InstrumentCore::Note::scratch), "Note data overflows scratch space!");
std::weak_ptr<Data::Sample> smp = std::weak_ptr<Data::Sample>();
NodeLib::ADSR adsr;
public:
Capaxitor();
~Capaxitor() override = default;
void init() override;
void reset() override;
void release() override;
void process() override;
//void onRename() override;
void saveData(QCborMap&) const override;
void loadData(const QCborMap&) override;
//void onUnparent(std::shared_ptr<Data::Graph>) override;
//void onParent(std::shared_ptr<Data::Graph>) override;
void onGadgetCreated() override;
//void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) override;
void onDoubleClick() override;
//void initUI(UI::NodeUIScene*) override;
};
}

BIN
xybrid/res/default.xyp Normal file

Binary file not shown.

View File

@ -6,4 +6,7 @@
<qresource prefix="/img">
<file>xybrid-logo-tiny.png</file>
</qresource>
<qresource prefix="/template">
<file>default.xyp</file>
</qresource>
</RCC>

120
xybrid/settingsdialog.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
using namespace Xybrid;
#include <QDialogButtonBox>
#include <QPushButton>
#include <QRegularExpression>
#include <QDebug>
#include "fileops.h"
#include "config/audioconfig.h"
#include "config/uiconfig.h"
using namespace Xybrid::Config;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "util/strings.h"
SettingsDialog* SettingsDialog::instance = nullptr;
namespace { // clazy:excludeall=non-pod-global-static
std::vector<std::function<void()>>* bnd;
void bind(QCheckBox* o, bool& v) {
o->setChecked(v);
bnd->push_back([o, &v] {
v = o->isChecked();
});
}
const QRegularExpression numeric("[0-9.]+");
void bind(QComboBox* o, int& v, const QStringList& items) {
o->clear();
o->addItems(items);
int ld = 100000000;
QString cm;
for (auto& i : items) { // find closest match
auto q = numeric.match(i).captured().toInt();
int id = std::abs(q - v);
if (id < ld) {
ld = id;
cm = i;
}
}
o->setCurrentText(cm);
bnd->push_back([o, &v] { // convert back to int
v = numeric.match(o->currentText()).captured().toInt();
});
}
void bind(QSpinBox* o, int& v, int min, int max, const QString& suffix = { }) {
o->setRange(min, max);
o->setValue(std::clamp(v, min, max));
o->setSuffix(suffix);
bnd->push_back([o, &v] {
v = o->value();
});
}
}
SettingsDialog::SettingsDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::SettingsDialog) {
ui->setupUi(this);
connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &SettingsDialog::apply);
instance = this;
bnd = &this->binds;
// audio page
QStringList sampleRates = { qs("44100Hz"), qs("48000Hz"), qs("96000Hz") };
const constexpr int minBufMs = 25, maxBufMs = 250;
const QString ms = qs("ms");
bind(ui->playbackSampleRate, AudioConfig::playbackSampleRate, sampleRates);
bind(ui->playbackBufferMs, AudioConfig::playbackBufferMs, minBufMs, maxBufMs, ms);
bind(ui->previewSampleRate, AudioConfig::previewSampleRate, sampleRates);
bind(ui->previewBufferMs, AudioConfig::previewBufferMs, minBufMs, maxBufMs, ms);
bind(ui->renderSampleRate, AudioConfig::renderSampleRate, sampleRates);
// UI page
bind(ui->verticalKnobs, UIConfig::verticalKnobs);
bind(ui->invertScrollWheel, UIConfig::invertScrollWheel);
}
SettingsDialog::~SettingsDialog() {
if (instance == this) instance = nullptr;
delete ui;
}
void SettingsDialog::apply() {
for (auto& f : binds) f();
FileOps::saveConfig();
// if left in preview mode, stop to allow settings to take
if (audioEngine->playbackMode() == audioEngine->Previewing) audioEngine->stop();
}
void SettingsDialog::reject() {
QDialog::reject();
if (instance == this) instance = nullptr;
deleteLater();
}
void SettingsDialog::tryOpen() {
if (!instance) {
(new SettingsDialog(nullptr))->show();
} else {
instance->show();
instance->raise();
instance->activateWindow();
}
}

35
xybrid/settingsdialog.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <QDialog>
#include <functional>
namespace Ui {
class SettingsDialog;
}
namespace Xybrid {
class SettingsDialog : public QDialog {
Q_OBJECT
std::vector<std::function<void()>> binds;
public:
static SettingsDialog* instance;
explicit SettingsDialog(QWidget *parent = nullptr);
~SettingsDialog() override;
static void tryOpen();
public slots:
void apply();
void reject() override;
private:
Ui::SettingsDialog *ui;
};
}

336
xybrid/settingsdialog.ui Normal file
View File

@ -0,0 +1,336 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>360</height>
</rect>
</property>
<property name="windowTitle">
<string>Xybrid Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabAudio">
<attribute name="title">
<string>Audio</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Playback Sample Rate</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="playbackSampleRate"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Playback Buffer Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="playbackBufferMs"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_3" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Preview Sample Rate</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="previewSampleRate"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_5" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Preview Buffer Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="previewBufferMs"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_4" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Rendering Sample Rate</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="renderSampleRate"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabUI">
<attribute name="title">
<string>UI</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QCheckBox" name="verticalKnobs">
<property name="text">
<string>Knobs scroll vertically</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="invertScrollWheel">
<property name="text">
<string>Invert scroll wheel for knobs, sliders, etc.</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabAbout">
<attribute name="title">
<string>About</string>
</attribute>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="buttonBoxContainer" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -58,6 +58,6 @@ QVariant BreadcrumbModel::data(const QModelIndex& index, int role) const {
if (role == Qt::DisplayRole) {
return actions[static_cast<size_t>(index.column())]->text();
}
if (role == Qt::TextAlignmentRole) return Qt::AlignHCenter + Qt::AlignVCenter;
if (role == Qt::TextAlignmentRole) return {Qt::AlignHCenter | Qt::AlignVCenter};
return QVariant();
}

View File

@ -39,7 +39,7 @@ void DirectoryNode::sortChildren() {
void DirectoryNode::sortTree() {
sortChildren();
for (auto c : children) c->sortTree();
for (auto c : qAsConst(children)) c->sortTree();
}
DirectoryNode* DirectoryNode::subdir(QString name) {
@ -47,7 +47,7 @@ DirectoryNode* DirectoryNode::subdir(QString name) {
QString first = name.section('/', 0, 0, QString::SectionSkipEmpty);
QString rest = name.section('/', 1, -1, QString::SectionSkipEmpty);
DirectoryNode* sd = nullptr;
for (auto c : children) if (c->isDirectory() && c->name == first) { sd = c; break; }
for (auto c : qAsConst(children)) if (c->isDirectory() && c->name == first) { sd = c; break; }
if (!sd) sd = new DirectoryNode(this, first);
return sd->subdir(rest);
}
@ -69,14 +69,14 @@ DirectoryNode* DirectoryNode::findPath(QString path) {
QString first = path.section('/', 0, 0, QString::SectionSkipEmpty);
QString rest = path.section('/', 1, -1, QString::SectionSkipEmpty);
DirectoryNode* sd = nullptr;
for (auto c : children) if (c->name == first) { sd = c; break; }
for (auto c : qAsConst(children)) if (c->name == first) { sd = c; break; }
if (!sd) return nullptr;
return sd->findPath(rest);
}
DirectoryNode* DirectoryNode::findData(const QVariant& d) {
if (data == d) return this;
for (auto c : children) {
for (auto c : qAsConst(children)) {
if (auto cd = c->findData(d); cd) return cd;
}
return nullptr;
@ -92,7 +92,7 @@ bool DirectoryNode::isChildOf(Xybrid::UI::DirectoryNode* dn) const {
}
void DirectoryNode::treeExec(const std::function<void(Xybrid::UI::DirectoryNode*)>& f) {
for (auto c : children) c->treeExec(f);
for (auto c : qAsConst(children)) c->treeExec(f);
f(this);
}

14
xybrid/ui/floaterbg.cpp Normal file
View File

@ -0,0 +1,14 @@
#include "floaterbg.h"
#include <QVariant>
using Xybrid::UI::FloaterBG;
FloaterBG::FloaterBG(QWidget* parent) : QWidget(parent) {
// set custom appearance
setStyleSheet(R"css(
background: palette(window);
border: 1px solid palette(mid);
border-radius: 5;
)css");
}

13
xybrid/ui/floaterbg.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include <QWidget>
namespace Xybrid::UI {
class FloaterBG : public QWidget {
Q_OBJECT
// this is really just here for QSS purposes
public:
FloaterBG(QWidget* parent);
~FloaterBG() override = default;
};
}

View File

@ -9,7 +9,7 @@ using Xybrid::UI::ButtonGadget;
#include <QMenu>
namespace {
namespace { // clazy:excludeall=non-pod-global-static
const QFont font("Arcon Rounded", 8);
}
@ -50,7 +50,7 @@ void ButtonGadget::paint(QPainter* p, const QStyleOptionGraphicsItem* opt, QWidg
p->setPen(QColor(255, 255, 255));
QFontMetricsF f(font);
auto t = f.elidedText(text, Qt::ElideRight, static_cast<int>(r.width() - 8));
p->drawText(QPointF(r.center().x() - f.width(t)/2, r.center().y() + f.ascent()/2), t);
p->drawText(QPointF(r.center().x() - f.horizontalAdvance(t)/2, r.center().y() + f.ascent()/2), t);
}
}

View File

@ -1,9 +1,13 @@
#include "knobgadget.h"
using namespace Xybrid::UI;
#include "util/strings.h"
#include "ui/gadgets/layoutgadget.h"
#include "nodelib/basics.h"
#include "config/uiconfig.h"
using namespace Xybrid::Config;
#include <cmath>
#include <QDebug>
@ -113,13 +117,37 @@ void KnobGadget::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidg
painter->drawPie(ir, 250*16 + static_cast<int>(-320.0*16*proportion), 0);
}
namespace { // interaction tracking vars
int acc = 0;
int wAcc = 0;
double trackVal;
inline double stepRound(double v, double s) { return std::round(v / s) * s; }
int accumulate(int& acc, int step) {
int sgn = acc < 0 ? -1 : 1;
int rm = std::abs(acc) % step;
int diff = (std::abs(acc)-rm)*sgn / step;
acc = rm*sgn;
return diff;
}
}
void KnobGadget::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
wAcc = 0; //reset wheel accumulator on hovering over a new knob
}
void KnobGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
if (e->button() == Qt::LeftButton) {
highlighted = true;
startVal = get();
if (step != 0.0) {
startVal = std::round(startVal / step) * step;
}
trackVal = get();
auto mod = e->modifiers();
if (mod.testFlag(Qt::AltModifier)) ; // no rounding until alt released
else if (mod.testFlag(Qt::ShiftModifier) && subStep > 0.0)
trackVal = stepRound(trackVal, subStep);
else if (step > 0.0)
trackVal = stepRound(trackVal, step);
fSet(trackVal);
} else if (e->button() == Qt::RightButton) {
fSet(defaultVal);
e->accept();
@ -136,46 +164,69 @@ void KnobGadget::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
void KnobGadget::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
if (highlighted) {
auto tdelta = -(e->screenPos().y() - e->buttonDownScreenPos(Qt::LeftButton).y());
tdelta /= stepPx;
fSet(std::clamp(startVal + tdelta * step, min, max));
auto curPos = e->pos().toPoint();
auto lastPos = e->lastPos().toPoint();
if (UIConfig::verticalKnobs) acc -= curPos.y() - lastPos.y();
else acc += curPos.x() - lastPos.x();
auto d = accumulate(acc, stepPx);
auto mod = e->modifiers();
if (mod.testFlag(Qt::AltModifier)) trackVal = std::clamp(trackVal, min, max); // soft release; just keep track within range
else if (mod.testFlag(Qt::ShiftModifier) && subStep > 0) {
trackVal = stepRound(trackVal + subStep*d, subStep);
fSet(std::clamp(trackVal, min, max));
} else if (step > 0) {
trackVal = stepRound(trackVal + step*d, step);
fSet(std::clamp(trackVal, min, max));
}
}
update();
}
void KnobGadget::wheelEvent(QGraphicsSceneWheelEvent* e) {
e->accept(); // never pass through
wAcc += e->delta();
auto d = accumulate(wAcc, 120);
if (d == 0) return; // don't need to update anything if incomplete accumulation
if (UIConfig::invertScrollWheel) d *= -1;
auto mod = e->modifiers();
if (highlighted) { // wheel while dragging
/*if (mod.testFlag(Qt::ShiftModifier) && subStep > 0) {
trackVal = stepRound(trackVal + subStep*d, subStep);
fSet(std::clamp(trackVal, min, max));
} else if (step > 0) {
trackVal = stepRound(trackVal + step*d, step);
fSet(std::clamp(trackVal, min, max));
}*/
return;
}
if (mod.testFlag(Qt::ShiftModifier) && subStep > 0)
fSet(std::clamp(stepRound(get()+subStep*d, subStep), min, max));
else if (step > 0)
fSet(std::clamp(stepRound(get()+step*d, step), min, max));
update();
}
void KnobGadget::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
e->accept();
}
QString KnobGadget::textPercent(double d) { return qs("%1%").arg(d*100); }
QString KnobGadget::textOffset(double d) { return (d > 0 ? qs("+%1") : qs("%1")).arg(d); }
QString KnobGadget::textFrequency(double d) { return qs("%1Hz").arg(d); }
QString KnobGadget::textGain(double d) { return (d > 0 ? qs("+%1dB") : qs("%1dB")).arg(d); }
QString KnobGadget::textBalance(double d) { return (d > 0 ? qs("+%1%") : qs("%1%")).arg(d*100); }
void KnobGadget::autoCreate(LayoutGadget* l, NodeLib::ADSR& adsr) {
KnobGadget* k;
KnobGadget* k [[maybe_unused]];
k = new KnobGadget(l);
k->min = 0.0;
k->max = 5.0;
k->step = .01;
k->bind(adsr.a);
k->setLabel("Attack");
k = new KnobGadget(l);
k->min = 0.0;
k->max = 5.0;
k->step = .01;
k->bind(adsr.d);
k->setLabel("Decay");
k = new KnobGadget(l);
k->min = 0.0;
k->max = 1.0;
k->defaultVal = 1.0;
k->step = .01;
k->bind(adsr.s);
k->setLabel("Sustain");
k = new KnobGadget(l);
k->min = 0.0;
k->max = 5.0;
k->step = .01;
k->bind(adsr.r);
k->setLabel("Release");
k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.a)->setLabel(qs("Attack"));
k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.d)->setLabel(qs("Decay"));
k = KnobGadget::autoPercent(l, adsr.s)->setLabel(qs("Sustain"));
k = (new KnobGadget(l))->setRange(0.0, 5.0, 0.01)->bind(adsr.r)->setLabel(qs("Release"));
}

View File

@ -1,11 +1,14 @@
#pragma once
#include "ui/gadgets/gadget.h"
#include "nodelib/param.h"
#include <memory>
#include <functional>
#include <atomic>
#include "util/strings.h"
namespace Xybrid::NodeLib { class ADSR; }
namespace Xybrid::UI {
class LayoutGadget;
@ -13,7 +16,6 @@ namespace Xybrid::UI {
Q_OBJECT
bool highlighted = false;
double startVal;
double lastVal = 0.0;
bool dirty = true;
@ -25,6 +27,7 @@ namespace Xybrid::UI {
enum Step : int {
NoStep = 1,
SmallStep = 3,
MedStep = 7,
BigStep = 15,
};
@ -35,6 +38,7 @@ namespace Xybrid::UI {
double min = 0.0;
double max = 1.0;
double step = 0.01;
double subStep = -1;
int stepPx = SmallStep;
double defaultVal = 0.0;
@ -48,18 +52,22 @@ namespace Xybrid::UI {
QPainterPath shape() const override;
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent*) override;
void mousePressEvent(QGraphicsSceneMouseEvent*) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent*) override;
void wheelEvent(QGraphicsSceneWheelEvent*) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
inline KnobGadget* setLabel(const QString& s) { label->setText(s); dirty = true; update(); return this; }
inline KnobGadget* setTextFunc(const std::function<QString(double)>& f) { fText = f; dirty = true; update(); return this; }
inline KnobGadget* setRange(double min, double max, double step = -1, int px = -1) {
inline KnobGadget* setSize(decltype(size) s) { this->size = s; return this; }
inline KnobGadget* setRange(double min, double max, double step = -1, int px = -1, double sub = -1) {
this->min = min;
this->max = max;
if (step > 0) this->step = step;
if (px > 0) stepPx = px;
if (sub > 0) subStep = sub;
update();
return this;
}
@ -80,8 +88,48 @@ namespace Xybrid::UI {
update();
return this;
}
KnobGadget* bind(NodeLib::Param& par) {
auto p = &par;
fGet = [p] { return p->value; };
fSet = [p](double d) {
p->value = d;
p->vt = std::numeric_limits<double>::quiet_NaN();
};
// binding to one of these populates metadata
min = p->min;
max = p->max;
defaultVal = p->def;
label->setText(p->name);
dirty = true;
update();
return this;
}
static QString textPercent(double);
static QString textOffset(double);
static QString textFrequency(double);
static QString textGain(double);
static QString textBalance(double);
static void autoCreate(LayoutGadget*, NodeLib::ADSR&);
};
// macro to vastly reduce boilerplate
#define preset(NAME) \
template<typename T> static inline KnobGadget* auto##NAME (QGraphicsItem* p, T& v) { return auto##NAME (p)->bind(v); } \
static inline KnobGadget* auto##NAME (QGraphicsItem* p)
// common presets
preset(Percent) { return (new KnobGadget(p))->setRange(0.0, 1.0, .01)->setTextFunc(KnobGadget::textPercent); }
preset(Gain) { return (new KnobGadget(p))->setRange(-60, 12, .1)->setLabel(qs("Gain"))->setTextFunc(KnobGadget::textGain); }
preset(Balance) { return (new KnobGadget(p))->setRange(-1.0, 1.0, .01)->setLabel(qs("Balance"))->setTextFunc(KnobGadget::textBalance); }
preset(Cutoff) { return (new KnobGadget(p))->setRange(0, 16000, 10, NoStep, 1)->setLabel(qs("Cutoff"))->setTextFunc(KnobGadget::textFrequency); }
};
}
// keep this local
#undef preset

View File

@ -6,7 +6,7 @@ using Xybrid::UI::LabelGadget;
#include <QPainter>
namespace {
namespace { // clazy:excludeall=non-pod-global-static
const QFont font("Arcon Rounded", 8);
}
@ -36,7 +36,7 @@ void LabelGadget::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*)
LabelGadget* LabelGadget::setText(const QString& t) {
text = t;
QFontMetricsF fm(font);
size = {fm.width(text) + 2, fm.height() + 2};
size = {fm.horizontalAdvance(text) + 2, fm.height() + 2};
update();
return this;
}

View File

@ -2,6 +2,7 @@
using Xybrid::UI::LayoutGadget;
#include "ui/patchboard/nodeobject.h"
#include "ui/patchboard/nodeuiscene.h"
namespace Xybrid::UI {
class LayoutSpacerGadget : public Gadget {
@ -51,19 +52,26 @@ LayoutGadget::LayoutGadget(NodeObject* parent, bool vertical) : LayoutGadget(par
});
}
LayoutGadget::LayoutGadget(NodeUIScene* parent, bool vertical) : LayoutGadget(static_cast<QGraphicsItem*>(nullptr), vertical) {
QObject::connect(parent, &NodeUIScene::finalized, this, [this] {
updateGeometry();
});
parent->addItem(this);
}
LayoutGadget* LayoutGadget::addSpacer() { new LayoutSpacerGadget(this); return this; }
void LayoutGadget::updateGeometry() {
qreal cur = margin;
auto ms = orient(minSize);
qreal h = ms.height();
for (auto c : childItems()) {
for (auto c : childItems()) { // clazy:exclude=range-loop-detach
auto g = static_cast<Gadget*>(c);
g->updateGeometry();
auto r = orient(g->layoutBoundingRect());
h = std::max(h, r.height());
}
for (auto c : childItems()) {
for (auto c : childItems()) { // clazy:exclude=range-loop-detach
auto g = static_cast<Gadget*>(c);
auto r = orient(g->layoutBoundingRect());
g->centerOn(orient(QPointF(cur + r.width()/2, r.height()/2 + (h - r.height())*bias)));
@ -74,3 +82,7 @@ void LayoutGadget::updateGeometry() {
}
QRectF LayoutGadget::boundingRect() const { return { QPointF(), size }; }
void LayoutGadget::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget*) {
if (drawPanel) NodeObject::drawPanel(painter, opt, boundingRect().adjusted(-panelMargin, -panelMargin, panelMargin, panelMargin));
}

View File

@ -4,6 +4,7 @@
namespace Xybrid::UI {
class NodeObject;
class NodeUIScene;
class LayoutGadget : public Gadget {
QSizeF size;
@ -19,9 +20,12 @@ namespace Xybrid::UI {
qreal bias = 0.5;
bool vertical = false;
bool drawPanel = false;
qreal panelMargin = 6;
LayoutGadget(QGraphicsItem* parent = nullptr, bool vertical = false);
LayoutGadget(NodeObject* parent, bool vertical = false);
LayoutGadget(NodeUIScene* parent, bool vertical = false);
~LayoutGadget() override = default;
inline LayoutGadget* setMetrics(qreal margin = -1, qreal spacing = -1, qreal bias = -1) {
@ -30,11 +34,17 @@ namespace Xybrid::UI {
if (bias >= 0) this->bias = std::clamp(bias, 0.0, 1.0);
return this;
}
inline LayoutGadget* setPanel(bool draw, qreal margin = 6) {
drawPanel = draw;
panelMargin = margin;
return this;
}
LayoutGadget* addSpacer();
void updateGeometry() override;
QRectF boundingRect() const override;
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
};
}

View File

@ -86,7 +86,6 @@ void SampleSelectorGadget::paint(QPainter* p, const QStyleOptionGraphicsItem* op
}
}
p->setRenderHint(QPainter::Antialiasing, true);
p->setRenderHint(QPainter::HighQualityAntialiasing, true);
p->setPen(QPen(outline, 2));
p->setBrush(QBrush(Qt::NoBrush));
p->drawRoundedRect(br, corner, corner);
@ -106,7 +105,7 @@ void SampleSelectorGadget::paint(QPainter* p, const QStyleOptionGraphicsItem* op
void SampleSelectorGadget::buildSubmenu(DirectoryNode* dir, QMenu* menu) {
auto smp = currentSample.lock();
bool needSeparator = false;
for (auto c : dir->children) {
for (auto c : qAsConst(dir->children)) {
if (c->isDirectory()) {
needSeparator = true;
buildSubmenu(c, menu->addMenu(c->name));
@ -118,11 +117,12 @@ void SampleSelectorGadget::buildSubmenu(DirectoryNode* dir, QMenu* menu) {
wa->setDefaultWidget(wfp);
wfp->showName = true;
wfp->highlightable = true;
wfp->showLoopPoints = false;
wfp->setSample(s);
wfp->setMinimumSize(192, 48);
menu->addAction(wa);
if (s == smp) wa->setDisabled(true);
else connect(wa, &QWidgetAction::triggered, [this, s] { setSample(s); });
else connect(wa, &QWidgetAction::triggered, this, [this, s] { setSample(s); });
}
}
}
@ -135,10 +135,10 @@ void SampleSelectorGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
auto* m = new QMenu();
if (project->samples.empty()) m->addAction("(no samples in project)")->setDisabled(true);
else {
m->addAction("(no sample)", [this] { setSample(nullptr); });
m->addAction("(no sample)", this, [this] { setSample(nullptr); });
m->addSeparator();
DirectoryNode root;
for (auto s : project->samples) root.placeData(s->name, s->uuid);
for (auto& s : qAsConst(project->samples)) root.placeData(s->name, s->uuid);
root.sortTree();
buildSubmenu(&root, m);

View File

@ -57,10 +57,10 @@ void SelectorGadget::mousePressEvent(QGraphicsSceneMouseEvent* e) {
std::vector<Entry> v = fGetList ? fGetList() : std::vector<Entry>();
if (v.empty()) m->addAction("(no entries)")->setDisabled(true);
else {
for (auto e : v) {
for (auto& e : v) {
if (_entry.first == e.first) {
m->addAction(e.second)->setDisabled(true);
} else m->addAction(e.second, [this, e] {
} else m->addAction(e.second, this, [this, e] {
setEntry(e);
});
}

View File

@ -47,6 +47,6 @@ namespace Xybrid::UI {
inline SelectorGadget* setEditFunc(const std::function<void(QMenu*)>& f) { fEditMenu = f; return this; }
signals:
void onSelect(const Entry&);
void onSelect(const Xybrid::UI::SelectorGadget::Entry&);
};
}

View File

@ -12,7 +12,10 @@ void GadgetScene::toolTip(QGraphicsItem* g, const QString& s, const QPointF& pos
if (s.isEmpty()) {
if (toolTipSource && toolTipSource != g) return;
// remove existing tool tip
if (toolTipObject) toolTipObject->deleteLater();
if (toolTipObject) {
toolTipObject->setVisible(false); // hide immediately to ensure ghosting doesn't happen
toolTipObject->deleteLater();
}
toolTipObject = nullptr;
} else {
// create/set up

View File

@ -17,6 +17,6 @@ namespace Xybrid::UI {
GadgetScene(QGraphicsView* view);
~GadgetScene() override = default;
void toolTip(QGraphicsItem*, const QString&, const QPointF&, const QColor&);
void toolTip(QGraphicsItem*, const QString& = { }, const QPointF& = {0, 0}, const QColor& = {255, 255, 255});
};
}

View File

@ -48,6 +48,20 @@ void PortObject::connectTo(PortObject* o) {
if (port->type == Port::Input) { in = this; out = o; }
else { out = this; in = o; }
// splice-between logic
auto dt = port->dataType();
if (in->port->type == Port::Input && in->port->singleInput() && in->port->isConnected() && out->port->dataType() == dt) {
if (auto oi = out->port->owner.lock()->port(Port::Input, dt, 0); oi) {
auto c = in->port->connections[0].lock();
if (!oi->isConnected() || oi->connections[0].lock() == in->port->connections[0].lock()) {
if (auto pc = in->connections[c->obj]; pc) delete pc;
in->port->disconnect(c);
if (!oi->isConnected() && oi->connect(c)) new PortConnectionObject(oi->obj, c->obj);
}
}
}
if (out->port->connect(in->port)) {
/*auto* pc =*/ new PortConnectionObject(in, out);
}
@ -60,11 +74,11 @@ void PortObject::setHighlighted(bool h, bool hideLabel) {
auto gs = static_cast<GadgetScene*>(scene());
if (h && !hideLabel) {
auto c = tcolor[port->dataType()];
auto txt = qs("%1 %2").arg(Util::enumName(port->dataType()).toLower()).arg(Util::hex(port->index));
if (!port->name.isEmpty()) txt = qs("%1 (%2)").arg(port->name).arg(txt);
auto txt = qs("%1 %2").arg(Util::enumName(port->dataType()).toLower(), Util::hex(port->index));
if (!port->name.isEmpty()) txt = qs("%1 (%2)").arg(port->name, txt);
double side = port->type == Port::Input ? -1.0 : 1.0;
gs->toolTip(this, txt, {side, 0}, c);
} else gs->toolTip(this, { }, { }, { });
} else gs->toolTip(this);
update();
}
@ -76,7 +90,7 @@ PortObject::PortObject(const std::shared_ptr<Data::Port>& p) {
setAcceptedMouseButtons(Qt::LeftButton);
setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
for (auto c : port->connections) {
for (auto& c : port->connections) {
if (auto cc = c.lock(); cc && cc->obj) connectTo(cc->obj);
}
@ -175,7 +189,7 @@ NodeObject::NodeObject(const std::shared_ptr<Data::Node>& n) {
node->onGadgetCreated();
emit finalized();
emit finalized(); // clazy:exclude=incorrect-emit
}
void NodeObject::setGadgetSize(QPointF p) {
@ -204,7 +218,7 @@ void NodeObject::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
setPos(std::round((x()+cx)/grid)*grid-cx, std::round((y()+cy)/grid)*grid-cy);
auto posDelta = pos() - oPos;
for (auto itm : scene()->selectedItems()) {
for (auto itm : scene()->selectedItems()) { // clazy:exclude=range-loop-detach
if (itm == this) continue;
itm->moveBy(posDelta.x(), posDelta.y()); // apply snap to everything else, in relative terms
}
@ -217,7 +231,7 @@ void NodeObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) {
void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
if (!isSelected()) {
for (auto* s : scene()->selectedItems()) s->setSelected(false);
for (auto* s : scene()->selectedItems()) s->setSelected(false); // clazy:exclude=range-loop-detach
setSelected(true);
}
auto* m = new QMenu();
@ -247,7 +261,7 @@ void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
}
void NodeObject::bringToTop(bool force) {
for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this);
for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this); // clazy:exclude=range-loop-detach
}
void NodeObject::createPorts() {
@ -261,8 +275,8 @@ void NodeObject::createPorts() {
QPointF inc(0, PortObject::portSize + PortObject::portSpacing);
QPointF cursor = QPointF(0, 0);
for (auto mdt : node->inputs) {
for (auto pp : mdt.second) {
for (auto& mdt : node->inputs) {
for (auto& pp : mdt.second) {
auto* p = new PortObject(pp.second);
p->setParentItem(ipc);
p->setPos(cursor);
@ -275,8 +289,8 @@ void NodeObject::createPorts() {
ipc->setPen(p);
cursor = QPointF(0, 0);
for (auto mdt : node->outputs) {
for (auto pp : mdt.second) {
for (auto& mdt : node->outputs) {
for (auto& pp : mdt.second) {
auto* p = new PortObject(pp.second);
p->setParentItem(opc);
p->setPos(cursor);
@ -297,7 +311,7 @@ void NodeObject::updateGeometry() {
if (autoPositionPorts) {
qreal pm = PortObject::portSize * .5 + PortObject::portSpacing;
if (inputPortContainer) inputPortContainer->setPos(QPointF(-pm, PortObject::portSize));
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize));
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize)); // NOLINT we're calling this on *this*
}
emit postGeometryUpdate();
}
@ -334,13 +348,7 @@ void NodeObject::focusInEvent(QFocusEvent *) {
bringToTop(true);
}
void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
if (customChrome) {
node->drawCustomChrome(painter, opt);
return;
}
QRectF r = boundingRect();
void NodeObject::drawPanel(QPainter* painter, const QStyleOptionGraphicsItem* opt, QRectF r, double rad) {
QColor outline = QColor(31, 31, 31);
if (opt->state & QStyle::State_Selected) outline = QColor(127, 127, 255);
@ -353,7 +361,17 @@ void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, Q
painter->setRenderHint(QPainter::RenderHint::Antialiasing);
painter->setBrush(QBrush(fill));
painter->setPen(QPen(QBrush(outline), 2));
painter->drawRoundedRect(r, 8, 8);
painter->drawRoundedRect(r, rad, rad);
}
void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
if (customChrome) {
node->drawCustomChrome(painter, opt);
return;
}
QRectF r = boundingRect();
drawPanel(painter, opt, r);
if (showName) {
static QFont f = [] {
@ -394,12 +412,11 @@ PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) {
in->connections[out] = this;
out->connections[in] = this;
QTimer::singleShot(1, [this] { this->in->scene()->addItem(this); });
QTimer::singleShot(1, this, [this] { this->in->scene()->addItem(this); });
setZValue(-100);
setAcceptHoverEvents(true);
//setFlag(QGraphicsItem::GraphicsItemFlag::)
QTimer::singleShot(1, [this] {
QTimer::singleShot(1, this, [this] {
auto* op = static_cast<QGraphicsObject*>(this->out->parentItem()->parentItem());
auto* ip = static_cast<QGraphicsObject*>(this->in->parentItem()->parentItem());
connect(op, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds);

View File

@ -126,6 +126,7 @@ namespace Xybrid::UI {
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
static void drawPanel(QPainter*, const QStyleOptionGraphicsItem*, QRectF, double radius = 8);
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
QRectF boundingRect() const override;

View File

@ -29,7 +29,14 @@ NodeUIScene::NodeUIScene(QGraphicsView* v, const std::shared_ptr<Xybrid::Data::N
connect(view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &NodeUIScene::queueResize);
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &NodeUIScene::queueResize);
autoResize();
// force full redraw to eliminate graphical glitches
connect(this, &QGraphicsScene::changed, this, [this] { update(); });
// queue up final setup in event loop; this should happen after code surrounding creation but before display
QMetaObject::invokeMethod(this, [this] {
emit finalized(); // emit before display
autoResize();
}, Qt::QueuedConnection);
}
NodeUIScene::~NodeUIScene() {

View File

@ -45,6 +45,7 @@ namespace Xybrid::UI {
template<typename T> inline T* makeStateObject() { auto o = std::make_shared<T>(); stateObject = o; return o.get(); }
signals:
void finalized();
void notePreview(int16_t);
};

View File

@ -26,6 +26,7 @@ using Xybrid::UI::PatchboardScene;
#include "data/graph.h"
#include "data/project.h"
using namespace Xybrid::Data;
#include "config/uiconfig.h"
#include "config/pluginregistry.h"
using namespace Xybrid::Config;
#include "fileops.h"
@ -114,7 +115,7 @@ PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr<Xy
auto v = Node::multiFromCbor(QCborValue::fromCbor(data->data("xybrid-internal/x-graph-copy")), graph, center);
setSelectionArea(QPainterPath()); // deselect all
for (auto n : v) { // and select pasted objects
for (auto& n : v) { // and select pasted objects
auto o = new NodeObject(n);
addItem(o);
o->setSelected(true);
@ -131,9 +132,9 @@ void PatchboardScene::drawBackground(QPainter* painter, const QRectF& rect) {
const constexpr int step = 32; // grid size
painter->setPen(QPen(QColor(127, 127, 127, 63), 1));
for (auto y = std::floor(rect.top() / step) * step + .5; y < rect.bottom(); y += step)
for (auto y = std::floor(rect.top() / step) * step + .5; y < rect.bottom(); y += step) // NOLINT
painter->drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y));
for (auto x = std::floor(rect.left() / step) * step + .5; x < rect.right(); x += step)
for (auto x = std::floor(rect.left() / step) * step + .5; x < rect.right(); x += step) // NOLINT
painter->drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()));
}
@ -151,7 +152,7 @@ void PatchboardScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
addItem(new NodeObject(n));
}, graph.get());
m->addAction("Import...", [this, p] {
m->addAction("Import...", this, [this, p] {
if (auto fileName = FileOps::showOpenDialog(nullptr, "Import node...", Config::Directories::presets, FileOps::Filter::node); !fileName.isEmpty()) {
auto n = FileOps::loadNode(fileName, graph);
if (!n) return; // right, that can return null
@ -224,7 +225,7 @@ void PatchboardScene::refresh() {
// build scene from graph
clear();
for (auto n : graph->children) {
for (auto& n : graph->children) {
auto* o = new NodeObject(n);
addItem(o);
}

View File

@ -136,7 +136,7 @@ void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewI
// and main data
QString s = index.data().toString();
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
//auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
int cc = index.column() % PatternEditorModel::colsPerChannel;
int align = Qt::AlignCenter;
if (cc > 1) { // param field
@ -170,7 +170,7 @@ bool PatternEditorItemDelegate::eventFilter(QObject *obj, QEvent *event) {
bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) {
if (index.data().isNull()) return false; // no channels?
auto type = event->type();
if (type == QEvent::KeyRelease) qDebug() << "key release";
//if (type == QEvent::KeyRelease) qDebug() << "key release";
if (type == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
@ -194,10 +194,11 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
} else {
if (k == Qt::Key_Space) { // TODO make this not "modify" if nothing was affected
if (mod & Qt::Modifier::SHIFT) return dc->cancel(); // TODO: once playback is a thing, shift+space to preview row?
if (mod & Qt::Modifier::SHIFT) return dc->cancel(); // nothing on shift yet
dc->cancel();
SelectionBounds s(sel);
if (s.x1 == s.x2 && s.x1 % PatternEditorModel::colsPerChannel <= 1) return false; // just previewing the note column
auto* cc = (new CompositeCommand())->reserve((1 + s.y2 - s.y1) * (1 + s.ch2 - s.ch1));
for (int c = s.ch1; c <= s.ch2; c++) {
@ -206,6 +207,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
auto mp = s.maxParamSelected(c);
if (mp < 0) continue;
auto mps = std::min(mpc, static_cast<size_t>(mp));
if (!multi) mps++; // allow strutting into new columns if not multiselecting
for (int r = s.y1; r <= s.y2; r++) {
if (multi && mps <= p->rowAt(c, r).numParams()) continue;
auto* dc = new PatternDeltaCommand(p, c, r-1);
@ -236,6 +238,30 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
return cc->commit("delete selection");
}
if (k == Qt::Key_Minus) {
dc->cancel();
SelectionBounds s(sel);
auto cc = (new CompositeCommand())->reserve((1 + s.y2 - s.y1) * (1 + s.ch2 - s.ch1));
for (int c = s.ch1; c <= s.ch2; c++) {
if (c == s.ch2 && s.x2 < 2) continue; // no params selected here
for (int r = s.y1; r <= s.y2; r++) {
auto dc = new PatternDeltaCommand(p, c, r-1);
//auto min = c == s.ch1 ? std::max(0, s.x1 - 2) : 0;
//auto max = c == s.ch2 ? s.x2 - 2 : PatternEditorModel::paramSoftCap;
for (auto i = 0; i < static_cast<int>(dc->row.numParams()); i++) {
if (s.paramSelected(c, i)) {
auto& pr = dc->row.param(i);
if (pr[0] != ' ') pr[1] *= -1;
}
}
cc->compose(dc);
}
}
return cc->commit("negate selection");
}
// for all other commands, reset selection to cursor and defer
sm->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
@ -296,7 +322,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
} else { // param column
size_t par = static_cast<size_t>((cc - (cc % 2)) / 2 - 1);
if (k == Qt::Key_Insert) { // insert from within any place in the param columns
if (row.numParams() >= PatternEditorModel::paramSoftCap) return false; // no overruns
if (row.numParams() >= PatternEditorModel::paramSoftCap) return dc->cancel(); // no overruns
row.insertParam(par, ' ');
auto view = static_cast<PatternEditorView*>(parent());
size_t cpar = row.numParams() - 1;
@ -321,6 +347,10 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
view->setCurrentIndex(index.siblingAtColumn(index.column()+1));
return dc->commit();
} else {
if (k == Qt::Key_Minus) { // negate current value
row.param(par)[1] *= -1;
return dc->commit();
}
if (k == Qt::Key_Comma) { // convenience; allow inserting an extend from number column
if (row.numParams() >= PatternEditorModel::paramSoftCap) return dc->cancel();
row.insertParam(par+1, ',');
@ -336,7 +366,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
}
}
} else { // new param; set to key pressed and move forward
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return false;
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return dc->cancel();
char chr = static_cast<QKeyEvent*>(event)->text().toUtf8()[0];
row.addParam(chr);
auto view = static_cast<PatternEditorView*>(parent());

View File

@ -43,7 +43,7 @@ namespace { // helper functions
QString s(3, ' ');
int nn = n % 12;
int oc = (n - nn) / 12;
s[2] = '0' + static_cast<char>(oc);
s[2] = static_cast<QChar>('0' + static_cast<char>(oc));
s[0] = notemap[nn*2];
s[1] = notemap[nn*2+1];
return s;
@ -112,13 +112,13 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
if (cc % 2 == 0) {
if (row.numParams() > cp) return QString(1,static_cast<char>(row.params->at(cp)[0]));
if (row.numParams() == cp) return qs("» ");
return qs("");
return qs(" ");
}
if (row.numParams() > cp) {
if (row.params->at(cp)[0] == ' ') return qs("- ");
return byteStr(row.params->at(cp)[1]);
}
return qs("");
return qs(" ");
}
} else if (role == Qt::SizeHintRole) {
if (index.row() >= pattern->rows) return QSize(-1, -1);

View File

@ -135,7 +135,7 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
} else { // note(s)
amt = std::clamp(amt, -12, 12);
auto cc = new CompositeCommand();
for (auto s : sel.indexes()) {
for (auto s : sel.indexes()) { // clazy:exclude=range-loop-detach
if (s.column() % Util::colsPerChannel != 1) continue;
int ch = Util::channelForColumn(s.column());
auto c = new PatternDeltaCommand(p, ch, s.row()-1);
@ -307,7 +307,7 @@ void PatternEditorView::startPreview(int key) {
auto ind = currentIndex();
int cc = ind.column() % PatternEditorModel::colsPerChannel;
int ch = (ind.column() - cc) / PatternEditorModel::colsPerChannel;
if (cc == 1) { // note column
if (cc == 1 || (cc == 0 && key == Qt::Key_Space)) { // note column or space on port
stopPreview(key); // end current preview first, if applicable
auto& r = mdl->getPattern()->rowAt(ch, ind.row()-1);
auto p = mdl->getPattern()->project->shared_from_this();
@ -401,7 +401,7 @@ void PatternEditorView::headerContextMenu(QPoint pt) {
});
if (idx < hdr->count() - 1) {
menu->addAction("Delete Channel", this, [this, idx, p]() {
if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name)).arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name), Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
(new PatternChannelDeleteCommand(p, idx))->commit();
});
menu->addAction("Rename Channel...", this, [this, idx, p]() {

View File

@ -10,6 +10,7 @@ using namespace Xybrid::Editing;
#include <QDebug>
#include <QMimeData>
#include <QIODevice>
#include "mainwindow.h"

View File

@ -2,6 +2,7 @@
using Xybrid::UI::PatternSequencerModel;
#include <QMimeData>
#include <QIODevice>
using Xybrid::Data::Pattern;
using Xybrid::Data::Project;
@ -40,7 +41,7 @@ QVariant PatternSequencerModel::data(const QModelIndex &index, int role) const {
if (pattern->name.isEmpty()) return QVariant(); // no tool tip without name
return QString("(%1) %2").arg(pattern->index, 1, 10, QChar('0')).arg(pattern->name);*/
}
if (role == Qt::TextAlignmentRole ) return Qt::AlignHCenter + Qt::AlignVCenter;
if (role == Qt::TextAlignmentRole ) return {Qt::AlignHCenter | Qt::AlignVCenter};
return QVariant();
}

View File

@ -18,6 +18,7 @@ using namespace Xybrid::Editing;
#include <QTimer>
#include <QStringBuilder>
#include <QMimeData>
#include <QIODevice>
#include <QUrl>
#include <QMenu>
@ -98,7 +99,7 @@ void SampleListModel::refresh() {
root = std::make_shared<DirectoryNode>();
auto* project = window->getProject().get();
if (!project) return;
for (auto s : project->samples) root->placeData(s->name, s->uuid);
for (auto& s : qAsConst(project->samples)) root->placeData(s->name, s->uuid);
root->sortTree();
view->setCurrentIndex(QModelIndex());
@ -146,7 +147,7 @@ void SampleListModel::propagateSampleNames(DirectoryNode* dn) {
if (!dn->data.isNull()) {
auto* project = window->getProject().get();
project->samples[dn->data.toUuid()]->name = dn->path();
} else for (auto c : dn->children) propagateSampleNames(c);
} else for (auto c : qAsConst(dn->children)) propagateSampleNames(c);
}
bool SampleListModel::setData(const QModelIndex& index, const QVariant& value, int role) {
@ -236,7 +237,7 @@ bool SampleListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
QList<QUrl> urls = data->urls();
bool success = false;
for (auto u : urls) {
for (auto& u : urls) {
if (!u.isLocalFile()) continue;
auto smp = Sample::fromFile(u.toLocalFile());
if (smp) { // valid sample returned

Some files were not shown because too many files have changed in this diff Show More